`

(转)设计模式之六大原则

 
阅读更多

 

源地址:http://blog.csdn.net/scboyhj__/article/details/47844639

 

1. 单一职责原则 -Single Responsibility Principle

 

SRP,Single Responsibility Principle:

There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更。(如果类需要变更,那么只可能仅由某一个原因引起)

问题由来:

        类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。


解决方案:

        遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

 

示例:

       如果一个接口包含了两个职责,并且这两个职责的变化不互相影响,那么就可以考虑拆分成两个接口。

       方法的职责应清晰、单一。一个method尽可能制作一件事情。changeUserInfo()可拆分为changeUserName()、changeUserAddr()....

 

 

 

        说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。

        为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。此时,按照SRP 应该再新建一个类负责职责P2,但是这样会修改花销很大!除了改接口 还需要改客户端代码!所以一般就直接在原有类方法中增加判断 支持职责P2;或者在原有类中新增一个方法来处理职责P2(做到了方法级别的SRP),

 

例如原有一个接口,模拟动物呼吸的场景:

  1. class Animal{    
  2.     public void breathe(String animal){    
  3.         System.out.println(animal+"呼吸空气");    
  4.     }    
  5. }    
  6. public class Client{    
  7.     public static void main(String[] args){    
  8.         Animal animal = new Animal();    
  9.         animal.breathe("牛");    
  10.         animal.breathe("羊");    
  11.         animal.breathe("猪");    
  12.     }    
  13. }    

 

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。

 

 

修改一:修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:

 

  1. class Terrestrial{    
  2.     public void breathe(String animal){    
  3.         System.out.println(animal+"呼吸空气");    
  4.     }    
  5. }    
  6. class Aquatic{    
  7.     public void breathe(String animal){    
  8.         System.out.println(animal+"呼吸水");    
  9.     }    
  10. }    
  11.     
  12. public class Client{    
  13.     public static void main(String[] args){    
  14.         <strong>Terrestrial </strong>terrestrial = new Terrestrial();    
  15.         terrestrial.breathe("牛");    
  16.         terrestrial.breathe("羊");    
  17.         terrestrial.breathe("猪");    
  18.             
  19.         <strong>Aquatic </strong>aquatic = new Aquatic();    
  20.         aquatic.breathe("鱼");    
  21.     }    
  22. }    

 

 

BUT,这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。

 

修改二:直接修改类Animal;虽然违背了单一职责原则,但花销却小的多

 

  1. class Animal{    
  2.     public void breathe(String animal){    
  3.         if("鱼".equals(animal)){    
  4.             System.out.println(animal+"呼吸水");    
  5.         }else{    
  6.             System.out.println(animal+"呼吸空气");    
  7.         }    
  8.     }    
  9. }    
  10.     
  11. public class Client{    
  12.     public static void main(String[] args){    
  13.         Animal animal = new Animal();    
  14.         animal.breathe("牛");    
  15.         animal.breathe("羊");    
  16.         animal.breathe("猪");    
  17.         animal.breathe("鱼");    
  18.     }    
  19. }    

 

这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。

 

这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。

 

 

修改三:

 

  1. class Animal{    
  2.     public void breathe(String animal){    
  3.         System.out.println(animal+"呼吸空气");    
  4.     }    
  5.     
  6.     public void breathe2(String animal){    
  7.         System.out.println(animal+"呼吸水");    
  8.     }    
  9. }    
  10.     
  11. public class Client{    
  12.     public static void main(String[] args){    
  13.         Animal animal = new Animal();    
  14.         animal.breathe("牛");    
  15.         animal.breathe("羊");    
  16.         animal.breathe("猪");    
  17.         animal.breathe2("鱼");    
  18.     }    
  19. }    


这种修改方式没有改动原来的方法,而是在类中新加了一个方法;虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。

 

 

好处:

       一个接口的修改只对相应的实现类有影响,对其他接口无影响;有利于系统的可扩展性、可维护性。

 

问题:

       “职责”的粒度不好确定!

        过分细分的职责也会人为地增加系统复杂性。

 

建议:

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

       只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;

2. 里氏替换原则 -Liskov Substitution Principle

LSP,Liskov Substitution Principle:

1) If for each object s of type S, there is an objectt of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when s is substituted fort when S is a subtype of T.

2) Functions that use pointers or references to base classes must be able to user objects of derived classes without knowing it.

所有引用基类的地方,都能透明地替换成其子类对象。只要父类能出现的地方,子类就可以出现。

 

引入里氏替换原则能充分发挥继承的优点、减少继承的弊端。

继承的优点:

  • 代码共享,减少创建类的工作量;每个子类都有父类的方法和属性;
  • 提高代码重用性;
  • 子类可以形似父类,但又异于父类;
  • 提高代码可扩展性;
  • 提高产品开放性。

继承的缺点:

  • 继承是侵入性的——只要继承,就必须拥有父类的属性和方法;
  • 降低代码的灵活性——子类必须拥有父类的属性和方法,让子类自由的世界多了些约束;
  • 增强了耦合性——当父类的属性和方法被修改时,必须要考虑子类的修改。

 

示例(继承的缺点):

        原有类A,实现减法功能:

  1. class A{    
  2.     public int func1(int a, int b){    
  3.         return a-b;    
  4.     }    
  5. }    
  6.     
  7. public class Client{    
  8.     public static void main(String[] args){    
  9.         A a = new A();    
  10.         System.out.println("100-50="+a.func1(10050));    
  11.         System.out.println("100-80="+a.func1(10080));    
  12.     }    
  13. }    

 

        新增需求:新增两数相加、然后再与100求和的功能,由类B来负责

  1. class B extends A{    
  2.     public int func1(int a, int b){    
  3.         return a+b;    
  4.     }    
  5.         
  6.     public int func2(int a, int b){    
  7.         return func1(a,b)+100;    
  8.     }    
  9. }    
  10.     
  11. public class Client{    
  12.     public static void main(String[] args){    
  13.         B b = new B();    
  14.         System.out.println("100-50="+b.func1(10050));    
  15.         System.out.println("100-80="+b.func1(10080));    
  16.         System.out.println("100+20+100="+b.func2(10020));    
  17.     }    
  18. }    


OOPS! 原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法!

 

 

 

问题由来:

        有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

 

解决方案:

        LSP为继承定义了一个规范,包括四层含义:

 

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

        如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生畸变;则建议不要用继承,而采用依赖、聚集、组合等关系代替继承。

 

        例如:父类AbstractGun有shoot()方法,其子类ToyGun不能完整实现父类的方法(玩具枪不能射击,ToyGun.shoot()中没有任何处理逻辑),则应该断开继承关系,另外建一个AbstractToy父类。

 

        2)子类可以有自己得个性

        即,在子类出现的地方,父类未必就能替代。

 

        3)重载或实现父类方法时,输入参数可以被放大(入参可以更宽松)

        否则,用子类替换父类后,会变成执行子类重载后的方法,而该方法可能“歪曲”父类的意图,可能引起业务逻辑混乱。

 

        4)重写或实现父类方法时,返回类型可以被缩小(返回类型更严格)

 

建议:

 

        在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

        父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

 

        里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

 

Q:

        LSP如何减少继承的弊端?

3. 依赖倒置原则 -Dependence Inversion Principle:

 

DIP,Dependence Inversion Principle:

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

 

“面向接口编程”

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;——模块间的依赖通过抽象发生。实现类之间不发生直接的依赖关系(eg. 类B被用作类A的方法中的参数),其依赖关系是通过接口或抽象类产生的;
  • 抽象不应该依赖细节;——接口或抽象类不依赖于实现类;
  • 细节应该依赖抽象;——实现类依赖接口或抽象类。

何为“倒置”?

        依赖正置:类间的依赖是实实在在的实现类间的依赖,即面向实现编程,这是正常人的思维方式;

        而依赖倒置是对现实世界进行抽象,产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖。

 

依赖倒置可以减少类间的耦合性、降低并行开发引起的风险。

 

示例(减少类间的耦合性):

        例如有一个Driver,可以驾驶Benz:

 

  1. public class Driver{  
  2.     public void drive(Benz benz){  
  3.         benz.run();  
  4.     }  
  5. }  
  6. public class Benz{  
  7.     public void run(){  
  8.         System.out.println("Benz开动...");  
  9.     }  
  10. }  


        问题来了:现在有变更,Driver不仅要驾驶Benz,还需要驾驶BMW,怎么办?

 

        Driver和Benz是紧耦合的,导致可维护性大大降低、稳定性大大降低(增加一个车就需要修改Driver,Driver是不稳定的)。

 

示例(降低并行开发风险性):

        如上例,Benz类没开发完成前,Driver是不能编译的!不能并行开发!

 

 

问题由来:

        类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

        

解决办法:

        将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

 

        上例中,新增一个抽象ICar接口,ICar不依赖于BMW和Benz两个实现类(抽象不依赖于细节)。

        1)Driver和ICar实现类松耦合

        2)接口定下来,Driver和BMW就可独立开发了,并可独立地进行单元测试

 

        依赖有三种写法:

       1)构造函数传递依赖对象(构造函数注入)

 

  1. public interface IDriver{  
  2.     public void drive();  
  3. }  
  4.   
  5. public class Driver implements IDriver{  
  6.     private ICar car;  
  7.   
  8.     public <strong>Driver(ICar _car)</strong>{  
  9.         this.car = _car;  
  10.     }  
  11.   
  12.     public void drive(){  
  13.         this.car.run();  
  14.     }  
  15. }  


      2)setter方法传递依赖对象(setter依赖注入)

 

 

  1. public interface IDriver{  
  2.     public void setCar(ICar car);  
  3.     public void drive();  
  4. }  
  5.   
  6. public class Driver implements IDriver{  
  7.     private ICar car;  
  8.     public void <strong>setCar(ICar car)</strong>{  
  9.         this.car = car;  
  10.     }  
  11.     public void drive(){  
  12.         this.car.run();  
  13.     }  
  14. }  

 

      3)接口声明依赖对象(接口注入)

 

建议:

        DIP的核心是面向接口编程;DIP的本质是通过抽象(接口、抽象类)使各个类或模块的实现彼此独立,不互相影响。

在项目中遵循以下原则:

  1. 每个类尽量都有接口或抽象类
  2. 变量的表面类型尽量使接口或抽象类
  3. 任何类都不应该从具体类派生*——否则就会依赖具体类。
  4. 尽量不要重写父类中已实现的方法——否则父类就不是一个真正适合被继承的抽象。
  5. 结合里氏替代原则使用

4. 接口隔离原则 -Interface Segregation Principle

 

Interface Segregation Principle:

Clients should not be forced to depend upon interfaces that they don't use.——客户端只依赖于它所需要的接口;它需要什么接口就提供什么接口,把不需要的接口剔除掉。

The dependency of one class to another one should depend on the smallest possible interface.——类间的依赖关系应建立在最小的接口上。

即,接口尽量细化,接口中的方法尽量少

 

 

问题由来:

        类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法

 

解决方案:

        将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。包含4层含义:

        1)接口要尽量小

        不能出现Fat Interface;但是要有限度,首先不能违反单一职责原则(不能一个接口对应半个职责)。

 

        2)接口要高内聚

        在接口中尽量少公布public方法。

       接口是对外的承诺,承诺越少对系统的开发越有利。

 

       3)定制服务

       只提供访问者需要的方法。例如,为管理员提供IComplexSearcher接口,为公网提供ISimpleSearcher接口。

 

       4)接口的设计是有限度的

 

建议:

  • 一个接口只服务于一个子模块或业务逻辑;
  • 通过业务逻辑压缩接口中的public方法;
  • 已被污染了的接口,尽量去修改;若变更的风险较大,则采用适配器模式转化处理;
  • 拒绝盲从

 

与单一职责原则的区别:

        二者审视角度不同;

        单一职责原则要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分;

        接口隔离原则要求接口的方法尽量少。。。

5. 迪米特法则 -Law of Demeter

 

LoD,Law of Demeter:

又称最少知识原则(Least Knowledge Principle),一个对象应该对其他对象有最少的了解

一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。

 

 

问题由来:

        类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

 

解决方案:

        迪米特法则包含4层含义:

        1)只和朋友交流

        Only talk to your immediate friends.两个对象之间的耦合就成为朋友关系。即,出现在成员变量、方法输入输出参数中的类就是朋友;局部变量不属于朋友。

--> 不与无关的对象发生耦合!

        方针:不要调用从另一个方法中返回的对象中的方法!只应该调用以下方法:

  • 该对象本身的方法
  • 该对象中的任何组件的方法
  • 方法参数中传入的对象的方法
  • 方法内部实例化的对象的方法

        例如:Teacher类可以命令TeamLeader对Students进行清点,则Teacher无需和Students耦合,只需和TeamLeader耦合即可。

反例:

 

  1. public float getTemp(){  
  2.      Thermometer t = station.getThermometer();  
  3.      return t.getTemp();  
  4. }  

客户端不应该了解气象站类中的温度计对象;应在气象站类中直接加入获取温度的方法。改为:

 

 

  1. public float getTemp(){  
  2.      return station.getTemp();  
  3. }  



 

        2)朋友间也应该有距离

        即使是朋友类之间也不能无话不说,无所不知。

--> 一个类公开的public属性或方法应该尽可能少!

 

        3)是自己的就是自己的

        如果一个方法放在本类中也可以、放在其他类中也可以,怎么办?

--> 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

 

        4)谨慎使用Serializable

        否则,若后来修改了属性,序列化时会抛异常NotSerializableException。

 

建议:

        迪米特法则的核心观念是:类间解耦。

        其结果是产生了大量中转或跳转类。

 

6. 开闭原则 -Open Closed Principle

 

Open Closed Principle:

Software entities like classes, modules and functions should be open for extension but closed for modifications.

对扩展开放,对修改关闭。一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。

一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。——but,并不意味着不做任何修改;底层模块的扩展,必然要有高层模块进行耦合。

 

“变化”可分为三种类型:

  1. 逻辑变化——不涉及其他模块,可修改原有类中的方法;
  2. 子模块变化——会对其他模块产生影响,通过扩展来完成变化;
  3. 可见视图变化——界面变化,有时需要通过扩展来完成变化。

 

 

问题由来:

        在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

 

解决方案:

        当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。要求:

 

       1)抽象约束(要实现对扩展开放,首要前提就是抽象约束)

        通过接口或抽象类可以约束一组可能变化的行为,并能实现对扩展开放。包含三层含义:

  • 通过接口或抽象类可以约束扩展,对扩展进行边界限定,不允许出现在接口抽象类中不存在的public方法;
  • 参数类型、引用对象尽量使用接口或抽象类,而不是实现类;
  • 抽象应尽量保持稳定,一旦确定即不允许修改。

 

 

        2)元数据(metadata)控制模块行为

       元数据,即用来描述环境和数据的数据,即配置数据。例如SpingContext。

     

      3)制定项目章程

 

      4)封装变化

      封装可能发生的变化。将相同的变化封装到一个接口或抽象类中;将不同的变化封装到不同的接口或抽象类中。

 

好处:

  • 易于单元测试

 

       如果直接修改已有代码,则需要同时修改单元测试类;而通过扩展,则只需生成一个测试类。

 

  • 可提高复用性

 

  • 可提高可维护性

 

  • 面向对象开发的要求

 

建议:

      开闭原则是最基础的原则,前5个原则都是开闭原则的具体形态。

 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 

源地址: http://www.cnblogs.com/yangboyu/archive/2010/09/26/1835254.html

 

设计模式之六大原则——开闭原则(OCP)

 

开闭原则(Open Closed Principle)是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。

 

定义:

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

Softeware entities like classes,modules and functions should be open for extension but closed for modifications.

 

开闭原则的含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。

软件实体包括以下几个部分:

  • 项目或软件产品中按照一定的逻辑规则划分的模块
  • 抽象和类
  • 方法

开闭原则是为软件实体的未来事物而制定的对现行开发设计进行约束的一个原则。

 

注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段了。

 

变化的类型:

  • 逻辑变化
  • 子模块变化
  • 可见试图变化

一个项目的基本路径应该是这样的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码修改,保持历史代码的纯洁性,提高系统的稳定性。

 

开闭原则的重要性:

  • 开闭原则对测试的影响

开闭原则可是保持原有的测试代码仍然能够正常运行,我们只需要对扩展的代码进行测试就可以了。

  • 开闭原则可以提高复用性

在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。

  • 开闭原则可以提高可维护性
  • 面向对象开发的要求

 

如何使用开闭原则:

  • 抽象约束

第一,通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;

第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;

第三,抽象层尽量保持稳定,一旦确定即不允许修改。

  • 元数据(metadata)控制模块行为

元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

Spring容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)

  • 制定项目章程

在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。

  • 封装变化

对变化的封装包含两层含义:

第一,将相同的变化封装到一个接口或者抽象类中;

第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

 

设计模式之六大原则——迪米特法则(LoD,LKP)

 

定义:

迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)。

一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。

 

含义:

  • 只和朋友交流

朋友类的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。

 

下面的代码在方法体内部依赖了其他类,这严重违反迪米特法则

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Teacher {
  
    public void commond(GroupLeader groupLeader) {
        List<Girl> listGirls = new ArrayList<Girl>();
  
        for (int i = 0; i < 20; i++) {
            listGirls.add(new Girl());
        }
  
        groupLeader.countGirls(listGirls);
    }
  
}

方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系,这是不允许的。

正确的做法是:

1
2
3
4
5
6
7
public class Teacher {
  
    public void commond(GroupLeader groupLeader) {
        groupLeader.countGirls();
    }
  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class GroupLeader {
  
    private List<Girl> listGirls;
  
    public GroupLeader(List<Girl> _listGirls) {
        this.listGirls = _listGirls;
    }
  
    public void countGirls() {
        System.out.println("女生数量是:" + listGirls.size());
    }
  
}

注意:一个类只和朋友交流,不与陌生类交流,不要出现getA().getB().getC().getD()这种情况(在一种极端情况下允许出现这种访问,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外。

  • 朋友间也是有距离的

一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前不加访问权限,则默认为包类型)、protected等访问权限,是否可以加上final关键字等。

注意:迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。

 

  • 是自己的就是自己的

如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

 

  • 谨慎使用Serializable

 

最后,迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。

 

设计模式之六大原则——接口隔离原则(ISP)

 

接口隔离原则 Interface Segregation Principle 

 

定义:

  • 客户端不应该依赖它不需要的接口
  • 类间的依赖关系应该建立在最小的接口上

我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。

提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。

 

接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。

 

含义:

  • 接口要尽量小

这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。

根据接口隔离原则拆分接口时,首先必须满足单一职责原则。

  • 接口要高内聚

高内聚就是要提高接口、类、模块的处理能力,减少对外的交互。

具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺地越少对系统开发越有利,变更的风险也就越少,同时也有利于降低成本。

  • 定制服务

定制服务就是单独为一个个体提供优良的服务。

  • 接口设计是有限度的

接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度,这个度只能根据经验和常识判断,没有一个固化或可测量的标准。

 

设计模式之六大原则——依赖倒置原则(DIP)

 

 

依赖倒置原则(Dependence Inversion Principle,DIP)的原始定义: 

 

  • 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。 

依赖倒置原则在Java语言中的表现是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。 

依赖倒置原则实际上就是要求“面向接口编程”。

采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

例如:

司机接口 

1
2
3
4
5
public interface IDriver {
  
    public void driver(ICar car);
      
}

司机实现类

1
2
3
4
5
6
7
public class Driver implements IDriver {
  
    public void driver(ICar car) {
        car.run();
    }
  
}

汽车接口

1
2
3
4
5
6
7
public class Benz implements ICar {
  
    public void run() {
        System.out.println("奔驰汽车开始运行...");
    }
  
}
1
2
3
4
5
6
7
public class BMW implements ICar {
  
    public void run() {
        System.out.println("宝马汽车开始运行...");
    }
  
}

场景类

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
  
    /**
     * @param args
     */
    public static void main(String[] args) {
        IDriver zhangSan = new Driver();
        ICar benz = new Benz();
        zhangSan.driver(benz);
    }
  
}

抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到“言必信,行必果”。

 

依赖的三种写法

只要做到抽象依赖,即使是多层的依赖传递也无所谓惧!

  • 构造函数传递依赖对象——构造函数注入

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Driver implements IDriver {
  
    private ICar car;
      
    public Driver(ICar _car) {
        this.car = _car;
    }
      
    public void driver() {
        this.car.run();
    }
  
}

  

Setter方法传递依赖对象——Setter依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Driver implements IDriver {
  
    private ICar car;
      
    /**
     * @param car the car to set
     */
    public void setCar(ICar car) {
        this.car = car;
    }
  
    public void driver() {
        this.car.run();
    }
  
}

 

接口声明依赖对象——接口注入

1
2
3
4
5
6
7
public class Driver implements IDriver {
  
    public void driver(ICar car) {
        car.run();
    }
  
}

 

本质:

依赖倒置原则的本质就是通过抽象(接口或者抽象类)使各个类或模型的实现彼此独立,不互相影响,实现模块间的松耦合。

  

规则:

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备;
  • 变量的表面类型尽量是接口或者抽象类;
  • 任何类都不应该从具体类派生;
  • 尽量不要覆写基类的方法;
  • 结合里氏替换原则使用。

接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

  

依赖倒置与依赖正置

依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结构就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。

 

设计模式之六大原则——里氏替换原则(LSP)

 

里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题。

 

继承的优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的重用性;
  • 子类可以形似父类,但又异于父类;
  • 提高代码的可扩展性;
  • 提高产品或项目的开放性。

继承的缺点:

  • 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性,子类必须拥有父类的属性和方法,让子类增加了约束;
  • 增强了耦合性,当父类的常量、变量和方法被修改时,必须考虑子类的修改。  

定义:

所有引用基类的地方必须能透明地使用其子类的对象。

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

 

含义:

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

在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了里氏替换原则。

如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中发生了“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。 

  • 子类可以有自己的个性  
  • 覆盖或实现父类的方法时输入参数可以被放大

如果子类的前置条件较小,子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱。 

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

目的:

采用里氏替换原则的目的就是增强程序的健壮性,版本升级是也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。

 

设计模式之六大原则——单一职责原则(SRP)

 

定义:

应该有且仅有一个原因引起类的变更。

There should never be more than one reason for a class to change.

 

优点:

1、类的复杂性降低,实现什么职责都有清晰明确的定义;

2、可读性提高,复杂性减低,可读性当然提高;

3、可维护性提高,可读性提高,可维护性当然提高;

4、变更引起的风险减低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

 

注意:

单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

 

建议:

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

 

========================================================

源地址: http://blog.csdn.net/maritimesun/article/details/7854937

 

计模式六大原则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
 
  1. class A{  
  2.     public int func1(int a, int b){  
  3.         return a-b;  
  4.     }  
  5. }  
  6.   
  7. public class Client{  
  8.     public static void main(String[] args){  
  9.         A a = new A();  
  10.         System.out.println("100-50="+a.func1(10050));  
  11.         System.out.println("100-80="+a.func1(10080));  
  12.     }  
  13. }  

 

 运行结果:

100-50=50
100-80=20

        后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:

  • 两数相减。
  • 两数相加,然后再加100。

        由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:

[java] view plaincopy
 
  1. class B extends A{  
  2.     public int func1(int a, int b){  
  3.         return a+b;  
  4.     }  
  5.       
  6.     public int func2(int a, int b){  
  7.         return func1(a,b)+100;  
  8.     }  
  9. }  
  10.   
  11. public class Client{  
  12.     public static void main(String[] args){  
  13.         B b = new B();  
  14.         System.out.println("100-50="+b.func1(10050));  
  15.         System.out.println("100-80="+b.func1(10080));  
  16.         System.out.println("100+20+100="+b.func2(10020));  
  17.     }  
  18. }  

类B完成后,运行结果:

100-50=150
100-80=180
100+20+100=220

      我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

 

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。即覆写或者实现父类的方法时输入的参数可以被放大。                                                             “契约优先”的原则,就是接口,这种设计方法也叫做Design by Contract.                                                             前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。即
    覆写或者实现父类的方法时输出的结果可以被缩小。

        父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说要么S和T是同一个类型,要么S是T的子类。

     

    后两层含义其实就是:继承类方法必须接受任何基类方法能接受的任何条件(参数)。同样,继承类必须顺从基类的所有后续条件。这样,我们就有了基于合同的LSP,基于合同的LSP是LSP的一种强化。

    好处:

    增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。

设计模式六大原则3—依赖倒置原则

192人阅读 评论(0) 收藏 举报

依赖倒置原则(Dependence Inversion Principle DIP)
定义:依赖倒置原则具有以下三层含义:
1、高层模块不应该依赖底层模块,两者都应该依赖其抽象;
2、抽象不应该依赖细节;
3、细节应该依赖抽象。
问题由来类A直接依赖类B,若要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般为高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为直接依赖接口I,类B和类C实现接口I,这样类A通过接口I和类B或者类C发生联系。
我们用一个例子说明依赖倒置原则。如下述代码所示:
  1. public class BMWCar {  
  2.       
  3.     public void run(){  
  4.         System.out.println("BMW is runing.....");  
  5.     }  
  6.   
  7. }  
public class BMWCar {
	
	public void run(){
		System.out.println("BMW is runing.....");
	}

}
  1. public class Driver {  
  2.     public void drive(BMWCar bmw){  
  3.         System.out.println("Driver is driving");  
  4.         bmw.run();  
  5.     }  
  6. }  
public class Driver {
	public void drive(BMWCar bmw){
		System.out.println("Driver is driving");
		bmw.run();
	}
}
  1. public class Client {  
  2.   
  3.     /** 
  4.      * @param args 
  5.      */  
  6.     public static void main(String[] args) {  
  7.         // TODO Auto-generated method stub   
  8.         Driver driver=new Driver();  
  9.         driver.drive(new BMWCar());  
  10.   
  11.     }  
  12.   
  13. }  
public class Client {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Driver driver=new Driver();
		driver.drive(new BMWCar());

	}

}
     那现在如果司机开的是Benz的车,那么我们就得要修改Driver类的drive行为了。那如果司机还开别的类型的车,比如Bick等,那我们岂不是都是对Driver的drive行为作出更改。这是什么原因呢?因为Driver和BMWCar之间的耦合度太强了!
   因此我们引入一个抽象的接口ICar,Driver类与ICar发生依赖关系,BMWCar和BenzCar等实现ICar.
  1. public interface ICar {  
  2.     public void run();  
  3.   
  4. }  
public interface ICar {
	public void run();

}
  1. public class BMWCar implements ICar{  
  2.       
  3.     public void run(){  
  4.         System.out.println("BMW is runing.....");  
  5.     }  
  6.   
  7. }  
public class BMWCar implements ICar{
	
	public void run(){
		System.out.println("BMW is runing.....");
	}

}
  1. public class BenzCar implements ICar {  
  2.   
  3.     public void run(){  
  4.         System.out.println("Benz is runing.....");  
  5.     }  
  6. }  
public class BenzCar implements ICar {

	public void run(){
		System.out.println("Benz is runing.....");
	}
}
  1. public class Driver {  
  2.     public void drive(ICar car){  
  3.         System.out.println("Driver is driving");  
  4.         car.run();  
  5.     }  
  6. }  
public class Driver {
	public void drive(ICar car){
		System.out.println("Driver is driving");
		car.run();
	}
}
  1. public class Client {  
  2.   
  3.     /** 
  4.      * @param args 
  5.      */  
  6.     public static void main(String[] args) {  
  7.         // TODO Auto-generated method stub   
  8.         Driver driver=new Driver();  
  9.         driver.drive(new BMWCar());  
  10.         driver.drive(new BenzCar());  
  11.   
  12.     }  
  13. }  
public class Client {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Driver driver=new Driver();
		driver.drive(new BMWCar());
		driver.drive(new BenzCar());

	}
}
      这样修改后,无论以后怎样扩展Client类,都不需要再修改Driver类了。这只是一个简单的例子,实际情况中,代表高层模块的Driver类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

      从这个例子,我们可以看出,依赖倒置原则的核心思想是面向接口编程。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。

     依赖关系有三种方式:

1)接口传递依赖对象,如上述例子中使用的方法是接口传递;

2)构造方法传递依赖对象

3)setter方法传递依赖对象。

    在实际编程中,对于依赖倒置原则的使用,我们需要做到如下3点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 任何类都不应该从具体类派生。
  • 尽量不要覆写基类的方法。
  • 使用继承时遵循里氏替换原则。

设计模式六大原则4—接口隔离原则

419人阅读 评论(0) 收藏 举报

接口隔离原则
定义:
1)客户端不该依赖它不需要的接口;
2)类间的依赖关系应该建立在最小的接口上。

问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类C来说不是最小接口,则类B和类D必须去实现他们不需要的方法,如下图所示。

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

举例来说明接口隔离原则:

 

       使用Manager类代表一个管理工人的管理者。有两种类型的工人:普通的和高效的,这两种工人都需要吃午饭。现在来了一批机器人,它们同样为公司工作,但是他们不需要吃午饭。一方面Robot类需要实现IWoker接口,因为他们要工作;另一方面,它们又不需要实现IWorker接口,因为它们不需要吃饭。在这种情况下IWorker就被认为是一个被污染了的接口。

 

  1.  
    1. // interface segregation principle - bad example  
    2.   
    3. interface IWorker {  
    4.     public void work();  
    5.   
    6.     public void eat();  
    7. }  
    8.   
    9. class Worker implements IWorker {  
    10.     public void work() {   
    11.         // ....working  
    12.     }  
    13.   
    14.     public void eat() {   
    15.         // ...... eating in launch break  
    16.     }  
    17. }  
    18.   
    19. class SuperWorker implements IWorker{     
    20.     public void work() {          
    21.         //.... working much more      
    22.     }     
    23.       
    24.     public void eat() {       
    25.         //.... eating in launch break     
    26.     }  
    27. }  
    28.           
    29. class Manager {   
    30.     IWorker worker;   
    31.       
    32.     public void setWorker(IWorker w) {   
    33.         worker=w;  
    34.     }   
    35.       
    36.     public void manage() {  
    37.         worker.work();   
    38.     }  
    39. }

 

    依据接口隔离的原则,我们应该把IWorker接口划分为两个独立的接口,如下面的代码所示。

 

  1. //interface segregation principle - good example  
  2.   
  3. interface IWorkable {     
  4.     public void work();  
  5. }  
  6.   
  7. interface IFeedable{  
  8.     public void eat();  
  9. }  
  10.   
  11. class Worker implements IWorkable, IFeedable {    
  12.     public void work() {          
  13.         // ....working    
  14.     }     
  15.       
  16.     public void eat() {       
  17.         //.... eating in launch break     
  18.     }  
  19. }  
  20.   
  21. class SuperWorker implements IWorkable, IFeedable{    
  22.     public void work() {          
  23.         //.... working much more      
  24.     }     
  25.       
  26.     public void eat() {       
  27.         //.... eating in launch break     
  28.     }  
  29. }  
  30.   
  31. class Robot implements IWorkable{     
  32.     public void work() {          
  33.     // ....working    
  34.     }  
  35. }  
  36.   
  37. class Manager {  
  38.     IWorkable worker;  
  39.   
  40.     public void setWorker(IWorkable w) {  
  41.         worker = w;  
  42.     }  
  43.   
  44.     public void manage() {  
  45.         worker.work();  
  46.     }  
  47. }  
 

 

 这样使用接口隔离原则可以防止庞大、臃肿的接口,这也就接口隔离原则的目的所在。

      接口隔离尽量使用多个专门的接口,这和单一职责原则很类似。其实不然,其一,单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

     在依据接口隔离原则拆分接口时,首先必须满足单一职责原则。其次,在使用接口隔离原则时,应做到以下几点:

 

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。                                                                       那么,在实际应用中,通常一个接口只服务于一个子模块或是业务逻辑;
  • 力求通过业务逻辑压缩接口中的“public”方法,让接口“满身筋骨”。
  • 已被污染的接口,尽量去修改;若变更太大的话,可以使用适配器模块。

设计模式六大原则5—迪米特原则

迪米特原则(Least Knowledge Principle)
定义:迪米特原则,又叫“最少知识原则”。其含义是:一个对象对其他对象有最少的了解,即一个类应该对自己需要耦合或调用的类知道得最少,只需知道它所提供的接口就行。

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

      迪米特法则还有一个更简单的定义:只与直接的朋友通信所谓的直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,如依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

下面的代码在方法体内部依赖了其他类,严重违反迪米特法则。例子为老师让体育委员确认全部女生的人数。

 

  1. public class Teacher {  
  2.     public void commond(GroupLeader groupLeader) {  
  3.         List<Girl> listGirls = new ArrayList<Girl>();  
  4.         for (int i = 0; i < 20; i++) {  
  5.             listGirls.add(new Girl());  
  6.         }  
  7.         groupLeader.countGirls(listGirls);  
  8.     }  
  9.   
  10. }  
public class Teacher {
	public void commond(GroupLeader groupLeader) {
		List<Girl> listGirls = new ArrayList<Girl>();
		for (int i = 0; i < 20; i++) {
			listGirls.add(new Girl());
		}
		groupLeader.countGirls(listGirls);
	}

}

 

上面的例子中,体育教师知道了与自己没有直接关系的女生类,这是不允许的。

正确的做法是:

 

  1. public class Teacher {  
  2.     public void commond(GroupLeader groupLeader) {  
  3.         groupLeader.countGirls();  
  4.     }  
  5. }  
public class Teacher {
	public void commond(GroupLeader groupLeader) {
		groupLeader.countGirls();
	}
}
  1. public class GroupLeader {  
  2.     private List<Girl> listGirls;  
  3.        
  4.     public GroupLeader(List<Girl> _listGirls) {  
  5.         this.listGirls = _listGirls;  
  6.     }  
  7.    
  8.     public void countGirls() {  
  9.         System.out.println("女生数量是:" + listGirls.size());  
  10.     }  
  11. }  
public class GroupLeader {
	private List<Girl> listGirls;
	 
    public GroupLeader(List<Girl> _listGirls) {
        this.listGirls = _listGirls;
    }
 
    public void countGirls() {
        System.out.println("女生数量是:" + listGirls.size());
    }
}

 

 修改后,教师与女生类之间不存在强耦合,直接调用体育委员的countGirls()。

    注意:一个类只和朋友交流,不与陌生类交流,不要出现getA().getB().getC().getD()这种情况(在一种极端情况下允许出现这种访问,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外。

 

  •    朋友间也是有距离的。一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前不加访问权限,则默认为包类型)、protected等访问权限,是否可以加上final关键字等。注意:迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。

 

  • 是自己的就是自己的

       如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

 

  •  迪米特法则的使用是有限度的。
       这如同在接口隔离原则中提到接口的设计是有限度的一样,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。本例中,我们就是通过体育委员GroupLeader类来传递的,所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

设计模式六大原则6—开闭原则

开闭原则
定义:软件实体如类、模块、函数等应该对外扩展开放,对修改关闭。
也就是说,一个软件实体应该通过扩展来实现变化,而不是修改已有的代码来实现变化。

问题由来:在软件的生命周期内,因为需求变化、系统升级或维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过回归测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

 开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。但是,我感觉开闭原则可能是设计模式六项原则中定义最模糊的一个,到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。

以下我参考了写得比较精彩的博文中的观点:

 

    其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

         开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

         说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

         最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。

      至此,设计模式的六大原则就写完了。写出来的目的一方面是对这六项原则系统地整理一下,另一方面也与广大的网友分享。因为设计模式的确非常重要。以下是前面5项

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics