设计模式之禅 单一职责原则
我是“牛”类,我可以担任多职吗
- 单一职责原则的英文名称是 Single Responsibility Principle,简称是 SRP。
这个设计原则备受争议,只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。如果你是老大,看到一个接口或类是这样或那样设计的,你就问一句:“你设计的类符合 SRP 原则吗?”保准对方立马“萎缩”掉,而且还一脸崇拜地看着你,心想:“老大确实英明”。这个原则存在争议之处在哪里呢?就是对职责的定义,什么是类的职责,以及怎么划分类的职责。我们先举个例子来说明什么是单一职责原则。
- 只要做过项目,肯定要接触到用户、机构、角色管理这些模块,
基本上使用的都是 RBAC 模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。我们这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中,都是用户管理类嘛,我们先来看它的类图,如图所示:
- 太 Easy 的类图了,我相信,即使是一个初级的程序员也可以看出这个接口设计得有问题,用户的属性和用户的行为没有分开,这是一个严重的错误!
这个接口确实设计得一团糟,应该把用户的信息抽取成一个 BO(Business Object,业务对象),把行为抽取成一个 Biz(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图所示:
重新拆封成两个接口,IUserBO 负责用户的属性,简单地说,IUserBO 的职责就是收集和反馈用户的属性信息;IUserBiz 负责用户的行为,完成用户信息的维护和变更。
各位可能要说了,这个与我实际工作中用到的 User 类还是有差别的呀!别着急,我们先来看一看分拆成两个接口怎么使用。OK,我们现在是面向接口编程,所以产生了这个 UserInfo 对象之后,当然可以把它当 IUserBO 接口使用。也可以当 IUserBiz 接口使用,这要看你在什么地方使用了。要获得用户信息,就当是 IUserBO 的实现类;要是希望维护用户的信息,就把它当做 IUserBiz 的实现类就成了,如代码所示:
……
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的 BO
IUserBO userBO = (IUserBO) userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz) userInfo;
userBiz.deleteUser(userBO);
……
- 确实可以如此,问题也解决了,但是我们来分析一下刚才的动作,为什么要把一个接口拆分成两个呢?其实,在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是 IUserBO,一个是 IUserBiz,类图如图:
- 以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更。
绝杀技,打破你的传统思维
- 解释到这里,估计你已经很不屑了,“切!这么简单的东西还要讲?!”好,我们来讲点复杂的。SRP 的原话解释是:
- There should never be more than one reason for a class to change.
- 这句话初中生都能看懂,不多说,但是看懂是一码事,实施就是另外一码事了。上面讲的例子很好理解,在实际项目中大家都已经这么做了,那我们再来看看下面这个例子是否好理解。
电话这玩意,是现代人都离不了,电话通话的时候有4个过程发生:拨号、通话、回应、挂机,那我们写一个接口,其类图如图:
- 我不是有意要冒犯 IPhone 的,同名纯属巧合,我们来看一下这个过程的代码,如代码所示:
/** * 电话过程 */ public interface IPhone { //拨通电话 public void dial(String phoneNumber); //通话 public void chat(Object o); //通话完毕,挂电话 public void hangup(); }
- 实现类也比较简单,我就不再写了,大家看看这个接口有没有问题?我相信大部分的读者都会说这个没有问题呀,以前我就是这么做的呀,某某书上也是这么写的呀,还有什么什么的源码也是这么写的!是的,这个接口接近于完美,看清楚了,是“接近”!