设计模式六大原则
1、单一职责原则(SingleResponsibility Principle ,SRP)
定义:应该有且只有一个原因引起类的变更问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
其实,一般在编程中,我们会有意识地遵守这一原则,这也是常识。但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)
举个例子,比如有如下的接口IUserInfo:
那么,这个接口承担了用户属性操作和增加/删除用户操作的职责。按照单一职责原则,应该把用户信息抽取成BO(Bussiness Object,业务对象),把行为抽取成一个Biz(Bussiness Logic,业务逻辑),按照这个思路进行修改,那么就要重新拆封成2个接口,IUserBO负责用户属性,IUserBiz负责用户的行为。如下图:

好处:
1)、类的复杂性降低,实现的职责都有清晰明确的定义;
2)、可读性和可维护性提高;
3)、变更引起的风险降低。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
2 、里氏替换原则(Liskov Substitution Principle, LSP)
看到里氏替换原则,感觉很好奇,名字很怪,哈哈哈,其实这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的,向伟大的IT届的女精英们致敬!
定义1:如果对应类型为S的对象o1,有类型为T的对象o2,使得以T定义的所有程序P,在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义2:所有引用基类的地方都必须能够透明地使用其子类的对象。
问题由来:有一功能P1,由类A完成。现需要对功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的非抽象方法,也尽量不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,子类在继承父类的同时,会对父类中的非抽象方法进行重写或重载,那么在一定程度上污染了父类;此外,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
举例说明继承的风险,我们需要完成一个两数相减的功能,由类A来负责。特别说明:以下的例子来自于网友卡奴达摩的专栏,在此特别感谢!
[java] view plaincopy
- class A{
- public int func1(int a, int b){
- return a-b;
- }
- }
- public class Client{
- public static void main(String[] args){
- A a = new A();
- System.out.println("100-50="+a.func1(100, 50));
- System.out.println("100-80="+a.func1(100, 80));
- }
- }
运行结果:
100-50=50
100-80=20
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:
- 两数相减。两数相加,然后再加100。
- class B extends A{
- public int func1(int a, int b){
- return a+b;
- }
- public int func2(int a, int b){
- return func1(a,b)+100;
- }
- }
- public class Client{
- public static void main(String[] args){
- B b = new B();
- System.out.println("100-50="+b.func1(100, 50));
- System.out.println("100-80="+b.func1(100, 80));
- System.out.println("100+20+100="+b.func2(100, 20));
- }
- }
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。即覆写或者实现父类的方法时输入的参数可以被放大。 “契约优先”的原则,就是接口,这种设计方法也叫做Design by Contract. 前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。即覆写或者实现父类的方法时输出的结果可以被缩小。
父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说要么S和T是同一个类型,要么S是T的子类。
后两层含义其实就是:继承类方法必须接受任何基类方法能接受的任何条件(参数)。同样,继承类必须顺从基类的所有后续条件。这样,我们就有了基于合同的LSP,基于合同的LSP是LSP的一种强化。
好处:
增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。
未完待续。。。。
由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:
[java] view plaincopy类B完成后,运行结果:
100-50=150
100-80=180
100+20+100=220
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义: