Object-Oriented Design (OOD) is a design paradigm centered on organizing software around objects that combine data and behavior. Good OOD creates systems that are maintainable, extensible, and robust by carefully managing state, responsibilities, and relationships between objects.

Core Ideas of Good Object-Oriented Design

1. Classes Exist to Maintain Invariants

The mathematical foundation of OOP: A class should exist if and only if it has an invariant to maintain.

“My rule of thumb is that you should have a real class with an interface and a hidden representation if and only if you can consider an invariant for the class.” — Bjarne Stroustrup

Key Insights:

  • An invariant defines the “valid region” - the set of all possible states where every public method works correctly
  • Step outside the valid region → you guarantee a bug
  • If every field can have any value (no invariant), use a struct or data class, not a real class
  • The constructor establishes the invariant; the destructor tears it down
  • Methods may temporarily violate invariants internally, but must restore them before returning

Example:

public class BankAccount
{
    private decimal _balance;
    // INVARIANT: balance >= 0 (no overdrafts allowed)
 
    public BankAccount(decimal initialDeposit)
    {
        if (initialDeposit < 0)
            throw new ArgumentException("Initial deposit must be non-negative");
        _balance = initialDeposit; // Establish invariant
    }
 
    public void Withdraw(decimal amount)
    {
        if (amount < 0)
            throw new ArgumentException("Withdrawal amount must be positive");
        if (_balance - amount < 0)
            throw new InvalidOperationException("Insufficient funds");
        _balance -= amount; // Maintain invariant
    }
}

2. Encapsulation Protects Invariants

Encapsulation is the mechanism for defending invariants by:

  • Hiding internal representation: Private fields, public methods
  • Controlling access: Only methods that maintain invariants can modify state
  • Creating boundaries: Clear separation between interface and implementation

Why it matters:

  • Without encapsulation, external code can violate invariants directly
  • Encapsulation enables safe evolution of internal representations
  • Reduces coupling by hiding implementation details

Related Principles:

3. Cohesion and Coupling Shape Design Quality

Cohesion: How strongly related the responsibilities within a class are

  • High cohesion: Related functionality grouped together, easier to understand and maintain
  • Low cohesion: Unrelated responsibilities mixed, harder to reason about and change

Coupling: The degree of interdependence between classes

  • Low coupling: Classes can change independently, system is flexible
  • High coupling: Changes ripple across classes, system is brittle

The Goal: High cohesion within classes, low coupling between classes

Related Principles:

4. Polymorphism Enables Extension

Polymorphism allows objects of different types to be treated uniformly through shared interfaces.

Benefits:

  • Open/Closed Principle: Open for extension, closed for modification
  • Dependency Inversion: Depend on abstractions, not concrete implementations
  • Substitutability: Derived classes can replace base classes (Liskov Substitution)

Implementation Mechanisms:

  • Interface implementation
  • Inheritance with method overriding
  • Duck typing (in dynamic languages)

Related Principles:

5. Responsibility Assignment Determines Quality

Who does what? The most fundamental OOD question.

Good responsibility assignment:

  • Maintains invariants: Methods that change state belong on the class that owns the invariant
  • Minimizes coupling: Operations that don’t need internal access live outside the class
  • Maximizes cohesion: Related operations group together
  • Respects information hiding: Data stays close to the operations that use it

GRASP Patterns provide systematic guidance:

  • Information Expert: Assign responsibility to the class with the information needed
  • Creator: Who creates objects? The class that contains, aggregates, or has initializing data
  • Controller: What handles system events? A facade or coordinator object
  • Low Coupling / High Cohesion: Explicit design goals

See: GRASP Patterns

6. Interfaces Should Be Minimal and Complete

Interface Design Principles:

Minimal Interface:

  • Only include methods that must access internal representation
  • Operations that can be built on existing methods should be non-members
  • Smaller interfaces are easier to understand, test, and maintain

Complete Interface:

  • Provide all necessary operations for clients to accomplish their goals
  • Don’t force clients to work around missing functionality
  • Balance minimalism with usability

