2.2 单一职责原则
2.2.1 引言
一个优良的系统设计,强调模块间保持低耦合、高内聚的关系,在面向对象设计中这条规则同样适用,所以面向对象的第一个设计原则就是:单一职责原则(SRP,Single Responsibility Principle)。
单一职责,强调的是职责的分离,在某种程度上对职责的理解,构成了不同类之间耦合关系的设计关键,因此单一职责原则或多或少成为设计过程中一个必须考虑的基础性原则。
2.2.2 引经据典
关于单一职责原则,其核心的思想是:
一个类,最好只做一件事,只有一个引起它变化的原因。
单一职责原则可以看作是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一职责,通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
因此,SRP原则的核心就是要求对类的改变只能是一个,对于违反这一原则的类应该进行重构,例如以Façade模式或Proxy模式分离职责,通过基本的方法Extract Interface、Extract Class和Extract Method进行梳理。
2.2.3 应用反思
我们以一个常见的数据库管理系统为例来进行说明,通常情况下根据不同的权限进行数据删改查的系统比比皆是,然而正是这样一个比较的常见的需求场景,却存在着很多充满臭味的设计。一个违背SRP原则的设计看起来如图2-1所示。
图2-1 违背SRP的设计
在以下设计中,DBManager类对数据库的操作和用户权限的判别封装在一个类中实现,一添加记录为例,其实现逻辑可以表示为:
{
if (GetPermission(m_id) == "CanAdd")
{
Console.WriteLine("管理员可以增加数据。");
}
}
这显然是一个充满了僵化味道的实现,如果权限设置的规则发生改变,那就必须重新修改所有的数据库操作逻辑,在成千上万代码中搜索有可能带来隐患的代码,你必须选择将这种代码扫地出门。
在此,权限判断的职责和数据库操作的职责被无理的实现在一个类中,权限的规则变化和数据库操作的规则变化,都有可能引起DBManager修改当前代码。按照单一职责原则,一个类应该只有一个引起它改变的原因。所以我们选择以合适的方式来重构有缺陷的设计,在此显然可以通过实现一个Proxy模式来解决职责交叉的窘境,修改之后的设计思路如图2-2所示。
图2-2 职责分离的设计
以Proxy模式调整之后,有效实现了职责的分离,DBManager类只关注数据操作逻辑,而不用关心权限判断逻辑,在此仅以Add操作为例来说明:
{
private string m_id = string.Empty;
public DBManager(string id)
{
m_id = id;
}
IDBAction Members
}
而将权限的判断交给DBManagerProxy代理类来完成,例如:
{
private IDBAction dbManager;
public DBManagerProxy(IDBAction dbAction)
{
dbManager = dbAction;
}
public string GetPermission(string id)
{
return string.Empty;
}
IDBAction Members
}
通过代理,将数据操作和权限判断两个职责分离,而实际的数据操作由DBManager来执行,此时客户端的调用变得非常简单:
{
static void Main(string[] args)
{
IDBAction dbManager = new DBManagerProxy(new DBManager("CanAdd"));
dbManager.Add();
}
}
在该例中,接口IDBAction其实实现了设计模式的另一个重要原则:依赖倒置,对此我们在后文有详细论述。在本例中,通过DBManagerProxy代理类实现了职责分离,DBManager类将仅有一个变化的原因,那就是数据操作的需求变更;而权限的变更和修改不对DBManager造成任何影响,体现了单一职责原则的基本精神。
2.2.4 规则建议
关于单一职责原则,我们的建议是:
l 一个类只有一个引起它变化的原因,否则就应当考虑重构。
l SRP由引起变化的原因决定,而不由功能职责决定。虽然职责常常是引起变化的轴线,但是有时却未必,应该审时度势。
l 测试驱动开发,有助于实现合理分离功能的设计。
l 可以通过Façade模式或Proxy模式进行职责分离。