设计模式的原则

设计模式的原则是面向对象设计的指导思想,它们为我们提供了设计高质量、可维护、可扩展软件系统的基本准则。这些原则是设计模式背后的理论基础。

SOLID 原则

SOLID 是五个重要设计原则的首字母缩写,由 Robert C. Martin 提出。

1. 单一职责原则 (Single Responsibility Principle - SRP)

定义:一个类应该只有一个引起变化的原因。

解释

  • 每个类应该只负责一个特定的功能或职责
  • 避免创建”万能类”,这样的类难以维护和测试
  • 将不同的职责分离到不同的类中

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 违反SRP的示例
class Employee {
public:
void calculateSalary() { /* 计算工资 */ }
void saveToDatabase() { /* 保存到数据库 */ }
void generateReport() { /* 生成报告 */ }
};

// 遵循SRP的示例
class Employee {
public:
void calculateSalary() { /* 计算工资 */ }
};

class EmployeeRepository {
public:
void save(Employee& employee) { /* 保存到数据库 */ }
};

class ReportGenerator {
public:
void generate(Employee& employee) { /* 生成报告 */ }
};

2. 开闭原则 (Open-Closed Principle - OCP)

定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

解释

  • 可以通过添加新代码来扩展功能,而不是修改现有代码
  • 使用抽象和多态来实现这一原则
  • 减少修改现有代码带来的风险

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 违反OCP的示例
class Shape {
// ...
};

class AreaCalculator {
public:
double calculateArea(Shape* shape) {
if (auto circle = dynamic_cast<Circle*>(shape)) {
return 3.14 * circle->radius * circle->radius;
} else if (auto rectangle = dynamic_cast<Rectangle*>(shape)) {
return rectangle->width * rectangle->height;
}
// 添加新形状需要修改此函数
}
};

// 遵循OCP的示例
class Shape {
public:
virtual double calculateArea() const = 0;
virtual ~Shape() {}
};

class Circle : public Shape {
public:
double calculateArea() const override {
return 3.14 * radius * radius;
}
private:
double radius;
};

class Rectangle : public Shape {
public:
double calculateArea() const override {
return width * height;
}
private:
double width, height;
};

// 添加新形状不需要修改AreaCalculator
class AreaCalculator {
public:
double calculateArea(const Shape& shape) {
return shape.calculateArea();
}
};

3. 里氏替换原则 (Liskov Substitution Principle - LSP)

定义:子类型必须能够替换它们的基类型。

解释

  • 派生类应该能够完全替代基类,而不影响程序的正确性
  • 子类不应该加强前置条件或削弱后置条件
  • 子类不应该改变基类的行为

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 违反LSP的示例
class Rectangle {
protected:
double width, height;
public:
virtual void setWidth(double w) { width = w; }
virtual void setHeight(double h) { height = h; }
double getArea() const { return width * height; }
};

class Square : public Rectangle {
public:
void setWidth(double w) override {
width = height = w; // 改变了行为
}
void setHeight(double h) override {
width = height = h; // 改变了行为
}
};

// 使用时会发现问题
void testRectangle(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(4);
assert(rect.getArea() == 20); // 对于Square会失败
}

// 遵循LSP的示例
class Shape {
public:
virtual double getArea() const = 0;
virtual ~Shape() {}
};

class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override { return width * height; }
private:
double width, height;
};

class Square : public Shape {
public:
Square(double side) : side(side) {}
double getArea() const override { return side * side; }
private:
double side;
};

4. 接口隔离原则 (Interface Segregation Principle - ISP)

定义:客户端不应该被迫依赖它们不使用的接口。

解释

  • 将庞大的接口拆分为更小、更具体的接口
  • 客户端只应该知道它们实际使用的方法
  • 避免”胖接口”和不需要的依赖

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 违反ISP的示例
class Worker {
public:
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
};

class Robot : public Worker {
public:
void work() override { /* 机器人工作 */ }
void eat() override { throw std::runtime_error("机器人不需要吃饭"); }
void sleep() override { throw std::runtime_error("机器人不需要睡觉"); }
};

// 遵循ISP的示例
class Workable {
public:
virtual void work() = 0;
};

class Eatable {
public:
virtual void eat() = 0;
};

class Sleepable {
public:
virtual void sleep() = 0;
};

class Human : public Workable, public Eatable, public Sleepable {
public:
void work() override { /* 人工作 */ }
void eat() override { /* 人吃饭 */ }
void sleep() override { /* 人睡觉 */ }
};