Example:

public class Date
{
    // MINIMAL: Only operations that must access internal state
    public int Day { get; }
    public int Month { get; }
    public int Year { get; }
    public Date AddDays(int days) { }
    public int DaysBetween(Date other) { }
}
 
// COMPLETE: Build rich functionality outside the class
public static class DateExtensions
{
    public static Date NextSunday(this Date date) { }
    public static Date NextWeekday(this Date date) { }
    public static bool IsWeekend(this Date date) { }
}

Related Principles:

7. Composition Over Inheritance

Composition over Inheritance is a fundamental OOD guideline:

Why favor composition:

  • Flexibility: Change behavior at runtime by composing different objects
  • Loose coupling: “Has-a” relationships are less brittle than “is-a”
  • Avoids fragile base class problem: Changes to parent don’t break children
  • Explicit dependencies: Composition makes relationships clear

When inheritance is appropriate:

  • True “is-a” relationships (substitutability)
  • Shared behavior that’s unlikely to change
  • Framework extension points designed for inheritance

Related Patterns:

SOLID: The Five Principles of Class Design

SOLID Principles are the cornerstone of object-oriented class design, grounded in invariant maintenance:

  1. Single Responsibility: A class should have one, and only one, reason to change
  2. Closed: Open for extension, closed for modification
  3. Liskov Substitution: Derived classes must be substitutable for base classes
  4. Interface Segregation: Clients shouldn’t depend on interfaces they don’t use
  5. Dependency Inversion: Depend on abstractions, not concretions

Understanding SOLID through Invariants:

Stop thinking of SOLID as “wishy washy design guidelines.” They are mathematical necessities:

  • SRP: Can you decompose the class invariant into independent sub-invariants?
  • OCP: What happens to the invariant when you extend behavior?
  • LSP: Derived classes must preserve the parent’s invariant
  • ISP: Each interface represents a specific invariant contract
  • DIP: Abstractions define invariant contracts that details implement

“Class invariant doesn’t hold === you have hit a bug” — It’s as simple and as hard as that.

Key Principles and Patterns

Design Principles

Design Patterns

  • Design Patterns - Proven solutions to recurring OOD problems
  • GRASP Patterns - Responsibility assignment guidance
  • Gang of Four Patterns - Classic OOD patterns

Anti-Patterns to Avoid

  • Anemic Domain Model: Objects with data but no behavior (violates encapsulation)
  • God Object: One class that knows/does too much (low cohesion, high coupling)
  • Primitive Obsession: Using primitives instead of meaningful value objects (missing invariants)
  • Feature Envy: Methods that use another class’s data more than their own (misplaced responsibility)

Resources:

When NOT to Use Classes

Not everything needs to be a class. Use simpler structures when:

Use structs/records/data classes when:

  • No invariant exists (any combination of values is valid)
  • Pure data containers for transferring information
  • Immutable value objects with no behavior

Use functions when:

  • Pure computation with no state
  • Operations that don’t belong to any particular object
  • Utility operations that work across many types

Example:

// GOOD: No invariant, just data
public record PersonName(string First, string Last);
 
// BAD: Unnecessary class
public class PersonName
{
    private string _first;
    private string _last;
 
    public string GetFirst() => _first;
    public void SetFirst(string value) => _first = value;
    public string GetLast() => _last;
    public void SetLast(string value) => _last = value;
}

Practical Application

Design Workflow

  1. Identify invariants: What makes this object valid?
  2. Define interface: What operations maintain the invariant?
  3. Hide representation: Make fields private
  4. Assign responsibilities: Who should do what?
  5. Minimize coupling: Reduce dependencies between classes
  6. Maximize cohesion: Group related functionality

Code Review Questions

  • What invariant does this class maintain?
  • Can external code violate the invariant?
  • Does this method belong here or outside the class?
  • Are we favoring composition over inheritance?
  • Does this follow SOLID principles?

See Also

Further Reading

References

From Good Software Practices

0 items under this folder.