bullseye-arrowClasses, Objects & Data Containers

Classes are blueprints that define the structure and behavior of objects. Think of a class as a cookie-cutter and objects as the cookies made from it.

Objects vs Data Containers

This is one of the most important distinctions in clean code. Understanding when to use each will dramatically improve your code quality.

Data Containers (Data Structures)

Purpose: Hold data with no behavior

Characteristics:

  • All properties are public

  • No methods (or only getters/setters)

  • Like a struct or record

Example:

When to Use:

  • Transferring data between systems

  • Configuration objects

  • API request/response payloads

  • Database query results

Objects (With Behavior)

Purpose: Hide internal data and expose behavior through methods

Characteristics:

  • Private properties

  • Public methods (API)

  • Encapsulation of logic

Example:

When to Use:

  • Business logic

  • Domain models

  • Services and utilities

  • Anything that "does" something

Why This Matters

❌ Bad Practice:

✅ Good Practice:

Benefits:

  • If the object's internals change, your code doesn't break

  • Code is more readable (method names explain intent)

  • Easier to maintain and test

When Data Containers Are Perfect

Core Principles

Classes Should Be Small

Just like functions, classes should be small. But what does "small" mean?

Small ≠ Few Lines of Code

Size is measured by responsibilities, not lines.

The Single Responsibility Test

Ask yourself: "What is this class responsible for?"

❌ Too Large:

Problem: This class handles user authentication, profile management, shopping, AND payments. That's at least 4 responsibilities!

✅ Better:

Benefits:

  • Each class is easier to understand

  • Changes to payments don't affect user authentication

  • Easier to test each part independently

  • Team members can work on different classes without conflicts

High Cohesion

Cohesion measures how much the methods in a class use the class's properties.

High Cohesion Example

High cohesion: Every method uses both properties. The class is focused.

Low Cohesion Example

Low cohesion: Each method uses different properties. This should be 3 separate classes!

✅ Refactored:

The Law of Demeter

Also known as: The Principle of Least Knowledge

The Rule: Don't access the internals of an object through another object.

What You Can Access

A method can access:

  1. Its own class properties and methods

  2. Objects stored in its properties

  3. Objects passed as parameters

  4. Objects it creates

The Problem: Chaining

❌ Violates Law of Demeter:

Problems:

  • Long chains create fragile code

  • Changes deep in the structure break everything

  • Hard to read and understand

  • Creates tight coupling

✅ Better:

Even Better: Tell, Don't Ask

Instead of asking for data and then acting on it, tell the object what to do.

Important Exception

The Law of Demeter doesn't apply to:

Method chaining (fluent interfaces):

Data containers:

Polymorphism

Polymorphism means "many forms." It allows you to use the same interface for different types.

The Problem It Solves

❌ Without Polymorphism:

Problems:

  • Same if Checks are repeated in every method

  • Adding a new delivery type requires changing multiple methods

  • Easy to forget to update all places

  • Violates Open-Closed Principle (more on this later)

The Solution: Polymorphism

✅ With Polymorphism:

Using a Factory

The if Check only appears once now:

Benefits

  1. Easy to extend: Add new delivery types without changing existing code

  2. No code duplication: The if Logic exists in one place

  3. Type safety: Each class handles its own logic

  4. Testable: Test each delivery type independently

SOLID Principles

SOLID is an acronym for five principles that help you write clean, maintainable classes.

Single Responsibility Principle (SRP)

Rule: A class should have only one reason to change.

In Practice: Each class should do one thing and do it well.

Example: Violation

❌ Multiple Responsibilities:

Problems:

  • Changes to report generation affect PDF creation

  • Changes to the PDF format affect report logic

  • Two different teams might need to modify the same class

  • Hard to test each part independently

Solution: Separate Responsibilities

✅ Single Responsibility:

Benefits

  • Easier to understand: Each class has a clear purpose

  • Easier to test: Test report generation separately from PDF creation

  • Easier to maintain: Changes are isolated

  • Better team collaboration: Different developers can work on different classes

Open-Closed Principle (OCP)

Rule: Classes should be open for extension but closed for modification.

Translation: You should be able to add new functionality without changing existing code.

Example: Violation

❌ Requires Modification:

Problems:

  • Every new document type requires modifying the Printer class

  • Risk of breaking existing functionality

  • Code duplication across similar methods

Solution: Extension Through Inheritance

✅ Open for Extension:

Usage

Liskov Substitution Principle (LSP)

Rule: Objects should be replaceable with instances of their subtypes without breaking the program.

Translation: If class B extends class A, you should be able to use B anywhere you use A.

Example: Violation

❌ Subtype Breaks Contract:

Problem: Penguin violates the contract that all Birds can fly.

Solution: Proper Hierarchy

✅ Correct Modeling:

Real-World Example

❌ Violates LSP:

✅ Better Design:

Interface Segregation Principle (ISP)

Rule: Many small, specific interfaces are better than one large, general interface.

Translation: Don't force classes to implement methods they don't need.

Example: Violation

❌ General Interface:

Problem: InMemoryDatabase is forced to implement connect() even though it doesn't need it.

Solution: Segregate Interfaces

✅ Specific Interfaces:

Real-World Example

❌ Fat Interface:

✅ Segregated Interfaces:

Dependency Inversion Principle (DIP)

Rule: Depend on abstractions, not on concrete implementations.

Translation: High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.

Example: Violation

❌ Depends on Concretions:

Problems:

  • App knows too much about database types

  • Hard to add new database types

  • The app must change when database implementations change

Solution: Depend on Abstractions

✅ Depends on Abstractions:

Benefits:

  • The app is simpler and more focused

  • Easy to swap database implementations

  • Better testability (can mock Database)

The "Inversion" Part

The dependency is inverted:

The concrete class now depends on the abstraction, not the other way around!

Advanced Example: Dependency Injection

Common Patterns and Best Practices

Composition Over Inheritance

Principle: Favor object composition over class inheritance.

Factory Pattern

Centralize object creation logic.

Strategy Pattern

Define a family of algorithms and make them interchangeable

Practical Guidelines

Use Common Sense

Remember: The goal is readable, maintainable code, not following rules blindly.

Last updated