圖解設計模式
技術部落格:www.zhenganwen.top
暨上文介紹了
PlantUML
之後,謹以此文記錄學習《圖解設計模式》過程中的心得體會,歡迎留言一起交流想法~
Todo List
-
Adapt to Design Pattern——適應設計模式
- Iterator模式——一個一個遍歷
- Adaptor模式——加個“介面卡”以便複用
-
Left to subclass——交給子類
- Template Method——將具體處理交給子類
- Factory Method——將例項的生成交給子類
-
Generating Instance——生成例項
- Singleton模式——只有一個例項
- Builder模式——組裝複雜的例項
- Abstract Factory——將關聯零件組裝成產品
-
Consider Individualy——分開考慮
- Bridge模式——將類的功能層次結構與實現層次結構分離
- Strategy模式——整體地替換演演算法
-
Consistency——一致性
- Composite模式——容器與內容的一致性
- Decorator模式——裝飾邊框與被裝飾物的一致性
-
Access Data Structure——訪問資料結構
- Visitor模式——訪問資料結構並處理資料
- Chain of Responsibility模式——推卸責任
-
Simplify——簡單化
- Facade模式——簡單視窗
- Mediator模式——只有一個仲裁者
-
Manage Status——管理狀態
- Observer模式——傳送狀態變化通知
- Memento模式——儲存物件狀態
- State模式——用類表示狀態
-
Avoid wasting
- Flyweight模式——共享物件,避免浪費
- Proxy模式——只在必要時生成物件
-
Represent with Class——用類來表現
- Command模式——命令也是類
- Interpreter模式——語法規則也是類
約定
由於每個人的行文風格不同,因此表情達意的方式也不同,在本文中有著如下約定:
- 每個設計模式的示例程式碼中,
Client
類都表示業務類,即脫離於設計模式之外的,將設計模式應用於業務程式碼的測試類。 - 本文每個類圖都是使用
IDEA
外掛PlantUML
所畫,可參考《遇見PantUML~》一文 - 本文中的類通常指廣義上的類,包含抽象類和介面
- 本書中的所有例項程式碼完整版已託管到碼雲:gitee.com/zhenganwen/…
Principle
通常,優良的程式碼設計需要遵循以下原則
Single Responsibility
每個類的存在應該都只是為了滿足一個特定的需求,例如Collection
類中的方法應該都是為了維護內部元素的結構組織而存在,而應該將如何遍歷Collection
中元素的職責交給Iterator
。
單一職責保證專業的事交給專業的人來做,這樣每個類發生修改的原因只會有一個(因為每個類的責任只有一個),這樣就保證了後續若有需求變更只會導致負責解決該需求的類發生改變,而其他的類均不會受到影響。改變越少,系統發生BUG的機率就會越小。
Open/Closed Principle
系統要對修改關閉,對擴充套件開放。程式碼設計要儘量避免對現有程式碼的修改,因為一旦修改一處就可能導致依賴該類的其他類發生改變,一旦改變,就有可能引入新的潛在的BUG。如果需求變更,程式碼設計應該通過新增類(多為實現類)的方式來滿足新的需求,而客戶端程式碼(依賴該類的其他類)應該無需修改或只需少量修改。
Liskov Substitution Principle
里氏代換原則依託於OOP的多型性。在執行時,客戶端依賴的物件可被其他“同源”的(有相同的父類或介面)物件替換而客戶端的呼叫邏輯不受任何影響,這要求我們在宣告物件的外觀型別(宣告型別)時儘量選擇高層次一些的類(類的層次結構)。
Interface Segregation Principle
介面隔離原則要求我們將介面方法按照介面功能分開定義在不同的介面中,例如createItertor()
應該定義在Iterable
中,fly()
應該定義在Flyable
之中,這樣能夠減輕實現類的負擔,避免實現類被捆綁著要求實現不必要的介面方法。
同時,Java8之後,介面方法如果有通用實現應該定義為default
Dependency Inversion Principle
依賴倒置原則要求我們儘量消除點對點的“強”依賴,而應該使兩者依賴抽象,例如Controller
依賴AbstractService
中的抽象方法進行宣告式程式設計,XxxServiceImpl
則對AbstractService
的抽象方法進行實現,這樣就實現了控制器和具體業務處理類之間的解耦,一旦後續業務變更,我們只需要新增一個XxxServiceImpl2
並藉助多型就能夠輕鬆實現業務處理的切換。
Adapt to Design Pattern
本章將以Iterator
模式和Adaptor
模式兩個較為簡單的作為設計模式的入門,切身體會設計模式存在的價值和軟體開發中應遵循的一些原則。
Iterator模式
Before
Iterator
中譯“迭代器”,起逐個訪問集合中的元素的作用。在資料結構中組合元素的方式有很多,如陣列、連表、雜湊表、二叉樹等,根據集合的不同的組織形式,我們遍歷訪問集合元素的方式也是不一樣的。這時我們的業務程式碼中的訪問邏輯(即遍歷到當前元素時需要幹什麼)和遍歷邏輯(需要知道集合內部結構)是耦合在一起的,一旦集合換一種組織形式,那麼我們的業務程式碼也需要跟著改變。
例如,如下書架BookShelf
通過陣列的形式組織了一些書Book
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private String name;
}
public class BookShelfWithArr {
private final Book[] books;
private final int size;
private int index;
public BookShelfWithArr(int size) {
this.size = size;
this.books = new Book[size];
this.index = 0;
}
public void put(Book book) {
if (book == null) {
throw new IllegalArgumentException("book can't be null");
}
if (index == size) {
throw new RuntimeException("the bookshelf is full");
}
books[index++] = book;
}
public Book get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("index should be equal or big than 0 but less than " + size);
}
return books[index];
}
public int size() {
return this.size;
}
}
複製程式碼
如果不使用Iterator
模式,那麼你的業務程式碼可能如下所示:
public class Client {
public static void main(String[] args) {
BookShelfWithArr bookShelf = new BookShelfWithArr(4);
bookShelf.put(new Book("Java"));
bookShelf.put(new Book("C"));
bookShelf.put(new Book("php"));
bookShelf.put(new Book("python"));
for (int i = 0; i < bookShelf.size(); i++) {
System.out.println(bookShelf.get(i));
}
}
}
複製程式碼
這是我們初學Java集合章節時司空見慣的程式碼。現在我們來考慮一個問題,假設這個書架被打造成可伸縮的,即可以根據我們所放書籍數量而變大變小,此時我們該怎麼辦?
BookShelf
好說,根據可擴容的特性,我們可以應用ArrayList
來代替陣列
@Data
public class BookShelf {
private ArrayList<Book> bookList;
public BookShelf() {
this.bookList = new ArrayList<>();
}
public BookShelf(ArrayList<Book> bookList) {
this.bookList = bookList;
}
}
複製程式碼
如此的話,我們的遍歷訪問邏輯就要做出相應調整
BookShelf bookShelf2 = new BookShelf();
bookShelf2.getBookList().add(new Book("Java"));
bookShelf2.getBookList().add(new Book("C"));
bookShelf2.getBookList().add(new Book("php"));
bookShelf2.getBookList().add(new Book("python"));
for (int i = 0; i < bookShelf2.getBookList().size(); i++) {
System.out.println(bookShelf2.getBookList().get(i));
}
複製程式碼
這裡一旦集合改變組織元素的方式,任何其他存在遍歷該集合物件的程式碼(相對於該集合來說,這些程式碼稱為客戶端程式碼)都需要跟著改變。意味著,客戶端程式碼和集合是緊耦合的,客戶端程式碼不應該關心集合內部是如何組織元素的,而只應該關心遍歷該集合拿到元素之後應該做什麼。
於是我們用Iterator
模式來改造一下
After
首先無論集合如何組織元素,它都應該是可遍歷的,因此需要抽象出兩個方法:
-
boolean hasNext
——集合中是否還有未遍歷的元素 -
E next()
——取出下一個未遍歷的元素
public interface Iterator<E> {
boolean hasNext();
E next();
}
複製程式碼
集合應該只關注如何組織元素,因此應該將上述遍歷邏輯交由他人Iterator
來做
public interface Iterable<E> {
Iterator<E> iterator();
}
複製程式碼
通過呼叫BookShelf
的iterator
方法我們可以獲取BookShelf
的迭代器,通過使用該迭代器的方法可以實現對BookShelf
中元素的遍歷訪問:
public class BookShelfIterator implements Iterator<Book> {
private int index;
private BookShelf bookShelf;
public BookShelfIterator(BookShelf bookShelf) {
this.index = 0;
this.bookShelf = bookShelf;
}
@Override
public boolean hasNext() {
return index < bookShelf.getBookList().size();
}
@Override
public Book next() {
if (!hasNext()) {
throw new RuntimeException("you have arrived the end")
}
return bookShelf.getBookList().get(index++);
}
}
@Data
public class BookShelf implements Iterable<Book> {
private ArrayList<Book> bookList;
public BookShelf() {
this.bookList = new ArrayList<>();
}
public BookShelf(ArrayList<Book> bookList) {
this.bookList = bookList;
}
@Override
public Iterator<Book> iterator() {
return new BookShelfIterator(this);
}
}
複製程式碼
客戶端程式碼:
public class IteratorClient {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf();
bookShelf.getBookList().add(new Book("Java"));
bookShelf.getBookList().add(new Book("C"));
bookShelf.getBookList().add(new Book("php"));
bookShelf.getBookList().add(new Book("python"));
Iterator<Book> iterator = bookShelf.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
複製程式碼
如此,如果BookShelf
換用Map
來組織元素,我們只需新增一個BookShelfMapIterator
即可,而客戶端程式碼無需任何改動:
@Data
public class MapBookShelf implements Iterable{
/**
* book's name -> book
*/
private Map<String,Book> bookMap = new HashMap<>();
@Override
public Iterator iterator() {
return new MapBookShelfIterator(this);
}
}
複製程式碼
public class MapBookShelfIterator implements Iterator {
private final MapBookShelf mapBookShelf;
private final Object[] keys;
private int index;
public MapBookShelfIterator(MapBookShelf mapBookShelf) {
this.mapBookShelf = mapBookShelf;
this.keys = mapBookShelf.getBookMap().keySet().toArray();
this.index = 0;
}
@Override
public boolean hasNext() {
return index < keys.length;
}
@Override
public Object next() {
if (!hasNext()) {
throw new RuntimeException("you have arrived the end");
}
return mapBookShelf.getBookMap().get(keys[index++]);
}
}
複製程式碼
MapBookShelf bookShelf = new MapBookShelf();
bookShelf.getBookMap().put("Java",new Book("Java"));
bookShelf.getBookMap().put("C",new Book("C"));
bookShelf.getBookMap().put("PHP",new Book("php"));
bookShelf.getBookMap().put("Python",new Book("python"));
Iterator iterator = bookShelf.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
複製程式碼
雖然上述客戶端程式碼也發生了改變,如getBookMap
、put
(這些改變可以通過抽象出一個AbstractBookShelf
來避免),但是遍歷訪問邏輯沒變,即首先取得集合的Iterator
例項,然後呼叫介面方法hasNext
和next
進行遍歷,hasNext
是對遍歷界限的一個控制,其本身不做任何事,僅判斷當前位置是否有元素;而next
則做兩件事:返回當前位置上的元素,將遍歷指標後移。
UML & Summarize
完整的Iterator
模式可表述如下
其中Iterable
和Iterator
對應,ConcreteIterable
和ConcreteIterator
對應,客戶端僅知道Iterator
和Iterable
中的3個方法,可用此實現集合的遍歷而不管集合內部組織形式,不同的集合例項則將其對應的ConcreteIterator
實現隱藏在了createIterator
方法中
Roles
-
Iterator
,迭代器,專門負責迭代集合,符合單一職責boolean hasNext()
E next()
-
Iterable
,集合需要實現該介面,將遍歷責任委託給具體的迭代器例項Iterator createIterator()
-
Collection
,集合 -
ConcreteIterator
,具體的迭代器,和具體的集合例項之間是相互依賴的關係
Adapter模式
Adapter
是為了將已有的實現適應不同的介面而存在。生活中的典型例子是,為了能使兩個插銷插頭查到三個插孔的插座上,通常會在兩者之間加上一個插口轉換器,這個轉換器承擔的角色就是本設計模式的用意。
為了使已有的方法適應新的介面(例如已有方法健壯沒有毛病,針對相同功能的新介面我們又不像寫重複程式碼),我們通常會編寫一個Adapter
,它僅僅起著一個轉換器的作用。
Adapter
可以通過繼承和委託兩種方式實現
Extends or Delegates
例如,系統中遺留著他人已寫好的字串列印類Banner
public class Banner {
public void printWithBracket(String s) {
System.out.println("(" + s + ")");
}
public void printWithStar(String s) {
System.out.println("*" + s + "*");
}
}
複製程式碼
現在你正在對系統迭代,需要為新的介面Print
編寫實現類
public interface Print {
void printWeak(String s);
void printStrengthen(String s);
}
複製程式碼
而你的實現邏輯和Banner
中的兩個已有方法不謀而和,於是你可通過繼承舊類、實現新介面的方式,既能避免重複程式碼的編寫,又對新介面有所交代
public class PrintBanner extends Banner implements Print {
@Override
public void printWeak(String s) {
this.printWithBracket(s);
}
@Override
public void printStrengthen(String s) {
this.printWithStar(s);
}
}
複製程式碼
此種方式的缺點是,若目標類(這裡指
你還可以通過聚合的方式,將實現邏輯委託給舊類:
public class CustomPrint implements Print {
private Banner banner;
public CustomPrint(Banner banner) {
this.banner = banner;
}
@Override
public void printWeak(String s) {
banner.printWithBracket(s);
}
@Override
public void printStrengthen(String s) {
banner.printWithStar(s);
}
}
複製程式碼
UML & Summarize
以下是兩種方式的類圖
其中方式1受目標類必須是介面的限制。
Roles
-
Adaptee
,被適配方,通常為系統中的舊類,且類中存在對某功能A的實現 -
Target
,目標類,通常為新新增到系統中的類,包含需要實現某功能A的抽象方法 -
Adapter
,適配類,作為被適配方和目標類之間的橋樑,通過繼承或聚合的方式實現程式碼複用
Left to Subclass
Template Method
宣告式程式設計 & 面對抽象程式設計
模板方法模式屬於特殊的“宣告式”程式設計,即通過抽象定義一個業務處理邏輯中各步驟的先後執行順序,但對於各步驟的具體實現並不關心,交由執行時實際的子類物件受理。這也充分利用了OOP的多型性
例如,現有一個訂單業務類OrderService
如下:
public class OrderService {
public void makeOrder() {
safeVerification();
reduceStock();
reduceBalance();
noticeDelivery();
}
public void safeVerification() {
System.out.println("安全校驗");
}
public void reduceStock() {
System.out.println("去MySQL減庫存");
}
public void reduceBalance() {
System.out.println("去MySQL減餘額");
}
public void noticeDelivery() {
System.out.println("通知發貨");
}
}
複製程式碼
客戶端程式碼如下
public class Client {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.makeOrder();
}
}
複製程式碼
上述程式碼邏輯清晰,看起來沒什麼問題。但假設現在要做秒殺,需要將庫存、餘額等資訊做一個快取,那你就需要在原有的OrderService
上做修改了。這違反了“開閉原則”(應對修改關閉而對擴充套件開放)。
此時我們就需要將業務步驟抽象出來,具體的實現交由特定的子類去做,以滿足不同的業務場景:
public abstract class AbstractOrderService {
public void makeOrder() {
safeVerification();
reduceStock();
reduceBalance();
noticeDelivery();
}
public void safeVerification(){
System.out.println("安全校驗");
}
public abstract void reduceStock() ;
public abstract void reduceBalance();
public void noticeDelivery(){
System.out.println("通知發貨");
}
}
複製程式碼
此時若需要快取支援,只需新增一個實現類即可
public class OrderServiceWithCache extends AbstractOrderService {
@Override
public void reduceStock() {
System.out.println("從快取中減庫存");
}
@Override
public void reduceBalance() {
System.out.println("從快取中減餘額");
}
}
複製程式碼
客戶端程式碼只需切換具體的實現類:
public class Client {
public static void main(String[] args) {
AbstractOrderService orderService = new OrderServiceWithCache(); //使用快取
orderService.makeOrder();
orderService = new OrderService(); // 切換到MySQL
orderService.makeOrder();
}
}
複製程式碼
UML & Summary
模板方法模式就是在抽象類中進行宣告式程式設計(使用抽象方法的形式強調該業務的完成應該執行哪些步驟以及這些步驟的執行順序,而不關注每個步驟具體是如何實現的),而將具體業務步驟的實現交由子類(執行時通過多型)完成。
在客戶端看來,雖然只是切換了一下子類例項,但好像被切換的例項實現的具體步驟就被注入到整體地業務處理之中一樣。
並且對於通用的步驟,如上述的safeVerification
和noticeDelivery
,可能它們的處理邏輯是固定的,這時可以將它們提取到父類中,實現複用。
Roles
-
SuperClass & Template Method
,模板方法通常宣告在抽象類或介面中,呼叫本類的抽象方法(當然也可以是包含通用邏輯的非抽象方法)完成特定的業務邏輯。 -
Concrete SubClass & Realization
,子類只需實現相應的抽象方法就可以將具體的功能步驟注入到整體業務功能中,無需自己顯式呼叫(模板方法會根據執行時資訊動態呼叫)
Factory Method
Delay the Instance’s Creation
工廠方法模式就是模板方法模式的一個應用,只不過就是抽象父類中的抽象方法特化為一個建立例項的方法,將例項的建立延遲到了子類。
例如Person
類有一個獲取自己交通工具的抽象方法getVehicle
,並且能夠在其他地方呼叫該Vehicle
暴露的屬性、方法,而將Vehicle
例項的獲取延遲到了子類(說是延遲,因為本身是抽象類,是無法被例項化的,因此在例項化Person
的具體子類時能夠確保其getVehicle
已被重寫了)。
如下是示例程式碼:
public class Vehicle {
private String name;
public Vehicle(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Bicycle extends Vehicle {
public Bicycle() {
super("自行車");
}
}
public class JeepCar extends Vehicle {
public JeepCar() {
super("小汽車");
}
}
public abstract class Person {
public void useVehicle() {
System.out.println("使用交通工具"+getVehicle().getName()+"來代步");
}
protected abstract Vehicle getVehicle();
}
public class Student extends Person {
@Override
protected Vehicle getVehicle() {
return new Bicycle();
}
}
public class Boss extends Person {
@Override
protected Vehicle getVehicle() {
return new JeepCar();
}
}
public class Client {
public static void main(String[] args) {
Person p = new Student();
p.useVehicle();
p = new Boss();
p.useVehicle();
}
}
複製程式碼
UML & Summary
Roles
-
Factory Method
,抽象父類中將某類例項的獲取延遲到具體子類,但本類中的其他方法可以呼叫該方法並認為已獲取到了該類例項,然後進行屬性、方法的訪問 -
Concrete Method
,真正的獲取並返回所需例項的邏輯
Generating Instance
Singleton
Seven Method
-
Lazy loading
懶載入模式,
instance
會在getInstance
第一次被呼叫時被初始化public class LazyLoadingSingleton { private LazyLoadingSingleton() { } private static LazyLoadingSingleton instance; public static LazyLoadingSingleton getInstance() { if (instance == null) { instance = new LazyLoadingSingleton(); } return instance; } } 複製程式碼
-
Synchronized Block Singleton
上述程式碼在多執行緒併發執行時會出現
instance
被多次賦值的問題,為此可用內部鎖語義Synchronized
解決public class SynchronizerBlockSingleton { private SynchronizerBlockSingleton() { } private static SynchronizerBlockSingleton instance; public static SynchronizerBlockSingleton getInstance() { synchronized (SynchronizerBlockSingleton.class) { if (instance == null) { instance = new SynchronizerBlockSingleton(); } } return instance; } } 複製程式碼
-
Double-Checked Lock
雙重檢查鎖定,上述程式碼先鎖定,然後檢查,會導致
instance
被賦值後synchronized
對後續併發呼叫getInstance
帶來上下文切換的開銷,為此可以在鎖定前先檢查一次public class DoubleCheckedSingleton { private DoubleCheckedSingleton() { } private static DoubleCheckedSingleton instance; public static DoubleCheckedSingleton getInstance() { if (instance == null) { synchronized (DoubleCheckedSingleton.class) { if (instance == null) { instance = new DoubleCheckedSingleton(); } } } return instance; } } 複製程式碼
-
DCL with
volatile
由於指令重排序可能會導致
new
關鍵字初始化物件還未完成就返回物件的記憶體地址,進而導致後續訪問instance
屬性時拋空指標異常,需要使用volatile
保證物件初始化完畢後才返回引用地址public class VolatileSingleton { private VolatileSingleton() { } private static volatile VolatileSingleton instance; public static VolatileSingleton getInstance() { if (instance == null) { synchronized (VolatileSingleton.class) { if (instance == null) { instance = new VolatileSingleton(); } } } return instance; } } 複製程式碼
-
Eager Mode
餓漢模式,如果例項物件的初始化開銷較小(佔用記憶體、初始化時間),那麼完全可以在類初始化時完成
public class EagerSingleton { private EagerSingleton() { } private static EagerSingleton instance = new EagerSingleton(); public static EagerSingleton getInstance() { return instance; } } 複製程式碼
instance
會在類初始化時被初始化,類只會在發生主動引用時被初始化一次,由JVM來保證 -
Instance Holder
如果你仍想使用懶漢模式又想優雅些,則可使用靜態內部類的方式
public class InstanceHolderSingleton { private static class SingletonHolder { private static InstanceHolderSingleton instance = new InstanceHolderSingleton(); } private InstanceHolderSingleton() { } public static InstanceHolderSingleton getInstance() { return SingletonHolder.instance; } } 複製程式碼
初始化
InstanceHolderSingleton
時並不會初始化其靜態內部類SingletonHolder
,只有在呼叫InstanceHolderSingleton.getInstance()
時,instance
才會隨著SingletonHolder
的初始化而初始化。以下種情況會立即導致類的初始化:
- 使用
new
關鍵字建立該類例項 - 訪問該類的靜態域(包括靜態屬性和靜態方法,但不包括常量)
- 通過
java.reflect
包下的類反射訪問該類,如Class.forName
- 初始化子類時會檢查其父類是否已被初始化,若父類未被初始化則先初始化其父類
- 使用
-
列舉類的優雅
JVM也會保證列舉例項在初始化列舉時被初始化一次
public class EnumSingleton { private EnumSingleton() { } private static EnumSingleton instance; private enum InstanceEnum{ INSTANCE; private EnumSingleton instance; InstanceEnum() { instance = new EnumSingleton(); } } public static EnumSingleton getInstance() { return InstanceEnum.INSTANCE.instance; } } 複製程式碼
Builder
組裝具有複雜結構的例項
建造者模式通常應用於需要通過一系列複雜步驟才能得到最終例項的情況。就像建造房子一樣,我們需要經過打地基、搭建框架、添磚加瓦、粉飾美化等一系列步驟才能得到最終能住人的房子。並且,這些步驟可以個性化定製,例如有人喜歡歐美風格的,那麼房屋框架頂部就要打造成錐形的;有人喜歡粉色,那就可以鋪上粉色的牆紙……
本例中,我們以一個郵件內容String
例項的生成來演示Builder
設計模式的應用。
public abstract class EmailBuilder {
protected String content = "";
public String getContent() {
return content;
}
public abstract void makeTitle(String title);
public abstract void makeBody(String body);
public abstract void makeGreeting(String greeting);
}
public class TextEmailBuilder extends EmailBuilder {
@Override
public void makeTitle(String title) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append(title).append("\n");
content = stringBuilder.toString();
}
@Override
public void makeBody(String body) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append(body).append("\n");
content = stringBuilder.toString();
}
@Override
public void makeGreeting(String greeting) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append(greeting).append("\n");
content = stringBuilder.toString();
}
}
public class HTMLEmailBuilder extends EmailBuilder {
@Override
public void makeTitle(String title) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append("<h3>").append(title).append("</h3>").append("\n");
content = stringBuilder.toString();
}
@Override
public void makeBody(String body) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append("<p style=\"font-family: Microsoft Ya Hei; font-size: 16px\">").append(body).append("</p>").append("\n");
content = stringBuilder.toString();
}
@Override
public void makeGreeting(String greeting) {
StringBuilder stringBuilder = new StringBuilder(content);
stringBuilder.append("<i>").append(greeting).append("</i>").append("\n");
content = stringBuilder.toString();
}
}
public class Director {
private EmailBuilder emailBuilder;
public Director(EmailBuilder emailBuilder) {
this.emailBuilder = emailBuilder;
}
public void construct() {
emailBuilder.makeTitle("About Interview");
emailBuilder.makeBody("We are honor to tell you that you can participate our interview.");
emailBuilder.makeGreeting("Good Luck!");
}
}
複製程式碼
UML & Summary
其中EmailBuilder
中宣告瞭例項的初始狀態(空串)和構建例項的一系列過程,而TextEmailBuilder
和HTMLEmailBuilder
則對這一系列過程進行了個性化實現。最終Director
是建造例項整個過程的監工,由它確保例項的成型規則地經歷了哪些建造過程。
Roles
-
Builder
,定義了產品成型需要經過的一系列工藝(介面方法) -
ConcreteBuilder
,針對每道工藝進行個性化處理 -
Director
,監工,根據產品成型流程呼叫介面方法打造產品 -
Client
,模式使用者,通知Director
根據傳入的ConcreteBuilder
打造特定風格的產品
Abstract Factory
將一組關聯的零件組裝成產品
AbstractFactory
其實就是包含了一系列Factory Method
的類,只不過這些Factory Method
生成的例項都是相互關聯的,一起組成某個共同體,少了誰都不行。
例如汽車Car
需要汽車外殼Facade
、輪胎Wheel
、發動機Engine
等部件,那麼我們就可以建立一個CarFactory
抽象工廠,其中宣告瞭一系列部件的獲取(抽象方法,不關心該部件是哪個廠家生產的或是哪個牌子的),並提供了產品的構造過程(呼叫這一系列抽象方法獲取所需部件組裝成車)
public class Engine {
String name;
public Engine(String name) {
this.name = name;
}
}
複製程式碼
public class Wheel {
String name;
public Wheel(String name) {
this.name = name;
}
}
複製程式碼
public class Facade {
String name;
public Facade(String name) {
this.name = name;
}
}
複製程式碼
public class Car {
Engine engine;
Wheel wheel;
Facade facade;
public Car(Engine engine,Wheel wheel,Facade facade) {
this.engine = engine;
this.wheel = wheel;
this.facade = facade;
}
}
複製程式碼
public abstract class CarFactory {
public Car getCar() {
return new Car(getEngine(),getWheel(),getFacade());
}
public abstract Engine getEngine();
public abstract Wheel getWheel();
public abstract Facade getFacade();
}
複製程式碼
public class CustomEngine extends Engine {
public CustomEngine() {
super("自定義牌發動機");
}
}
複製程式碼
public class CustomWheel extends Wheel{
public CustomWheel() {
super("自定義牌輪胎");
}
}
複製程式碼
public class CustomFacade extends Facade {
public CustomFacade() {
super("自定義牌車殼");
}
}
複製程式碼
public class CustomCarFactory extends CarFactory{
@Override
public Engine getEngine() {
return new CustomEngine();
}
@Override
public Wheel getWheel() {
return new CustomWheel();
}
@Override
public Facade getFacade() {
return new CustomFacade();
}
}
複製程式碼
public class Client {
public static void main(String[] args) {
CarFactory carFactory = new CustomCarFactory();
Car car = carFactory.getCar();
System.out.println("custom car -> " + car.engine.name + "+" + car.wheel.name + "+" + car.facade.name);
}
}
複製程式碼
UML
Consider Individualy
Bridge
將類的功能層次和類的實現層次分開
-
類的功能層次結構
通過繼承我們能夠繼承基類已有的功能,在此之上我們能夠:重寫基類已有功能(使該功能具備本類特色)、也可以新增功能,重寫(這裡的重寫特指重寫基類的已有實現)或新增功能的子類與基類構成類的功能層次結構
public class Animal { public void eat() { System.out.println("動物會覓食"); } } public class Bird extends Animal { @Override public void eat() { System.out.println("鳥覓食蟲子"); } public void fly() { System.out.println("鳥會飛"); } } 複製程式碼
-
類的實現層次結構
通過繼承,我們能夠實現基類的抽象方法,通過運用
Template Method
,我們可以將子類的邏輯注入到基類模板過程中,這時新增子類僅為了實現基類的抽象方法,子類和基類構成類的實現層次結構public abstract class Animal { public void hunt() { lockTarget(); quickAttack(); swallow(); } public abstract void lockTarget(); public abstract void quickAttack(); public abstract void swallow(); } public class Snake extends Animal { @Override public void lockTarget() { System.out.println("鎖定獵物"); } @Override public void quickAttack() { System.out.println("迅速咬住獵物喉部"); } @Override public void swallow() { System.out.println("一口吞掉整個獵物"); } } 複製程式碼
如果我們將兩個例子的Animal
整合在一起:
public abstract class Animal {
public void eat() {
System.out.println("動物會覓食");
}
public void hunt() {
lockTarget();
quickAttack();
swallow();
}
public abstract void lockTarget();
public abstract void quickAttack();
public abstract void swallow();
}
複製程式碼
你會發現,Bird
無法編譯,作為具體子類它必須實現抽象方法lockTarget
、quickAttack
、swallow
,但是我們新增Bird
的初衷只是為了繼承Animal
的eat
方法,並新增一個自己會fly
的功能。
這時就需要我們將類的功能層次和實現層次分開了
public abstract class Animal {
private Hunt hunt;
public Animal(Hunt hunt) {
this.hunt = hunt;
}
public void eat() {
System.out.println("動物會覓食");
}
public void hunt() {
hunt.hunt();
}
}
public abstract class Hunt {
public void hunt() {
lockTarget();
quickAttack();
swallow();
}
public abstract void lockTarget();
public abstract void quickAttack();
public abstract void swallow();
}
public class Bird extends Animal {
public Bird(Hunt hunt) {
super(hunt);
}
@Override
public void eat() {
System.out.println("鳥覓食蟲子");
}
public void fly() {
System.out.println("鳥會飛");
}
}
public class DefaultHunt extends Hunt {
@Override
public void lockTarget() {
System.out.println("用眼睛鎖定獵物");
}
@Override
public void quickAttack() {
System.out.println("快速咬死獵物");
}
@Override
public void swallow() {
System.out.println("一口一口吃掉獵物");
}
}
public class Snake extends Animal {
public Snake(Hunt hunt) {
super(hunt);
}
}
public class SnakeHunt extends Hunt {
@Override
public void lockTarget() {
System.out.println("紅外線感知鎖定獵物");
}
@Override
public void quickAttack() {
System.out.println("使用尖牙和毒液快速致死獵物");
}
@Override
public void swallow() {
System.out.println("一口吞掉整個獵物");
}
}
public class Client {
public static void main(String[] args) {
Hunt defaultHunt = new DefaultHunt();
Hunt snakeHunt = new SnakeHunt();
Animal snake = new Snake(snakeHunt);
System.out.println("蛇開始狩獵==========");
snake.hunt();
Animal bird = new Bird(defaultHunt);
System.out.println("鳥開始狩獵===========");
bird.hunt();
System.out.println("鳥有不同於一般動物的功能");
((Bird) bird).fly();
}
}
複製程式碼
UML & Summary
如上,Animal
、Bird
、Snake
組成功能層次結構、Hunt
、DefaultHunt
、Snake
則組成了實現層次結構。這樣,以後如果我們想擴充套件功能(重寫或新增),那麼就可以找對應功能層次結構中的類繼承;如果想針對狩獵方式進行個性化實現,則繼承實現層次結構中的類即可。
橋接模式避免了將實現和擴充套件捆綁在一起,減少底層類擴充套件基類的壓力
Roles
- 功能層次結構
- 實現層次結構
- 功能層次結構的基類持有實現層次結構基類的引用,並將實現層面的邏輯委託給該引用
Strategy
封裝一個特定的演演算法
假如你是一個農場主,需要按照顧客的需求從已採摘的蘋果中挑選出符合顧客標準的蘋果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
private AppleColorEnum color;
private double weight;
public enum AppleColorEnum {
RED,YELLOW,GREEN
}
}
複製程式碼
於是你編寫瞭如下挑選蘋果的業務處理類
public class AppleService {
List<Apple> findAppleByColor(List<Apple> apples,Apple.AppleColorEnum color) {
List<Apple> res = new ArrayList<>();
for (Apple apple : apples) {
if (Objects.equals(apple.getColor(),color)){
res.add(apple);
}
}
return res;
}
}
複製程式碼
但是如果你遇到了一個刁鑽的顧客,他不僅要求顏色為紅色,而且還要求重量在500g以上呢?你可以再新增一個findRedAndWeightGreatThan500
,但是每個顧客可能對顏色和重量的標準都是不一樣的,並且你無法估量有哪些顧客,對應有哪些挑選標準。
這時就需要把挑選標準(實際上就是一個演演算法)單獨抽離出來,分析輸入和輸出:
- 輸入:
Apple
,給你一個蘋果 - 輸出:
boolean
,該蘋果是否符合標準
public interface AppleFilterStrategy {
/**
* 如果該蘋果符合挑選標準,那麼就返回true
* @param apple
* @return
*/
boolean filterApple(Apple apple);
}
複製程式碼
這時,挑選蘋果業務類就無需預知和預置眾多挑選蘋果的方法了,因為挑選策略交給了AppleFilterStrategy
public class AppleService {
List<Apple> findApple(List<Apple> apples,AppleFilterStrategy strategy) {
List<Apple> res = new ArrayList<>();
for (Apple apple : apples) {
if (strategy.filterApple(apple)) {
res.add(apple);
}
}
return res;
}
}
複製程式碼
客戶端可以根據客戶提出的挑選需求,通過匿名類的方式隨意地注入挑選策略
public class Client {
public static void main(String[] args) {
// 農場主採摘的蘋果
List<Apple> apples = Arrays.asList(
new Apple(Apple.AppleColorEnum.RED,200),new Apple(Apple.AppleColorEnum.RED,400),600),new Apple(Apple.AppleColorEnum.YELLOW,100),500),900),new Apple(Apple.AppleColorEnum.GREEN,600)
);
AppleService appleService = new AppleService();
// A顧客需要紅色的重量大於500g的蘋果
List<Apple> res1 = appleService.findApple(apples,new AppleFilterStrategy() {
@Override
public boolean filterApple(Apple apple) {
return Objects.equals(apple.getColor(),Apple.AppleColorEnum.RED) &&
apple.getWeight() >= 500;
}
});
System.out.println(res1);
System.out.println("======================");
// B顧客需要青色的種類小於400的
List<Apple> res2 = appleService.findApple(apples,Apple.AppleColorEnum.GREEN) &&
apple.getWeight() <= 400;
}
});
System.out.println(res2);
}
}
複製程式碼
函式語言程式設計
在Java8之後,像AppleFilterStrategy
這種只包含一個介面方法的介面可以被標註為@FunctionalInterface
,並使用Lambda
表示式替代冗餘的匿名類
@FunctionalInterface
public interface AppleFilterStrategy {
boolean filterApple(Apple apple);
}
複製程式碼
List<Apple> res3 = appleService.findApple(apples,apple -> apple.getColor() != Apple.AppleColorEnum.GREEN && apple.getWeight() >= 300);
System.out.println(res3);
複製程式碼
UML & Summary
策略模式的核心思想就是將複雜多變的演演算法邏輯抽取出來,交給客戶端實現,而業務層只負責應用客戶端傳遞的演演算法實現
Role
-
Strategy Interface
,策略介面,定義了某個特定的演演算法 -
Service
,應用策略介面的演演算法,面對抽象程式設計 -
Client
,呼叫Service
時注入具體的演演算法實現 -
Concrete Strategy
,具體的演演算法實現,通常以匿名類的形式存在,Java8之後可用Lambda
代替
Consistency
Composite
讓容器和容器中的內容有著一致的外觀
混合模式的典型應用就是檔案系統,一個目錄Directory
中可以存放若干條目Entry
,每個條目既可以是目錄又可以是檔案File
。混合模式的目的就是讓容器(如目錄)和容器中的內容(如目錄或檔案)有著一致性的外觀(如Entry
)
public abstract class Entry {
private String name;
private int size;
private List<Entry> items;
public Entry(String name,int size) {
this.name = name;
this.size = size;
items = new ArrayList<>();
}
public void addEntry(Entry entry) {
this.items.add(entry);
}
public abstract void print(int... layer);
}
public class Directory extends Entry {
public Directory(String name,int size) {
super(name,size);
}
@Override
public void print(int... layer) {
int n;
if (layer == null || layer.length == 0) {
n = 0;
} else {
n = layer[0];
}
for (int i = 0; i < n; i++) {
System.out.print("\t");
}
System.out.println(getName());
getItems().forEach(entry -> entry.print(n + 1));
}
}
public class File extends Entry {
public File(String name,size);
}
@Override
public void print(int... layer) {
int n;
if (layer == null || layer.length == 0) {
n = 0;
} else {
n = layer[0];
}
for (int i = 0; i < n; i++) {
System.out.print("\t");
}
System.out.println(getName() + " size=" + getSize()+"kb");
getItems().forEach(entry -> entry.print(n + 1));
}
}
public class Client {
public static void main(String[] args) {
Entry root = new Directory("/root",2);
Entry bin = new Directory("/bin",0);
root.addEntry(bin);
Entry usr = new Directory("/usr",1);
root.addEntry(usr);
Entry etc = new Directory("/etc",0);
root.addEntry(etc);
Entry local = new Directory("/local",3);
usr.addEntry(local);
Entry java = new File("java.sh",128);
local.addEntry(java);
Entry mysql = new File("mysql.sh",64);
local.addEntry(mysql);
Entry hadoop = new File("hadoop.sh",1024);
local.addEntry(hadoop);
root.print();
}
}
/root
/bin
/usr
/local
java.sh size=128kb
mysql.sh size=64kb
hadoop.sh size=1024kb
/etc
複製程式碼
UML
Roles
-
元件
如本例的
Entry
,是容器和容器內容的同一外觀。系統不直接操作容器和容器中的內容,而是操作元件 -
容器
其中可包含若干容器和條目
-
條目
系統中的基本單元
Decorator
裝飾者模式是一種結合繼承和組合(委託)設計模式,通過繼承能夠實現統一外觀(裝飾類和目標類有共同的父類),通過委託能夠在目標功能的基礎之上進行增強。並且裝飾類不管目標類是源目標類還是被裝飾過的目標類,它對目標類是否已被儲存過和被哪種包裝器(裝飾類)包裝過以及被包裝了幾層都不關心,它只負責當前的裝飾邏輯。
不改變目標類的行為,只是在其之上做一些包裝
本例中,有一個糕點師Baker
,他有一個bake
烘焙麵包的抽象方法,BreadBaker
則是他的一個實現。現在我們需要根據顧客的不同口味對原味的麵包進行包裝,例如應該加哪些佐料Ingredient
,以下是示例程式碼
public abstract class Baker {
public abstract void bake();
}
public class BreadBaker extends Baker {
@Override
public void bake() {
System.out.println("麵包被烘焙");
}
}
複製程式碼
public class IngredientDecorator extends Baker {
private Baker baker;
private String ingredient;
public IngredientDecorator(Baker baker,String ingredient) {
this.baker = baker;
this.ingredient = ingredient;
}
@Override
public void bake() {
System.out.print("添加了" + ingredient + "的");
baker.bake();
}
}
複製程式碼
public class Client {
public static void main(String[] args) {
Baker baker = new BreadBaker();
baker.bake();
Baker pepper = new IngredientDecorator(baker,"胡椒粉");
pepper.bake();
Baker mustard = new IngredientDecorator(baker,"芥末");
mustard.bake();
Baker oliveOil = new IngredientDecorator(baker,"橄欖油");
oliveOil.bake();
Baker pepperAndOlive = new IngredientDecorator(new IngredientDecorator(baker,"橄欖油"),"胡椒粉");
pepperAndOlive.bake();
Baker mustardAndOliveAndPepper =
new IngredientDecorator(
new IngredientDecorator(
new IngredientDecorator(baker,"胡椒粉"),"芥末");
mustardAndOliveAndPepper.bake();
}
}
麵包被烘焙
添加了胡椒粉的麵包被烘焙
添加了芥末的麵包被烘焙
添加了橄欖油的麵包被烘焙
添加了胡椒粉的添加了橄欖油的麵包被烘焙
添加了芥末的添加了橄欖油的添加了胡椒粉的麵包被烘焙
複製程式碼
當然本例只是單純演示裝飾者模式的思想,你完全可以將Ingredient
具體化為PepperIngredient
、MustardIngredident
等代替字串魔法值
UML & Summary
應用裝飾者模式有一個口訣:是你(IS-A)還有你(HAS-A),一切拜託你(所有的重寫方法委託給目標類物件,在此之上自己可以新增當前這一層包裝的邏輯)。
Roles
-
Parent
,裝飾類和目標類有一個共同的父類,通過在父類宣告抽象方法使得兩者有共同的外觀 -
Target
,目標類,需要被包裝的類 -
Decorator
,裝飾類,既可以直接裝飾目標物件,又可以裝飾裝飾過目標物件的裝飾物件,因為兩者有共同的外觀
Access Data Structure
Visitor
將資料結構的管理和訪問處理分開
visitor
模式目的是將資料結構的管理(對元素的增刪改查)和對資料結構的訪問處理邏輯分離開,通常和迭代器模式結合使用。我們將對資料訪問處理的邏輯單獨定義一個Visitor
介面以及宣告相應的visit(E element)
方法,而資料介面則對應提供一個受理Visitor
的accept(Visitor visitor)
方法(其實就是簡單的呼叫visitor.visit()
。visit(E element)
相當於對訪問到的元素進行消費。
以下是資料結構為多叉樹時的示例程式碼(其中迭代器用的是前文實現的而非JDK自帶的)
@Data
public abstract class Node<E> implements Iterable<Node<E>> {
private E element;
private List<Node<E>> children;
public Node(E element) {
this.element = element;
}
public abstract void accept(NodeVisitor<E> visitor);
@Override
public Iterator<Node<E>> iterator() {
return new NodeIterator(this);
}
}
public class Leaf<E> extends Node<E> {
public Leaf(E element) {
super(element);
this.setChildren(Collections.emptyList());
}
@Override
public void accept(NodeVisitor<E> visitor) {
visitor.visitLeaf(this);
}
}
public class Branch<E> extends Node<E> {
public Branch(E elemnt) {
super(elemnt);
this.setChildren(new ArrayList<>());
}
@Override
public void accept(NodeVisitor<E> visitor) {
visitor.visitBranch(this);
}
}
複製程式碼
public class NodeIterator<E> implements Iterator<Node<E>> {
private Node<E> root;
private Stack<Node<E>> stack;
public NodeIterator(Node<E> root) {
this.root = root;
this.stack = new Stack<>();
stack.push(root);
}
@Override
public boolean hasNext() {
return stack.size() > 0;
}
@Override
public Node<E> next() {
if (!hasNext()) {
throw new RuntimeException("no more elements");
}
Node<E> node = stack.pop();
List<Node<E>> children = node.getChildren();
for (int i = children.size() - 1; i >= 0; i--) {
stack.push(children.get(i));
}
return node;
}
}
複製程式碼
public interface NodeVisitor<E> {
void visitLeaf(Leaf<E> leaf);
void visitBranch(Branch<E> branch);
}
public class PrintNodeVisitor<E> implements NodeVisitor<E> {
@Override
public void visitLeaf(Leaf<E> leaf) {
System.out.print(leaf.getElement()+" ");
}
@Override
public void visitBranch(Branch<E> branch) {
System.out.print(branch.getElement()+" ");
}
}
public class PlusOneNodeVisitor implements NodeVisitor<Integer> {
/**
* 訪問到葉子節點則將節點值+1
* @param leaf
*/
@Override
public void visitLeaf(Leaf<Integer> leaf) {
leaf.setElement(leaf.getElement() + 1);
}
/**
* 訪問到分叉節點,則將節點值+其孩子節點數
* @param branch
*/
@Override
public void visitBranch(Branch<Integer> branch) {
branch.setElement(branch.getElement() + branch.getChildren().size());
}
}
複製程式碼
UML & Summary
visitor
模式將對資料結構的訪問邏輯通過accept
委託給Visitor
介面和迭代器模式將遍歷訪問邏輯通過createIterator
委託給Iterator
介面有異曲同工之妙。將專業的事交給專業的人做,滿足Single Responsibility
和Interface Segregation Principle
;需要修改訪問處理邏輯我們只需要新增一個NodeVisitor
的實現,滿足Open/Closed Principle
;邏輯實現和客戶端程式碼都面向介面NodeVisitor
程式設計,滿足Dependency Inversion
。
Roles
-
Visitor
,拜訪者,宣告相關的visit
方法,用於對資料結構進行訪問處理 -
DataStructure
,資料結構,提供元素的增刪改查介面,通過accept(visitor)
將訪問處理邏輯交給Visitor
-
ConcreteVisitor
,根據業務所需,實現具體的訪問處理邏輯
Chain Of Responsibility
責任鏈模式通過維護若干請求受理者形成一條鏈來處理客戶端發起的請求,該模式最大的優點在於它弱化了客戶端和請求受理者之間的關係,客戶端只需要將請求傳送到責任鏈,在責任鏈中通過連環委託的機制就能夠做到無法受理請求的人直接忽略請求,而能夠受理請求的人截斷請求並受理。
客戶端不用關心請求具體會被誰受理,這樣就提高了客戶端的獨立性。
弱化請求方和受理方之間的關係
本例中,存在一條公司組織架構責任鏈(Employee->Leader->Manager->Boss
),他們都能夠受理報銷費用的請求handle(int amount)
,Client
無需關心多少面值的報銷金額應該由誰來受理
public abstract class Handler {
private Handler handler = null;
public abstract void handle(int amount);
public Handler setNext(Handler handler) {
this.handler = handler;
return handler;
}
public Handler getNext() {
return handler;
}
}
public class Employee extends Handler {
@Override
public void handle(int amount) {
if (amount <= 100) {
System.out.println("$" + amount + " is handled by employee");
return;
}
this.getNext().handle(amount);
}
}
public class Leader extends Handler{
@Override
public void handle(int amount) {
if (amount <= 1000) {
System.out.println("$" + amount + " is handled by leader");
return;
}
this.getNext().handle(amount);
}
}
public class Manager extends Handler{
@Override
public void handle(int amount) {
if (amount <= 5000) {
System.out.println("$" + amount + " is handled by manager");
return;
}
this.getNext().handle(amount);
}
}
public class Boss extends Handler {
@Override
public void handle(int amount) {
System.out.println("$" + amount + " is handled by boss");
}
}
複製程式碼
public class Client {
public static void main(String[] args) {
Handler employee = new Employee();
Handler leader = new Leader();
Handler manager = new Manager();
Handler boss = new Boss();
employee.setNext(leader).setNext(manager).setNext(boss);
for (int i = 0; i < 6; i++) {
int amount = (int) (Math.random() * 10000);
employee.handle(amount);
}
}
}
$6643 is handled by boss
$4964 is handled by manager
$684 is handled by leader
$9176 is handled by boss
$8054 is handled by boss
$909 is handled by leader
複製程式碼
參考資料
《圖解設計模式》