Classes, 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:
Its own class properties and methods
Objects stored in its properties
Objects passed as parameters
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
ifChecks are repeated in every methodAdding 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
Easy to extend: Add new delivery types without changing existing code
No code duplication: The
ifLogic exists in one placeType safety: Each class handles its own logic
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