设计模式原则

设计模式的原则是理解和使用设计模式的基石。这些原则是面向对象设计的核心指导思想,设计模式本身就是这些原则在特定场景下的具体体现。

最重要的设计原则是 SOLID 原则,此外还有一些其他关键原则。

一、 SOLID 原则

SOLID 原则是五个最常用、最经典的设计原则,由 Robert C. Martin 提出。

1. 单一职责原则

  • 核心思想: 一个类应该只有一个引起变化的原因。
  • 详细解释:一个类只负责一项职责或功能。如果一個类承担的功能过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。
  • 比喻: 一家餐厅,厨师负责做饭,服务员负责点菜和上菜,清洁工负责打扫。如果他们职责混杂,效率就会低下。
  • 违反示例: 一个 UserService 类,既负责用户信息的增删改查,又负责将用户数据导出为PDF报表,还负责发送邮件通知。
  • 修正: 将 UserService 拆分为 UserService(用户业务逻辑)、UserReportGenerator(报表生成)和 EmailService(邮件服务)。

2. 开闭原则

  • 核心思想: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
  • 详细解释: 当需求发生变化时,我们应该通过添加新的代码来扩展功能,而不是修改已有的、已经工作正常的代码。这是最重要的原则,是很多设计模式的目标。
  • 比喻: 一台电脑,你可以扩展USB设备(鼠标、键盘、硬盘),而无需为了支持新设备而拆开主机修改主板。
  • 违反示例: 一个 AreaCalculator 类,有一个 calculate 方法,里面用 if-else 来判断是圆形还是矩形来计算面积。如果要增加三角形,就必须修改这个类的代码。
  • 修正: 定义一个 Shape 接口,包含 calculateArea 方法。让 Circle 和 Rectangle 类实现这个接口。AreaCalculator 只需遍历 Shape 列表调用其方法即可。要新增三角形,只需创建 Triangle 类实现 Shape,无需修改任何现有类。

3. 里氏替换原则

  • 核心思想: 所有引用基类的地方必须能透明地使用其子类的对象。
  • 详细解释: 子类必须能够完全替代它们的父类,而不产生任何错误或意外的行为。也就是说,子类只能在保持原有行为的基础上进行扩展,而不能覆盖或改变父类的核心行为。
  • 比喻: 你父亲会开车,你作为儿子继承了他,你也会开车(保持了父亲的行为),但你可能会开得更快或者还会修车(扩展),你绝不能把“开车”这个行为重定义为“游泳”。
  • 违反示例: Rectangle 类有 setWidth 和 setHeight 方法。Square 类继承 Rectangle,并重写了 setWidth 和 setHeight,使其同时设置宽和高。那么,一个接收 Rectangle 参数并调整其宽高的函数,如果传入一个 Square 对象,就会得到错误的结果。
  • 修正: 重新考虑继承关系,或者让 Rectangle 和 Square 都实现一个 Shape 接口,而不是使用继承。

4. 接口隔离原则

  • 核心思想: 客户端不应该被迫依赖于它不使用的接口。
  • 详细解释: 将一个庞大的、臃肿的接口拆分成更小、更具体的接口,使得客户端只需要知道它们感兴趣的方法。这样可以避免实现类被迫实现一些它们根本用不到的方法。
  • 比喻: 一台多功能打印机,有打印、复印、传真功能。如果做一个全能接口,老式打印机(只能打印)实现这个接口时,复印和传真方法只能空着或抛异常。不如拆分成 Printer、Scanner、Faxer 三个接口。
  • 违反示例: 一个 Animal 接口,有 eat(), fly(), swim() 方法。Dog 类实现它时,fly() 方法只能空实现。
  • 修正: 将 Animal 拆分为 Eatable, Flyable, Swimmable 等更精细的接口。Dog 实现 Eatable 和 Swimmable 即可。

5. 依赖倒置原则

  • 核心思想:
    • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
    • 抽象不应该依赖于细节,细节应该依赖于抽象。
  • 详细解释: 要面向接口编程,而不是面向实现编程。这减少了类间的耦合性,提高了系统的稳定性。
  • 比喻: 你想读书(高层),你不应该直接依赖一本具体的纸质书(低层),而应该依赖于“书本”这个抽象概念。这样,无论是纸质书、电子书还是有声书,你都可以“读”。
  • 违反示例: UserService 类内部直接 new 了一个 MySQLDatabase 对象。如果将来要换用 OracleDatabase,就必须修改 UserService 的代码。
  • 修正: 定义一个 Database 接口。UserService 的构造函数接收一个 Database 类型的参数。这样,UserService 依赖于抽象的 Database,而 MySQLDatabase 和 OracleDatabase 都实现这个接口。

其他重要原则

除了 SOLID,还有一些非常实用的原则。

6. 合成复用原则

  • 核心思想: 尽量使用对象组合,而不是继承来达到复用的目的。
  • 详细解释: 继承破坏了封装性,子类与父类耦合度高。而组合只要求对象具有良好定义的接口,耦合度低,更灵活。
  • 例子: 汽车类 Car 需要引擎功能。不应该让 Car 继承 Engine,而应该在 Car 类中持有一个 Engine 对象的引用。这样可以轻松更换不同的引擎。

7. 迪米特法则

  • 核心思想: 一个对象应该对其他对象保持最少的了解。只与直接的朋友通信。

  • 详细解释: 降低类之间的耦合度。一个类不应该知道它不需要知道的细节。在方法中,只调用以下对象的方:

    1. 当前对象自身的成员
    2. 方法参数传入的对象
    3. 方法内部创建的对象
    4. 当前对象的成员对象
  • 违反示例: 你在办公室,想让同事打印一份文件。你不应该自己去操作他的打印机(colleague.getPrinter().print(doc)),你只需要告诉他“请打印这个”(colleague.print(doc))。

8. 不要重复你自己

  • 核心思想: 减少代码中的重复部分。
  • 详细解释: 重复的代码意味着当需要修改时,要在多个地方进行同样的修改,容易出错且难以维护。通过提取公共部分到方法、类或抽象层,可以消除重复。

9. 关注点分离

  • 核心思想: 将程序分解成不同的、功能重叠尽可能少的部分。
  • 详细解释: 这是一个更广泛的概念。比如在Web开发中,我们通常将应用分为表示层(UI)、业务逻辑层(Service)和数据访问层(DAO),每一层只关注自己的事情。

三、总结

这些原则共同的目标是:创建高内聚、低耦合、易于扩展、维护和复用的软件结构。

  • SOLID 是微观层面的类与接口设计指南。

  • 合成复用、迪米特法则 是对象协作关系的指南。

  • DRY、关注点分离 是更宏观的架构和代码组织指南。

在实际开发中,要深刻理解这些原则的精神,而不是教条地遵守。它们是你思考和设计时的“罗盘”,帮助你写出更优雅的代码。设计模式则是这些原则在特定问题上的“地图”,为你提供了具体的实现路径。