纯干货!23种设计模式大总汇
2016-09-15 本文已影响2940人
龙猫小爷
一、面向对象的六大原则
- 单一职责原则——Single Responsiblity Principle
- SRP的定义:就一个类而言,应该仅有一个引起它变化的原因(也就是说一个接口或类只有一个职责,只负责一件事情)。简单来说,一个类应该是一组相关性很高的函数、数据的封装。
-
这样就没有遵循单一职责原则,因为该接口同时包含了用户的属性和用户的行为。
- 这样也没有遵循单一职责原则,dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()实现的是数据的传送。
-
将其修改成如此结构就满足了单一职责原则。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘记了我们是面向接口编程,我们对外公布的是接口而不是实现类。
Phone phone=new Phone();
//面向接口编程,对外公布接口而不是实现类
IConnectionManager icm=(IConnectionManager)phone;
IDataTransfer idt=(IDataTransfer)phone;
- 单一原则适用于接口、类,同时也适用于方法。一个方法尽可能作一件事情。
- 对于类来说做到单一职责原则比较困难,所以接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
- 里氏替换原则——Liskov Substitution Principle
- 定义:所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
- 子类必须完全实现父类的方法。如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发 生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
- 子类可以有自己的个性。从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。
- 覆盖或实现父类的方法时输入参数可以被放大。 结果是:父类被执行。父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就 必须覆写父类的方法。
- 覆写或实现父类的方法时输出结果可以被缩小。父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆 写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一 个类型,要么S是T的子类。
- 依赖倒置原则——Dependence Inversion Principle
- 依赖倒置原则指代了一种特定的解耦形式,使得高层的模块不依赖与低层次的模块的实现细节的目的。
- 关键点:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节; 细节应该依赖抽象。
- 。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立地运行。
- 依赖的三种写法:构造函数传递依赖对象;setter方法传递依赖对象;接口声明依赖对象。
- 遵循的规则:
- 接口隔离原则——InterfaceSegregation Principles
- 定义:客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。说第一种“客户端不应该依赖它不需要的接口”,那依赖什么?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性;再看第二种定义:“类间的依赖关系应该建立在最小的接口上”,它要求是最小的接口,也是要求接口细化,接口纯洁,与第一个定义如出一辙,只是一个事物的两种不同描述。
-
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
public interface IGreatTemperamentGirl {
//要有气质
public void greatTemperament();
}
public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
private String name;
//美女都有名字
public PettyGirl(String _name){
this.name=_name;
}
//脸蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}
//气质要好
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}
- 接口隔离原则是对接口进行规范约束,包含4层含义:接口要尽量小,但是不能违反单一职责原则;接口要高内聚;定制服务,就是单独为一个个体提供优良的服务;接口的设计师有限度的。
- 迪米特原则——Law Of Demeter
- 定义:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。
- 迪米特法则告诉我们一个类只和朋友类交流。朋友类的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
- 一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少 public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前 不加访问权限则默认为包类型)、protected等访问权限,是否可以加上final关键字等。
-
- 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
- 开闭原则——Open Close Principle
- 定义:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。程序一旦开发完成,程序中的一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建类可以通过继承的方式来重用原类代码。
-
- 使用开闭原则——抽象约束:第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。
- 使用开闭原则——元数据控制模块行为:是尽量使用元数据来控制程序的行为,减少重复开发。元数据用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。通过扩展一个子类,修改配置文件,完成业务变化。
- 使用开闭原则——制定项目章程:章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
- 使用开闭原则——封装变化::第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或 抽象类中。
二、单例模式
http://callmegod.iteye.com/blog/1474441
- 定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。(许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为)
- 单利模式的关键点
- 构造函数不对外开放,一般为private
- 通过一个静态方法或者枚举返回单例类对象
- 确保单例类对象有且只有一个,尤其是在多线程环境下
- 确保单例类对象在反序列化时不会重新构建对象
- 单例模式通用代码——饿汉单例模式,在类加载时就生成实例。
public class Singleton{
private static final Singleton singleton=new Singleton();
private Stingleton(){}
public static Singleton getInstance(){
return singleton;
}
}
- 懒汉单例模式:每次调用getInstance()都会进行同步,这样会消耗不必要的资源。在第一次调用getInstance方法是才会生成实例。
public class Singleton{
private static Singleton singleton;
private Stingleton(){}
public static synchronized Singleton getInstance(){
if(singleton==null)
singleton=new Singleton();
return singleton;
}
}
- Double Check Lock(DCL)单例模式:既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。
public class Singleton{
private (volatile 防止编译器乱序执行) static Singleton singleton;
private Stingleton(){}
public static Singleton getInstance(){
//第一层判空是为了避免不必要的同步
if(singleton==null)
{
synchronized(Singleton.class){
//第二层判空则是为了在null情况下创建实例
if(singleton==null)
singleton=new Singleton();
}
}
return singleton;
}
}
- 静态内部类单例模式
public class MaYun {
private static class SigletonHolder {
private static final instance = new MaYun();
}
public static final getInstance() {
return SigletonHolder.instance;
}
private MaYun() {
//MaYun诞生要做的事情
}
- 枚举单例模式:实现简单,并且创建时线程安全,在任何情况下都是一个单例。无偿提供序列化机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候
public enum SingletonEnum{
INSTANCE;
public void doSomething(){...}
}
//使用
SingletonEnum.INSTANCE.doSomething();
三、工厂方法模式
- 定义
- 定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
-
在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂 ConcreteCreator完成的。
- 通用代码
- 工厂方法模式的优点
- 良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需 要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。
- 工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体 的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。
- 屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关 心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。也就是说一旦创建产品类的过程需要修改时,只要修改工厂类即可,不需要一个个地区修改每一个创建的对象。
- 工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!
- 工厂方法模式的扩展
- 缩小为简单工厂模式:一个模块仅需要一个工厂类,没有必要把它生产出来,使用静态的方法就可以了。简单工厂模式,去掉了工厂的抽象类,再把创建对象的方法改为静态类型,简化了类的创建过程。
- 升级为多个工厂类:当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰。考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。
- 代替单例模式
- 延迟初始化:一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用。
四、抽象工厂模式
- 抽象工厂模式定义
- 为创建一组相关或相互依赖的对象提供一个接口,而且无须指定他们的具体类。
-
有N个产品族,在抽象工厂类中应该有N个创建方法;有M个产品等级就应该有M个实现工厂类,在每个实现工厂中,实现不同产品族的生产任务。
- 抽象工厂模式的优点
- 封装性,每个产品的实现类不是高层模块要关心的,它要关心的是什么?是接口,是抽象,它不关心对象是如何创建出来,这由谁负责呢?工厂类,只要知道工厂类是谁,我就能创建出一个需要的对象,省时省力,优秀设计就应该如此。
- 产品族内的约束为非公开状态。
- 抽象工厂模式的缺点:产品族扩展非常困难。
- 使用场景:一个对象族(或是一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式。
五、模板方法模式
- 模板方法模式定义
- 定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 模板方法模式非常简单,仅仅使用了JAVA的继承机制。AbstractClass叫做抽象模板,它的方法分为两类:基本方法和模板方法。
- 基本方法:也叫做基本操作,是由子类实现的方法,并且在模板方法被调用。
- 模板方法:可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆盖。
- 抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为public类型。实现类若非必要,尽量不要扩大父类中的访问权限。
- 例子
- 模板方法模式的优点
- 封装不变部分,扩展可变部分:把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展。
- 提取公共部分代码,便于维护。
- 行为由父类控制,子类实现。
- 使用场景
- 多个子类有公有的方法,并且逻辑基本相同。
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通 过钩子函数(见“模板方法模式的扩展”)约束其行为。
- 扩展
- 子类的返回值影响了模板方法的执行结果,该方法就叫做钩子方法。
-
五、备忘录模式
- 例子
- 一个类如果想要保存和恢复其原来的状态,需要在类中增加一个方法或者其他的内部类来保存这个状态。但是这样做事对单一职责原则的一种破坏,例如,BOY类本身的职责是追女孩,而保留和恢复状态则应该由另外一个类来承担,这个类我们取名为备忘录。
-
- BOY类与备忘录类直接产生联系破坏了迪米特原则,因此多创建一个备忘录的管理类Caretaker对Memento进行管理,这就实现了保存多种状态的功能。
- 备忘录模式的定义
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象回复到原先保存的状态。
-
- 备忘录模式的使用场景
- 需要保存和回复数据的相关状态场景。
- 提供一个可回滚(rollback)的操作;比如Word中的CTRL+Z组合键,IE浏览器中的后 退按钮,文件管理器上的backspace键等。
- 需要监控的副本场景中。例如要监控一个对象的属性,但是监控又不应该作为系统的主业务来调用,它只是边缘应用,即使出现监控不准、错误报警也影响不大,因此一般的做法是备份一个主线程中的对象,然后由分析程序来分析。
- 备忘录模式扩展——clone方式的备忘录
- 该模式就是通过复制的方式产生一个对象的内部状态,恢复和保存都在对象的内部进行,只要实现Cloneable就可以。
-
- 使用Clone方式的备忘录模式,可以使用在比较简单的场景或者比较单一的场景 中,尽量不要与其他的对象产生严重的耦合关系。
- 备忘录模式扩展——多状态的备忘录模式
-
- BeanUtils通过反射的方法将对象的多个状态保存到HashMap中,并将HashMap交给备忘录进行保存。恢复时使用BeanUtils中的方法,将备忘录中保存的HashMap中的内容通过反射重新恢复。
-
-
-
- 多备份的备忘录
-
-
- 内存溢出问题,该备份一旦产生就装入内存,没有任何销毁的意向,这是非常危 险的。因此,在系统设计时,要严格限定备忘录的创建,建议增加Map的上限,否则系统很容易产生内存溢出情况。
六、原型模式
- 定义
- 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
-
-
- 原型模式的优点
- 性能优良:原型模式是对内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时。
- 避免构造函数的约束。
- 原型模式的使用场景
- 资源优化创景:类初始化需要消耗非常多的资源。
- 性能和安全要求的场景:通过new产生一个对象需要非常繁琐的数据准备或访问权限。
- 一个对象多个修改者的场景:一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时。
- 原型模式的注意事项
- 构造函数不会被执行:对象拷贝时构造函数确实没有被执行,这点从原理来讲也是可以讲得通的,Object类的 clone方法的原理是从内存中(具体地说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是非常正常的了。
- 浅拷贝:Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址(基本类型和string类型还是会被拷贝),这种拷贝就叫做浅拷贝。也就是说当拷贝对象修改了内部引用对象的值,那么原型对象内部引用对象的值也会被改变。 使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一是类的成员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是一个原始类型或不可变对象。
- 深拷贝:对私有的成员变量进行独立拷贝,原生对象和拷贝对象互不影响,这种拷贝就叫做深拷贝。
- 对象的clone与对象内的final关键字是有冲突的,所以要使用clone方法,类的成员变量上不要增加final关键字。
七、代理模式
- 定义
- 为其他对象提供一种代理以控制对这个对象的访问。代理模式也叫做委托模式,它是一项基本设计技巧。许多其他模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。
-
- Subject抽象主题角色:抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求; RealSubject具体主题角色:也叫做被委托角色、被代理角,它是业务逻辑的具体执行者;Proxy代理主题角色:也叫做委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。
public interface Subject {
//定义一个方法
public void request();
}
public class RealSubject implements Subject {
//实现方法
public void request() {
//业务逻辑处理
}
}
- 代理模式的优点
- 职责清晰:真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件事物,附带的结果就是编程简洁清晰。
- 高扩展性:具体主题角色是随时都会发生变化的,只要它实现了接口,无论如何变化,代理类完全就可以不在做任何修改的情况下使用。
- 智能化
- 代理模式的使用场景:只需专注地做好自己的工作,对于其他不属于自己的事情不关心,全部都交给代理来完成。例如,打官司要找律师,你不想参与中间过程的是是非非,只要完成自己的答辩就成,其他的比如事前调查、事后追查都由律师来搞定,这就是为了减轻你的负担。
- 代理模式的扩展——普通代理
- 首先说普通代理,它的要求就是客户端只能访问代理角色,而不能访问真实角色。也就是场景类不能再直接new一个GamePlayer对象了,它必须由GamePlayerProxy来进行模拟场景。
-
-
- 在该模式下,调用者只知代理而不用知道真实的角色是谁,屏蔽了真实角色的变更对高层模块的影响,真实的主题角色想怎么修改就怎么修改,对高层次的模块没有任何的影响,只要你实现了接口所对应的方法,该模式非常适合对扩展性要求较高的场合。
- 代理模式的扩展——强制代理
- 一般的思维都是通过代理找到真实 的角色,但是强制代理却是要“强制”,你必须通过真实角色查找到代理角色,否则你不能访 问。甭管你是通过代理类还是通过直接new一个主题角色类,都不能访问,只有通过真实角 色指定的代理类才可以访问,也就是说由真实角色管理代理角色。
-
- 代理类需要增加个性服务
- 一个类可以实现多个接口,完成不同任务的整合。也就是说代理类不仅仅可以实现主题接口,也可以实现其他接口完成不同的任务,而且代理的目的是在目标对象方法的基础上作增强,这种增强的本质通常就是对目标对象的方法进行拦截和过滤。
-
-
- 代理模式的扩展——动态代理
- 动态代理是在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。本章节的核心部分 就在动态代理上,现在有一个非常流行的名称叫做面向横切面编程,也就是AOP(Aspect Oriented Programming),其核心就是采用了动态代理机制。
-
- 在类图中增加了一个InvocationHandler接口和GamePlayIH类,作用就是产生一个对象的代理对象,其中InvocationHandler是JDK提供的动态代理接口,对被代理类的方法进行代理。
- 我们主要通过invoke方法来调用具体的被代理方法,也就是调用真实的方法。我们声明一个Object引用,该引用将指向被代理类,而我们调用被代理类的具体方法则在invoke方法中执行。 动态代理是根据被代理的接口生成所有的方法, 也就是说给定一个接口,动态代理会宣称“我已经实现该接口下的所有方法了”,那各位读者想想看,动态代理怎么才能实现被代理接口中的方法呢?默认情况下所有的方法返回值都是空的,是的,代理已经实现它了,但是没有任何的逻辑含义,那怎么办?好办,通过 InvocationHandler接口,所有方法都由该Handler来进行处理,即所有被代理的方法都由 InvocationHandler接管实际的处理任务。
-
- 将动态产生代理者的方法进行封装:
八、责任链模式
- 定义
- 使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关 系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
-
因为责任链中的请求和对应的处理规则是不尽相同的,在这种情况下可以将请求进行封装,同时对请求的处理规则也进行封装作为一个独立的对象。
-
-
-
- 责任链模式的优点:责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。
- 责任链模式的缺点::一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长,环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。
- 注意事项:链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。
九、建造者模式
- 定义
- 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
-
- Product产品类——通常是实现了模板方法模式,也就是由模板方法和基本方法;Builder抽象建造者——规范产品的组件,一般由子类实现;ConcreteBuilder具体建造者——实现抽象类定义的所有方法,并且返回一个组件好的对象;Director导演类——负责安排已有模块的顺序,然后告诉Builder开始建造。
- 建造者模式是将所有的先后顺序进行封装?如果我突然要增加一个过程,那不是要修改大量的代码吗?
-
- 例子:
- 建造者模式的优点
- 封装性:使用建造者模式可以使客户端不必知道产品内部组成的细节。
- 建造者独立,容易扩展。
- 便于控制细节风险:由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
- 使用场景:建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大不同的地方,虽然同为创建类模式,但是注重点不同。
- 建造者模式最主要的功能是基本方法的调用顺序安排,也就是这些基本方法已经实现了,通俗地说就是零件的装配,顺序不同产生的对象也不同;而工厂方法则重点是创建,创建零件是它的主要职责,组装顺序则不是它关心的。
十、桥接模式
- 定义
- 将抽象和实现解耦,使得两者可以独立地变化。(桥接模式其实就是将一个类中可能会变化的方法放出去,子类如果想要获得该方法,只要将桥梁搭过去即可)
-
- Abstraction抽象化角色——它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用;Implementor实现化角色——它是接口或者抽象类,定义角色必须的行为和属性;RefinedAbstraction修正抽象化角色——它引用实现化角色对抽象化角色进行修正;ConcreteImplementor具体实现化角色——它实现接口或抽象类定义的方法和属性。
- 抽象角色引用实现角色,或者说抽象角色的部分实现是由实现角色完成的。
-
- 桥接模式的优点
- 抽象和实现分离:这也是桥梁模式的主要特点,它完全是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。
- 优秀的扩充能力。
- 实现细节对客户透明:客户不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装。
- 桥接模式使用场景
- 不希望或不适用使用继承的场景。
- 接口或抽象类不稳定的场景。
- 重用性要求较高的场景:设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。
- 注意事项:桥梁模式的意图还是对变化的封装,尽量把可能变化的因素封装到最细、最小的逻辑单元中,避免风险扩散。因此读者在进行系统设计时,发现类的继承有N层时,可以考虑使用桥梁模式。
十一、访问者模式
- 定义
- 封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新操作。
-
- Visitor抽象访问者——抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的;ConcreteVisitor具体访问者——它影响访问者访问到一个类后该怎么干,要做什么事情;Element抽象元素——接口或者抽象类,声明接收哪一类访问者访问,程序上是通过accept方法中的参数来定义的;ConcreteElement具体元素——实现accept方法,通常是visitor.visit(this);ObjectStruture结构对象——元素生产者,一般容纳在多个不同类、不同接口的容器。
-
-
- 访问者模式的优点
- 符合单一职责原则:具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。
- 优秀的扩展性:由于职责分开,继续增加对数据的操作是非常快捷的。
- 灵活性非常高。
- 访问者模式的缺点
- 具体元素对访问者公布细节:访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的。
- 具体元素变更比较困难。
- 违背了依赖倒置原则:访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
- 访问者模式的使用场景
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
- 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操 作“污染”这些对象的类。
- 访问者模式的扩展——统计功能
-
- 当访问者依次访问所有的数据元素时,每访问一个就将其计数,就实现了统计的功能。
- 访问者模式的扩展——多个访问者:通过增加不同的访问者实现对数据元素不同的操作。
十二、中介者模式
- 例子
-
- 中介者最重要的方法叫做事件方法,处理多个对象之间的关系 可以根据业务的要求产生多个具体的中介者,并划分各中介者的职责。
- 定义
- 用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变他们之间的交互。
-
- Mediator抽象中介者角色——定义统一的接口,用于各同事角色之间的通信;Concrete Mediator具体中介者角色——通过协调各同事角色实现协作行为,因此它必须依赖于各个同事角色;Colleague同事角色——每一个同事角色都知道中介者角色,而且与其他的同事角色通信的时候,一定要通过中介者角色协作。
- 每个同事类的行为分为两种:一种是同事本身的行为,比如改变对象本身的状态,处理自己的行为等,这种行为叫做自发行为(Self-Method),与其他的同事类或中介者没有任何的依赖;第二种是必须依赖中介者才能完成的行为,叫做依赖方法(DepMethod)。
-
中介者所具有的方法doSomething1和doSomething2都是比较复杂的业务逻辑,为同事类服务,其实现是依赖各个同事类来完成的。
- 中介者模式的优缺点
- 中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合。
- 中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
- 中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中出现了蜘蛛网状结构。在这种情况下一定要考虑使用中介者模式,这有利于把蜘蛛网梳理为星型结构,使原本复杂混乱的关系变得清晰简单。
十三、享元模式
- 定义
- 享元模式是池技术的重要实现方式,其定义如下:使用共享对象可有效地支持大量的细粒度的对象。
- 要求细粒度对象,那么不可避免地使得对象数量多且性质相近,这些对象信息分为两个部分:内部状态与外部状态。
-
- Flyweight抽象享元角色——产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现;ConcreteFlyweight具体享元角色——具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。unsharedConcreteFlyweight不可共享的享元角色——不存在外部状态或者安全要求不能够使用共享技术的对象,这类对象一般不出现在享元工厂中;FlyweightFactory享元工厂——构造一个池容器,同时提供从池中获取对象的方法。
-
- 享元模式的优缺点:
- 享元模式的使用场景:
- 享元模式扩展——线程安全问题:享元模式极有可能导致线程不安全,当多个线程同时到享元工厂获取了相同的对象进行修改,必然会导致数据错误。这个时候必须通过一些手段保证线程的安全(同步互斥、非阻塞同步、无同步代码等)。所以我们在使用享元模式时,对象池中的享元对象尽量多,多道满足业务为止。
- 享元模式扩展——性能平衡:享元对象的外部状态最好以Java的基本类型作为标志,如String、int等,可以大幅地提升效率。虽然也可以使用自己定义的类作为标志,但是代码复杂且效率低下。
- 虽然可以使用享元模式可以实现对象池,但是这两者还是有比较大 的差异,对象池着重在对象的复用上,池中的每个对象是可替换的,从同一个池中获得A对 象和B对象对客户端来说是完全相同的,它主要解决复用,而享元模式在主要解决的对象的共享问题,如何建立多个可共享的细粒度对象则是其关注的重点。
十四、外观模式
- 定义
- 要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。外观模式提供一个高层次的接口,使得子系统更易于使用。
- 门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生。
- Facade门面角色——客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就是说该角色没有实际的业务逻辑,只是一个委托类。SubSystem子系统角色——可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。
-
- 外观模式的优点
- 减少系统的相互依赖:所有的依赖都是对门面对象的依赖,以子系统无关。
- 提高了灵活性:不管子系统内部如何变化,只要不影响到门面对象,就可以自由活动。
- 提高安全性:限定用户访问子系统的业务,不在门面上开通的方法,无法访问。
- 外观模式的缺点:门面模式最大的缺点就是不符合开闭原则,对修改关闭,对扩展开放,看看我们那个门面对象吧,它可是重中之重,一旦在系统投产后发现有一个小错误,你怎么解决?完全遵从开闭原则,根本没办法解决。继承?覆写?都顶不上用,唯一能做的一件事就是修改门面角色的代码,这个风险相当大,这就需要大家在设计的时候慎之又慎,多思考几遍才会有好收获。
- 外观模式使用场景
- 一个子系统可以有多个外观,当外观已经过于庞大时,可以进行业务拆分。
- 外观不参与子系统内的业务逻辑,否则会产生一个倒依赖的问题:子系统必须依赖外观才能被访问。决解办法就是将逻辑封装:外观模式中,外观角色应该是稳定的,不应该经常变化
十五、命令模式
-
- 定义
- 命令模式是一个高内聚的模式,将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
- Receiver接收者角色——该角色就是干活的角色,命令传递到这里是应该被执行的;Command命令角色——需要执行的所有命令都在这里声明;Invoker调用者角色——接收到命令,并执行命令。
- 命令模式的优点
- 类间解耦:调用者角色与接收者角色之间没有任何依赖关系,调用者实现实现功能时只需调用Command抽象类的execute方法即可。
- 可扩展性:Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合。
- 命令模式的缺点:如果有N个命令,问题就出来 了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大,这个就需要读者在项目中慎重考虑使用。
-
十六、观察者模式
- 例子
- Observable:实现该接口的都是被观察者;Observer:实现该接口的都是观察者。这样做就完成了一个被观察者可以被多个观察者观察的需求。
- 定义
- 观察者模式也叫做发布订阅模式:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
- 观察者模式的优点
- 观察者和被观察者之间是抽象耦合。
- 建立一套触发机制。
- 观察者模式的缺点:观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发 和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式。
- 观察者模式的使用场景:
- 观察者模式扩展——Java世界
- 在Java中一开始就提供了一个可扩展的父类,即java.util.Observable,这个类喜欢把自己的状态变更让别人去欣赏,去触发。同时Java还提供了Observer接口。
- 项目中真实的观察者模式
- 观察者与被观察者之间的消息沟通:被观察者状态改变会触发观察者的一个行为,同时会传递一个消息给观察者,这是正确 的,在实际中一般的做法是:观察者中的update方法接受两个参数,一个是被观察者,一个 是DTO(Data Transfer Object,据传输对象),DTO一般是一个纯洁的JavaBean,由被观察者生成,由观察者消费。
- 观察者响应方式:一是采用多线程技术,甭管是被观察者启动线程还是观察者启动线程,都可以明显地提高系统性能,这也就是大家通常所说的异步架构;二是缓存技术,甭管你谁来,我已经准备了足够的资源给你了,我保证快速响应,这当然也是一种比较好方案,代价就是开发难度很大,而且压力测试要做的足够充分,这种方案也就是大家说的同步架构。
- 被观察者尽量自己做主:被观察者的状态改变是否一定要通知观察者呢?不一定吧,在设计的时候要灵活考虑,否则会加重观察者的处理逻辑,一般是这样做的,对被观察者的业务逻辑 doSomething方法实现重载,如增加一个doSomething(boolean isNotifyObs)方法,决定是否通知观察者,而不是在消息到达观察者时才判断是否要消费。
十七、装饰模式
- 例子
- 定义
- 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。(类似于代理模式,都是对原始的对象进行更改)
- Component抽象构件——是一个接口或者抽象类,就是定义我们最核心的对象,也就是最原始的对象(在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件);ConcreteComponent具体构件——最核心、最原始、最基本的接口或抽象类的实现,需要装饰的就是该组件;Decorator装饰角色——一般是一个抽象类,它的属性里必然有一个private变量指向Component抽象构件;ConcreteDecorator具体装饰角色——把最核心、最原始、最基本的东西装饰成其他东西。
- 装饰模式优点
- 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
- 装饰模式是继承关系的一个替代方案。我们看装饰类Decorator,不管装饰多少层,返回的对象还是Component,实现的还是is-a的关系。
- 装饰模式可以动态地扩展一个实现类的功能。
- 装饰模式的缺点:多层的装饰是比较复杂的。为什么会复杂呢?你想想看,就像剥洋葱一样,你剥到了最后才发现是最里层的装饰出现了问题,想象一下工作量吧,因此,尽量减少装饰类的数量,以便降低系统的复杂度。
- 装饰模式最佳实践:
十八、策略模式
- 定义
- 定义一组算法,将每个算法都封装起来,并且使他们之间可以互换。
- Context封装角色——也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化;Strategy抽象策略角色——策略的抽象,通常为借口,定义每个策略必须具有的方法和属性;ConcreteStrategy具体策略角色——实现抽象策略中的操作,该类含有具体的算法。
- 策略模式的重点就是封装角色,它是借用了代理模式的思路。它和代理模式的差别就是策略模式的封装角色和被封装的策略类不用是同一个接口,如果是同一个接口那就成了代理模式。
- 策略模式的优点
- 算法可以自由切换:只要实现抽象策略即可,通过封装角色对其进行封装,保证对外提供“可自由切换”的策略。
- 避免使用多重条件判断:使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
- 扩展性良好。
- 策略模式的缺点
- 策略类数量增多。
- 所有的策略类都需要对外暴露。
- 如果系统中的一个策略家族的具体策略数量超过4个,则需要考虑使用混合模式,解决策略类膨胀和对外暴露的问题,否则日后的系统维护就会成为一个烫手山芋,谁都不想接。(使用工厂方式模式、代理模式或享元模式)。
十九、适配器模式
- 定义
- 将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起的工作的两个类能够在一起工作。
- 适配器模式就是把一个接口或类转换成其他的接口或类,从另一方面来说,适配器模式也就是一个包装模式。
- Target目标角色——该角色定义把其他类转换为何种接口,也就是我们期望接口;Adaptee源角色——已经存在的、运行良好的类或对象,经过适配器角色的包装,重新成为另一个角色;Adapter适配器角色——把源角色转换为目标角色,通过继承或是类关联的方式。
- 适配器模式的优点
- 适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定就成。
- 增加了类的透明性。
- 提高了类的复用度。
- 灵活性非常好:灵活使用删除适配器。
- 适配器模式的使用场景:
- 适配器模式的注意事项:这个模式使用的主要场景是扩展应用,系统扩展了,不符合原有设计的时候才考虑通过适配器模式减少代码修改带来的风险。同时,项目一定要遵守依赖倒置原则和里氏替换原则,否则即使在使用适配器的场合下,也会带来非常大的改造。
- 适配器模式的扩展
- 适配器模式不一定要通过继承,当出现了多个接口时可以使用关联。
二十、迭代器模式
- 定义
- 目前已经是一个没落的模式,基本上没人会单独写一个迭代器。它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
- Iterator抽象迭代器——抽象迭代器负责定义访问和遍历元素的接口,而且基本上是有固定的3个方法,first、next、isDone;ConcreteIterator具体迭代器——具体迭代器角色要实现迭代器接口,完成容器元素的遍历;Aggregate抽象容器——容器角色负责提供创建具体迭代器角色的接口,必然提供一个类似createInterator这样的方法,在Java中一般是iterator方法;Concrete Aggregate具体容器——实现容器接口定义的方法,创建出容纳迭代器的对象。
二十一、组合模式
- 定义
- 组合模式也叫做合成模式,有时又叫做部分-整体模式,主要是用来描述部分与整体的关系。将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
- Component抽象构件角色——定义参加组合对象的公有方法和属性,可以定义一些默认的行为或属性;Leaf叶子构建——叶子对象,其下再没有其他的分支;Composite树枝构建——组合树枝节点和叶子结点形成一个树形结构。
- 组合模式的优点
- 高层模块调用简单:一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别,也就是说,高层模块不关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。
- 节点自由增加:要增加一个树枝结点、树叶结点非常容易,只要找到它的父节点就可以,非常容易扩展。
- 组合模式的缺点:组合模式有一个非常明显的缺点,看到我们在场景类中的定义,提到树叶和树枝使用时的定义了吗?直接使用了实现类!这在面向接口编程上是很不恰当的,与依赖倒置原则冲突,读者在使用的时候要考虑清楚,它限制了你接口的影响范围。
- 组合模式扩展——透明的组合模式
- 透明模式是把用来组合使用的方法放到抽象类中,比如add、remove等方法,不管叶子对象还是树枝对象都有相同的结构,通过判断是getChildren的返回值确认是叶子结点还是树枝结点,如果处理不当,会在运行期出现问题,所以不是很建议。
- 组合模式的扩展——组合模式的遍历
- 树从上到下遍历没有问题,但是如果要从下往上遍历呢?
二十二、状态模式
- 定义
- 当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。状态模式的核心是封装,状态的变更引起了行为的变更,从外部看起来就好像这个对象对应的类发生了改变一样。
- State抽象状态角色——抽象或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换;ConcreteState具体状态角色——每一个具体状态必须完成两个职责,本状态的行为管理以及趋向状态处理,也就是本状态要做的事情,以及本状态如何过渡到其他状态;Context环境角色——定义客户端需要的接口,并且负责具体状态的切换。
- 状态模式的优点
- 结构清晰:避免了过多的switch..case或者if..else语句的使用,避免了程序的复杂性,提高系统的可维护性。
- 遵循设计原则:很好地体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就要增加子类,你要修改状态,你只修改一个子类就可以了。
- 封装性好:状态变换放置到类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变化。
- 状态模式的缺点:子类太多,也就是类膨胀。如果一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理,这个需要大家在项目中自己衡量。其实有很多方式可以解决这个状态问题,如在数据库中建立一个状态表,然后根据状态执行相应的操作。
- 状态模式的使用场景:行为随状态改变而改变的场景;条件、分支判断语句的替代者。
二十三、解释器模式
- 定义
- 解释模式是一种按照规定语法进行解析的方案,在现在项目中使用较少。给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
- AbstractExpression抽象解释器——具体的解释任务由各个实现类完成;TerminalExpression终结符表达式——实现与文法中的元素相关联的解释操作,通常一个解释器模式只有一个终结符表达式,但是有多个实例,对应不同的终结符;NonterminalExpression非终结符表达式——文法中的每条规则对应于一个非终结表达式,非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式;Context环境角色。
- 解释器模式的优点:其实一个简单语法分析工具,最著名的有点就是扩展性,修改语法规则只要修改相应的非终结符表达式就可以了,若扩展语法,则只要增加非终结符类就可以了。
- 解释器模式的缺点
- 解释器模式会引起类膨胀:每个语法都要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,为维护带来了非常多的麻烦。
- 解释器模式采用递归调用方法:每个非终结符表达式只关系与自己有关的表达式,每个表达式需要知道最终的结果,必须一层层剥茧,无论是面向过程还是面向对象,递归都是在必要条件下使用的。
- 效率问题:解释器模式使用了大量的循环和递归,效率是一个不容忽视的问题。