设计模式六大原则 - 实践
设计模式的六大原则,也称为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 | 只和直接的朋友说话 | 减少耦合、最少知识 |
这六大原则是编写高质量代码的指导思想,理解和运用它们能极大地提升你的软件设计能力。