文章目录
设计模式的六大基本原则:
- 开闭原则
- 单一职责原则
- 里氏替换原则
- 接口隔离原则
- 依赖倒置原则
- 迪米特法则
1. 开闭原则
开闭原则(Open-Closed Principle, OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭,这意味着能够在不修改现有代码的情况下添加新的功能
剩下的五个原则都是开闭原则的具体形态,也就是说后面五个原则是指导设计的工具和方法,开闭原则才是精神领袖
下面的例子展示了一个ShapeService
类,该类负责计算不同形状的面积。目前,它只能处理矩形和圆形
假设现在需要添加一个新的形状,比如三角形,那么我们必须修改ShapeService
类,添加一个新的方法来处理三角形
每次添加新的形状,我们都需要修改ShapeService
类,违反了开闭原则
为了遵循开闭原则,我们可以使用接口或抽象类(关于接口和抽象类的区别,可以参考我的另一篇博文:Java中接口和抽象类的区别(语法层面的区别、设计理念层面的区别))
首先,定义一个 Shape
接口,接着让所有形状类实现 shape
接口
public interface Shape {
double calculateArea();
}
ShapeService
类可以简单地通过 Shape
接口来计算任何形状的面积,而不需要知道具体的形状类型
当需要添加新的形状时,我们只需要创建一个新的类实现 Shape
接口,而无需修改 ShapeService
类
2. 单一职责原则
单一职责原则(Single Responsibility Principle, SRP):一个类应该只负责一项职责,如果类承担的职责过多,就相当于将这些职责耦合在一起,当一个职责变化时,可能会影响其他职责的运作
单一职责原则符合 高内聚,低耦合
的思想
下面的例子展示了一个 UserService
类,这个类违反了单一职责原则,因为它同时处理了用户管理(添加用户)的职责和通知用户(发送邮件)的职责
如果发送邮件的逻辑需要更改(例如,更换邮件服务提供商),那么 UserService
类也需要更改,即使用户管理部分的逻辑并没有变化
下面是修改后的代码,我们将 UserService
拆分为两个类:UserService
和 EmailService
,每个类只负责一个职责
3. 里氏替换原则
里氏替换原则(Liskov Substitution Principle, LSP):子类对象能够替换父类对象,而且不会影响程序的正确性。也就是说,子类应该能够继承父类的方法和行为,并在此基础上添加新的行为,而不是改变父类原有的行为
里氏替换原则通俗来讲就是:
- 子类可以扩展父类的功能,但不能改变父类原有的功能
- 只要父类出现的地方子类就可以出现,而且将父类替换为子类不会出现任何错误
3.1 不符合里氏替换原则的设计
假设有一个 Number
类,它有一个 add
方法,然后有一个子类 PositiveNumber
,它对 add
方法引入了新的约束
3.2 符合里氏替换原则的设计
改正方法是将 PositiveNumber
的行为改为一个独立的方法,而不是重写 add
方法
现在,PositiveNumber
类不会违反里氏替换原则,因为它没有改变Number
类的方法行为
通过上述例子,我们可以看到里氏替换原则的核心在于确保子类可以在不改变程序预期行为的前提下替换父类
当子类引入新的约束或者改变父类的方法行为时,就可能违反这一原则
4. 接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP):将不同的功能定义在不同的接口中,避免其他类在依赖该接口时依赖其不需要的方法。这意味着接口应该被细分为更小的、更具体的接口,以减少接口之间的依赖冗余性和复杂性
接口隔离原则可以确保每个类只实现它真正需要的方法,不仅可以减少代码中的冗余,也使得系统更加模块化和易于维护
假设你正在开发一个智能家居系统,其中包括各种设备,如灯、空调等
这些设备都有自己的控制功能,例如开关、调节亮度等。我们将通过接口隔离原则(ISP)来设计这些设备的接口
4.1 不符合接口隔离原则的设计
首先,我们创建一个包含了多种功能的大接口 SmartDevice
在这个设计中,LightBulb
类实现了 setTemperature
, open
, 和 close
方法,尽管这些方法对它来说是不必要的
4.2 符合接口隔离原则的设计
接下来,我们将接口细化为更具体的接口
在这个设计中,LightBulb
类实现了它所需要的 on
, off
, 和 setBrightness
方法,而 AirConditioner
类实现了 on
, off
, 和 setTemperature
方法,Curtain
类实现了 on
, off
, open
, 和 close
方法
5. 依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象
简单来说,就是鼓励我们通过接口或抽象类来进行编程,而不是直接依赖于具体的实现类
依赖倒置原则两个方面:
- 高层模块不应该依赖于低层模块:高层模块(比如业务逻辑层)应该依赖于抽象(如接口或抽象类),而不是低层模块的具体实现
- 抽象不应该依赖于细节:抽象应该是稳定的,不应该随着底层实现的变化而变化。底层实现应该依赖于抽象,这样当底层实现改变时,不会影响到高层模块
为了更好地理解依赖倒置原则,让我们看一个简单的例子。假设我们需要设计一个发送通知的系统,该系统可以通过不同的渠道(如电子邮件、短信等)发送消息
5.1 不符合依赖倒置原则的设计
首先,我们看看一个不符合依赖倒置原则的设计:
在这个设计中,NotificationService
直接依赖于具体的实现类 EmailNotification
和 SmsNotification
,如果未来需要添加新的通知渠道,就需要修改 NotificationService
类
5.2 符合依赖倒置原则的设计
接下来,我们看看如何使用依赖倒置原则来改进这个设计:
在这个设计中,我们定义了一个 NotificationChannel
接口,所有的通知渠道类都实现了这个接口
NotificationService
类不再直接依赖于具体的实现类,而是依赖于抽象接口 NotificationChannel
6. 迪米特法则
迪米特法则(Demeter Principle):一个对象应当对其他对象有尽可能少的了解,只与直接的朋友通信,不与陌生人说话。"直接的朋友"指的是出现在成员变量、成员方法的参数和返回值中的类,"陌生人"则是指那些仅出现在方法体内部的类
假设我们正在为一个在线书店编写代码。我们有两个类:Customer
(顾客)和Book
(书籍)
在没有应用迪米特法则的情况下,Customer
类可能会直接调用 Book
类的方法来获取书籍的价格并计算总价
但根据迪米特法则,Customer
应该只与直接的朋友通信,所以我们可以引入一个新的类 ShoppingCart
(购物车),由它来处理与 Book
的交互
6.1 不遵循迪米特法则的设计
在这个例子中,Customer
类直接与 Book
类通信,违反了迪米特法则
6.2 遵循迪米特法则的代码示例
在这个改进的例子中,Customer
类不再直接与 Book
类通信,而是通过 ShoppingCart
类
Customer
只与它直接的朋友 ShoppingCart
通信,而不直接与 Book
通信,遵循了迪米特法则
这样的设计使得 Customer
类与 Book
类之间的耦合度降低,代码更加模块化和易于维护