37.設計模式(3)行為型

1. 責任鏈

使多個對象都有機會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系。將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。

責任鏈模式(Chain of Responsibility)是一種處理請求的模式,它讓多個處理器都有機會處理該請求,直到其中某個處理成功為止。責任鏈模式把多個處理器串成鏈,然后讓請求在鏈上傳遞:

     ┌─────────┐
     │ Request │
     └─────────┘
          │
┌ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┐
          ▼
│  ┌─────────────┐  │
   │ ProcessorA  │
│  └─────────────┘  │
          │
│         ▼         │
   ┌─────────────┐
│  │ ProcessorB  │  │
   └─────────────┘
│         │         │
          ▼
│  ┌─────────────┐  │
   │ ProcessorC  │
│  └─────────────┘  │
          │
└ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┘
          │
          ▼

財務審批
假設某個員工需要報銷一筆費用,審核者可以分為:

  • Manager:只能審核1000元以下的報銷;

  • Director:只能審核10000元以下的報銷;

  • CEO:可以審核任意額度。

  • 首先,要抽象出請求對象,它將在責任鏈上傳遞:

public class Request {
    private String name;
    private BigDecimal amount;
    public Request(String name, BigDecimal amount) {
        this.name = name;
        this.amount = amount;
    }
    public String getName() {
        return name;
    }
    public BigDecimal getAmount() {
        return amount;
    }
}
  • 其次,我們要抽象出處理器:
public interface Handler {
    // 返回Boolean.TRUE = 成功
    // 返回Boolean.FALSE = 拒絕
    // 返回null = 交下一個處理
    Boolean process(Request request);
}
  • 然后,依次編寫ManagerHandlerDirectorHandlerCEOHandler。以ManagerHandler為例:
public class ManagerHandler implements Handler {
    public Boolean process(Request request) {
        // 如果超過1000元,處理不了,交下一個處理:
        if (request.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0) {
            return null;
        }
        return !request.getName().equalsIgnoreCase("bob");
    }
}
  • 有了不同的Handler后,我們還要把這些Handler組合起來,變成一個鏈,并通過一個統(tǒng)一入口處理:
public class HandlerChain {
    // 持有所有Handler:
    private List<Handler> handlers = new ArrayList<>();
    public void addHandler(Handler handler) {
        this.handlers.add(handler);
    }
    public boolean process(Request request) {
        // 依次調(diào)用每個Handler:
        for (Handler handler : handlers) {
            Boolean r = handler.process(request);
            if (r != null) {
                // 如果返回TRUE或FALSE,處理結(jié)束:
                System.out.println(request + " " + (r ? "Approved by " : "Denied by ") + handler.getClass().getSimpleName());
                return r;
            }
        }
        throw new RuntimeException("Could not handle request: " + request);
    }
}
  • 客戶端組裝出責任鏈,然后用責任鏈來處理請求:
// 構(gòu)造責任鏈:
HandlerChain chain = new HandlerChain();
chain.addHandler(new ManagerHandler());
chain.addHandler(new DirectorHandler());
chain.addHandler(new CEOHandler());
// 處理請求:
chain.process(new Request("Bob", new BigDecimal("123.45")));
chain.process(new Request("Alice", new BigDecimal("1234.56")));
chain.process(new Request("Bill", new BigDecimal("12345.67")));
chain.process(new Request("John", new BigDecimal("123456.78")));

