设计模式
设计模式是前辈对代码开发经验的总结,是解决特定问题的一系列套路。不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案
设计模式的本质是面对对·象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的 充分理解
优点
提高思维能力、编程能力和设计能力
是程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提供,从而缩短软件的开发周期
使代码可复用行高、可读性强、可靠性高、灵活性好、可维护性强
设计模式的基本要素
- 模式名称
- 问题
- 解决方案
- 效果
GoF23
- 创建型模式(对象创建与使用分离)
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
- 结构型模式
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
- 行为型模式
- 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
OOP七大原则
开闭原则:对扩展开放,对修改关闭
里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立
依赖倒置原则:要面向接口编程,不要面向实现编程。
单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。
接口隔离原则:要为各个类建立它们需要的专用接口.
迪米特法则:只与你的直接朋友交谈,不跟"陌生人”说话。
合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
单一职责原则:
控制类的粒度大小、将对象解耦、提高其内聚性。
用职责或变化原因来衡量接口或类的设计。
定义是:应该有且仅有一个原因引起类的变动
里氏替换原则:
继承必须确保超类所拥有的性质在子类中仍然成立
1、子类必须完全实现父类的方法
2、子类可以拥有自己的特性
3、子类覆写或实现父类方法时输入参数可以被放大; ==输入时父类的参数是子类对象,子类的参数是父类引用==
3、子类覆写或实现父类方法时输出参数可以被缩少;==输处时父类的返回类型是父类引用,子类的返回类型是子类对象==
发起和调用的关系
依赖倒置原则:
要面向接口编程,不要面向实现编程。
- 高层模块不应该依赖底层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
java表述
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象发生的
- 接口或抽象类不依赖于实现类
- 实现类依赖接口或抽象类
依赖传递的三种写法:
- 构造函数传递依赖对象
- Setter方法传递依赖对象
- 接口声明依赖对象
接口隔离原则:
要为各个类建立它们需要的专用接口.保证接口的纯洁性
接口隔离原则是对接口进行规范约束,其包含以下4层含义
1、接口要尽量小。注意:根据接口隔离原则拆分接口时,首先必须满足单一职责原则
2、接口要高内聚
3、接口设计是有限度的
迪米特法则
1、只与你的直接朋友交谈,不跟"陌生人”说话。
朋友类的定义: 出行在成员变量、方法的输入输出参数中的类称为成员朋友类,而出行在方法体内部的类不属于朋友类
2、与朋友之间也要保持距离
如果一个类大量引用的另外一个类。如果需要修改,必然导致大面积的修改。设计时反复衡量:==是否还可以再减少public方法和属性,是否可以修改为private、package-private(包类型,在类、方法、变量前不加访问权限,则默认为包类型)、protected等访问权限,是否可以加上final关键字等==
3、如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,那就放置在本类中
4、谨慎使用Serializable:很少出行
核心观念是:类间耦合,弱耦合。但是如果一个类跳转两次以上才能访问到另外一个类,就不太好了
开闭原则
对扩展开放,对修改关闭:一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化
软件实体包括以下部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块
- 抽象和类
- 方法
创建型模式(5)
单例模式
构造器私有,保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
饿汉式单例
/**
* @author cyp
* @date 2021/5/10 9:26
* @Description: 饿汉式单例模式
*/
public class Hungry {
private Hungry() {
}
private static final Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
因为一开始就加载的对象,会造成内存资源的浪费
懒汉式单例
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
此单例在单线程下没有问题,但多线程下无法正常运行,每次输出结果都不同
所以需要加锁
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+"ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
//不是原子性操作
/*
* 会分配空间
* 执行构造方法,初始化对象
* 把这个对象指向这个空间
*
* 会出行指令重排问题
* 多线程下一次是123 A
* 第二次是132 B
* 123
*
* 在属性前加volatile关键字
* */
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
静态内部类
public class Holder {
private Holder(){
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
}
单例不安全,都可以通过反射破解
public class LazyMan {
private static boolean cheng = false;
private LazyMan() {
synchronized (LazyMan.class){
if (!cheng){
cheng = true;
}else {
throw new RuntimeException("不允许破解单例");
}
if (lazyMan != null){
throw new RuntimeException("不允许破解单例");
}
}
System.out.println(Thread.currentThread().getName()+"方法成功运行");
}
public static void main(String[] args) throws Exception {
//第一次情况破坏单例,在已经new的情况下,通过反射破坏
/* 在构造器中加锁
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不允许破解单例");
}
}
* */
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
// Field cheng = LazyMan.class.getDeclaredField("cheng");
// cheng.setAccessible(true);
// cheng.set(instance,false);
LazyMan instance1 = declaredConstructor.newInstance();
// System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
//第二种情况破解单例,没有new,全是通过反射获取单例
/*
* 在构造器中加一个flog标签位
* */
// LazyMan instance2 = declaredConstructor.newInstance();
// System.out.println(instance2.hashCode());
// 第三种 获取到字段,修改字段
Field cheng = LazyMan.class.getDeclaredField("cheng");
cheng.setAccessible(true);
cheng.set(instance1,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance2.hashCode());
}
}
通过枚举创建单例
//enum 本身也是一个Class类
public enum EnumSingle {
/*
* 123
* */
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(enumSingle);
}
}
常见场景
- windows的任务管理器以及回收站
- 项目中 ,读取配置文件的类。一般也只有一个对象,无需每次都去new对象读取
- 网站的计数器,保证同步
- 数据库连接池的设计
- 在servlet编程中,每个servlet都是单例
- 在spring中,每个bean默认都是单例
工厂模式
作用
- 实现创建者和调用者分离
- 分类
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
核心本质
- 实例化对象不需要new,用工厂方法代替
- 将选择实现类,创建对象统一管理和控制,从而将调用者跟我们的实现类解耦
三种模式:
- 简单工程模式
- 用来生产同一等级结构中的任意产品(对于增加新的产品,需要球盖已有代码)
- 工厂方法模式
- 用来生成同一个级别的固定产品(支持增加任意产品)
- 抽象工厂模式
- 围绕一个超级工厂创建其它工厂
简单工厂模式
中间工厂类
/**
* @author cyp
* @date 2021/5/10 15:55
* @Description: 简单工厂模式(静态工厂模式)
*/
public class CarFactory {
public static final String WU_LING = "五菱";
public static final String TESLA = "特斯拉";
//方法1
public static Car getCar(String car){
if (car.equals(WU_LING)){
return new WuLing();
}else if (car.equals(TESLA)){
return new Tesla();
}
return null;
}
//方法2
public static Car getWuLing(){
return new WuLing();
}
public static Car getTesla(){
return new Tesla();
}
}
消费者获取
public static void main(String[] args) {
Car car1 = CarFactory.getCar(CarFactory.WU_LING);
Car car2 = CarFactory.getTesla();
car1.name();
car2.name();
}
工厂方法模式
为了满足开闭原则,将工厂写为一个接口,每种产品有自己的工厂实现
消费者获取
public static void main(String[] args) {
Car car1 = new WuLingFactory().getCar();
Car car2 = new TeslaFactory().getCar();
car1.name();
car2.name();
}
抽象工厂模式
定义
抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类
适用场景
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出行,从而使得客户端不依赖与具体的实现
优点
- 具体产品在应用层的代码隔离,无需关心创建的细节
- 将一个系列的产品同一到一起创建
缺点
- 规定了所有可能被创建的产品集合,扩展产品族比较困难,但是扩展产品等级是否容易,符合开闭原则
- 增加了系统的抽象性和理解难度
总结
结构复杂度,代码复杂度,代码复杂度,管理复杂度都是简单工厂模式更加低
虽然根据设计原则是工厂方法好,但是实际业务使用简单工厂模式更好
应用场景
- JDK中Calendar的getInstance方法
- JDBC中的Connection对象的获取
- Spring中IOC容器创建管理bean对象
- 反射中Class对象的newInstance方法
建造者模式
建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式。
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
主要作用: 在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。
用户只需要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)
优点
- 产品的建造和表示分离,实现了解耦。使用建造者模式可以使客户端不必知道产品内部组成的细节。
- 将复杂产品的创建步骤分解在不同的方法中, 使得创建过程更加清晰
- 具体的建造者类之间是相互独立的,这有利于系统的扩展。增加新的具体建造者无需修改原有类库的代码,符合“开闭原则”。
缺点
- 建造者模式所创建的产品-般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
原型模式
以某一个对象为原型复制一份新的对象
有原型模式三个方法
- Prototype
- Cloneable接口
- clone()方法—-》object类里面的
第一种实现:浅克隆
/**
* @author cyp
* @date 2021/5/11 17:09
* @Description: 以搬运视频为例,此类代表视频的原型
*
* 1.实现一个接口 Cloneable
* 2.重写一个方法 clone()
*/
public class Video implements Cloneable{
private String name;
private Date createTime;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
虽然v1与v2的hashcode不同,但组合对象date都是引用的同一个对象
第二种实现:深克隆
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
//实现深克隆的一种方法,改造clone方法。(序列化和反序列化也可以实现,但会涉及到io,影响效率)
Video v = (Video) obj;
//将对象的属性也进行克隆
v.createTime = (Date) this.createTime.clone();
return obj;
}
和final关键字冲突
优缺点
- 性能优越
原型模式是内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点
逃避构造函数的约束
这既是他的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点是减少了约束,缺点也是减少了约束,需要在实际应用中考虑
应用场景
资源优化场景
类初始化需要消化非常多的资源,这资源包括数据、硬件资源等
性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备和方法权限,则可以使用原型模式
一个对象多个修改者的场景
一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改器其值时,可以考虑使用原型模式拷贝多个对象供调用者使用
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过close的方法创建一个对象,然后由工厂方法提供给调用者。
结构型模式(7)
适配器模式
例如USB网线转换器
将一个类的接口转换成客户端希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作!
角色分析
- 目标接口:客户所期待的接口,目标可以是具体的或抽象的类,也可以是接口(电脑的usb接口)
- 需要适配的类:需要适配的类(网线)
- 适配器:通过包装一个需要适配的对象,把原来接口转换为目标对象(转换头)
public static void main(String[] args) {
//电脑,适配器,网线,类适配器实现
Computer computer = new Computer();//电脑
NetToUSB adapterByExtends = new AdapterByExtends();//适配器
computer.net(adapterByExtends);
//对象适配器实现
ReticleAdaptee reticleAdaptee = new ReticleAdaptee();//网线
NetToUSB adapterByCombination = new AdapterByCombination(reticleAdaptee);//适配器
computer.net(adapterByCombination);
}
对象适配器优点
一个对象适配器可以把多个不同的适配者适配到同一个目标
可以适配一一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。
类适配器缺点
对于Java、 C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配
在Java、 C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
适用场景
系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
适配器模式是一种补偿模式,或者说是一个“补救”模式,通常用来解决接口不相容的问题。在面对快速变更的业务需求,适配器模式是一种很好的补救方法。
桥接模式
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式, 又称为柄体(Handle and Body)模式或接口(Interfce)模式。
好处分析:
- 桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则, 复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法。极大的减少了子类的个数,从而降低管理和维护的成本
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度, 都不需要修改原有系统。符合开闭原则,就像一座桥, 可以把两个变化的维度连接起来!
劣势分析:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开
发者针对抽象进行设计与编程。 - 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
最佳实践:
如果一个系统需要在构建的抽象化角色和具体化角 色之间增加更多的灵活性,避免在两个层次之间建
立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色
可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将-个抽象化子类的对象和一个实现
化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需
要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,
桥接模式尤为适用。
场景
- Java语言通过Java虚拟机实现了平台的无关性。
- AWT中的Peer架构
- JDBC驱动程序也是桥接模式的应用之一。
思考?桥接模式+适配器模式
代理模式
SpringAOP的底层就是代理模式
代理模式也叫委派模式,是一项基本设计技巧。许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委派模式。
代理模式的分类:
- 静态代理
- 动态代理
类图定义
Subject抽象主题角色
抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求
RealSubject具体主题角色
也叫做委派角色、被代理角色。他才是业务逻辑的具体执行者
Proxy代理主题角色
也叫做委派类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委派给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作
通用源码
public interface Subject{
// 一个方法
public void request();
}
public class RealSubject implements Subject{
//实现方法
public void request(){
// 业务逻辑
}
}
public class proxy impements Subject{
private Subject subject = null;
public Proxy(){
this.subject = new Proxy();
}
public Proxy(Subject _subject){
this.subject = _subject
}
@override
public void request(){
this.before();
this.subject.request();
this.after();
}
private void before(){
}
private void after(){
}
}
静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
- 客户:访问代理对象的人
代理模式的好处:
- 可以使真实角色的操作更加纯粹,不需关注一些公共的业务
- 公共业务交给代理角色,实现业务的分工
- 公共业务发生扩展的时候。方便集中管理
缺点:
- 一个真实角色就会产生一个代理角色,代码量会翻倍,导致开发效率变低。
AOP就是使用代理模式
强制代理
从真实角色查找到代理角色,不运行直接访问代理角色。
通常情况下代理的职责并不一定单一,可以组合其他真实角色,也可以实现自己的职责。比如消息转发。代理角色可以为真实角色做各种处理。当然一个代理类,可以代理多个真实角色,并且真实角色一直可以有耦合关系。
动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口–JDK动态代理
- 基于类:CGLIB
- java字节码实现:javasist
需要了解两个类Proxy:代理,InvocationHandler:调用处理程序
InvocationHandler
- 每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时,方法调用将被编码并分派到其调用处理程序的
invoke
方法。 - 意思就是:调用处理程序,返回结果
Proxy
Proxy
提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。为某个接口创建代理
Foo
:InvocationHandler handler = new MyInvocationHandler(...); Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class); Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class). newInstance(handler);
或更简单地:
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] { Foo.class }, handler);
实现测试
package com.cheng.demo04;
import com.cheng.demo03.Rent;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
/**
* @author cyp
* @date 2021/4/16 21:47
* @Description: 用这个类自动生成代理类
*/
public class ProxyInvocationHandler implements InvocationHandler {
private Long startTime;
//被代理的接口
private Object target;
public void setTarget(Object target) {
this.target = target;
}
/**
* @Description: 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
* @Author: cyp
* @param : loader 类加载器来定义代理类
* @param : interfaces 代理类实现的接口列表
* @param : h 调度方法调用的调用处理函数
* @Date: 2021/4/16
* @return: 具有由指定的类加载器定义并实现指定接口的代理类的指定调用处理程序的代理实例
*/
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
/**
* @Description: 处理代理实例,并返回结果
* @Author: cyp
* @Date: 2021/4/16
* @param proxy : 调用该方法的代理实例
* @param method : 所述方法对应于调用代理实例上的接口方法的实例
* @param args : 包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数
* @return: null
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
methodHint(method.getName());
//动态代理的本质就是使用反射机制实现
Object result = method.invoke(target, args);
Thread.sleep(50);
runTime(method.getName());
return result;
}
private void methodHint(String msg){
System.out.println("使用了"+msg+"方法");
startTime = System.currentTimeMillis();
}
private void runTime(String msg){
Long endTime = System.currentTimeMillis();
System.out.println(msg+"方法一共使用了"+(endTime -startTime)+"毫秒");
}
}
```java
package com.cheng.demo04;
/**
* @author cyp
* @date 2021/4/16 21:19
* @Description:
*/
public class Client {
public static void main(String[] args) {
//真实角色
UserServiceImpl userService = new UserServiceImpl();
//代理,现在没有,通过处理程序生成代理
ProxyInvocationHandler pih = new ProxyInvocationHandler();
//通过调用程序处理角色来处理我们要调用的接口对象
pih.setTarget(userService);
//动态生成代理类
UserService proxy = (UserService) pih.getProxy();
proxy.add();
}
}
通用类图
装饰模式
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
相比较与代理模式,更加注重功能的变化。而代理模式是对代理过程的控制。装饰模式是代理模式的一共特殊应用
Component抽象构件
Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是原始的对象
public abstract class Component {
public abstract void operate();
}
ConcreteComponent具体构件
ConcreteComponent是最核心、最原始、最基本的接口或者抽象类的实现,最终要装饰的就是它
public class ConcreteComponent extends Component{
@Override
public void operate() {
System.out.println("核心");
}
}
Decorator 装饰角色
一般是一个抽象类。实现接口或者抽象类,它里面可不一定有抽象的方法,在它的属性里必然有一个private变量指向Component抽象构件
public abstract class Decorator extends Component{
private final Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operate() {
this.component.operate();
}
}
ConcreteDecorator具体装饰角色
把最核心、最原始、最基本的东西装饰成其他东西。
public class ConcreteDecorator1 extends Decorator{
public ConcreteDecorator1(Component component) {
super(component);
}
@Override
public void operate() {
this.method1();
super.operate();
}
private void method1(){
System.out.println("1号修饰");
}
}
测试
public class Client {
public static void main(String[] args) {
Component component = new ConcreteComponent();
component = new ConcreteDecorator1(component);
component = new ConcreteDecorator2(component);
component.operate();
}
}
1号修饰
核心
2号修饰
优点
- 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件
- 装饰模式是继承关系的一个替代方案。无法装饰类Decorator装饰多少层,返回的对象还是Component,实现还是is-a的关系
- 装饰模式可以动态地扩展一个实现类的功能。
缺点
对于装饰模式记住一点可以:多层的装饰是比较复杂的。如果出现问题,要一层层的定位,一旦是在最里层,工作量会十分麻烦。因此,尽量减少装饰类的数量,以便减低系统的复杂度。
使用场景
- 需要扩展一个类的功能,或给一个类增加附加功能
- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销
- 需要为一批的兄弟类进行改装或加装功能,当然首选装饰模式
装饰模式是对继承的有利补充。继承不是万能的,继承可以解决实际问题,但是在项目中要考虑诸如易维护、易扩展、易复用等,而且在一些情况下使用继承就会增加很多子类,而且灵活性非常差,自然维护会十分麻烦。
装饰模式可以替代继承,解决类膨胀的问题。而且继承是静态地给类增加功能,而装饰模式是动态的给类增加功能。有着非常好的扩展性
组合模式
主要描述部分与整体的关系:将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象使用具体一致性
Component抽象构件角色
定义参加组合对象的共有方法和属性,可以定义一些默认的行为和属性
Leaf叶子构件
叶子对象,其下再也没有其他的分支,也就是遍历的最小单位
Composite树枝构件
树枝对象,他的作用是组合树枝节点和叶子节点形成一个树形结构
应用
优点
高层模块调用简单
一棵树形结构中的所有节点都是Component,局部和整体对调用者来说没有任何区别,也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码
节点自由增加
使用了组合模式后,只有找到父节点,对于扩展是否容易,符合开闭原则
缺点
在场景类中直接使用了实现类,与依赖倒置原则冲突。
使用场景
- 维护1和展示部分-整体的场景,如树形菜单,文件和文件夹管理
- 从一个整体中能够独立出部分模块或功能的场景
扩展
真实的组合模式:关系型数据库字段实现
透明的组合模式:在抽象类中添加移除节点、获取节点以及添加节点的方法,
遍历:加一个父节点标识,可以支持倒序遍历
门面模式
也叫外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生,其通用类图
Facade门面角色
客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就是说没有实际的业务逻辑,只是一个委派类
subsystem子系统角色
可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的聚合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已
// 三个相邻类,处理相关的业务逻辑,可以认为是一个子系统的不同逻辑的处理模块
public class ClassA{
public void doSomethingA(){
}
}
public class ClassB{
public void doSomethingB(){
}
}
public class ClassC{
public void doSomethingC(){
}
}
public class Facade{
// 被委派的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
// 提供给外部的访问的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){
this.b.doSomethingB();
}
public void methodC(){
this.c.doSomethingC();
}
}
应用
优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
使用场景: 1、为复杂的模块或子系统提供外界访问的模块。 2、子系统相对独立。 3、预防低水平人员带来的风险。
注意事项:在层次化结构中,可以使用外观模式定义系统中每一层的入口。
注意事项
一个子系统可以有多个门面
门面已经庞大到不能忍受的程度
子系统可以提供不同的访问路径
门面不参与子系统内部的业务逻辑
门面只是提供一个访问子系统的一个路径而已,它不应该也不能参与具体的业务逻辑,否则就会产生一个倒依赖的问题:子系统必须依赖门面才能被访问。
享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。我们将通过创建 5 个对象来画出 20 个分布于不同位置的圆来演示这种模式。由于只有 5 种可用的颜色,所以 color 属性被用来检查现有的 Circle 对象。
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景: 1、系统有大量相似对象。 2、需要缓冲池的场景。
注意事项: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。
行为型模式(11)
模板方法模式
定义一个操作中算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
其中AbstractClass叫做抽象模板,他的方法分为两个类型:
- 基本方法:基本方法也叫做基本操作,是由子类实现的方法,并且再模板方法被调用
- 模板方法:可以有一个或几个,一般是一个具体的方法,也就是一个框架,实现对基本方法的调度完成固定的逻辑。
为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写
基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限
基础版
/**
* 做一道菜
*/
public abstract class CookDinnerModel {
/**
* 准备食物
*/
protected abstract void prepareFood();
/**
* 下锅
*/
protected abstract void aFriedDish ();
/**
* 加调料
*/
protected abstract void addSeasoning();
/**
* 出锅
*/
protected abstract void outPot();
final public void start(){
prepareFood();
aFriedDish();
addSeasoning();
outPot();
}
}
public class TomatoOmeletteModel extends CookDinnerModel {
@Override
protected void prepareFood() {
System.out.println("切两个西红柿");
System.out.println("敲三个鸡蛋,加盐搅拌均匀");
}
@Override
protected void aFriedDish() {
System.out.println("鸡蛋先下锅,炒半熟");
System.out.println("加入西红柿,小火翻炒");
}
@Override
protected void addSeasoning() {
System.out.println("加入适量盐味精以及糖");
}
@Override
protected void outPot() {
System.out.println("出锅装盘");
}
}
扩展版
/**
* 做一道菜
*/
public abstract class CookDinnerV2Model {
/**
* 准备食物
*/
protected abstract void prepareFood();
/**
* 下锅
*/
protected abstract void aFriedDish ();
/**
* 加调料
*/
protected abstract void addSeasoning();
/**
* 出锅
*/
protected abstract void outPot();
final public void start(){
prepareFood();
aFriedDish();
if (isAddSeasoning()) {
addSeasoning();
}
outPot();
}
// 钩子方法,默认需要加调料
protected boolean isAddSeasoning(){
return true;
}
}
public class TomatoOmeletteV2Model extends CookDinnerV2Model {
/**
* 根据个人的口味,是否要加调料
*/
private boolean addSeasoningFlag = true;
@Override
protected void prepareFood() {
System.out.println("切两个西红柿");
System.out.println("敲三个鸡蛋,加盐搅拌均匀");
}
@Override
protected void aFriedDish() {
System.out.println("鸡蛋先下锅,炒半熟");
System.out.println("加入西红柿,小火翻炒");
}
@Override
protected void addSeasoning() {
System.out.println("加入适量盐味精以及糖");
}
@Override
protected void outPot() {
System.out.println("出锅装盘");
}
@Override
protected boolean isAddSeasoning() {
return addSeasoningFlag;
}
public void setAddSeasoningFlag(boolean addSeasoningFlag) {
this.addSeasoningFlag = addSeasoningFlag;
}
}
public class SauteVegetableModel extends CookDinnerV2Model {
@Override
protected void prepareFood() {
System.out.println("洗三个青菜");
}
@Override
protected void aFriedDish() {
System.out.println("青菜下锅");
}
@Override
protected void addSeasoning() {
System.out.println("加入适量调料");
}
@Override
protected void outPot() {
System.out.println("出锅装盘");
System.out.println("撒一点葱花");
}
@Override
protected boolean isAddSeasoning() {
return false;
}
}
测试
public class Client {
public static void main(String[] args) {
// 基础版
TomatoOmeletteModel tomatoOmeletteModel = new TomatoOmeletteModel();
tomatoOmeletteModel.start();
//扩展版
TomatoOmeletteV2Model tomatoOmeletteV2 = new TomatoOmeletteV2Model();
// 不想加调料
tomatoOmeletteV2.setAddSeasoningFlag(false);
tomatoOmeletteV2.start();
SauteVegetableModel sauteVegetableModel = new SauteVegetableModel();
sauteVegetableModel.start();
}
}
/**
基础版西红柿炒鸡蛋:
切两个西红柿
敲三个鸡蛋,加盐搅拌均匀
鸡蛋先下锅,炒半熟
加入西红柿,小火翻炒
加入适量盐味精以及糖
出锅装盘
扩展版西红柿炒鸡蛋:
切两个西红柿
敲三个鸡蛋,加盐搅拌均匀
鸡蛋先下锅,炒半熟
加入西红柿,小火翻炒
出锅装盘
扩展版清炒青菜:
洗三个青菜
青菜下锅
出锅装盘
撒一点葱花
*/
总结
扩展版模板方法模式是父类调用子类方法应用需求的一种比较好的方式。
而其他方式:
- 把子类传递到父类的有参构造中,然后调用
- 利用反射的方式调用
- 父类调用子类的静态方法
但是这个三种方式不允许再项目中使用
而模板方法模式是另外一种角度,父类建立框架,子类再重写了父类部分的方法后,再调用从父类继承的方法,产生不同的结果
优点
- 封装不变的部分,扩展可变的部分
- 提取公共部分代码,便于维护
- 行为有父类控制,子类实现
缺点
一般的设计,抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法。但是模板方法模式却颠倒了,抽象类定义部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难道。
模板方法的使用场景:
- 多个子类有共有的方法,并且逻辑基本相同时
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现
- 重构时,模板方法时一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为
中介者模式
用一个中介者封装一系列的对象交互,中介者是各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变他们之间的交互
类图分析
Mediator抽象中介者角色
抽象中介者角色定义统一的接口,用于各同事之间的通信
ConcreteMediator具体中介者角色
具体中介者角色通过协调个同事角色实现协作行为,因此它必须依赖于各个同事角色
Colleagus同事角色
每一个同事角色都知道中介者角色,而且与其他的同事角色通信的时候,一定要通过中介者角色协作。每个同事类的行为分为两种:
一种是同事本身的行为,比如改变对象本身的状态,处理自己的行为等,这种行为叫做自发行为(Self-Method), 与其他的同事类或中介者没有任何的依赖:
第二种是必须依赖中介者才能完成的行为,叫做依赖方法(Dep Method)。
其中中介者对于同事采用set注入方式。同事一般采用构造器注入中介者。
优点
中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事件类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合。
缺点
中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
中介者模式的使用场景
中介者模式简单,但是简单不代表容易使用,很容易被误用。在面向对象的编程中,对象和对象之间必然会有依赖关系,如果某个类和其他类没有任何相互依赖的关系,那这个类就是一个“孤岛”在项目中就没有存在的必要了!。
类之间的依赖关系是必然存在的,一个类依赖多个类的情况也是存在的,存在即合理,那有多个依赖关系就考虑使用中介者模式呢?答案是否定的。中介者模式未必能把原本凌乱的逻辑整理得清清楚楚,而且中介者模式也是有缺点的,这个缺点在使用不当时会被放大,比如原本就简单的几个对象依赖关系,如果为了使用模式而加人了中介者,必然导致中介者的逻辑复杂化,因此中介者模式的使用需要“量力而行”!
中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中出现了蜘蛛网状结构。在这种情况下一定要考虑使用中介者模式,这有利于把蜘蛛网梳理为星型结构,使原本复杂混乱的关系变得清晰简单
最佳实践
首先,既然是同事类而不是兄弟类(有相同的血缘),那就说明这些类之间是协作关系,完成不同的任务,处理不同的业务,所以不能在抽象类或接口中严格定义同事类必须具有的方法(从这点也可以看出继承是高侵人性的)
。如果两个对象不能提炼出共性,那就不
要刻意去追求两者的抽象,抽象只要定义出模式需要的角色即可。当然如果严格遵守面向接口编程的话,则是需要抽象的,这就需要读者在实际开发中灵活掌握。其次,一个中介者抽象类一般只有一个实现者, 除非中介者逻辑非常复杂,代码量非常大。这时才会出现多个中介者的情况。所以,对于中介者来说,抽象已经没有太多的必要
。
中介者模式是一个非常好的封装模式,也是一个很容易被滥用的模式,一个对象依赖几个对象是再正常不过的事情,但是纯理论家就会要求使用中介者模式来封装这种依赖关系,这是非常危险的!使用中介模式就必然会带来中介者的膨胀问题,这在一个项目中是很不恰当的。
可以在如下的情况下尝试使用中介者模式:
- N个对象之间产生了相互的依赖关系(N>2)。
- 多个对象有依赖关系,但是依赖的行为尚不确定或者有发生改变的可能,在这种情况下一般建议采用中介者模式,降低变更引起的风险扩 散。
- 产品开发。一个明显的例子就是MVC框架,把中介者模式应用到产品中,可以提升产品的性能和扩展性,但是对于项目开发就未必,因为项目是以交付投产为目标,而产品则是以稳定、高效、扩展为宗旨。
命令模式
命令模式是一种高内聚的模式,其定义为:将一个请求封装成一个对象,从而然你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
命令模式的通用类图:
Receive接收者角色
该角色是干活的角色,命令传递到这里是应该被执行的,具体到例子就是Group三个实现类
Command命令角色
需要执行的所有命令都在这里声明
Invoker调用者角色
接收到命令,并执行命令,例子里代表项目经理
优点
类间解耦
调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只需调用Command抽
象类的execute方法就可以,不需要了解到底是哪个接收者执行。可扩展性
Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严重
的代码耦合。命令模式结合其他模式会更优秀
命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少
Command子类的膨胀问题。
缺点
命令模式也是有缺点的,请看Command的子类:如果有N个命令,问题就出来了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大,这个就需要读者在项目中慎重考虑使用。
通用代码
public abstract class Receiver{
//
public abstract void doSmoething();
}
/**
接收者可以多个,这要依赖业务的具体定义。有多个就需要定义一个所有特性的抽象集合。
*/
public class ConcreteReciver extends Receiver{
// 执行者的业务逻辑
@Override
public abstract void doSmoething(){
};
}
public abstract class Command{
// 定义一个子类全局共享变量
protected final Receiver receiver;
// 实现类必须定义一个执行者
public Receiver(Receiver receiver) {
this.receiver = receiver;
}
//
public abstract void execute();
}
/**
命令角色是命令模式的核心。根据环境要求,具体的命令类可以有N个,
*/
public class ConcreteCommand extends Command{
// 执行者
public ConcreteCommand() {
super(new ConcreteReciver());
}
// 必须实现一个命令
@Override
public abstract void execute(){
// 业务处理
super.receiver.doSmoething();
};
}
public class Invoker{
**
* 接受客户的一项命令
*/
private Command command;
public void setCommand(Command command) {
this.command = command;
}
/**
* 执行一项命令
*/
public void action(){
this.command.execute();
}
}
责任链模式
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
责任链模式的重点是在“链”上,由一条链去处理相似的请求再链中决定谁来处理这个请求,并返回相应的结果,核心在“链”上,“链”是由多个处理者ConcreteHandler组成的。
public abstract class Handler {
private Handler nextHandler;
public final Response handlerMessage(Request request){
Response response = null;
if (this.getHandlerLevel() == request.getRequestLevel()){
response = this.echo(request);
}else {
if (this.nextHandler != null){
response = this.nextHandler.handlerMessage(request);
}else {
// 没有适当的处理者
}
}
return response;
}
public void setNextHandler(Handler nextHandler){
this.nextHandler = nextHandler;
}
protected abstract Integer getHandlerLevel();
protected abstract Object echo(Request request);
}
抽象的处理者实现三个职责:
- 定义一个请求的处理方法handleMessage,唯一对外开放的方法
- 定义一共链的编排方法setNext,设置下一个处理者
- 定义了具体的请求者必须实现的两个方法:定义自己能够处理的级别getHandlerLevel和具体的处理任务echo
在处理者的中涉及的三个类:Level类负责定义请求和处理级别,Request类负责封装请求。Response负责封装链中返回的结果。
优点
责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。
缺点
责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试很不方便,特别是链条比较长,环节比较多的时候,由于采用了类似递归的方法,调试的时候逻辑可能比较复杂
注意事项
链中节点数量需要控制,避免出现超长链的情况,一般的做法是在handle中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。
策略模式
定义一组算法,将每个算法都封装起来,并且使它们之间可以互转。
策略模式使用的就是面对对象的继承和多态机制,非常容易理解和掌握,三个角色的功能分别是:
Context封装对象
这也叫上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在变化。
Strategy抽象策略角色
策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。
ConcreteStrategy具体策略角色
实际抽象策略中的操作,该类含有具体的算法。
策略模式的重点就是封装角色,它借用了代理模式的思路。和代理模式的区别就是策略模式的封装角色和被封装角色不是同一个接口,如果是同一个接口就是代理模式了。
通用代码
// 抽象策略角色
public interface Strategy {
public void doSomething();
}
// 具体策略角色
public class ConcreteStrategy1 implements Strategy{
@Override
public void doSomething() {
System.out.println("具体策略1的运算法则");
}
}
//封装角色
public class Context {
private final Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void doAnything(){
this.strategy.doSomething();
}
}
应用
优点
算法可以自由切换本身的定义,只有实现抽象策略,他就成为策略家族的一个成员,通过封装角色对其进行封装,保证对外提供“可自由切换”的策略
避免使用多重判断可以由其它模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断
扩展性良好
缺点
策略类数量增多每一个策略都是一个类,复用可能性很小,类数量增多
所有的策略类都需要对外暴露上层模块必须知道由哪些策略,然后才能决定使用哪一个策略。这与迪米特法则时相违背的。不过,这个缺点可以通过其他模式来修正,例如工厂方法模式、代理模式或享元模式
使用场景
- 多个类只有在算法或行为上稍有不同的场景
- 算法需要自由切换的场景
- 需要屏蔽算法规则的场景。(只需要知道方法名以及参数)
扩展(策略枚举)
public enum Calculator {
ADD("+"){
public int exec(int a, int b){
return a+b;
}
},
SUB("-"){
public int exec(int a, int b){
return a-b;
}
};
String value = "";
Calculator(String value) {
this.value = value;
}
public String getValue(){
return this.value;
}
public abstract int exec(int a, int b);
}
迭代器模式
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口:hasNext, next。
应用实例:JAVA 中的 iterator。
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
java中需要用的类型都自带迭代器,无须再自行手写迭代器
观察者模式
也叫发布订阅模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们。
应用实例: 1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。
优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而并不知道这些对象是谁。
- 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
注意事项: 1、JAVA 中已经有了对观察者模式的支持类
。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
备忘录模式
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。
如何解决:通过一个备忘录类专门存储对象状态。
关键代码:客户不与备忘录类耦合,与备忘录管理类耦合。
优点: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。
注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、备忘录的声明周期,3、备忘录的性能,不要在频繁建立备份的场景中使用备忘录模式(例如for循环中)
Originator发起人角色
记录当前时刻的内部状态,负责定义哪些属于备忘录范围的状态,负责创建和恢复备忘录数据
Memento备忘录角色
负责存储发起人对象的内部状态,在需要的时候提供发起人需要的内部状态
Caretaker备忘录管理员角色
对备忘录进行管理、保存和提供备忘录
扩展
clone方式的备忘录
public class Originator2 implements Cloneable{
private Originator2 backup;
private String state = "";
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public void createMemento(){
this.backup = this.clone();
}
public void restoreMemento(){
this.state = this.backup.state;
}
@Override
public Originator2 clone() {
try {
return (Originator2) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
注意点:
使用原型模式需要考虑深拷贝和浅拷贝的问题,在复杂的场景下会让程序逻辑异常混乱,出现错误也很难跟踪。因此原型模式的备忘录模式适用于比较简单的场景或比较单一的场景中
多状态的备忘录
多备份的备忘录
给管理备忘录的备忘录容器设置为map,有一个key进行映射
更换的封装
在内部类Memento中全部都private的访问权限。而管理员角色使用没有任何方法的IMemento接口访问。这种设计方法叫做双接口设计。一个是业务的正常接口,实现必要的业务逻辑,叫做宽接口;另外一个接口时一个空接口,什么方法都没有,其目的时提供给子系统外的模块访问,比如容器对象,叫做窄接口
访问者模式
封装一些作用与某种数据结构中的各元素的操作,他开业在不改变数据结构的前提下定义作用与这些元素的新的操作。
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
Visitor抽象访问者
抽象类或接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的
ConcreteVisitor具体访问者
它影响访问者访问到一个类后该怎么干,要做什么事情
Element抽象元素
接口或者抽象类,声明接收哪一类访问者访问,程序上通过accept方法中的参数来定义的
ConcreteElement具体元素
实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式
ObjectStruture结构对象
元素的产生者,一般容纳在多个不同类、不同接口的容器。
应用
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
扩展
统计
例如在访问者中设置一个求和字段,每次元素进来进行求和
多访问者
例如一个访问者做求和功能,一个访问者做报表展示功能
双分派
单分派: 处理一个操作时根据请求者的名称和接收到的参数决定的,在java中有静态绑定和动态绑定之说,他的实现时依据重载和覆写实现的。
public interface Role {
}
public class KongfuRole implements Role{
}
public abstract class AbsActor {
public void act(Role role){
System.out.println("演员可以扮演任何角色");
}
public void act(KongfuRole role){
System.out.println("演员可以扮演功夫角色");
}
}
public class OldActor extends AbsActor{
@Override
public void act(KongfuRole role) {
System.out.println("年纪大了。扮演不了功夫角色");
}
}
public class Client {
public static void main(String[] args)
AbsActor actor = new OldActor();
Role role = new KongfuRole();
actor.act(role);
actor.act(new KongfuRole());
// 演员可以扮演任何角色
// 年纪大了。扮演不了功夫角色
}
}
重载在编译器期就决定了要调用哪个方法,它是根据role的表面类型而决定调用act(Role role)方法,这是静态绑定。而actor的执行方法则是由其实际类型决定的,这是动态绑定(没理解)
而使用访问者模式可以解决这个问题,也就是双分派
双分派:意味着得到执行的操作决定于请求的类型和两个接收者的类型,它是多分派的一个特例。所以也说明java是一个支持双分派的单分派语言
public interface Role {
public void accept(AbsActor actor);
}
public class KongfuRole implements Role{
@Override
public void accept(AbsActor actor) {
actor.act(this);
}
}
public class Client {
AbsActor actor = new OldActor();
Role role = new KongfuRole();
// 单分派调用
actor.act(role);
actor.act(new KongfuRole());
// 双分派调用
role.accept(actor);
// 演员可以扮演任何角色
// 年纪大了。扮演不了功夫角色
// 年纪大了。扮演不了功夫角色
}
}
状态模式
当一个对象内在状态改变时允许其改变行为,这个对象看起来像是改变了其类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。
应用
优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构建语法树,定义终结符与非终结符。
关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。
应用实例:编译器、运算表达式计算。
优点: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。
缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。
使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 2、一些重复出现的问题可以用一种简单的语言来进行表达。 3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
创建类模式PK
工厂方法vs建造者
意图不同
在工厂方法模式立,我们关注的是一个产品整体,例如汽车,无须关心产品的各部分是如何创建处来的;但在建造者模式中,一个具体产品的产生是依赖各个部件的产生以及装配顺序,它关注的是“由零件一步一步组装出产品对象”。简单地说,工厂模式是一个对象创建的粗线条应用,建造者模式则是通过细线条勾出一个复杂对象,关注的是产品组成部分的创建过程。
产品的复杂度不同
工厂方法模式创建的产品一般都是单一性质产品,都是一个模板,而建造者模式创建的则是一个复合产品,它有各个部件复合而成,部件不同产品对象当然不同。
抽象工厂模式vs建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关系构建过程,只需要关心什么产品由什么工厂生成即可。而建造者模式则是要求按照指定的意图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
结构类模式PK
代理模式VS装饰模式
代理模式和装饰模式有非常相似的地方,甚至代码实现都非常相似。
区别:
代理模式
是把当前的行为或功能委派给其他对象执行,代理类负责接口限定:是否可以调用真实角色,以及是否对发送到真实角色的消息进行变形处理,他不对被主题角色(也就是被代理类)的功能做任何处理,保证原汁原味的调用。装饰模式
是在要保证接口不变的情况下加强类的功能,它保证的是被修饰的对象功能比原始对象丰富(也可以是减弱),但不做准入条件判断和准入参数过滤,如是否可以执行类的功能,过滤输入参数是否合规等。装饰模式是一种比较拘谨的模式,在实际开发中使用比较少,比较典型的是java.io.*包中大量实现装饰模式。==OutputStream out = new DataOutputStream(FileOutputStream(“test.txt”))==
装饰模式VS适配器模式
装饰模式和适配器模式在通用类图上没有太多的相似度,差别比较大,但是它们的功能有相似的地方:都是包装作用,都是通过委托方式实现其功能。不同点是:装饰模式包装的是自己的兄弟类,隶属于同一个家族(相同接口或父类),适配器模式则是修饰非血缘关系类,把一个非本家族的对象伪装成本家族的对象,注意是伪装,因此他的本质还是非相同接口的对象。
- 意图不同
- 装饰模式的意图是加强对象的功能;
- 而适配器模式关注的则是转化,它的注意意图是两个不同对象之间的转化。
- 施与对象不同
- 装饰模式装饰的对象必须是自己的同宗,也就是相同的接口或父类,只要具有相同的属性和行为的情况下,才能比较行为是否增加还是减弱;
- 适配器模式则必须是两个不同的对象,因为它着重于转换,只有两个不同的对象才有转换的必要
- 场景不同
- 装饰模式在任何时候都可以使用,只要是想增强类的功能
- 适配器模式则是一个补救模式,一般出现在系统成熟或已经构建完毕的项目中,作为一个紧急处理手段采用
- 扩展性不同
- 装饰模式很容易扩展,而且装饰类可以继续扩展下去
- 适配器模式不同,它在两个不同对象之间架起一座桥梁,建立容易,去除需要从系统整体考虑是否能够撤销。
行为类模式PK
命令模式VS策略模式
命令模式和策略模式的类图部分十分类似,只是命令模式多了一个接收者(Receiver)角色。它们虽然同为行为类模式,但是两者的区别还是很明显的。策略模式的意图是封装算法,它认为算法已经是一个完整的、不可拆分的原子业务,其意图是让这些算法独立,并且可以相互替换,让行为的变化独立于拥有行为的客户;而命令模式则是让动作的解耦,把一个动作的执行分为执行对象(接收者对象)、执行行为(命令角色),让两者相互独立而不相互影响。
当命令模式退化时,比如无接收者(接收者非常简单或者接收者是一个java的基础操作,无需专门编写一个接收者),在这种情况下,命令模式和策略模式类图完全一样,代码实现也比较类似,但是两者还是有区别:
关注点不同
策略模式:关注的时算法替代的问题,可以根据业务需求随时更改算法。换句话说,策略模式关注的是算法的完整性、封装性,只有具备这两个条件才能保证其可以自由切换
命令模式:关注的是解耦问题,如何让请求者和执行者解耦是它需要首先解决的,解耦的要求就是把请求的内容封装为一个个的命令,由接收者执行。由于封装了命令,就同时可以对命令进行多种处理,例如撤销、记录等
角色功能不同
策略模式:其中的具体算法是负责一个完整算法逻辑,它是不可再拆分的原子业务单元,一旦变更就是对算法整体的变更
命令模式:它关注命令的实现,也就是功能的实现。例如接收者的变更问题,它只影响到命令族的变更,对请求者没有任何影响,从这方面来说,接收者对命令负责,而与请求者无关。命令模式中的接收者只有符合六大设计原则,完全不用关心它是否完成了一个具体逻辑,他的影响范围也仅仅是抽象命令和具体命令,对它的修改不会扩散到模式外的模块。
使用场景不同
策略模式适用于算法变更的场景,而命令模式适用于解耦两个由紧耦合关系的对象场合或多命令多撤销的场景
策略模式VS状态模式
这两种模式的类图是否相似,都同时通过Context类封装一个具体的行为,都提供了一个封装的方法,是高扩展性的设计模式。但根据两者的定义:策略模式封装的是不同的算法,算法之间没有交互,以达到算法可以自由切换的目的;而状态模式是不同的状态,以达到状态切换行为随之发生改变的目的。这两种模式虽然都有变换的行为,达式两者的目标却不同。
区别
环境角色的职责不同
两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委派行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务
解决问题的重点不同
状体模式的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看好像是行为改变。而策略模式是为了解决内部算法如何改变的问题。
解决问题的方法不同
策略模式无法决定自身什么时候被调用;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果
应用场景不同
策略模式解决一系列平行的、可相互替换的算法封装后的结果;状态模式则要求由一系列状态发生变化的场景,它要求的是有状态且有行为的场景,也就是一个对象必须具有二维(状态和行为)描述才能采用状态模式,如果只有状态没有行为,则没有意义。
观察者模式VS责任链模式
在观察者模式中提到过触发链(也叫观察者链)的问题,一个具体的角色既可以是观察者,也可以是被观察者,这样就形成了一个观察者链。这就与责任链模式非常类似,它们都实现了事物的链条化处理。
例子
责任链模式实现DNS解析过程
请求者发出请求,由上海DNS进行解析,如果能够解析,则返回结果,若不能解析,则提交给父服务器(中国顶级DNS)进行解析,若还不能解析,则提交给全国顶级DNS进行解析。Recorder是一个BO对象,记录DNS服务器解析后的结果,包括域名,IP地址,解析者。
触发链模式实现DNS解析过程
在实际情况中会发现,无论是什么域名,返回的信息中解析者都是同一个DNS服务器解析。实际设计应该如下:
所有DNS服务器都具有双重身份:既是观察者也是被观察者,它声明所有的服务器都具有相同的身份标识,具有身份标识后就可以在链中随意移动,而无需固定在链中的某个位置。
方法setUpperServer的作用设置父DNS,也就是设置自己的观察者,update方法不仅仅是一个事件的处理者,也同时是事件的触发者。
两者模式的区别
- 链中的消息对象不同
从首节点开始到最终的尾节点,两个链中传递的消息对象是不同的。责任链模式基本上不改变消息对象的结果,虽然每个节点都可以参与消费(一般是不参与消费),类似于‘雁过拔毛’,但是它的结构不会改变,比如首节点传递进来一个String对象或者Person对象,不会到链尾的时候成了int对象或其他,这在责任链模式中是不可能的,但是在触发链模式中是允许的,链中传递的对象可以自由变化,只要上下级节点对传递对象了解即可,它不要求链中的消息对象不变化,它只要求链中相邻两个节点的消息对象固定。
- 上下节点的关系不同
在责任链模式中,上下级节点没有关系,都是接收同样的对象,所有传递的对象都是从链首传递过来,上一节点是什么没有关系,只要按照自己的逻辑处理就成。而触发链模式就不同了,他的上下级关系很亲密,下级对上级顶礼膜拜,上级对下级绝对信任,链中的任意两个相邻节点都是一个牢固的独立团体
消息的分销渠道不同
在责任链模式中,一个消息从链首传递过来后,就开始沿着链条想链尾运动,方向是单一的、固定的;而触发链模式则不同,由于它采用的是观察者模式,所有有非常大的灵活性,一个消息传递到链首后,具体怎么传递是不固定的,可以以广播方式传递,也可以以跳跃方式传递,这取决于处理消息的逻辑
跨区PK
策略模式VS桥梁模式
类体比较
两者是如此相似,只能从它们的意图上来分析。
策略模式:一种行为模式,旨在封装一系列的行为,在例子中把邮件的必要信息封装成一个对象就是一个行为,封装的格式(算法不同),行为也就不同。简单来说就是使用了继承和多态建立一套可以自由切换算法的模式
桥梁模式:是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,他的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中邮件服务器和邮件模板分别独立。简单来说就是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式
门面模式VS中介者模式
门面模式为复杂的子系统提供一个统一的访问界面,它定义的是一个高层接口,该接口使用得子系统更加容易使用,避免外部模块深入到子系统而产生与子系统内部细节耦合的问题。
中介者模式使用一个中介对象来封装一系列同事对象的交互行为,它使各对象之间不再显式地引用,从而使其耦合松散,建立一个可扩展的应用架构。
主要区别:
功能区别
门面模式只是增加了一个门面,他对子系统来说没有增加任何的功能,子系统若脱离门面模式完全可以独立运行。而中介者模式则增加了业务功能,它把各个同事类中的原有耦合关系移植到了中介者,同事类不可能脱离中介者而独立存在,除非是想增加系统的复杂性和降低扩展性
知晓状态不同
对门面模式来说,子系统不知道门面的存在,而对中介者来说,每个同事类都知道中介者存在,因为要依靠中介者调和同事之间的关系
封装程度不同
门面模式是一种简单的封装,所有的请求处理都委派给子系统完成,而中介者模式则需要有一个中心,由中心协调同事类完成,并且中心本身也完成部分业务,它属于更进一步的业务功能封装
包装模式群PK
包装模式:模式其中一个角色只是充当黔首作用,当一个请求过来时自身并不处理,而是让其他角色处理。注意
,包装模式是一组模式而不是一个,包括:装饰模式、适配器模式、门面模式、代理模式、桥梁模式。
这五种包装模式都具有相似的特征:都是通过委托的方式对一个对象或一系列对象施行包装,有了包装,设计的系统才更加灵活、稳定,并且极具扩展性。从实现角度来看,它们都是代理的一种具体表现形式。但是在使用场景上有着区别:
代理模式:
主要用在不希望展一个对象内部细节的场景中,比如一个远程服务不需要把远程连接的所有细节都暴露给外部模块,通过增加一个代理类,可以很轻松地实现被代理类的功能封装。此外,代理模式还可以用在个对象的访问需要限制的场景中,比如AOP。
装饰模式:
是一种特殊的代理模式, 它倡导的是在不改变接口的前提下为对象增强功能, 或者动态添加额外职责。就扩展性而言,它比子类更加灵活,例如在一个已经运行的项目中,可以很轻松地通过增加装饰类来扩展系统的功能。
适配器模式:
主要意图是接口转换,把一个对象的接口转换成系统希望的另外一个接口,这里的系统指的不仅仅是一个应用,也可能是某个环境,比如通过接口转换可以屏蔽外界接口,以免外界接口深入系统内部,从而提高系统的稳定性和可靠性。
桥梁模式:
是在抽象层产生耦合,解决的是自行扩展的问题,它可以使两个有耦合关系的对象互不影响地扩展,比如对于使用笔画图这样的需求,可以采用桥梁模式设计成用什么笔(铅笔、毛笔)画什么图(圆形、方形)的方案,至于以后需求的变更,如增加笔的类型,增加图形等,对该设计来说是小菜一碟。
门面模式:
是一个粗粒度的封装,它提供一个方便访问子系统的接口, 不具有任何的业务逻辑,仅仅是一个访问复杂系统的快速通道,没有它,子系统照样运行,有了它,只是更方便访问而已。