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替换.
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任意 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需要对代码进行重用、增强可扩展性, or maintain code stability through inheritance hierarchies