世上一直有一個神話:設計可以並且應該獨立於實現的細節,設計通常被看作是一個抽
象的概念而實現是一個代碼的具體執行個體。如果我們堅信"設計是一個富有創造性和目的性
的活動:為某一個目標而精心制定的結構的概念,",一個結構如果不能夠說明它的環境
,或者不能與環境協作,那麼這個結構就不適合這一目標。環境中包括目標平台--語言
、工具、庫、中介軟體(middleware),等。還有它的功能性和非功能性的單元。
我們會認為在不知道地形布局的時候設計房屋,或者在不清楚使用的道材料的時候
建造摩天大廈是不合理的事情。我們將線程、分布這類概念看作為小的編碼的細節的看
法無疑是在設計中導致浪費精力(時間和金錢)的導火索,最終我們發現的是理論與實
踐的差距在實踐中要比在理論中還大。雖然在一些情況下一個高層次設計的某部分可以
在許多技術下保持不變,但是更多的情況是我們需要親自來補足這個圓圈,允許(甚至
鼓勵)細節和實際的資訊來影響並告知系統的結構。
模式(Patterns)的作用就是擷取這些結構上的資訊。它們可以描述--預見性的或回
顧性的--設計和設計的原理,講述從問題到解決,說明環境,擷取工作的動力以及應此
產生的結果。這裡,我將集中講述兩個模式--Command-Query Separation和Command Me
thod--為一個類介面中的方法分配任務,考察他們如何互相作用並影響並發的、分布的
和有序的環境以及本地執行。
介面設計。顧名思義,介面提供了不同系統之間或者系統不同組件之間的界定。在
軟體中,介面提供了一個屏障,從而從實現中分離了目標,從具體中分離了概念,從作
者中分離了使用者。在Java中,有許多介面的概念:public部分為潛在的使用者提供了類和
方法的介面,protected部分為它的子類(subclass)以及周圍的包提供了一個介面;一
個包有一個公用的部分;反射(Reflection)是另外一種提供、使用對象方法介面的機
制。
約束及供給。站在使用者對作者的角度,一個介面建立並命名了一個目的模型的使用
方法。類介面中的方法提供了一種特殊的使用方法。是這些約束--編譯時間的類型系統,
運行是的異常機制及傳回值--使得類作者的目的得以體現和加強。在這方面最簡單的例
子是對封裝的意義的理解:私人化可以保證類使用者只可以通過類的公用方法介面來操作
資訊和行為。
然而,對於封裝來說,遠不止資料私人那麼簡單。在設計中,封裝往往會涉及到自
我包含(self-containment)。一個需要你知道如何調用一個方法(e.g."在一個線程的
環境中,在一個方法調用後調用另一個方法,你必須明確地同步對象")的類的封裝就不
如將所有這些全部包含並隱藏的類(e.g."這個類是thread-safe的")好。前一個設計存
在著設計的漏洞,它的許多限定條件是模糊的而不是經過加強的。這就把責任推給了用
戶而不是讓類提供者做這些工作來完成類的設計,並且,這是不可避免的漏洞百出。
在這種情況下,供給(affordances)描述了使用的可行性和不可行性。
術語供給(affordances)指事物的被感知的真實的屬性,首先,這些屬性可以決定
事物的使用的可能方法。一個椅子可以用來支撐其他東西,所以,可以坐人。一個椅子
照樣可以搬運(carried)。玻璃可以透光,也可以被打碎……
供給提供了對事物操作的線索,板狀物可以壓、柄狀物可以旋轉,溝狀物可以插入
東西。球狀物可以扔或者反彈。當使用了供給的優勢後,使用者可以只通過看便確定該做
什麼:沒有圖、沒有標籤也沒有說明。複雜的事物可能會需要一些解釋,但是簡單的事
物不應該這樣。當簡單的東西也需要用圖片、標籤來說明的時候,設計就是失敗的。
類設計者的一個職責便是在介面中減小約束與供給之間的隔閡(gap),匹配目標以
及一定程度上的自由度,儘可能減小錯誤使用的可能。
對環境敏感的設計。在空間或者時間上分離方法的執行--例如,線程,遠程方法調
用,訊息佇列--能夠對設計的正確性和效率產生意義深遠的影響。這種分離帶來的結果
是不可忽視的:並發引入了不確定性和環境選擇的開銷;分布引入了錯誤的和不斷增加
的回程的調用開銷。這些是設計的問題,而不是修改bug那樣簡單。
無論是在何種情況下,結果都是將會阻礙所有權風格的程式設計(Property-Style
Programming)--當一個介面主要由set和get方法組成的時候,每個方法都相應的直接
指向私人地區。這樣的類的封裝很差(意思是毫無遮掩)。介面中的域訪問器(Field
accessors)通常是不會提供資訊的:他們在對象的使用中不能通訊、簡單化和抽象化,
這通常會導致冗長並易出現錯誤的代碼。所有權風格的程式設計在短時間內不是一個大
的活動。分布和並行通過引入了正確性和嚴重的效能開銷放大了這些格式上實踐的問題
。
透明度和bug災難。抽象允許我們在必要的時候可以忽略細節,所以我們的設計思想
可以平衡環境的因素而不是受制於它們。決定什麼樣的細節可以忽略便成為一個挑戰。
問題的嚴重性在重要的細節別忽略的情況下上升了。
設計往往會盡量使環境因素儘可能的透明。透明能夠成為一個誘人的主意:也許它可以
讓線程和遠程對象通訊完全透明,這樣使用者在進行對象通訊的時候什麼也不會覺察到。
Proxy模式支援一定程度上的透明度。這加強了RMI和COBRA的基礎。本地的代理的對象和
使用遠端對象在使用中具有相同的介面,並且編組上的細節允許調用著使用熟悉的方
法來調用模型。然而,這種分布透明並不完全:失誤和潛在的影響,不能被完全隱藏並
且需要考慮。畢竟透明不是毛巾。
Command-Query Separation
保證一個方法是不命令(Command)就是查詢(Query)
問題。方法,當它們返回一個值來回應一個問題的時候,具有查詢的性質,當它們
採取強制行動來的改變對象的狀態的時候,具有命令的屬性。所以一個方法可以是純的
Command模式或者是純的Query模式,或者是這兩者的混合體。
例如,在java.util.Iterator中,hasNext可以被看作一種查詢,remove是一種命令,n
ext和awkward合并了命令和查詢:
public interface Iterator
{
boolean hasNext();
Object next();
void remove();
}
如果不將一個Iterator對象的當前值向前到下一個的話,就不能夠查詢一個Iterator對
象。這導致了一個初始化(initialization)、增加(continuation)、訪問(access)和
前進(advance)分離而清晰定義的迴圈結構的錯位:
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...
}
解決方案。保證方法的行為嚴格的是命令或者是查詢,這樣可以傳回值的方法是純的函
數而沒有複效應(side effects),有負效應的方法不可能有傳回值。"另一個表述這點
的方法是問一個問題而不影響到答案。"
Combined Method
組合方法經常一起被使用線上程和分布環境中來保證正確性並改善效率。
問題。一些主要提供密集的方法的介面,起初,看來是最小化和附著性強的--都是
迷人的特點。然而,在使用的過程中,一些介面顯現得過於原始。它們過於簡單化,
從而迫使類使用者用更多的工作來實現普通的的任並操縱方法之間的依賴性(暫時耦合)
。這是非常麻煩並且容易出錯的,導致了代碼重複--代碼中應當避免的--並且為bug提供
了很好的滋生條件。
一些需要同時執行成功的方法,在執行的時候在多線程、異常、和分布的地方遇到
了麻煩。如果兩個動作需要同時執行,它們必須遵守協作或反轉(commit-or-rollback
)語意學--它們必須都完全成功的執行或者一個動作的失敗會反轉另一個動作的執行--
它們由兩個獨立的方法進行描述。
線程的引入使不確定程度大大增加。一系列方法調用一個易變的(mutable)對象並不
會確保結果是料想中的,如果這個對象線上程之間共用,即使我們假設單獨的方法是線
程安全的。看下面的對Event Source的介面,它允許安置控制代碼和對事件的查詢:
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);
}
}
private EventSource eventSource;
private Handler oldHandler;
}
如果目標對象是遠端,回程增加的開銷和對方法調用失敗並發的交織在一起成為
環境的一部分。在上一個例子中,我們可以假設執行每一個方法體的時間和通訊的延遲
相比是很短的。在這個例子中,開銷被重複了兩次,並可能在其他的執行個體中重複多次。
此外還有一個問題是對外部(extern)的synchronized同步塊的使用需求。Synchr
onized塊很明顯在分布的環境中使用但是也可以在本地的線程環境中應用的很好:在調
用者和目標之間的代理對象的使用。簡而言之,對synchronized塊的使用因為代理對象
而不是目標對象的同步而失敗。保守的說法是,這對系統的真確性可以有一個基本的影
響。因為代理使用是在介面後透明的,調用者不能對行為做太多的保證。
解決方案。Combined Method必須在分布,線程環境中同時執行。聯合應當反映出普
通的使用方法。這樣,一個Combined Method才可能比原有的方法要清晰,因為它反映了
直接的應用。恢複策略和一些笨拙的方法被封裝到Combined Method中,並簡化了類使用者
角度的介面。這改善的封裝降低了介面中不需要的累贅。Combined Method的全部效果是
支援一種更像交易處理風格的設計。
在一個聯合的Command-Query中提供一個單獨的Query方法通常是合理的。然而,這
需要按照需要而制定,而不是強制的執行。提供分離的Command方法是不太常見的,因為
Combined Method可以完成這一工作:調用者簡單的忽略結果。如果返回一個結果招致一
個開銷的話,才可能會體統一個單獨的Command方法。
回到前一個例子中,如果installHandler method返回上一個控制代碼設計變得更加簡單和獨
立:
class EventSourceExample
{
...
public void example(Event event, Handler newHandler)
{
oldHandler = eventSource.installHandler(event, newHandler);
}
private EventSource eventSource;
private Handler oldHandler;
}
調用者提供了一個更加安全介面,並且不再需要解決線程的問題。這降低了風險和
代碼的大小,將類設計的職責全部給了類設計者而不是推給使用者。代理對象的出現沒有
影響到正確性。
一個Combined Method可以是許多Query的集合,許多Command的集合,或者兩者兼有
。這樣,它可能補充或者抵觸Command-Query分離的方法。當衝突發生的時候,優先選擇
Combined Method會產生一個不同的正確性和適用性。
在另一個例子中,考慮獲得資源的情況。假設,在下面的介面中,獲得的方法在資源可
用前一直起到阻礙作用:
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;
}
然而,即使放棄可讀性和易用性,這樣的設計不是一個Command-Query分離設計的應用。
如果引入了代理,它就會失敗:
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。
結論
環境決定實踐的方法。