首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 中的設計模式:Command-Query Separation 與 Combined Method

     編輯:Java 相關知識

前言:設計與環境的關係

一直有個迷思:設計可以且應該獨立於實作細節。設計常被視為抽象概念,實作是具體程式碼實例。若我們堅信「設計是為某目標精心制定結構概念的創造性、目的性活動」,一個結構若無法說明其環境或無法與環境協作,就不適合該目標。環境包含目標平台——語言、工具、函式庫、中介軟體等,以及功能性與非功能性單元。

我們會認為在不知地形佈局時設計房屋、在不清楚材料時建造摩天大樓是不合理的。將執行緒、分散式等概念視為小編碼細節的看法,無疑是導致設計中浪費精力(時間與金錢)的導火索,最終發現理論與實踐的差距在實踐中比理論中更大。雖然高層設計某部分可在多技術下保持不變,但更多情況是我們需親自補足圓圈,允許甚至鼓勵細節與實際資訊影響並告知系統結構。

模式的作用就是獲取這些結構性資訊。它們可預見性或回顧性描述設計與設計原理,講述從問題到解決,說明環境、獲取工作動力及產生結果。本文集中探討兩個模式——Command-Query Separation 與 Combined Method——為類別介面中的方法分配任務,考察它們如何互相作用並影響並行、分散、有序環境以及本地執行。

介面設計、約束與供給

介面提供不同系統或組件間的界定。軟體中介面提供屏障,從實作中分離目標、從具體中分離概念、從作者中分離使用者。Java 中有許多介面概念:public 部分為潛在使用者提供類別與方法介面,protected 部分為子類別與套件提供介面;套件有公用部分;反射是另一種提供、使用物件方法介面的機制。

站在使用者對作者角度,介面建立並命名目的模型的使用方法。類別介面中的方法提供特殊使用方法。約束——編譯時型別系統、執行時例外機制、回傳值——使類別作者目的得以體現與加強。最簡單例子是封裝意義:私有化保證類別使用者只能透過公用方法介面操作資訊與行為。

然而封裝遠不止資料私有。設計中封裝常涉及自我包含。一個需要知道如何呼叫方法(如「在執行緒環境中,方法呼叫後必須明確同步物件」)的類別,封裝不如將所有這些包含並隱藏的類別(如「此類別是 thread-safe 的」)好。前者存在設計漏洞,許多限定條件模糊而非經過加強,將責任推給使用者而非讓類別提供者完成設計,不可避免地漏洞百出。此時供給描述使用的可行性與不可行性。

供給指事物被感知的真實屬性,決定事物可能的使用方法。椅子可支撐,所以可坐人;玻璃可透光、可打碎。供給提供操作線索:板狀物可壓、柄狀物可旋轉、溝狀物可插入、球狀物可丟擲反彈。複雜事物可能需解釋,但簡單事物不應需要。當簡單東西也需圖片、標籤說明時,設計即失敗。類別設計者責任是在介面中減小約束與供給間的鴻溝,匹配目標與一定程度的自由度,盡可能減小錯誤使用可能。

環境敏感設計

在空間或時間上分離方法執行——如執行緒、遠端方法呼叫、訊息佇列——能對設計正確性與效率產生深遠影響。分離帶來的結果不可忽視:並行引入不確定性與環境選擇開銷;分散引入錯誤與不斷增加的回程呼叫開銷。這些是設計問題,而非修改 bug 那麼簡單。

無論何種情況,結果都會阻礙屬性風格程式設計——介面主要由 set/get 方法組成,每個方法直接指向私有區域。這類類別封裝極差(毫無遮掩)。介面中的欄位存取器通常不提供資訊:它們在物件使用中無法溝通、簡化、抽象,常導致冗長易出錯程式碼。屬性風格程式設計短期內非大活動。分散與並行透過引入正確性與嚴重效能開銷放大這些格式上實踐問題。

透明度與 Bug 災難

抽象允許我們在必要時忽略細節,讓設計思想平衡環境因素而非受制於它們。決定什麼細節可忽略成為挑戰。重要細節被忽略時問題嚴重性上升。設計常盡量使環境因素透明。透明是誘人想法:或許能讓執行緒與遠端物件通訊完全透明,使用者進行物件通訊時什麼也不覺察。Proxy 模式支援一定程度透明度,強化 RMI 與 CORBA 基礎。本地代理物件與遠端物件使用相同介面,編組細節允許呼叫者使用熟悉方法呼叫模型。然而分散透明不完全:失誤與潛在影響無法完全隱藏且需考慮。畢竟透明不是毛巾。

Command-Query Separation (CQS)

問題

方法回應問題回傳值時具查詢性質;採取強制行動改變物件狀態時具命令屬性。方法可以是純 Command、純 Query,或兩者混合體。例如 java.util.Iterator 中,hasNext 為查詢,remove 為命令,next 與 awkward 合併命令與查詢:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

若不將 Iterator 物件當前值向前推進到下一個,就無法查詢 Iterator 物件。這導致初始化、持續、存取、前進分離而清晰定義的迴圈結構錯位:

for (initialization; continuation condition; advance) {
    ... access for use ...
}

將 Command 與 Query 功能合併入單一方法的結果是降低清晰度,可能阻礙基於斷言的程式設計,且需要變數保存查詢結果:

