六大设计原则解读

大旗不挥,谁敢冲锋。要学设计,先知原则!

单一职责原则

​ 单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。这个原则存在争议之处在哪里呢?就是对职责的定义,什么是类的职责,以及怎么划分类的职责。可以举个例子来说明一下单一职责原则。

​ 我想很多人都应该基于RBAC模型做过一些用户和角色管理的一些模块。RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,那么就把这些写到一个接口中,都是用户管理类嘛,这样先来看它的类图:

​ 设计这一个类对于大多数的初级程序员来说并不是难事。但是认真一看,就会发现有问题。这个类的属性和行为并没有分离开来。相信做过Java Web开发的朋友都是这样做的:将一个类所拥有的属性封装成一个BO(Business Object,业务对象),然后再把对于这个类的操作(即行为)也独立封装成Biz(Business Logic,业务逻辑)。如下图所示:

​ 重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。

​ 好了属性和行为分离开来了,但是这个和实际工作中用到的User类好像有差别的呀!现在先来看一看分拆成两个接口怎么使用。现在都是面向接口编程嘛,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方使用了。要获得用户信息,就当是IUserBO的实现类;要是希望维护用户的信息,就把它当作IUserBiz的实现类就成了,如:

1
2
3
4
5
6
IUserInfo userinfo = new UserInfo();

IUserBO userBo = (IUserBo)userInfo;

IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();

​ 其实,在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz,类图下所示:

​ 上面把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更。

​ 下面再举一个例子,电话通话的时候有4个过程发生:拨号、通话、回应、挂机。那么就来写一个电话接口。

​ 代码:

1
2
3
4
5
6
7
8
public interface IPhone {
	//拨通电话
	public void dial(String phoneNumber);
	//通话
	public void chat(Object o);
	//通话完毕,挂电话
	public void hangup();
}

​ 单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情,看看上面的接口只负责一件事情吗?是只有一个原因引起变化吗?好像不是!

IPhone这个接口可不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。dial()hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。这里有两个原因都引起了类的变化,这样就发现IPhone这个接口其实是包含了两个职责,而且这两个职责的变化不互相影响,因为电话拨号成功了之后,并不关心传输的是什么数据。这样就可以考虑把IPhone接口再拆分成两个接口。

​ 这样的设计就变得完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘记了我们是面向接口编程,我们对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

​ 单一职责原则有什么好处:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 可读性提高,复杂性降低,那当然可读性提高了;
  • 可维护性提高,可读性提高,那当然更容易维护了;
  • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

​ 其实单一职责原则最男划分的就是职责。一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类。可是一个接口有两个可以变化的原因,这就为以后的变化带来了风险。

​ **单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。**生硬照搬单一职责原则会一起类的剧增,这就会给维护带来很多的麻烦,过细的划分职责,也会人为地增加了系统的复杂性。总之一句话,原则是死的,人是活的,原则要灵活使用。

对于单一职责原则,接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

里氏替换原则

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;
  • 提高产品或项目的开放性。自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:
  • 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
  • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

​ Java使用extends关键字来实现继承,它采用了单一继承的规则,从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(Liskov Substitution Principle,LSP),什么是里氏替换原则呢?它有两种定义:

  • 第一种定义,也是最正宗的定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
  • 第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

​ 第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

里氏替换原则的四层含义

子类必须完全实现父类的方法

子类可以有自己的个性

覆盖或实现父类的方法时输入参数可以被放大

覆写或实现父类的方法时输出结果可以被缩小