設計模式 精華一頁紙

設計模式自從推出就一直很火,個人的體驗是,模式運用存乎於心,理解最重要。重點是幾個理念,從理念出發去理解模式;面向接口編程、消除重複、職責單一、接口隔離、開放-封閉等。而不是死記硬背和硬套各種模式。
本文從一個簡單場景,結合理念,引出一些常用模式。

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
可以看到相似的模式應用何其多,變化和差異也很多。所以回答最開始的問題,不用硬背模式,記住 編碼原則和規範,模式就應運而生了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章