Design Patterns: The Seven Fundamental Principles of Object-Oriented Development
Understanding Design Patterns
A design pattern represents a proven solution to commonly occurring problems in software development. These patterns emerge from successful practices that have been repeatedly validated across different projects and applications.
Object-Oriented Programming Fundamentals
To truly leverage design patterns, one must understand both the internal mechanisms and the abstract reasoning behind object-oriented programming:
Internal mechanisms (going downward):
Encapsulation — hiding implementation details
Inheritance — reusing existing code
Polymorphism — redefining object behavior
Abstract reasoning (going upward): Understanding how these mechanisms translate to real-world problem solving and what constitutes effective object-oriented design
The Seven Design Principles
Open-Closed Principle
Dependency Inversion Principle
Single Responsibility Principle
Interface Segregation Principle
Liskov Substitution Principle
Law of Demeter
Composite Reuse Principle
The Open-Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Implementation approach: Utilize virtual functions and inheritance hierarchies to add new functionality without altering existing code. When requirements evolve, extend the current classes through derived implementations rather than modifying established code paths.
The Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change. Each class should encapsulate a single responsibility or functionality.
Implementation approach: Separate distinct concerns into different classes. For example, when handling file operations, logging, and compression, each should be isolated in its own class. Consider creating abstract base classes to facilitate future extensibility and replacement.
The Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Key points:
High-level components (stable) should not rely on low-level components (volatile)
Abstractions should not depend on implementation details
Implementation details should depend on abstractions
This principle facilitates loose coupling and makes systems more maintainable over time.
The Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
Requirements:
Subclasses must maintain interface consistency with their parent class
Subclasses can extend parent functionality
Subclasses should not introduce new error conditions or exceptions
Practical interpretation: While subclasses may extend capabilities, they should not fundamentally alter the behavior of parent methods. Avoid overriding methods in ways that change the expected behavior.
The Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they do not use. Prefer specialized interfaces over monolithic ones.
Implementation approach:
Keep interfaces narrow and focused
Ensure interface methods are minimal yet complete
Divide broad interfaces into role-specific counterparts based on business requirements
Note: This differs from the Single Responsibility Principle. SRP focuses on class responsibilities from a business logic perspective, while ISP focuses on interface method granularity.
The Law of Demeter (LoD)
Definition: A class should have limited knowledge of other classes. Only interact with close friends, not strangers.
Key points:
Minimize dependencies between classes
Only use classes that appear as member variables, method parameters, or return values
Avoid accessing objects obtained indirectly through other objects
This principle helps reduce coupling by limiting the knowledge one object has about another's internals.
The Composite Reuse Principle (CRP)
Definition: Prefer composition over inheritance when reusing functionality.
Comparison with inheritance approach: Consider a scenario involving colored shapes with multiple colors (red, green, blue) and shapes (square, rectangle, circle). Using inheritance would require creating nine separate classes—one for each color-shape combination.
Example using composition:
class Color {
public:
virtual void apply() = 0;
};
class RedColor : public Color {
public:
void apply() override {
std::cout << "Applying red color" << std::endl;
}
};
class Shape {
public:
explicit Shape(Color* colorPtr) : color(colorPtr) {}
virtual void render() = 0;
protected:
Color* color;
};
class CircleShape : public Shape {
public:
explicit CircleShape(Color* colorPtr) : Shape(colorPtr) {}
void render() override {
std::cout << "Rendering circle with ";
color->apply();
}
};
// Usage
Color* primaryColor = new RedColor();
Shape* coloredCircle = new CircleShape(primaryColor);By using composition, only six classes are needed (three shapes + three colors), and any combinations can be created dynamically.
Relationship between CRP and LSP: These principles are complementary rather than conflicting. CRP emphasizes reusing existing classes through composition to achieve flexibility, while LSP ensures that derived classes maintain behavioral consistency with their base types. Together, they enable both code reuse and polymorphic correctness.
When to apply each:
Use CRP when aiming to reduce coupling, compose complex behaviors, or design replaceable components
Use LSP when Need to reuse code and enhance scalability, or maintain code stability through inheritance hierarchies