責任鏈模式有很多變種。

  • 有些責任鏈的實現(xiàn)方式是通過某個Handler手動調(diào)用下一個Handler來傳遞Request
  • 還有一些責任鏈模式,每個Handler都有機會處理Request,通常這種責任鏈被稱為攔截器(Interceptor)或者過濾器(Filter

2. 命令

將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數(shù)化,對請求排隊或記錄請求日志,以及支持可撤銷的操作。

命令模式(Command)是指,把請求封裝成一個命令,然后執(zhí)行該命令。

public class TextEditor {
    private StringBuilder buffer = new StringBuilder();
    public void copy() {
        ...
    }
    public void paste() {
        String text = getFromClipBoard();
        add(text);
    }
    public void add(String s) {
        buffer.append(s);
    }
    public void delete() {
        if (buffer.length() > 0) {
            buffer.deleteCharAt(buffer.length() - 1);
        }
    }
    public String getState() {
        return buffer.toString();
    }
}

public interface Command {
    void execute();
}

public class CopyCommand implements Command {
    // 持有執(zhí)行者對象:
    private TextEditor receiver;
    public CopyCommand(TextEditor receiver) {
        this.receiver = receiver;
    }
    public void execute() {
        receiver.copy();
    }
}
public class PasteCommand implements Command {
    private TextEditor receiver;
    public PasteCommand(TextEditor receiver) {
        this.receiver = receiver;
    }
    public void execute() {
        receiver.paste();
    }
}

TextEditor editor = new TextEditor();
editor.add("Command pattern in text editor.\n");
// 執(zhí)行一個CopyCommand:
Command copy = new CopyCommand(editor);
copy.execute();
editor.add("----\n");
// 執(zhí)行一個PasteCommand:
Command paste = new PasteCommand(editor);
paste.execute();
System.out.println(editor.getState());
┌──────┐      ┌───────┐
│Client│─ ─ ─>│Command│
└──────┘      └───────┘
                  │  ┌──────────────┐
                  ├─>│ CopyCommand  │
                  │  ├──────────────┤
                  │  │editor.copy() │─ ┐
                  │  └──────────────┘
                  │                    │  ┌────────────┐
                  │  ┌──────────────┐   ─>│ TextEditor │
                  └─>│ PasteCommand │  │  └────────────┘
                     ├──────────────┤
                     │editor.paste()│─ ┘
                     └──────────────┘

命令模式雖然增加了系統(tǒng)的復雜度,但是當TextEditor比較復雜時,就需要用到命令模式。

3.解釋器

給定一個語言,定義它的文法的一種表示,并定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。

解釋器模式(Interpreter)是一種針對特定問題設計的一種解決方案。例如,匹配字符串的時候,由于匹配條件非常靈活,使得通過代碼來實現(xiàn)非常不靈活。

  • +開頭的數(shù)字表示的區(qū)號和電話號碼,如+861012345678
  • 以英文開頭,后接英文和數(shù)字,并以.分隔的域名,如www.liaoxuefeng.com;
  • /開頭的文件路徑,如/path/to/file.txt;

正則表達式就是一個字符串,但要把正則表達式解析為語法樹,然后再匹配指定的字符串,就需要一個解釋器。
實現(xiàn)一個完整的正則表達式的解釋器非常復雜,但是使用解釋器模式卻很簡單:

String s = "+861012345678";
System.out.println(s.matches("^\\+\\d+$"));

4. 迭代器

提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內(nèi)部表示。

實現(xiàn)Iterator模式的關(guān)鍵是返回一個Iterator對象,該對象知道集合的內(nèi)部結(jié)構(gòu),因為它可以實現(xiàn)倒序遍歷。我們使用Java的內(nèi)部類實現(xiàn)這個Iterator

public class ReverseArrayCollection<T> implements Iterable<T> {
    private T[] array;
    public ReverseArrayCollection(T... objs) {
        this.array = Arrays.copyOfRange(objs, 0, objs.length);
    }
    public Iterator<T> iterator() {
        return new ReverseIterator();
    }
    class ReverseIterator implements Iterator<T> {
        // 索引位置:
        int index;
        public ReverseIterator() {
            // 創(chuàng)建Iterator時,索引在數(shù)組末尾:
            this.index = ReverseArrayCollection.this.array.length;
        }
        public boolean hasNext() {
            // 如果索引大于0,那么可以移動到下一個元素(倒序往前移動):
            return index > 0;
        }
        public T next() {
            // 將索引移動到下一個元素并返回(倒序往前移動):
            index--;
            return array[index];
        }
    }
}

使用內(nèi)部類的好處是內(nèi)部類隱含地持有一個它所在對象的this引用,可以通過ReverseArrayCollection.this引用到它所在的集合。

迭代器模式(Iterator)實際上在Java的集合類中已經(jīng)廣泛使用了。
我們以List為例,要遍歷ArrayList,即使我們知道它的內(nèi)部存儲了一個Object[]數(shù)組,也不應該直接使用數(shù)組索引去遍歷,因為這樣需要了解集合內(nèi)部的存儲結(jié)構(gòu)。如果使用Iterator遍歷,那么,ArrayListLinkedList都可以以一種統(tǒng)一的接口來遍歷:

List<String> list = ...
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String s = it.next();
}