for (Iterator iterator = collection.iterator(); iterator.hasNext(); ) {
    Object current = iterator.next();
    ... use current ...
    ... again use current ...
}

解決方案

保證方法行為嚴格為命令或查詢:可回傳值的方法為純函數無副作用;有副作用的方法不可有回傳值。「另一種表述:問一個問題而不影響答案。」

Combined Method

問題

一些主要提供細粒度方法的介面,起初看來最小化且內聚性強——都是吸引人特點。然而使用過程中,一些介面顯現過於原始。它們過度簡化,迫使類別使用者用更多工作實作普通任務並操縱方法間依賴(暫時耦合)。這非常麻煩且容易出錯,導致程式碼重複——應避免的——並為 bug 提供溫床。

一些需同時執行成功的方法,在多執行緒、例外、分散環境遇到麻煩。若兩個動作需同時執行,必須遵守協作或反轉語意——必須都完全成功執行,或一個動作失敗會反轉另一個動作執行——它們由兩個獨立方法描述。執行緒引入使不確定程度大幅增加。一系列方法呼叫可變物件不會確保結果符合預期,若物件在執行緒間共享,即使假設單獨方法是 thread-safe 的。看 EventSource 介面,允許安置處理器與查詢事件:

interface EventSource {
    Handler getHandler(Event event);
    void installHandler(Event event, Handler newHandler);
    ...
}

執行緒間交叉呼叫可能引起意外結果。假設 source 參照共享物件,極可能在 1、2 之間物件被另一執行緒安裝處理器:

class EventSourceExample {
    public void example(Event event, Handler newHandler) {
        oldHandler = eventSource.getHandler(event); // 1
        eventSource.installHandler(event, newHandler); // 2
    }
    private EventSource eventSource;
    private Handler oldHandler;
}

同樣,這也是類別使用者而非設計者關注這些、制定約束:

class EventSourceExample {
    public void example(Event event, Handler newHandler) {
        synchronized(eventSource) {
            oldHandler = eventSource.getHandler(event);
            eventSource.installHandler(event, newHandler);
        }
    }
    ...
}

若目標物件是遠端的,回程增加開銷與方法呼叫失敗並發交織成為環境一部分。上例假設每個方法體執行時間與通訊延遲相比極短。此例開銷重複兩次,可能在其他實例中重複多次。此外,外部 synchronized 同步區塊使用需求也是問題。同步區塊在分散環境明顯使用,但也可在本地執行緒環境應用:呼叫者與目標間代理物件使用。簡言之,因代理物件而非目標物件同步,導致同步區塊使用失敗。保守說法:對系統正確性有基本影響。因代理使用在介面後透明,呼叫者無法對行為做太多保證。

解決方案

Combined Method 必須在分散、執行緒環境中同時執行。聯合應反映普通使用方法。這樣 Combined Method 可能比原有方法更清晰,因它反映直接應用。恢復策略與筆拙方法被封裝到 Combined Method 中,簡化類別使用者角度介面。改善封裝降低介面不必要贅餘。Combined Method 整體效果支援更像交易處理風格設計。

在聯合 Command-Query 中提供單一 Query 方法通常合理。但需按需制定,而非強制執行。提供分離 Command 方法較少見,因 Combined Method 可完成此工作:呼叫者簡單忽略結果。若回傳結果招致開銷,才可能需要單一 Command 方法。

回到前例,若 installHandler 方法回傳上一個處理器,設計變得更簡單獨立:

class EventSourceExample {
    public void example(Event event, Handler newHandler) {
        oldHandler = eventSource.installHandler(event, newHandler);
    }
    ...
}

呼叫者提供更安全介面,不再需解決執行緒問題。降低風險與程式碼規模,將類別設計責任全給設計者而非推給使用者。代理物件出現不影響正確性。

Combined Method 形式

Combined Method 可以是多個 Query 集合、多個 Command 集合、或兩者兼有。可能補充或抵觸 CQS 方法。衝突發生時,優先選擇 Combined Method 會產生不同正確性與適用性。

另一範例:取得資源

假設介面中 acquire 方法在資源可用前持續阻塞:

interface Resource {
    boolean isAcquired();
    void acquire();
    void release();
    ...
}

類似下列程式碼在執行緒系統中推薦使用:

class ResourceExample {
    public void example() {
        boolean acquired = true;
        synchronized(resource) {
            if (!resource.isAcquired())
                resource.acquire();
            else
                acquired = false;
        }
        if (!acquired) ...
    }
    private Resource resource;
}

然而即使放棄可讀性與易用性,這也非 CQS 設計應用。若引入代理,將會失效:

class ActualResource implements Resource {...}
class ResourceProxy implements Resource {...}
// 一個 Combined Method 解決此問題,使並發與間接性更透明。
interface Resource {
    ...
    boolean tryAcquire();
    ...
}

下列程式碼清晰、簡單且正確:

class ResourceExample {
    public void example() {
        if (!resource.tryAcquire()) ...
    }
    private Resource resource;
}

Combined Method 帶來一個結果:使一些測試與基於斷言程式設計變得筆拙。然而對比原設計,原方法解決執行緒與分散問題非合適途徑。此情況下單元測試提供較好分級與分離。Combined Method 能使方法介面模糊並使類別使用者程式碼更冗長筆拙。某些條件下 Execute Around Method 提供保證自動與彈性的另一個 Combined Method。

結論

環境決定實踐方法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved