目录
单一职责原则(Chapter3)
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。单一职责原则(Single Responsibility Principle, SRP):
- 一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。简单说,一个类应该是一组高度相关的函数、数据的封装;也就是高内聚。
职责过多的坏处
单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
下面通过一个简单实例来进一步分析单一职责原则:
Sunny软件公司开发人员针对某CRM(Customer Relationship Management,客户关系管理)系统中客户信息图形统计模块提出了如图1所示初始设计方案:
![]()
图1 初始设计方案结构图 在图1中,CustomerDataChart类中的方法说明如下:
- getConnection()方法用于连接数据库,
- findCustomers()用于查询所有的客户信息,
- createChart()用于创建图表,
- displayChart()用于显示图表。
现使用单一职责原则对其进行重构。
在图1中,CustomerDataChart类承担了太多的职责,既包含与数据库相关的方法,又包含与图表生成和显示相关的方法。如果在其他类中也需要连接数据库或者使用findCustomers()方法查询客户信息,则难以实现代码的重用。无论是修改数据库连接方式还是修改图表显示方式都需要修改该类,它不止一个引起它变化的原因,违背了单一职责原则。因此需要对该类进行拆分,使其满足单一职责原则,类CustomerDataChart可拆分为如下三个类:
- DBUtil:负责连接数据库,包含数据库连接方法getConnection();
- CustomerDAO:负责操作数据库中的Customer表,包含对Customer表的增删改查等方法,如findCustomers();
- CustomerDataChart:负责图表的生成和显示,包含方法createChart()和displayChart()。
使用单一职责原则重构后的结构如图2所示:
![]()
图2 重构后的结构图
在软件设计真正要做的许多内容, 就是发现职责并把那些职责相互分离。其实要去判断是否应该分离出类来, 也不难, 那就是如果你能够想到多于一个的动机去改变一个类, 那么这个类就具有多于一个的职责, 就应该考虑类的职责分离。不过如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。
单一职责原则的优点:
- 可以降低类的复杂度,一个类只负责一个职责,其逻辑肯定要比负责多个职责简单的多。
- 提高类的可读性,提高系统的可维护性。
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵循的好,当修改一个功能时,可以显著降低对其他功能的影响。
开放封闭原则(Chapter4)
开闭原则(Open-Closed Principle, OCP)是面向对象中的软件实体(类,函数,模块等)可复用设计的第一块基石,它是最重要的面向对象设计原则之一。其定义如下:
- 一个软件实体(类,函数,模块等)应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
Sunny软件公司开发的CRM系统可以显示各种类型的图表,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图1所示:

void display(string type)
{
if (type.equals("pie"))
{
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar"))
{
BarChart chart = new BarChart();
chart.display();
}
}
在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
现对该系统进行重构,使之符合开闭原则。
在本实例中,由于在ChartDisplay类的display()方法中针对每一个图表类编程,因此增加新的图表类不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之增加新的图表类时无须修改源代码,满足开闭原则。具体做法如下:
- 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
- ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。
重构后结构如图2所示:

在图2中,我们引入了抽象图表类AbstractChart,且 ChartDisplay 针对抽象图表类进行编程,并通过setChart()方法由客户端来设置实例化的具体图表对象,在ChartDisplay的display()方法中调用chart对象的display()方法显示图表。
如果需要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端向ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。
何时应对变化
无论模块是多么的 ' 封闭 ', 都会存在一些无法对之封闭的变化。既然不可能完全封闭, 设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类, 然后构造抽象来隔离那些变化。
在编写代码时,假设变化不会发生,或者猜测可能发生的变化种类,当然猜测很难,因此当发生小变化时,想办法应对更大的变化,有变化立刻采取行动,创建抽象隔离以后发生的同类变化,达到可维护,可扩展,可复用的目的。
开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都可以的进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
简单地说,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。“应该尽量”4个字说明OOP原则并不是说绝对不可以修改原始类的,当代码需要需要重构的时候要及时重构,使代码恢复正常,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。
开发过程中都没有那么理想的状况,因此,凡事也是需要结合具体情况再做决定,目的是更稳定、更灵活同时保有原有的正确性。
里氏代换原则(Chapter5)
里氏代换原则(Liskov Substitution Principle, LSP):
- 简单地说就是:所有引用基类的地方必须能透明地使用其子类的对象。只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下含义:
- 子类可以实现父类的抽象方法,但不能隐藏( 在C++中)父类的非抽象方法。
- 子类中可以增加自己特有的方法。
举例子:
在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始设计方案如图1所示:
![]()
图1原始结构图 在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。
在本实例中,可以考虑增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender 是针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。重构后的结构如图2所示:
![]()
图2 重构后的结构图 里氏代换原则是实现开闭原则的重要方式之一。在本实例中,EmailSender 中的 send()方法的参数类型为Customer,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类。
里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,继承的优缺点都相当明确
优点:
- 代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性
- 子类与父类基本相似,但又与父类有所区别
- 提高代码的可扩展性
缺点:
- 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法
- 可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法
事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。
依赖倒转原则(Chapter5)
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。
依赖倒转原则(Dependency Inversion Principle, DIP) 的定义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
换言之,要针对接口编程,而不是针对实现编程。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定的多。抽象(高层模块)指的是接口或者抽象类,细节(低层模块)就是具体的实现类。使用接口或者抽象类的目的就是制订好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。依赖倒置原则的核心就是要面向接口编程。
(1)低层模块尽量都要有抽象类或接口,或者两者都有。
(2)变量的声明类型尽量都是抽象类或者接口。
(3)使用继承时遵循里氏替换原则
依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,通过接口或抽象类产生依赖关系。什么是依赖关系呢?其实就是相互之间的调用关系。概括来说就是面向接口编程。
其实依赖倒置原则主要目的就是解耦
这张图来表达出来就是 ImageLoader
和 MemonyCache
等并没有直接关系,甚至ImageLoader
只需要实现ImageCache
类或继承其他已有的ImageCache
子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader
即可实现缓存功能的替换。这是依赖倒置原则的体现。
迪米特原则((Chapter11)
迪米特原则也称为最少知识原则(Least Knowledge Principle)定义:
- 一个软件实体应当尽可能少地与其他实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松耦合关系。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低
下面通过一个简单实例来加深对迪米特法则的理解:
Sunny软件公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为 如图1所示结构:
![]()
图1 初始设计方案结构图 在图1中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。
现使用迪米特对其进行重构。
在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图2所示:
![]()
图2 重构后的结构图