class Robot : public Workable {
public:
void work() override { /* 机器人工作 */ }
};

5. 依赖倒置原则 (Dependency Inversion Principle - DIP)

定义

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

解释

  • 减少模块间的耦合度
  • 提高代码的可测试性和灵活性
  • 通过依赖注入实现

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 违反DIP的示例
class MySQLDatabase {
public:
void saveData(const std::string& data) {
// 直接保存到MySQL数据库
}
};

class DataProcessor {
private:
MySQLDatabase database; // 直接依赖具体实现
public:
void processData(const std::string& data) {
// 处理数据
database.saveData(data);
}
};

// 遵循DIP的示例
class Database {
public:
virtual void saveData(const std::string& data) = 0;
virtual ~Database() {}
};

class MySQLDatabase : public Database {
public:
void saveData(const std::string& data) override {
// 保存到MySQL数据库
}
};

class PostgreSQLDatabase : public Database {
public:
void saveData(const std::string& data) override {
// 保存到PostgreSQL数据库
}
};

class DataProcessor {
private:
Database& database; // 依赖抽象
public:
DataProcessor(Database& db) : database(db) {} // 依赖注入

void processData(const std::string& data) {
// 处理数据
database.saveData(data);
}
};

其他重要原则

6. 迪米特法则 (Law of Demeter - LoD) 或最少知识原则

定义:一个对象应该对其他对象有最少的了解。

解释

  • 只与直接的朋友通信
  • 减少类之间的耦合
  • 避免链式调用:a.getB().getC().doSomething()

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 违反LoD的示例
class House {
public:
Kitchen& getKitchen() { return kitchen; }
private:
Kitchen kitchen;
};

class Person {
public:
void cook(House& house) {
house.getKitchen().getStove().turnOn(); // 链式调用,违反LoD
}
};

// 遵循LoD的示例
class House {
public:
void cookMeal() {
kitchen.prepareMeal();
}
private:
Kitchen kitchen;
};

class Person {
public:
void cook(House& house) {
house.cookMeal(); // 只与直接朋友通信
}
};

7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle - CARP)

定义:优先使用对象组合/聚合,而不是类继承。

解释

  • 组合比继承更灵活
  • 减少继承层次的深度
  • 提高代码的复用性和灵活性

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 使用继承(不推荐)
class Bird {
public:
virtual void fly() = 0;
};

class Sparrow : public Bird {
public:
void fly() override { /* 麻雀飞 */ }
};

class Penguin : public Bird {
public:
void fly() override { throw std::runtime_error("企鹅不会飞"); }
};

// 使用组合(推荐)
class Flyable {
public:
virtual void fly() = 0;
};

class FlyWithWings : public Flyable {
public:
void fly() override { /* 用翅膀飞 */ }
};

class NoFly : public Flyable {
public:
void fly() override { /* 不会飞 */ }
};

class Bird {
protected:
std::unique_ptr<Flyable> flyBehavior;
public:
virtual void fly() {
if (flyBehavior) flyBehavior->fly();
}
virtual ~Bird() {}
};

class Sparrow : public Bird {
public:
Sparrow() {
flyBehavior = std::make_unique<FlyWithWings>();
}
};

class Penguin : public Bird {
public:
Penguin() {
flyBehavior = std::make_unique<NoFly>();
}
};

原则之间的关系和应用

这些设计原则不是孤立的,它们相互关联、相互支持:

  1. SRP 是基础:单一职责是其他原则的基础
  2. OCP 是目标:开闭原则是我们追求的目标
  3. LSP 和 ISP 是规范:里氏替换和接口隔离是实现OCP的规范
  4. DIP 是手段:依赖倒置是实现其他原则的重要手段

实际应用建议

  1. 不要过度设计:原则是指导,不是教条。根据实际需求平衡
  2. 循序渐进:开始时可以简单实现,随着需求变化逐步重构
  3. 关注可读性:过于复杂的设计可能降低代码可读性
  4. 测试驱动:良好的测试可以帮助验证设计是否合理

总结

设计模式的原则为我们提供了创建高质量软件的指导思想。理解和应用这些原则可以帮助我们:

  • 编写更灵活、可维护的代码
  • 减少代码之间的耦合度
  • 提高代码的复用性和可测试性
  • 更好地应对需求变化

这些原则需要在实际项目中不断实践和体会,才能真正掌握其精髓。记住,原则是工具,而不是目的,最终目标是创建高质量的软件系统。