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
structor 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:
- Tell, Don’t Ask - Tell objects what to do; they protect their own invariants
- Law of Demeter - Minimize coupling through limited object interaction
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:
- Single Responsibility Principle - Each class has one reason to change (high cohesion)
- Composition over Inheritance - Reduces coupling through “has-a” relationships
- Package Principles - Robert C. Martin’s principles for cohesion and coupling at the package level
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:
- Closed Principle - Extend behavior without modifying existing code
- Liskov Substitution Principle - Subtypes must be substitutable for base types
- Dependency Inversion Principle - Depend on abstractions
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:
- Interface Segregation Principle - Clients shouldn’t depend on unused interfaces
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:
- Strategy Pattern - Encapsulate algorithms through composition
- Observer Pattern - Define relationships through composition
SOLID: The Five Principles of Class Design
SOLID Principles are the cornerstone of object-oriented class design, grounded in invariant maintenance:
- Single Responsibility: A class should have one, and only one, reason to change
- Closed: Open for extension, closed for modification
- Liskov Substitution: Derived classes must be substitutable for base classes
- Interface Segregation: Clients shouldn’t depend on interfaces they don’t use
- 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
- Tell, Don’t Ask - Command objects rather than querying state
- Law of Demeter - Only talk to immediate neighbors
- Command Query Separation - Separate state-changing commands from queries
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
- Identify invariants: What makes this object valid?
- Define interface: What operations maintain the invariant?
- Hide representation: Make fields private
- Assign responsibilities: Who should do what?
- Minimize coupling: Reduce dependencies between classes
- 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
Related Topics
- Design Concepts - Theoretical foundations (invariants, coupling, cohesion)
- Design Principles - Prescriptive guidelines
- Design Patterns - Proven solutions
- Software Design - Broader design topics
- Good Software Practices
Further Reading
- The C++ Style Sweet Spot - Bjarne Stroustrup on invariants
- Accessors Are Evil
- Stop Writing Classes