设计模式 精华一页纸

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

本文从一个简单场景,结合理念,引出一些常用模式。

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 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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容