这篇文章主要概括性地讲一下设计模式和它的一些原则。目的是让读者能对设计模式有一个整体上的了解与感受,为深入学习设计模式打下基础。
从“模式”讲起
“模式”一词最早诞生于建筑领域,在Christopher Alexander教授的一书中,他写道“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。后来“模式”这个概念被用于软件开发中,尤其是面向对象编程中。在面向对象设计模式中,核心的思想依然是这样。
设计模式(design pattern)是软件开发中广泛使用的解决方案,用于解决在日常工作中遇到的常见问题。它们是对重复的设计问题的可重复使用的解决方案,它们提供了一种封装和组织代码的方法,使代码更易于维护,理解和扩展。
设计模式的基本要素
一般而言,一个模式有四个基本要素(维基百科上有更多的要素,这里讲最主要的四个):
模式名称与分类(Pattern Name and classification):模式的名称与分类使我们易于表达与交流某一种设计。也让模式便于记忆。
问题(Problem):该模式是为了解决面向对象设计中所遇到的哪类问题或情况。
解决方案(Solution):模式的具体内容。主要是设计结构(structure),设计包含哪些类与对象(participants)、这些成分在模式中是如何使用和交互的(collaboration)。
效果(Consequences):使用该模式所带来的结果(results),以及所带来的副作用(side effects)和使用该模式的一些取舍(trade offs)
设计模式的原则
设计模式基于七个核心原则,包括:单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特法则、组合/聚合复用原则。其中前五个原则一般统称为SOLID原则。
(图片来源网络)
下面就来详细介绍这些原则。
1. 单一职责原则(Single Responsibility Principle, SRP)
(图片来源网络)
单一职责原则是指一个类只负责一个单独的功能领域中的相关职责,避免一个类承担太多的职责。
假设我们要开发一个用于管理员工信息的系统,每个员工都有自己的信息,例如名字、工号、部门等。如果把所有的信息都写在一个类中,就违反了单一职责原则。
下面是一种符合单一职责原则的设计方法:
- 定义一个
Employee
类,存储员工的基本信息。 - 定义一个
Department
类,存储员工的部门信息。 - 定义一个
EmployeeManager
类,负责管理员工的信息。
这种设计方法把每个功能分配到单独的类中,每个类只负责一个单独的职责,符合单一职责原则。
2. 开放-封闭原则(Open-Closed Principle, OCP)
(图片来源网络)
开闭原则要求软件实体(如类、模块、函数等)要对扩展开放,对修改封闭。也就是说,当需要新的功能时,可以通过扩展现有的代码来实现,而不是修改已有的代码。这样做的好处是:
可以保证原有代码的稳定性,因为没有修改已有代码;
可以节省维护代码的成本,因为不需要修改原有代码;
可以使代码更容易扩展,因为扩展新功能不需要修改已有代码。
举个例子:
一个电商网站的购物车系统,假如现在网站支持普通用户和会员用户,普通用户可以享受折扣,而会员用户还可以享受免运费的优惠。如果按照开放封闭原则,我们可以在购物车系统的基础上新建两个子类:普通用户购物车和会员用户购物车。在普通用户购物车中实现折扣计算,在会员用户购物车中实现折扣和免运费计算。这样,当需要增加或修改优惠政策时,只需要修改对应的购物车子类,而不需要修改购物车系统的核心代码,从而避免了对原有代码的影响。
3. 里氏替换原则(Liskov Substitution Principle, LSP)
(图片来源网络)
里氏替换原则是面向对象编程中的一个重要原则,它提出了一个继承关系的限制:子类对象(对象实例)能够替换其父类对象(对象实例),而不会对程序的正确性造成影响。
举个例子,如果有一个抽象类 Animal
,它有一个子类 Dog
,那么当在程序中使用Animal
类型的引用指向 Dog
类型的对象时,程序的正确性不会受到影响。也就是说,对于任何依赖于 Animal
类型的代码,通过使用 Dog
类型的对象都不会导致错误。
里氏替换原则强调了继承关系的一致性和稳定性,保证了在程序中使用父类型的引用来引用子类型的对象时,程序的行为是可预期的,不会因为替换了对象类型而产生意外的行为。
4. 接口隔离原则(Interface Segregation Principle, ISP)
(图片来源网络)
该原则提出了一个接口的设计原则:客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。
换句话说,一个接口中不应该包含客户端不需要的操作,而应该把接口划分成多个独立的、明确的接口,以更好地隔离客户端与实现的耦合。
接口隔离原则的目的是为了避免过多的依赖和冗余的接口,从而使得系统更加稳定、易于维护和扩展。
举个例子,假如有一个动物园的系统,它有一个动物接口,里面定义了吃东西、睡觉、钓鱼等操作。但是有些动物只会吃东西和睡觉,而不会钓鱼,那么在这种情况下,我们可以把钓鱼操作从动物接口中分离出来,创建一个新的接口,以更好地满足接口隔离原则。
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
(图片来源网络)
该原则规定了模块间的依赖方向,高层模块不应该依赖低层模块,两者都应该依赖其抽象,这样可以使得代码结构更加灵活。主要思想是通过抽象接口和抽象类来隔离高层代码和低层代码,使得高层代码对低层代码的依赖性降低,提高代码的灵活性和可维护性。
例如,在一个电商系统中,有一个高层模块,它负责管理订单;有一个低层模块,它负责对接支付系统。如果高层模块直接依赖于低层模块,那么如果支付系统的实现发生了变化,例如从支付宝换成了微信支付,那么高层模块就需要进行修改。为了避免这种情况,我们可以通过依赖倒置原则的思想,在高层模块和低层模块之间抽象出一个接口,让高层模块和低层模块都依赖于这个接口。这样,当支付系统的实现发生变化时,只需要修改低层模块的实现类即可,高层模块不需要进行任何修改。
6. 迪米特法则(Law of Demeter 也称最少知识原则, LoD)
(图片来源网络)
该原则认为,一个对象应该尽可能少地了解其他对象的内部细节,并且只与直接相邻的对象进行交互。
具体来说,迪米特法则要求我们将对象之间的交互限制在最小范围内。一个对象(A)不应该直接与没有耦合关系的对象(C)交互,而是应该通过与A有耦合关系的对象(B)间接地访问C。这样做可以减少对象之间的依赖关系,从而提高系统的可维护性和灵活性。
举个例子:假设你和你的朋友参加了一个派对,如果你需要与派对上的其他人进行交互(例如询问活动时间或位置),你应该尽可能地通过你的朋友来实现(你的朋友与你不认识的人可能是朋友)。这样做可以减少你与其他人的直接交互,降低你们之间的耦合度,从而提高整个派对的效率和协作性。
7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP or CRP)
这个原则倡导尽量使用组合/聚合关系来复用对象,而不是使用继承关系来复用行为。
继承在一定程度上会破坏封装,一旦父类实现发生改变,子类就会受到影响。相比之下,组合/聚合则具有更高的灵活性,更好的保证了封装。使得代码的维护和扩展都更加容易。
举个例子:
有Human
, Monkey
两个类都继承了Mammal
类(哺乳动物)。如果Mammal
类中的某个函数被修改了,那么Human
, Monkey
类可能就会收到影响,也需要修改,就会很麻烦。
如果换一种实现形式:设置一个基类或interfaceBioClass
。 Mammal
类继承(实现)BioClass
类,Human
类和Monkey
类只需要包含一个BioClass
类型的引用,这个引用实际指向Mammal
类。这样就是用聚合的方式实现,如果Mammal
类被修改,Human
, Monkey
类则无需修改。
原则重要性
上面的原则有的很重要,有的不那么重要,如果要排序,可以有如下的排序方式:
OCP(开放-封闭), LSP(里氏替换), DIP(依赖倒置) > SRP(单一职责), CARP(合成/聚合复用) > LoD(迪米特) > ISP(接口隔离)
参考来源:
[1]《设计模式:可复用面向对象软件的基础》
[2] 维基百科:设计模式