三、单一职责原则(SRP--Single-Responsibility Principle )
1. 定义
a). 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
b). 就一个类而言,应该仅有一个引起它变化的原因。
所谓职责,我们可以理解他为功能,
就是设计的这个类功能应该只有一个,而不是两个或更多。
也可以理解为引用变化的原因,当你发现有两个变化的因素会要求我们修改这个类,那么你就要考虑拆分这个类了。
因为职责是变化的一个轴线,当需求变化时,该变化会反映类的职责的变化。
“就像一个人身兼数职,而这些事情相互关联不大,
甚至有冲突,那他就无法很好的解决这些职责,应该分到不同的人身上去做才对。”
2. 分析
a). 一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,
而且, 如果一个类承担的职责过多,就相当于将这些职责耦合在一起,
当其中一个职责变化时,可能会影响其他职责的运作。
b). 类的职责主要包括两个方面:
数据职责 : 通过其属性来体现
行为职责 : 通过其方法来体现。
c). 单一职责原则是实现高内聚、低耦合的指导方针,
在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,
需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
3. 实例一
某基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现:
单一职责原则-图-1
单一职责原则-图-2
图一和图二有什么区别哪?
图(一)功能太过于集中,强耦合,严重违反类的单一原则。
图(二)中,
LoginForm类关联了UserDAO类,而UserDAO类再关联了数据库的连接类DBUtil;
这样,UserDAO和DBUtil类都是集中而单一的;
4. 代码实例
modem接口明显具有两个职责:连接管理和数据通讯;
class Modem{
/* 连接管理 */
public:
void dial(string pno);
void hangup();
/* 数据通信 */
public:
void send(char c);
void recv();
}
如果应用程序变化, 需要增加另一种连接方式 ,这影响连接函数,那么就需要重构:
class DataChannel{
public:
void send(char c);
void recv();
}
class Connection{
public:
void dial(string pno);
void hangup();
}
5. 总结
就一个类而言,它只能完整地干完一件事就好。
SRP优点:
消除耦合,减小因需求变化引起代码僵化性臭味
使用SRP注意点:
1)、一个合理的类,应该仅有一个引起它变化的原因,即单一职责;
2)、在没有变化征兆的情况下应用SRP或其他原则是不明智的;
3)、在需求实际发生变化时就应该应用SRP等原则来重构代码;
4)、使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;
5)、如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用【Facade】或【Proxy模式】对代码重构;
四、合成复用原则
1.定义
尽量使用对象组合、聚合、关联,而不是继承来达到复用的目的。
2.分析
1). 合成复用原则:
是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;
新对象通过委派调用已有对象的方法达到复用其已有功能的目的。
简言之:要尽量使用关联、组合、聚合关系,少用继承。
2). 在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
a) 继承复用:
实现简单,易于扩展。破坏系统的封装性;
从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;
只能在有限的环境中使用。(“白箱”复用 )
b) 组合/聚合复用:
耦合度相对较低,选择性地调用成员对象的操作;
可以在运行时动态进行。(“黑箱”复用 )
3). 组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,
因此一般首选使用组合/聚合来实现复用;其次才考虑继承;
在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,
而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
3. 实例
某教学管理系统部分数据库访问类设计如图所示:
合成复用原则-图-1
合成复用原则-图-2
图(一)和图(二)分析:
图(一)中:
StudentDAO类和TeacherDAO类继承了DBUtil类,其中,DBUtil类负责和数据库进行连接与交互。
a) 如果需要更换数据库连接方式,
如,原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改DBUtil类源代码。
b) 如果StudentDAO采用JDBC连接,但是TeacherDAO采用连接池连接,
则需要增加一个新的DBUtil类,支持新的连接方式。
并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,
这样的修复违背开闭原则,系统扩展性较差。
图(二)中:
DBUtil类继承了新类NewDBUtil类,以实现多种数据库的连接。这里使用了里氏代换(子代父)原则
而在StudentDAO类,TeacherDAO类中,定义了DBUtil成员变量及setDBOperator()函数,实现了它们之间的聚合关系,
4. 总结
类中应尽量使用对象组合/聚合,而不是用继承来达到复用的目的。