5. 備忘錄

在不破壞封裝性的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài)。

備忘錄模式(Memento),主要用于捕獲一個對象的內(nèi)部狀態(tài),以便在將來的某個時候恢復此狀態(tài)。
其實我們使用的幾乎所有軟件都用到了備忘錄模式。最簡單的備忘錄模式就是保存到文件,打開文件。對于文本編輯器來說,保存就是把TextEditor類的字符串存儲到文件,打開就是恢復TextEditor類的狀態(tài)。

public class TextEditor {
    private StringBuilder buffer = new StringBuilder();
    public void add(char ch) {
        buffer.append(ch);
    }
    public void add(String s) {
        buffer.append(s);
    }
    public void delete() {
        if (buffer.length() > 0) {
            buffer.deleteCharAt(buffer.length() - 1);
        }
    }
}

public class TextEditor {
    ...
    // 獲取狀態(tài):
    public String getState() {
        return buffer.toString();
    }
    // 恢復狀態(tài):
    public void setState(String state) {
        this.buffer.delete(0, this.buffer.length());
        this.buffer.append(state);
    }
}

6. 觀察者

定義對象間的一種一對多的依賴關(guān)系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新。

觀察者模式(Observer)又稱發(fā)布-訂閱模式(Publish-Subscribe:Pub/Sub)。它是一種通知機制,讓發(fā)送通知的一方(被觀察方)和接收通知的一方(觀察者)能彼此分離,互不影響。

public class Store {
    private List<ProductObserver> observers = new ArrayList<>();
    private Map<String, Product> products = new HashMap<>();
    // 注冊觀察者:
    public void addObserver(ProductObserver observer) {
        this.observers.add(observer);
    }
    // 取消注冊:
    public void removeObserver(ProductObserver observer) {
        this.observers.remove(observer);
    }
    public void addNewProduct(String name, double price) {
        Product p = new Product(name, price);
        products.put(p.getName(), p);
        // 通知觀察者:
        observers.forEach(o -> o.onPublished(p));
    }
    public void setProductPrice(String name, double price) {
        Product p = products.get(name);
        p.setPrice(price);
        // 通知觀察者:
        observers.forEach(o -> o.onPriceChanged(p));
    }
}

// observer:
Admin a = new Admin();
Customer c = new Customer();
// store:
Store store = new Store();
// 注冊觀察者:
store.addObserver(a);
store.addObserver(c);

7. 策略

定義一系列的算法,把它們一個個封裝起來,并且使它們可相互替換。本模式使得算法可獨立于使用它的客戶而變化。

策略模式:Strategy,是指,定義一組算法,并把其封裝到一個對象中。然后在運行時,可以靈活的使用其中的一個算法。
示例

// 打折策略接口
public interface DiscountStrategy {
    // 計算折扣額度:
    BigDecimal getDiscount(BigDecimal total);
}
// 普通用戶策略
public class UserDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // 普通會員打九折:
        return total.multiply(new BigDecimal("0.1")).setScale(2, RoundingMode.DOWN);
    }
}
//滿減策略
public class OverDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // 滿100減20優(yōu)惠:
        return total.compareTo(BigDecimal.valueOf(100)) >= 0 ? BigDecimal.valueOf(20) : BigDecimal.ZERO;
    }
}

