设计模式开篇(一)

来源:互联网 发布:java 停止线程 编辑:程序博客网 时间:2024/04/30 15:53


设计模式体系

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型 模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、 组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任 链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

这里写图片描述

分别简要概述如下

其中创建型有:
一、Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问

二、Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口, 而无须指定它们的具体类。
三、Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪 一个类,Factory Method 使一个类的实例化延迟到了子类。
四、Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建 过程可以创建不同的表示。
五、Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型 来创建新的对象。
行为型有:
六、Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不 需要暴露该对象的内部表示。(现在基本不用,因为Iterable接口基本作用于大多数API,一般不再需要自己写迭代器)
七、Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生 改变时,所有依赖于它的对象都得到通知自动更新。
八、Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟 到子类中,TemplateMethod 使得子类可以不改变一个算法的结构即可以重定义该算法得某 些特定步骤。
九、Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求 对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
十、State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎 改变了他的类。
十一、Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可 以互相替换,本模式使得算法可以独立于使用它们的客户。
十二、China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免 请求的送发者和接收者之间的耦合关系。
十三、Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
十四、Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你 可以在不改变各元素类的前提下定义作用于这个元素的新操作。
十五、Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一 个解释器,这个解释器使用该表示来解释语言中的句子。
十六、Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态, 并在该对象之外保存这个状态。
结构型有:
十七、Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite 使得用户对单个对象和组合对象的使用具有一致性。
十八、Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade 提供了一 高层接口,这个接口使得子系统更容易使用。
十九、Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问 。
二十、Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter 模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
二十一、Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功 能来说,Decorator 模式相比生成子类更加灵活。
二十二、Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变 化。
二十三、Flyweight,享元模式

还有并发型模式和线程池模式,下面是设计模式的图解:

这里写图片描述



设计原则

1. 单一职责原则
2. 里氏替换原则
3. 依赖倒置原则
4. 接口隔离原则
5. 迪米特法则
6. 开闭原则

单一职责原则(Single Resposibility Principle),简称SRP。定义:应该有且仅有一个原因引起类的变更(There should never be more than one reson for a class to change.)。

单一职责的好处
1. 降低类的复杂性,实现什么职责都有清晰明确的
2. 可读性提高,可维护性提高
3. 变更引起的损失降低,如果接口的单一职责做得好,就一个接口的修改只对相应实现类有影响,对其他接口没影响,这对系统的可扩展性、维护性都有帮助。


此原则难点在于对职责的定义,什么是类的职责、怎么划分类的职责?问题在于“职责”没有一个量化的标准,一个类到底要负责哪些职责?这些职责该怎么细化?细化后是否要有一个接口或一个类?这些需要在实际应用中考虑。

对于接口,设计时一定要做到单一。针对接口编程,而不是针对实现编程。对于实现类需要多方面考虑,对类的设计尽量做到只有一个原因引起变化。过分细分类的职责会人为地增加系统的复杂性,本来一个类可以实现的行为应分成两个类,然后使用聚合或组成的方式耦合在一起,这就人为造成系统复杂性。

里氏替换原则(Liskov Substitution Principle),简称LSP。

第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为不变,那么类型S是类型T的子类型。

第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

第二个定义可以理解为只要父类能出现的地方子类就能出现,而且将其替换成为子类也不会产生任何错误或异常,使用者可能并不需要知道是父类还是子类,但反之则不行,有子类出现的地方,父类未必能适应。
通俗地讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。里氏替换原则为良好的继承定义了一个规范,它包含4层含义:

1.子类必须完全实现父类的方法;
2.子类可以有自己的个性;
3.覆盖或实现父类的方法时输入参数可以被放大;
4.覆写或实现父类的方法时输出结果可以被缩小。

系统设计时,我们经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,这里就涉及到里氏替换原则。

针对第一条
在类中调用其他类时务必要使用父类或接口,如果不能用父类或接口,则说明类的设计已经违背LSP原则了。

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

针对第二条
子类当然是可以有自己的方法和属性,之所提出这条是因为里氏替换原则可以正着用,但不能反过来用。在子类出现的地方,父类未必能胜任。很多时候可以直接把子类传递进来,却不能直接使用父类传递进来,此时如果使用父类作为参数往往会在运行期抛出java.lang.ClassCastException异常,即向下转型(downcast)是不安全的。

针对第三条
方法中的输入参数称为前置条件,覆写或实现父类方法时,要求子类输入参数类型范围大于父类 ,小面通过代码示例说明:

//Father类,把HashMap转换为Collection集合类型public class Father{   public Collection doSomething(HashMap map){      System.out.println("父类被执行...");      return map.values();   }}
//Son子类public class Son extends Father{   public Collectioon doSomething(Map map){      System.out.println("子类被执行...");      return map.values();   }}


这里方法名相同,但输入参数类型不同,是重载。(这里是不在一个类里。)再看场景类代码:

public class Client{   public static void invoker(){      //父类存在的地方子类就应该能够存在      Father f = mew Father();      HashMap map = new HashMap();      f.doSomething(map);   }   public static void main(String[] args){      invoker();   }}

运行结果是:

父类被执行…


根据LSP,父类出现的地方子类就可以出现,

public class Client{   public static void invoker(){      //父类存在的地方子类就应该能够存在      Son f = mew Son();      HashMap map = new HashMap();      f.doSomething(map);   }   public static void main(String[] args){      invoker();   }}


运行结果是一样的,这里父类输入参数类型是HashMap,子类输入参数类型是Map,子类的输入参数类型范围扩大了,子类代替父类传递到调用者中,子类的方法不会被执行。如果想让子类的方法被执行,就必须覆写父类的方法。

这里是重载方法,扩大前置条件,就是输入参数类型范围宽于父类的类型范围。反过来,如果父类的输入参数类型宽于子类的输入参数类型范围,会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。即如果Father类doSomething()中参数是Map而Son类doSomething()中参数是HashMap,再引入LSP

public class Client{   public static void invoker(){      //父类存在的地方子类就应该能够存在      Son f = mew Son();      HashMap map = new HashMap();      f.doSomething(map);   }   public static void main(String[] args){      invoker();   }}


运行结果是:

子类被执行…

这里子类在没有覆写父类的方法的前提下,子类就被执行了,这会导致业务逻辑的混乱。实际应用中父类一般都是抽象类,子类是实现类,传递这样一个实现类会“歪曲”父类的意图。因此。子类中方法的前置条件必须与父类中被覆写方法相同或更宽松。

针对第四条
父类的方法返回值是类型T,子类对应方法(覆写或重载)的返回值是S,根据LSP要求S小于或等于T,即S和T要么同一类型,要么S是T的子类。覆写时,父类和子类方法名和参数相同,两个方法的范围值S小于T;重载时,要方法参数不同,根据LSP就要求子类输入参数宽于或等于父类输入参数,即这个方法是不会被调用的。

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

依赖倒置原则(Dependence Inversion Principle,DIP)

定义:High level moduals should not depend upon low level moduals. Both should depend upon abstractions. Abstractons should not depend upon details. Details should depend upon abstactions.

可以翻译成:
[] 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
[] 抽象不应该依赖细节;
[] 细节应该依赖抽象。

Java语言中,抽象一般指接口或抽象类,两者都不能被直接实例化;细节是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化,即可以加上一个关键字new产生一个对象。依赖倒置原则在Java语言中的表现是:
1. 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的;
2. 接口或抽象类不依赖与实现类
3. 实现类依赖接口或抽象类
更加精简的定义是“面向接口编程”——OOD面向对象设计。

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

依赖的三种写法

依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D……只要做到抽象依赖,即使是多层的依赖传递也可以。对象的依赖关系有三种方式来传递:

1. 构造函数传递依赖对象。

在类中通过构造函数声明依赖对象,也称为构造函数注入,代码示例:

public interface IDriver{   //是司机就应该会驾驶汽车   public void drive();}public class Driver implements IDriver{   private ICar car;   //构造函数注入   public Driver(ICar _car){    this.car = _car;   }   //司机主要职责是驾驶汽车   public void drive(){    this.car.run();   }}


2. Setter方法传递依赖对象。

在抽象中设置Setter方法声明依赖关系,称为Setter依赖注入,代码示例:

public interface IDriver{   //车辆型号   public void setCar(ICar car);   //司机驾驶汽车   public void drive();}public class Driver inplements IDriver{   private Icar car;   public void sertCar(ICar car){    this.car = car;   }   //司机主要职责驾驶汽车   public void driver(){    this.car.drive();   }}


3. 接口声明依赖对象。

在接口的方法中声明依赖对象,也叫作接口注入,代码示例:

public interface IDriver{   //司机驾驶汽车   public void drive(ICar car);}public class Driver implemens IDriver{   //司机驾驶汽车   public void drive(ICar car){    car.run();   }}


依赖倒置原则的本质是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,互不影响,实现模块间的松耦合。实际应用应遵循以下原则:

[] 每个类尽量都有接口或抽象类,或者抽象类和接口兼有。
这是依赖倒置的基本要求,有了抽象才可能依赖倒置;

[] 变量的表面类型尽量是接口或者抽象类。
Java中,定义变量必要有类型,一个变量可以两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型。如IDriver zhangSan = new Driver();这里zhangSan表面类型是IDriver,实际类型是Driver。但不绝对,如一个工具类xxxUtils一般不需要接口或是抽象类,还有如使用类的clone方法,就必须使用实现类,这是JDK的一个规范;

[] 类尽量不从具体类派生。
项目开发中一般不从具体类派生出子类。

[] 尽量不要覆写基类的方法
如果基类是抽象类且已经被实现,一般子类不覆写,类间的依赖是抽象的,覆写抽象方法,对依赖稳定性有影响。

[] 结合里氏替换原则使用。
接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确地实现业务逻辑,同时需要时对父类进行细化。

所谓依赖倒置中的“倒置”是相对于正常思维即面向实现编程而言的,依赖正置是类间的依赖,是实实在在的实现类之间的依赖;而编写程序需要对现实世界事物进行抽象,抽象的结果就是接口和抽象类,再根据系统设计需要产生抽象之间的依赖。

接口隔离原则(Interface Segregation Principle)

这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使
用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

迪米特法则(Low Of Demeter)

就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封
装在方法的内部,通过 public 方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦
合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。
局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

开闭原则(Open Close Principle)

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

设计的时候,时刻要考虑,尽量让这个类是足够好,写好了就不要去修改了,如果新需求来,我们增加一些类就完事了,原来的代码能不动则不动。这个原则有两个特性,一个是说“对于扩展是开放的”,另一个是说“对于更改是封闭的”。面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。

1 0