设计模式 精华一页纸

    xiaoxiao2021-04-14  44

    设计模式自从推出就一直很火,个人的体验是,模式运用存乎于心,理解最重要。重点是几个理念,从理念出发去理解模式;面向接口编程、消除重复、职责单一、接口隔离、开放-封闭等。而不是死记硬背和硬套各种模式。 本文从一个简单场景,结合理念,引出一些常用模式。

    1、一个需求引发的模式大战


    场景:设计一个文件读功能的模块 // 符合面向接口原则 设计一个 读接口 Reader; interface Reader{ public void read(byte[] data); }

    // 普通流读写 class IOReader implements Reader{ }

    // NIO通道读写 class NIOReader implements Reader{ }

    // AIO异步通道读写 class NIOReader implements Reader{ }

    产生策略模式 Strategy 对于一个操作,实现不同的策略读写 Client 应用,只需要持有一个 Reader的读写句柄(引用)即可。

    编码后发现,读的步骤都差不多,打开文件、获取输入句柄、读取块、关闭文件,这些重复需要消除,进行重构

    abstract class CommonReader implements Reader{ abstract void openHandle(); abstract void readSimple(byte[] data); abstract void close(); void read(char[] data){ openHandle(); readSimple(data); close(); } }

    class IOReader extends CommonReader{ }

    这次重构产生了 Template Method 模板模式 基础抽象类,实现了算法逻辑 继承子类,实现算法过程的抽象

    功能设计好后,应用开始使用 Reader myReader = new XXXReader(); 此处 违背了开放-封闭、接口隔离原则,对于应用而言并不需要知道具体实现类,实现类的变化与应用无关,应用只需关注接口的功能实现。

    Reader myReader = Factory.getReader();

    产生 Factory 工厂模式/Abstact Factory 抽象工厂模式 由工厂提供具体的实现 最简单,工厂里面根据情况直接new 对象 稍微深入一点,通过反射 依赖查找,实现动态可配置 最终就是Ioc的概念,依赖注入;你看,依赖注入其实并不复杂。 另外,从这里可以认识,接口和抽象类的本质区别了吧。

    新增需求一,能够读写 hdfs 集群上的文件,hdfs上的文件系统是单独构建的文件系统,和普通的文件系统API不同

    class HdfsReader implements Reader{ HdfsFileSystem system void read(char[] data){ system.operationXXX(); } }

    产生 adapter 适配器模式 两个系统融合,或者扩展两个接口,或者持有另一个系统的对象, 把相关操作委托给这个系统对象

    因为读写集群连接比较耗资源,HdfsFileSystem 只需要一个实例,并维护。 class HdfsFileSystem{ private HdfsFileSystem(); }

    产生 Singleton 单例模式 - 单例模式有很多变化 饿汉:一开始就提供实例化的对象 懒汉:用户第一次使用时就提供对象 枚举:因为在多线程情况下的问题(未实例化就被引用),强烈推荐 枚举方式的单例 新增需求二,现在每次读取先需要记录日志 首先想到的是修改 CommonReader,HdfsReader。 但,代码貌似重复了,更重要的是 职责不单一了,读 Reader 不应该关注其他功能。

    class LoggerReader implements Reader{ Reader reader void read(char[] data){ logOperationXXX; reader.read(data); } }

    产生 proxy 代理模式 把对一个对象的调用 代理/委托给 另一个对象 屏蔽隔离一个对象的直接调用 典型应用,java Collections 的只读、线程安全包装;java的动态代理

    新增需求三,读取需要记录线程号,要判断是否有读取权限...... 这些功能可以任意组合,如果扩展子类,则不符合 职责单一;更容易产生重复 ThreadReader implements Reader SecurityReader implements Reader

    产生 Decorator 装饰者模式 不同功能的实现,可以不断叠加和组合在基础功能之上。 典型应用 Java IO 流 zip(buffer(file))

    adapter VS proxy VS Decorator 三种模式看起来类似,区别在于使用的目的和用途不同 adapter - 两套异构系统对接时,对上层应用统一接口,在adapter类里面实现差异转换 proxy - 隔离、拦截 对目标对象的访问,经典的AOP模式就来源于此 Decorator - 同源,差异的功能叠加和组合

    新增需求四,监控凌晨3:00 - 5:00 读取文件的发消息通知告警 按照职责单一告警当然是一个独立的模块,假设告警接口 interface Alarm{ void alarm(String msg); }

    怎么通知呢,当前对象肯定要知道被通知的对象,也就是说需要持有被通知对象的句柄(引用)

    class AlarmReader implements Reader{ Reader reader; Alarm alarm; public void setAlarm(Alarm alarm){ this.alarm = alarm; } // 是否凌晨 boolean isNight(); void sendMessage(String msg){ alarm.alarm(msg); } void read(char[] data){ if(isNight) sendMessage } }

    AlarmA implements Alarm{ AlarmReader reader; public void register(){ reader.setAlarm(this); } }

    如果有多个告警组件的话 classAlarmReader implements Reader{ Reader reader; List<Alarm> alarmList; public void add(Alarm alarm){ alarmList.add(alarm); } void sendMessage(String msg){ for(Alarm alram: alarmList) alarm.alarm(msg); } void read(char[] data){ if(isNight) sendMessage } }

    产生Observer 观察者模式 一个对象关注另一个对象的状态。通过向这个对象注册,在对象状态发生变化时,通知关注的对象;观察者模式,在事件处理中非常多 观察者模式有很多演进 一、发送变化,有 推模式 - 即把数据发给 关注对象; 拉模式 - 即只发通知给 关注对象, 由关注对象自己取数据。 事实上,所有消息系统都有这两种模式,常见的 ActiveMQ/Kafka 都有这种设计。 二、现在的关注者 和 被关注者,紧密耦合,可以进一步拆分,由一个中介模块来实现注册和通知,这样关注者和被关注者互相之间不需要知道 进一步的强化了职责单一、开放-封闭原则

    class AlarmReader implements Reader{ Reader reader; Mediator mediator; void setMediator(Mediator mediator){ this.mediator = mediator; } // 是否凌晨 boolean isNight(); void sendMessage(){ mediator.alarm(); } void read(char[] data){ if(isNight) sendMessage } } class Mediator{ void add(Alarm alarm); void register(AlarmReader reader); void sendMessage(String msg){ for( XXX ) alarm.alarm(msg) } }

    产生Mediator 中介模式 如前所述,中介者模式,就是把两个模块通讯和交互,集中到中介者这个处理模块。中介者模块是解开 循环引用,复杂性的一个重要设计模式。像观察者这种模式,两个模块互相引用,如果设计不好耦合过多,后面就很难维护。所以,观察者模式应该尽量抽象向中介者模式靠齐。

    新增需求五 读操作需要支持多线程并发读 多线程执行框架,考虑IO阻塞,把 调用 和 执行分离开来。

    产生 Command 模式 把调用和执行分开来,最典型运用,就是 java并发的 Executors 执行框架。可以参照本博的《java 并发编程精华一页纸》,把可能耗时的部分,都封装在 Comand中,提交给执行框架执行。

    产生 Active Object 模式(非 23 种模式) ExecutorService ,持有线程句柄,执行自己管理自己的状态

    新增需求六 假设读数据只是流程的第一步,读完以后,需要把数据发送给 消息系统,最后还要插入数据库 此时对同一批数据的操作,形成了类似流水线的用途 怎么设计? 有几个点:第一、每一步完成与否,怎么感知;第二、下一步的流程要能灵活配置 首先想到的是 中介模式 每个步骤持有一个中介,处理完成以后,继续下一步 中介持有所有的 流程对象 -- 这种场景下,使用中介模式的缺点,很快中介就会成为 热点代码。而且 任何步骤的变化,中介模式改动都比较多。

    如果,让每个步骤自己持有下一步的操作呢? 就像 链表一样,很容易加入或者去掉 任何一个节点。

    iterface Operate{ void operate(String msg); }

    abstract AbstractOperate implements Operate{ Operate next; void operate(String msg); public void setNext(Operate next){ this.next = next; } }

    Class XXXOperate{ void operate(String msg){ xxx next.operate(xxx); } }

    产生 COR 职责链模式  如同链表一样,每个对象持有一个同样接口的 引用,调用引用的对象方法,像一个链表一样到最后。 职责链也是用的非常多的一个模式。典型应用 tomcat的 pipeline 流水线; struts,Spring AOP 的各种拦截链

    新增需求N ...

    好了,再搞下去,估计要疯了。可是,大家看到,一个小的需求衍生下去,有十几个模式都覆盖了。只要大家记住敏捷的那几个原则,模式自然就来了,我把他简单总结几句话 一次只做一件事 代码不能有重复 要接口不要实现 不关注的要隔离

    2、正式的模式讨论


    I、创建型 - 关注于调用者和被调用者的隔离 工厂 Factory /抽象工厂 Abstract Factory/单例 Singleton / 原型 Prototype /建设者 Builder

    工厂/抽象工厂/建设者 符合 开发封闭原则,对象的过程是变化的,而获取对象后的操作是固定的。 单例是个特殊的模型(懒汉,恶汉,枚举) ,对client来说也符合这个原则 原型模式,其实是一个 对象的副本拷贝

    II、结构型 - 关注于对象的组成和调用方式 适配 Adapter / 代理 Proxy /桥接 Bridge / 组合 Composite /装饰 Decorator/外观 Facade /享元 Flyweight

    适配:用在两个系统的融合。当前系统持有另一个系统的引用,通过委托引用,隔离对上层Client的变化。

    代理:把对一个对象的调用 代理/委托给 另一个对象。隔离、保护、拦截对象。

    桥接:把对象的抽象和具体行为分离出来。把各自的变化分离出来,把不变的组合封闭,组合不变;每个部分都功能单一

    组合:模式比较简单,就是一个类似于链表和树的数据结构来组织对象层次关系

    装饰:为功能包装新的功能。每个类职责单一

    外观:对外提供统一的访问接口。封闭内部实现,提供统一的访问接口;这个模式的特点是 不同【类型】 的操作最终合并在一起,不像前面其他的模式都具有一定的相似和关联 几乎所有的框架都提供了统一访问的接口,隔离内部实现,简化用户使用;比如iBatis的 Client;Jedis的 Jedis

    III、行为型 - 关注对象内部的执行过程 职责链 COR /命令 Comand /解析 Interpreter /迭代器 Iterator /中介 Mediator /备忘录 Memento /观察者 Observer /状态 state /策略 Strategy /模版模式 TEMPLATE METHOD /访问者 Visitor

    职责链:同样接口下的,不同处理模块通过职责链链接起来,每个模块都知道自己的下一步,像一个链表一样。PipeLine(tomcat),拦截器链(struts2) 都使用这种模式

    命令:封装成命令接口,把功能提交给Command 框架去执行。隔离调用和执行

    解释器:语法解析接口

    迭代器:提供一个访问内部数据的接口;当前对象需要实现这个访问接口,通过这个接口可以遍历内部数据

    中介:可以认为 任务消息队列 / 企业ESB 就是一种广义的中介方式;应用向中介注册,通过中介向其他模块发送消息;或者中介主动持有两个变量进行操作,典型的就是Spring的注入。

    备忘录:对象持有一个 备忘录对象,保存自己的状态,可以通过备忘录对象恢复

    观察者:最常见的模式,几乎所有的事件都采用这种方式。Observer观察者需要实现一个接口,观察者向 被观察的对象 Observlable 注册,被观察对象 发生事件时,遍历所有注册的观察者,调用其接口实现

    状态模式:不同的状态间切换

    策略模式:把行为抽象出来,分为不同的实现

    模版模式:其实就是抽象类,把一些公共操作整合到一起固化流程

    访问Visit模式:Visitable 被访问的对象,需要实现一个接口,accept 方法里面接受外面的访问器; Visitor 访问器提供访问各种对象的方法;所有 实现 Vistable的接口 把 自身 提供给 visitor的 具体某一个访问方法; 通常组合 Compsite 使用

    3、模式大比拼


    I、Adapter 适配 VS Bridge 桥接 两者非常类似的模式。长得也非常像。 几点区别: 目的/用途: Adapter - 包装一个异构系统到本系统来。 Bridge - 解耦本系统的 静态属性和 动态行为。 使用的阶段:Adapter - 已有系统的包装,属于事后弥补性质 Bridge - 架构设计时,设计好的层次,有点事前规划的感觉 实现的差异:Adapter - 一般是 一个系统持有另一个系统的对象 Bridge - 一般是 持有一个行为的接口 总之,Adapte是对象的包装,Bridge 是属性和行为的组合

    举一个例子,来做区分 设计一个带报警功能的门 abstract class Door{ Alarm alarm; public doorAlram(){ alarm.alarm(); } } 门 可以扩展;告警 也可以扩展 木门+音乐报警器 , 铁门+ 警铃, 可以任意组合。 此为 桥接 Bridge 模式

    如果此时,需要把一个门,比如是厨房的,移到房间,发现型号不能匹配,小了一点。 class CookRoomDoorAdapter implments BedRoom{ XXX 增加适配的边框门条 } 此为 适配 Adapter 模式

    II、Strategy 策略 VS TEMPLATE METHOD 模板 TEMPLATE METHOD 和 STRATEGY 模式都是解决类似问题,比如分离通用算法和具体应用上下文;区别是TEMPLATE METHOD用的是继承的方式,STRATEGY用的是委托的方式。 TEMPlATE METHOD 相对简单点,但由于在设计抽象基础类时,就固化了算法,扩展性受到很大限制;而STRATEGY 接口只抽象了每个实现细节,具体实现类也只是实现了细节,对逻辑组合并不了解,这样可以面对多种算法,可以有多个委托对象放来调用,扩展性非常好,

    Template method 模板 abstract class TemplateMethod{ abstract void a(); abstract void b(); abstract void c(); public void operate(){ a(); xxx b(); c(); } }

    Strategy 策略 interface Strategy{ void do(); }

    class Context{ Strategy a;

    } Spring 中的策略模式,比如 Ioc,不同类型的加载;资源不同类型资源的读取等等。

    III、Strategy 策略 VS Bridge 桥接 两者都是通过持有 引用 委托给另一个来实现功能。 差别在于 Strategy 除抽象行为外的部分是固定的,也就是说封闭的,没有演化和变化;而Bridge 是各自都需要演化的。比如上文的 Door 本身也是要演化的,而Context就是直接的使用者,没有演化。

    IV、Visitor 访问 VS Iterator 迭代 Visitor 双向持有,双向分发;iterator 单向持有 所有实现Visitable的 对象,因为把自己暴露给 访问者,在调用visit时,可以通过访问者增加功能 iterator,封装了内部数据实现,提供了唯一的访问接口

    V、State 状态 VS Strategy 策略 两者也是非常接近,从委托的方式也是一致的,类图模型图差不多 主要还是使用方式的差异, State 模式 动态变化的 ; Strategy 是静态的,一般在配置、加载时,就默认指定了一种策略。

    VI、 XXX 可以看到相似的模式应用何其多,变化和差异也很多。所以回答最开始的问题,不用硬背模式,记住 编码原则和规范,模式就应运而生了。

    转载请注明原文地址: https://ju.6miu.com/read-670488.html

    最新回复(0)