網上設計模式的文章較多,本篇主要是自己總結學習用,力求簡單,易於掌握。文章大量參考網絡資源,主要有https://javadoop.com/post/design-pattern 一文和 《設計模式之禪》一書。
文章目錄
一 創建類模式
1 單例模式
定義
確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例.
各種單例寫法對比
單例寫法 | 單例保障機制 | 單例對象初始化時機 | 優點 | 缺點 |
---|---|---|---|---|
餓漢模式 | 類加載機制 | 類加載 | 簡單,易理解 | 難以保證懶加載,無法應對反射和反序列化 |
雙重校驗鎖(DCL) | 鎖機制(需volatile防止重排序) | 第一次調用getInstance() | 實現懶加載 | 複雜,無法應對反射和反序列化 |
Holder模式(靜態內部類) | 類加載機制 | 第一次調用getInstance() | 實現懶加載 | 無法應對反射和反序列化 |
枚舉 | 枚舉語言特性 | 第一次引用枚舉對象 | 簡潔,安全(語言級別防止通過反射和反序列化破壞單例) | enum的另類用法 |
拓展點
- 雙重校驗鎖:雙重的原因和volatile關鍵字
- 餓漢模式:單例 final 關鍵字在併發情況下的作用,static 關鍵字修飾在類加載機制中的時機
- Holder模式:類加載條件
- enum:enum的相關用法
參考
2 工廠模式(含簡單工廠)
定義
定義一個用於創建對象的接口,讓子類決定實例化哪一個類。工廠方法使一個類的實例化延遲到其子類。
簡單工廠(靜態工廠模式)
只需一個工廠的時候使用。
一個工廠類 XxxFactory,裏面有一個靜態方法,根據我們不同的參數,返回不同的派生自同一個父類(或實現同一接口)的實例對象
代碼
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
工廠模式
需要多個工廠的時候使用。
將工廠類進行抽象提取。
核心在於,我們需要在第一步選好我們需要的工廠。比如,我們有 LogFactory 接口,實現類有 FileLogFactory 和 KafkaLogFactory,分別對應將日誌寫入文件和寫入 Kafka 中,顯然,我們客戶端第一步就需要決定到底要實例化 FileLogFactory 還是 KafkaLogFactory,這將決定之後的所有的操作。
例圖
3 抽象工廠模式
定義
爲創建一組相關或相互依賴的對象提供一個接口,而且無須指定它們的具體類。
一般是涉及對象族的時候使用,屏蔽一個對象族的相同約束。缺點是很難拓展。
例子
使用普通工廠模式
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 組裝 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
單獨看 CPU 工廠和主板工廠,它們分別是前面我們說的工廠模式。這種方式也容易擴展,因爲要給電腦加硬盤的話,只需要加一個 HardDiskFactory 和相應的實現即可,不需要修改現有的工廠。
但是,這種方式有一個問題,那就是如果 Intel 家產的 CPU 和 AMD 產的主板不能兼容使用,那麼這代碼就容易出錯,因爲客戶端並不知道它們不兼容,也就會錯誤地出現隨意組合。
下面就是我們要說的產品族的概念,它代表了組成某個產品的一系列附件的集合:
使用抽象工廠模式
當涉及到這種產品族的問題的時候,就需要抽象工廠模式來支持了。我們不再定義 CPU 工廠、主板工廠、硬盤工廠、顯示屏工廠等等,我們直接定義電腦工廠,每個電腦工廠負責生產所有的設備,這樣能保證肯定不存在兼容問題。
這個時候,對於客戶端來說,不再需要單獨挑選 CPU廠商、主板廠商、硬盤廠商等,直接選擇一家品牌工廠,品牌工廠會負責生產所有的東西,而且能保證肯定是兼容可用的。
public static void main(String[] args) {
// 第一步就要選定一個“大廠”
ComputerFactory cf = new AmdFactory();
// 從這個大廠造 CPU
CPU cpu = cf.makeCPU();
// 從這個大廠造主板
MainBoard board = cf.makeMainBoard();
// 從這個大廠造硬盤
HardDisk hardDisk = cf.makeHardDisk();
// 將同一個廠子出來的 CPU、主板、硬盤組裝在一起
Computer result = new Computer(cpu, board, hardDisk);
}
當然,抽象工廠的問題也是顯而易見的,比如我們要加個顯示器,就需要修改所有的工廠,給所有的工廠都加上製造顯示器的方法。這有點違反了對修改關閉,對擴展開放這個設計原則。
4 建造者模式
定義
將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示
常見使用形式
Food food = Food.builder().a().b().c().build();
寫法
核心:使用一個具有相關屬性的靜態內部類Builder,先把所有的屬性都設置給 Builder,然後 build() 方法的時候,將這些屬性複製給實際產生的對象。並可在 build() 的時候做自定義檢查。
簡易版
使用Lombok
@Builder
class User {
private String name;
private String password;
private String nickName;
private int age;
}
標準版
//--------------建造者類----------------------------------------
class User {
// 下面是“一堆”的屬性
private String name;
private String password;
private String nickName;
private int age;
// 構造方法私有化,不然客戶端就會直接調用構造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 靜態方法,用於生成一個 Builder,這個不一定要有,不過寫這個方法是一個很好的習慣,
// 有些代碼要求別人寫 new User.UserBuilder().a()...build() 看上去就沒那麼好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一樣的一堆屬性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 鏈式調用設置各個屬性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法負責將 UserBuilder 中設置好的屬性“複製”到 User 中。
// 當然,可以在 “複製” 之前做點檢驗
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用戶名和密碼必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年齡不合法");
}
// 還可以做賦予”默認值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
//---------------------客戶端調用----------------------------------//
public class APP {
public static void main(String[] args) {
User d = User.builder()
.name("foo")
.password("pAss12345")
.age(25)
.build();
}
}
5 原型模式
定義
用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象
實質即克隆
Java 實現
Object 類中有一個 clone() 方法,它用於生成一個新的對象,當然,如果我們要調用這個方法,java 要求我們的類必須先實現 Cloneable 接口,此接口沒有定義任何方法,但是不這麼做的話,在 clone() 的時候,會拋出 CloneNotSupportedException 異常。
protected native Object clone() throws CloneNotSupportedException;
注意
java 的克隆是淺克隆,碰到對象引用的時候,克隆出來的對象和原對象中的引用將指向同一個對象。通常實現深克隆的方法是將對象進行序列化,然後再進行反序列化。
二 結構類模式
1 代理模式(委託模式)
定義
爲其他對象提供一種代理以控制對這個對象的訪問
例圖
拓展點
動態代理與AOP(Spring),JDK動態代理(需接口),CGLIB(需可繼承)
2 適配器模式
定義
將一個類的接口變換成客戶端所期待的另一種接口,從而使原本因接口不匹配而無法在一起工作的兩個類能夠在一起工作。
默認適配器模式
對於有多個方法的接口,可以提供一個默認實現(空實現)的默認適配器實現類,這樣用戶只需繼承這個適配器類然後重寫個別需要用到的方法即可。
對象適配器模式
類適配器模式
通過繼承的方法,適配器自動獲得了所需要的大部分方法。這個時候,客戶端使用更加簡單,直接 Target t = new SomeAdapter(); 就可以了。
注意點
- 類適配和對象適配的異同
一個採用繼承,一個採用組合;
類適配屬於靜態實現,對象適配屬於組合的動態實現,對象適配需要多實例化一個對象。
總體來說,對象適配用得比較多。 - 適配器模式和代理模式的異同
在代碼結構上,它們很相似,都需要一個具體的實現類的實例。但是它們的目的不一樣,代理模式做的是增強原方法的活;適配器做的是適配的活,爲的是提供“把雞包裝成鴨,然後當做鴨來使用”,而雞和鴨它們之間原本沒有繼承關係。
3 裝飾模式
定義
動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾模式相比生成子類更加靈活。
結構圖
所有的具體裝飾者們 ConcreteDecorator 都可以作爲 Component 來使用,因爲它們都實現了 Component 中的所有接口。它們和 Component 實現類 ConcreteComponent 的區別是,它們只是裝飾者,起裝飾作用,也就是即使它們看上去牛逼轟轟,但是它們都只是在具體的實現中加了層皮來裝飾而已。(通常在構造方法中傳入被包裝的基類Component)
例圖
Java IO 中的裝飾模式
4 門面模式(外觀模式)
定義
要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行。門面模式提供一個高層次的接口,使得子系統更加易於使用。
提供“統一的對象”給外部訪問,不允許有任何直接訪問子系統的行爲發生,力求“金玉其表”
需要注意門面模式不符合開閉原則,難以拓展(因爲無法直接訪問內部)。
例圖(slf4j)
通用代碼
//--------------------------------子系統-----------------------------------
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 a=new ClassB();
private ClassC a=new ClassC();
//提供給外部訪問的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){
this.b.doSomethingB();
}
public void methodC(){
this.c.doSomethingC();
}
}
5 橋樑模式(橋接模式)
定義
將抽象和實現解耦,使得兩者可以獨立地變化
即把會變化的實現定義成一個接口Implementor(橋樑)
結構圖
例圖
6 組合模式(合成模式 / 部分-整體模式)
定義
將對象組合成樹形結構以表示“部分-整體”的層次結構,使得用戶對單個對象和組合對象的使用具有一致性。
當你發現需求中是體現部分與整體層次的結構時,以及你希望用戶可以忽略組合對象與單個對象的不同,統一地使用組合結構中的所有對象時,就應該考慮使用組合模式了。
例子
每個員工都有姓名、部門、薪水這些屬性,同時還有下屬員工集合(雖然可能集合爲空),而下屬員工和自己的結構是一樣的,也有姓名、部門這些屬性,同時也有他們的下屬員工集合。
public class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> subordinates; // 下屬
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList<Employee>();
}
public void add(Employee e) {
subordinates.add(e);
}
public void remove(Employee e) {
subordinates.remove(e);
}
public List<Employee> getSubordinates(){
return subordinates;
}
public String toString(){
return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]");
}
}
7 享元模式
定義
使用共享對象可有效地支持大量的細粒度的對象
理解
每個事物都是不同的,但是又有一定的共性,如果只有完全相同的事物才能共享,那麼享元模式可以說就是不可行的;
因此我們應該儘量將事物的共性共享,而又保留它的個性。爲了做到這點,享元模式中區分了內部狀態/內蘊狀態(Internal State)和外部狀態/外蘊狀態(External State)。內部狀態就是共性,外部狀態就是個性了。
內部狀態存儲在享元內部,不會隨環境的改變而有所不同,是可以共享的;
外部狀態是不可以共享的,它隨環境的改變而改變的,因此外部狀態是由客戶端來保持(因爲環境的變化是由客戶端引起的)。
在每個具體的環境下,客戶端將外部狀態傳遞給享元,從而創建不同的對象出來。
(外部狀態一般用基本類型或String,如果外部狀態也用類來表示則往往得不償失)
因爲把外部狀態的管理交由客戶端,故享元模式主要適用於數量多的、性質相近(外部狀態少)的對象。
結構圖
例子
//----------------------------抽象享元單元(Flyweight)----------------------
public interface Flyweight
{
public void operation(String state);
}
//----------------------------具體享元單元(ConcreteFlyweight )----------------------
public class ConcreteFlyweight implements Flyweight
{
private String str;
public ConcreteFlyweight(String str)
{
this.str = str;
}
@Override
public void operation(String state)
{
System.out.println("內蘊狀態:"+str);
System.out.println("外蘊狀態:"+state);
}
}
//----------------------------享元工廠(FlyWeightFactory)----------------------
public class FlyWeightFactory
{
private Map<String,ConcreteFlyweight> flyWeights = new HashMap<String, ConcreteFlyweight>();
public ConcreteFlyweight factory(String str)
{
ConcreteFlyweight flyweight = flyWeights.get(str);
if(null == flyweight)
{
flyweight = new ConcreteFlyweight(str);
flyWeights.put(str, flyweight);
}
return flyweight;
}
public int getFlyWeightSize()
{
return flyWeights.size();
}
}
測試如下,可以看到 “a fly weight” 是作爲外部狀態傳遞給 f1的operation 方法的。對於這種僅需臨時使用的對象,並不需要自己維持其外部狀態,就比較適合使用享元模式。
//----------------------------測試代碼----------------------
FlyWeightFactory factory = new FlyWeightFactory();
Flyweight f1 = factory.factory("a");
Flyweight f2 = factory.factory("b");
Flyweight f3 = factory.factory("a");
f1.operation("a fly weight");
f2.operation("b fly weight");
f3.operation("c fly weight");
System.out.println(f1 == f3);
System.out.println(factory.getFlyWeightSize());
//----------------------------測試結果----------------------
內蘊狀態:a
外蘊狀態:a fly weight
內蘊狀態:b
外蘊狀態:b fly weight
內蘊狀態:a
外蘊狀態:c fly weight
true
2
三 行爲類模式
1 策略模式
定義
定義一組算法,把每個算法都封裝起來,並且使它們之間可以互換。
例圖
策略模式和橋樑模式的區別
策略模式更加簡單,橋樑模式則是在Strategy使用類多加了一層抽象。
2 觀察者模式
定義
定義對象間一種一對多的依賴關係,使得每當一個對象改變狀態,則所有依賴於它的對象都會得到通知並被自動更新。
被觀察者(Subject)內部維護了一個觀察者(Observer)列表,當被觀察者執行操作的時候觸發 notify 遍歷 觀察者列表逐個執行 update()操作,相對於觀察者觀察到了主題的變化而執行一定的操作。
注意
JDK提供了 Observable 和 Observer 來代表被觀察者和觀察者。
生產中往往使用消息中間件來實現,這時候往往變成 發佈-訂閱模型,有一些文章強調這兩個模式是不同的。發佈-訂閱模型往往需要一箇中間件(如分佈式使用消息隊列、單機可以使用Guava的EventBus事件總線),觀察者向中間件訂閱主題,發佈者向中間件發佈主題,可以進一步解耦。
結構圖
3 責任鏈模式
定義
使多個對象都有機會處理請求,從而避免了請求的發送者和接受者之間的耦合關係。將這些對象連成一條鏈,並沿着這條鏈傳遞該請求,直到有對象處理它爲止。
模式本質:分離職責,動態組合。分離職責是前提,動態組合是精華所在。
拓展
SpringSecurity的攔截鏈,Netty的處理鏈等
結構圖
例子
代碼
@Data
@AllArgsConstructor
public class LeaveNote {
private String name;
private String leaveReason;
private int leaverDayNum;
}
public interface Handler {
void handLeave(LeaveNote leaveNote);
void setNextHandler(Handler h);
}
public class DirectorHandler implements Handler {
private Handler nextHandler;
public void handLeave(LeaveNote leaveNote) {
if (leaveNote.getLeaverDayNum() <= 3) {
System.out.println("主管同意" + leaveNote.getName() + "申請請假" + leaveNote.getLeaverDayNum() + "天,原因:" + leaveNote.getLeaveReason());
} else {
nextHandler.handLeave(leaveNote);
}
}
public void setNextHandler(Handler h) {
nextHandler = h;
}
}
public class ManagerHandler implements Handler {
private Handler nextHandler;
public void handLeave(LeaveNote leaveNote) {
System.out.println("總經理同意" + leaveNote.getName() + "申請請假" + leaveNote.getLeaverDayNum() + "天,原因:" + leaveNote.getLeaveReason());
}
public void setNextHandler(Handler h) {
nextHandler = h;
}
}
public class ManagerHandler implements Handler {
private Handler nextHandler;
public void handLeave(LeaveNote leaveNote) {
System.out.println("總經理同意" + leaveNote.getName() + "申請請假" + leaveNote.getLeaverDayNum() + "天,原因:" + leaveNote.getLeaveReason());
}
public void setNextHandler(Handler h) {
nextHandler = h;
}
}
public class Client {
public static void main(String[] args) {
LeaveNote leaveNote = new LeaveNote("小米","肚子疼",8);
DirectorHandler directorHandler = new DirectorHandler();
ViceManagerHandler viceManagerHandler = new ViceManagerHandler();
ManagerHandler managerHandler = new ManagerHandler();
directorHandler.setNextHandler(viceManagerHandler);
viceManagerHandler.setNextHandler(managerHandler);
directorHandler.handLeave(leaveNote);
}
}
4 模板方法模式
定義
定義一個操作中的算法的框架,而將一些步驟延遲到子類中。使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
常用於含有繼承結構的代碼中,將可能變化的步驟抽取出來,交由子類去實現而無須改變整體結構。
結構圖
5 狀態模式
定義
當一個對象內在狀態改變時允許其改變行爲,這個對象看起來像改變了其類。
優點:避免過多switch…case或者if…else,可以隱藏狀態變換
Context 持有各個State,不同的State執行的操作不同,客戶端通過Context來執行操作,將State的管理交由Context。適用於各個State有邏輯轉化順序的情景。
結構圖
例子
抽象環境角色
public abstract class State {
//定義一個環境角色,提供子類訪問
protected Context context;
//設置環境角色
public void setContext(Context _context){
this.context = _context;
}
//行爲1
public abstract void handle1();
//行爲2
public abstract void handle2();
}
具體環境角色
具體環境角色有兩個職責:(1)處理本狀態必須完成的任務,(2)決定是否可以過渡到其他狀態。
public class ConcreteState1 extends State {
@Override
public void handle1() {
//本狀態下必須處理的邏輯
}
@Override
public void handle2() {
//設置當前狀態爲stat2
super.context.setCurrentState(Context.STATE2);
//過渡到state2狀態,由Context實現
super.context.handle2();
}
}
public class ConcreteState2 extends State {
@Override
public void handle1() {
//設置當前狀態爲state1
super.context.setCurrentState(Context.STATE1);
//過渡到state1狀態,由Context實現
super.context.handle1();
}
@Override
public void handle2() {
//本狀態下必須處理的邏輯
}
}
上下文角色,提供給客戶使用
public class Context {
//定義狀態
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
//當前狀態
private State CurrentState;
//獲得當前狀態
public State getCurrentState() {
return CurrentState;
}
//設置當前狀態
public void setCurrentState(State currentState) {
this.CurrentState = currentState;
//切換狀態
this.CurrentState.setContext(this);
}
//行爲委託
public void handle1(){
this.CurrentState.handle1();
}
public void handle2(){
this.CurrentState.handle2();
}
}
客戶
public class Client {
public static void main(String[] args) {
//定義環境角色
Context context = new Context();
//初始化狀態
context.setCurrentState(new ConcreteState1());
//行爲執行
context.handle1();
context.handle2();
}
}
6 迭代器模式
定義
提供一種方法訪問一個容器對象中各個元素,而又不需要暴露該對象的內部細節。
理解即可,一般不需要自己寫
結構圖
7 命令模式
定義
將一個請求封裝成一個對象,從而讓你使用不同的請求把客戶端參數化,對請求排隊或者記錄請求日誌,可以提供命令的撤銷和恢復功能
命令模式把發出命令的責任和執行命令的責任分隔開,委派給不同的對象。
每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方接收到請求,並執行操作。命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的接口,更不必知道請求是怎麼被接收、以及操作是否被執行、何時被執行、怎麼被執行的。
命令允許請求的一方和接收請求的一方能夠獨立演化,從而具有如下的優點:
命令模式使新的命令很容易被加入到系統裏。
允許接受請求的一方決定是否要否決請求。
能較容易的設計一個命令隊列。
可以容易的實現對請求的撤銷和恢復。
在需要的情況下,可以較容易的將命令記入日誌。
對於命令Command來說,它必須直到由誰來執行,所以Command中持有Receiver的引用。對於Invoker來說,它只關心命令本身,雖然在構造命令的時候需要傳入接收者,但它不用去關係具體如何調用實現。
結構圖
例子
接收者角色類
public class Receiver {
/**
* 真正執行命令相應的操作
*/
public void action() {
System.out.println("執行操作");
}
}
抽象命令角色類
```java
public interface Command {
/**
* 執行方法
*/
void execute();
}
具體命令角色類
public class ConcreteCommand implements Command {
/**
* 持有相應的接收者對象
*/
private Receiver receiver = null;
/**
* 構造方法
* @param receiver
*/
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
//通常會轉調接收者的形影方法,讓接收者來真正執行功能
receiver.action();
}
}
請求者角色類
public class Invoker {
/**
* 持有命令對象
*/
private Command command = null;
/**
* 構造方法
* @param command
*/
public Invoker(Command command) {
this.command = command;
}
/**
* 行動方法
*/
public void action() {
command.execute();
}
}
客戶端角色類
public class Client {
public static void main(String[] args) {
//創建接收者
Receiver receiver = new Receiver();
//創建命令對象,設定其接收者
Command command = new ConcreteCommand(receiver);
//創建請求者,把命令對象設置進去
Invoker invoker = new Invoker(command);
//執行方法
invoker.action();
}
}
8 備忘錄模式
定義
在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態。
由於備忘錄模式有太多的變形和處理方式,每種方式都有它自己的優點和缺點,標準的備忘錄模式很難在項目中遇到,基本上都有一些變換處理方式。
結構圖
例子
/**
* 備忘錄
*/
@Data
@AllArgsConstructor
public class Memento {
private String state;
}
/**
* 發起人
*/
@AllArgsConstructor
public class Originator {
private String state;
public void setMemento(Memento memento) {
state = memento.getState();
}
public Memento createMemento() {
return new Memento(state);
}
public void show(){
System.out.println(state);
}
}
/**
* 管理者
*/
@AllArgsConstructor
public class Caretaker {
private Memento memento;
}
/**
* 客戶端
*/
public class Client {
public static void main(String[] args) {
Originator originator = new Originator();
originator.setState("start");
originator.show();
Memento memento = originator.createMemento();
Caretaker caretaker = new Caretaker();
caretaker.setMemento(memento);
originator.setState("over");
originator.show();
originator.setMemento(caretaker.getMemento());
originator.show();
}
}
9 訪問者模式
定義
封裝一些作用於某種數據結構中的各元素的操作,它可以在不改變數據結構的前提下定義作用於這些元素的新的操作
換言之,如果系統的數據結構是比較穩定的,但其操作(算法)是易於變化的,那麼使用訪問者模式是個不錯的選擇;如果數據結構是易於變化的,則不適合使用訪問者模式。
處理現已穩定的數據結構和易變的操作耦合問題,把數據結構和作用於結構上的操作解耦合,使得操作集合可相對自由地演化。
訪問者模式是一種集中規整模式,特別適用於大規模重構的項目,在這一個階段需求已經非常清晰,原系統的功能點也已經明確,通過訪問者模式可以很容易把一些功能進行梳理,達到最終目的——功能集中化,如一個統一的報表運算、UI展現等,我們還可以與其他模式混編建立一套自己的過濾器或者攔截器
較複雜。
結構圖
訪問者模式一共有五種角色:
- Vistor(抽象訪問者):爲該對象結構中具體元素角色聲明一個訪問操作接口。
- ConcreteVisitor(具體訪問者):每個具體訪問者都實現了Vistor中定義的操作。
- Element(抽象元素):定義了一個accept操作,以Visitor作爲參數。
- ConcreteElement(具體元素):實現了Element中的accept()方法,調用Vistor的訪問方法以便完成對一個元素的操作。
- ObjectStructure(對象結構):可以是組合模式,也可以是集合;能夠枚舉它包含的元素;提供一個接口,允許Vistor訪問它的元素。
例子
代碼:
完整例子見:https://segmentfault.com/a/1190000012495957 和 https://www.jianshu.com/p/cd17bae4e949
class Home {
private List<Animal> nodeList = new ArrayList<>();
void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
void add(Animal animal) {
nodeList.add(animal);
}
}
//-------------------------測試--------------------------------------
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.action(owner);
Someone someone = new Someone();
home.action(someone);
}
}
10 中介者模式
定義
用一箇中介對象封裝一系列的對象交互,中介者使各對象不需要顯示地相互作用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互
作用:
- 適當地使用中介者模式可以避免同事類之間的過度耦合,使得各同事類之間可以相對獨立地使用。
- 使用中介者模式可以將對象間一對多的關聯轉變爲一對一的關聯,使對象間的關係易於理解和維護。
- 使用中介者模式可以將對象的行爲和協作進行抽象,能夠比較靈活的處理對象間的相互作用。
結構圖
- Mediator(中介者接口)
在裏面定義各個同事之間交互需要的方法,可以是公共的通訊方法,比如changed方法,大家都用,也可以是小範圍的交互方法。 - ConcreteMediator(具體中介者類)
它需要了解並維護各個同事對象,並負責具體的協調各同事對象的交互關係。 - Colleague(同事類)
通常實現成爲抽象類,主要負責約束同事對象的類型,並實現一些具體同事類之間的公共功能,比如:每個具體同事類都應該知道中介者對象,也就是具體同事類都會持有中介者對象,就可以定義到這個類裏面。 - ConcreteColleague(具體同事類)
具體的同事類,實現自己的業務。每一個同事類都知道它的中介者對象。每一個同事對象在需與其他的同事通信的時候,與它的中介者通信。
例子
有兩個類A和B,類中各有一個數字,並且要保證類B中的數字永遠是類A中數字的100倍。也就是說,當修改類A的數時,將這個數字乘以100賦給類B,而修改類B時,要將數除以100賦給類A。類A類B互相影響,就稱爲同事類。代碼如下:
abstract class AbstractColleague {
protected int number;
public int getNumber() {
return number;
}
public void setNumber(int number){
this.number = number;
}
//注意這裏的參數不再是同事類,而是一箇中介者
public abstract void setNumber(int number, AbstractMediator am);
}
class ColleagueA extends AbstractColleague{
public void setNumber(int number, AbstractMediator am) {
this.number = number;
am.AaffectB();
}
}
class ColleagueB extends AbstractColleague{
@Override
public void setNumber(int number, AbstractMediator am) {
this.number = number;
am.BaffectA();
}
}
abstract class AbstractMediator {
protected AbstractColleague A;
protected AbstractColleague B;
public AbstractMediator(AbstractColleague a, AbstractColleague b) {
A = a;
B = b;
}
public abstract void AaffectB();
public abstract void BaffectA();
}
class Mediator extends AbstractMediator {
public Mediator(AbstractColleague a, AbstractColleague b) {
super(a, b);
}
//處理A對B的影響
public void AaffectB() {
int number = A.getNumber();
B.setNumber(number*100);
}
//處理B對A的影響
public void BaffectA() {
int number = B.getNumber();
A.setNumber(number/100);
}
}
public class Client {
public static void main(String[] args){
AbstractColleague collA = new ColleagueA();
AbstractColleague collB = new ColleagueB();
AbstractMediator am = new Mediator(collA, collB);
System.out.println("==========通過設置A影響B==========");
collA.setNumber(1000, am);
System.out.println("collA的number值爲:"+collA.getNumber());
System.out.println("collB的number值爲A的10倍:"+collB.getNumber());
System.out.println("==========通過設置B影響A==========");
collB.setNumber(1000, am);
System.out.println("collB的number值爲:"+collB.getNumber());
System.out.println("collA的number值爲B的0.1倍:"+collA.getNumber());
}
}
11 解釋器模式
定義
給定一門語言,定義它的文法的一種表示,並定義一個解釋器,該解釋器使用該表示來解釋語言中的句子
解釋器是一個簡單語法分析工具,它最顯著的優點就是擴展性,修改語法規則只要修改相應的非終結符表達式就可以了,若擴展語法,則只要增加非終結符類就可以了。
儘量不要在重要的模塊中使用解釋器模式,否則維護會是一個很大的問題。在項目中可以使用shell、JRuby、Groovy等腳本語言來代替解釋器模式,彌補Java編譯型語言的不足。
解釋器模式在實際的系統開發中使用得非常少,因爲它會引起效率、性能以及維護等問題,一般在大中型的框架型項目能夠找到它的身影,如一些數據分析工具、報表設計工具、科學計算工具等,若你確實遇到“一種特定類型的問題發生的頻率足夠高”的情況,準備使用解釋器模式時,可以考慮一下Expression4J、MESP(Math Expression String Parser)、Jep等開源的解析工具包(這三個開源產品都可以通過百度、Google搜索到,請讀者自行查詢),功能都異常強大,而且非常容易使用,效率也還不錯,實現大多數的數學運算完全沒有問題,自己沒有必要從頭開始編寫解釋器。有人已經建立了一條康莊大道,何必再走自己的泥濘小路呢?
結構圖
例子
-
抽象表達式角色(Expression):聲明一個所有的具體表達式角色都需要實現的抽象接口。這個接口主要是一個interpret()方法,稱作解釋操作。
-
終結符表達式角色(Terminal Expression):實現了抽象表達式角色所要求的接口,主要是一個interpret()方法;文法中的每一個終結符都有一個具體終結表達式與之相對應。比如有一個簡單的公式R=R1+R2,在裏面的R1和R2就是終結符,對應的解析R1和R2的解釋器就是終結符表達式。
-
非終結表達式角色(Nonterminal Expression):文法中的每一條規則都需要一個具體的非終結符表達式,非終結符表達式一般是文法中的運算符或者其他關鍵字,比如公式R=R1+R2中,+就是非終結符,解析+的解釋器就是一個非終結符表達式。
-
環境角色(Context):這個角色的任務一般是用來存放文法中各個終結符所對應的具體值,比如R=R1+R2,我們給R1賦值100,給R2賦值200。這些信息都需要存放到環境角色中,很多情況下我們使用Map來充當環境角色就夠了。