public class DiscountContext {
    // 持有某個策略:
    private DiscountStrategy strategy = new UserDiscountStrategy();
    // 允許客戶端設置新策略:
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
    public BigDecimal calculatePrice(BigDecimal total) {
        return total.subtract(this.strategy.getDiscount(total)).setScale(2);
    }
}

DiscountContext ctx = new DiscountContext();
// 默認使用普通會員折扣:
BigDecimal pay1 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay1);
// 使用滿減折扣:
ctx.setStrategy(new OverDiscountStrategy());
BigDecimal pay2 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay2);
// 使用Prime會員折扣:
ctx.setStrategy(new PrimeDiscountStrategy());
BigDecimal pay3 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay3);

策略模式在Java標準庫中應用非常廣泛,我們以排序為例,看看如何通過Arrays.sort()實現(xiàn)忽略大小寫排序:

String[] names = {"ABC", "XYZ", "zoo"};
// 忽略大小寫排序
Arrays.sort(names,String::compareToIgnoreCase);
//倒序排序
Arrays.sort(names,(s1, s2) -> -s1.compareTo(s2));

Arrays.sort(T[] a, Comparator<? super T> c)這個排序方法,它在內(nèi)部實現(xiàn)了TimSort排序,但是,排序算法在比較兩個元素大小的時候,需要借助我們傳入的Comparator對象,才能完成比較。

8. 模板方法

定義一個操作中的算法的骨架,而將一些步驟延遲到子類中,使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。
模板方法的核心思想是:父類定義骨架,子類實現(xiàn)某些細節(jié)。

模板方法(Template Method)是一個比較簡單的模式。它的主要思想是,定義一個操作的一系列步驟,對于某些暫時確定不下來的步驟,就留給子類去實現(xiàn)好了,這樣不同的子類就可以定義出不同的步驟。
示例:讀取配置

// 抽象類
public abstract class AbstractSetting {
    public final String getSetting(String key) {
        String value = lookupCache(key);
        if (value == null) {
            value = readFromDatabase(key);
            putIntoCache(key, value);
        }
        return value;
    }
    protected abstract String lookupCache(String key);
    protected abstract void putIntoCache(String key, String value);
}
// Map做緩存
public class LocalSetting extends AbstractSetting {
    private Map<String, String> cache = new HashMap<>();
    protected String lookupCache(String key) {
        return cache.get(key);
    }
    protected void putIntoCache(String key, String value) {
        cache.put(key, value);
    }
}

// Redis做緩存
public class RedisSetting extends AbstractSetting {
    private RedisClient client = RedisClient.create("redis://localhost:6379");
    protected String lookupCache(String key) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.get(key);
        }
    }
    protected void putIntoCache(String key, String value) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            commands.set(key, value);
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • """1.個性化消息: 將用戶的姓名存到一個變量中,并向該用戶顯示一條消息。顯示的消息應非常簡單,如“Hello ...
    她即我命閱讀 4,887評論 0 6
  • 為了讓我有一個更快速、更精彩、更輝煌的成長,我將開始這段刻骨銘心的自我蛻變之旅!從今天開始,我將每天堅持閱...
    李薇帆閱讀 2,227評論 1 4
  • 似乎最近一直都在路上,每次出來走的時候感受都會很不一樣。 1、感恩一直遇到好心人,很幸運。在路上總是...
    時間里的花Lily閱讀 1,712評論 1 3
  • 1、expected an indented block 冒號后面是要寫上一定的內(nèi)容的(新手容易遺忘這一點); 縮...
    庵下桃花仙閱讀 1,059評論 1 2
  • 一、工具箱(多種工具共用一個快捷鍵的可同時按【Shift】加此快捷鍵選取)矩形、橢圓選框工具 【M】移動工具 【V...
    墨雅丫閱讀 1,449評論 0 0

友情鏈接更多精彩內(nèi)容