设计模式六大原则 - 实践
设计模式的六大原则,也称为SOLID这些原则的具体实现。就是原则,是面向对象设计中五个基本原则的统称。它们是构建可维护、可扩展、灵活软件的基础。很多设计模式都
第六个原则常被认为是迪米特法则,它是对前五个原则的关键补充。
下面我将逐一详细解释这六大原则。
1. 单一职责原则 (Single Responsibility Principle - SRP)
核心思想:一个类只应该有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
通俗理解:专注一件事,并把这件事做好。不要设计“万能类”。
为什么重要:
降低复杂性:一个类只负责一件事,其代码量自然更少,逻辑更清晰,更容易理解和维护。
提高可维护性:当需要修改某个功能时,我们只需要修改负责该功能的类,不会影响到其他不相关的功能。
降低变更风险:修改一个单一职责的类,对系统其他部分造成意想不到的副作用的风险更低。
示例:
违反SRP:一个
User类,既包含用户属性(如name,email),又包含将用户数据保存到数据库的方法saveToDatabase(),还包含打印用户报告的方法printReport()。遵循SRP:将
User类拆分为:User:纯数据模型,只包含属性和基本的访问方法。UserRepository:负责用户数据的持久化操作(如save,findById)。UserReportPrinter:负责打印用户相关的报告。
2. 开闭原则 (Open/Closed Principle - OCP)
核心思想:软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。
通俗理解修改已有的、已经工作正常的代码。就是:当需要添加新功能时,应该通过添加新的代码来建立,而不
为什么重要:
稳定性:已有的、经过测试的代码不会被修改,从而保证了系统的稳定性。
可扩展性:通过继承、组合、多态等方式,可能轻松地扩展平台的行为。
如何实现:通常通过抽象化和多态来实现。定义稳定的抽象接口(对修改关闭),具体的实现细节则可能借助创建新的建立类来改变和扩展(对扩展开放)。
示例:
有一个
Shape类和一个计算总面积的方法calculateTotalArea(Shape[] shapes)。违反OCP:如果添加一个新的图形(如三角形),就需要修改
calculateTotalArea方法,在里面添加if (shape instanceof Triangle)的判断逻辑。遵循OCP:定义一个抽象类或接口
Shape,其中有一个抽象方法calculateArea()。让Circle,Rectangle,Triangle等都实现这个接口。calculateTotalArea方法只需遍历数组并调用每个元素的calculateArea()方法即可,无需知道具体的图形类型。添加新图形时,只需创建新类实现Shape接口,而无需修改calculateTotalArea方法。
3. 里氏替换原则 (Liskov Substitution Principle - LSP)
核心思想:所有引用基类(父类)的地方必须能透明地采用其子类的对象,而程序的行为不会发生变化。
通俗理解:子类可能扩展父类的功能,但不能改变父类原有的功能。子类必须完全实现父类的方式,并且行为要与父类的预期保持一致。
为什么重要:
它是对继承关系的约束,确保继承被正确使用。
保证了多态性的正确性。如果子类行为与父类不一致,那么多态替换时就会出现意想不到的错误。
示例:
违反LSP:
Rectangle类有setWidth和setHeight方法。你创建了一个子类Square(正方形),重写了 setter 方法,使得设置width时自动将height设为相同值,反之亦然。这看起来合理,但从行为上看,Square已经改变了Rectangle的行为。如果一个函数期望Rectangle的width和height可以独立设置,传入Square就会导致错误。遵循LSP:
Square不应继承Rectangle。它们可以有共同的父类Shape,但不应有直接的继承关系,因为它们在行为上并不一致。
4. 接口隔离原则 (Interface Separation Principle - ISP)
核心思想:客户端不应该被迫依赖于它不启用的接口。一个类对另一个类的依赖应该建立在最小的接口上。
通俗理解:不要制造“臃肿”的大接口,应该根据客户端的需求,将大接口拆分成更小、更具体的接口。
为什么重要:
避免实现类被迫实现一些它们根本用不到的方法(通常只能空实现或抛出异常)。
减少接口之间的耦合,提高系统的灵活性。
示例:
违反ISP:一个巨大的
Animal接口,包含了eat(),fly(),swim()等方法。那么Bird类需要实现swim()(可能空实现),Fish类需要实现fly()(空实现),Dog类需要实现fly()和swim()(可能空实现)。遵循ISP:将
Animal拆分为Eater,Flyer,Swimmer等多个精细的接口。Bird类实现Eater和Flyer,Fish类实现Eater和Swimmer,Dog类可以实现Eater和Swimmer。这样每个类都只依赖于它需要的方法。
5. 依赖倒置原则 (Dependence Inversion Principle - DIP)
核心思想:
高层模块不应该依赖低层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
通俗理解:要面向接口编程,而不是面向实现编程。依据抽象(接口或抽象类)使各个类或模块彼此独立,互不依赖。
为什么重要:
降低耦合:高层和低层模块都依赖于抽象,从而解耦。
提高可测试性:很容易通过 Mock 实现(依赖注入)来进行单元测试。
提高灵活性:更换低层模块(例如从 MySQL 数据库换到 Oracle 数据库)不会影响高层模块。
示例:
违反DIP:
Book类直接依赖于具体的MySQLDatabase类,在其方法中new MySQLDatabase().save(data)。遵循DIP:
定义一个抽象接口
Database,包含save方法。MySQLDatabase和OracleDatabase都实现Database接口。Book类只依赖于Database接口。具体的MySQLDatabase或OracleDatabase实例通过构造函数或Setter方法(这就是依赖注入)传递给Book类。
6. 迪米特法则 (Law of Demeter - LoD) / 最少知识原则
核心思想:一个对象应该对其他对象有最少的了解。只与你的直接朋友通信,不和陌生人说话。
通俗理解:一个类应该只和以下“朋友”交流:
当前对象本身 (
this)以参数形式传入到当前对象方式中的对象
当前对象的成员对象
要是成员对象是一个集合,那么集合中的元素也是朋友
当前对象所创建的对象
不要出现类似 a.getB().getC().doSomething() 这样的“链式”调用,这意味着当前对象对 a、b、c 都有了解,耦合度太高。
为什么重要:
降低类之间的耦合,提高模块的独立性。
使得系统更具可维护性和可扩展性。
示例:
违反LoD:
Customer类中有一个Wallet wallet属性。在商店Shop类中,有方法直接调用customer.wallet.getMoney()来获取顾客钱包里的钱。这意味着Shop类需要了解Customer的内部结构(它有一个Wallet)以及Wallet的内部方法(getMoney)。遵循LoD:在
Customer类中提供一个方法,如public float getPayment(float amount)。Shop类只调用customer.getPayment(100)。至于顾客是从钱包、手机还是银行卡付钱,Shop类完全不需要知道。这样就切断了Shop和Wallet之间的直接联系。
总结
| 原则 | 英文 | 核心思想 | 关键词 |
|---|---|---|---|
| 单一职责原则 | SRP | 一个类只干一件事 | 职责单一 |
| 开闭原则 | OCP | 对扩展开放,对修改关闭 | 抽象、多态 |
| 里氏替换原则 | LSP | 子类必须能替换父类 | 继承、行为一致 |
| 接口隔离原则 | ISP | 接口要小而专,不要大而全 | 精细接口 |
| 依赖倒置原则 | DIP | 面向接口编程,而非实现 | 抽象、依赖注入 |
| 迪米特法则 | LoD | 只和直接的朋友说话 | 减少耦合、最少知识 |
这六大原则是编写高质量代码的指导思想,理解和运用它们能极大地提升你的软件设计能力。
