OOP:Tell,Don't Ask -用命令代替查詢(譯)

 http://www.pragprog.com/ 看到有不少關於編程的好文章,當初好像是從《The Pragmatic Programmer》一書作者的介紹中得知這個網站的。有篇關於OOP的文章讀了好幾遍,每次讀總是會忘了上一次讀後理解到的意思,於是嘗試着翻譯一下。很明顯我的英文非常蹩腳,希望多多指正!謝謝!

 
原文鏈接:TellDon’t Askhttp://www.pragprog.com/articles/tell-dont-ask 
 

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things. — Alec Sharp

過程式程序獲取信息然後決策;OO程序則告訴對象做某事情。

 

也就是說,你應該儘量告訴對象你希望它們去做的事情;而不要詢問它們的狀態之後做出決定,最後才告訴它們做什麼事情。 

問題在於,調用方不應該基於被調用對象的狀態來做決定,這會導致被調用對象的狀態被改變。你正在實現的程序邏輯很可能是被調用對象的職責,而不是調用方本身的,因爲你在被調用對象外部做決定破壞了被調用對象的封裝。 

當然,你可能會說:這是顯而易見的,我從來沒有寫過這樣子的代碼。儘管如此,我們可以很容易地檢測一些被引用對象,然後基於返回結果來調用不同的方法。但是這或許不是最好的辦法。告訴被調用對象你想要的,讓它來決定怎麼做,用命令式而不是過程式的思維模式。 

基於各個類的職責來設計這些類,你就可以很容易地跳出這個陷阱,然後你能夠自然地指定這些類應該執行的命令,這與通過查詢獲得對象的狀態是相反的。

 

Just the Data:僅僅是數據

這種實踐的主要目的是確保正確分解類的職責,即將合適的功能點放置在恰當的類中,這不會引起該類對另一個類的過度耦合。 

請求並從對象中獲得數據的最大風險是,你不僅僅獲取數據。從更大的方面講,你不是在獲取一個對象。儘管你從查詢中接收到的是一個結構化的對象(例如,String 對象),該對象已經不是語義上的對象了。它與自身擁有者已經沒有關聯,因爲你獲取到的是包含內容爲“RED”的String 對象,而你不能夠詢問該 String 對象中它代表的是什麼。它代表擁有者的名字?Car的顏色?轉速錶的當前狀態?對象才知道這些含義,而數據(data)是不瞭解的。 

OOP 的基本原則就是方法與數據的統一,將這兩者不恰當的分離會使得你回到過程式編程。

 
Invariants aren’t enough:不變式還不充分

任何類都有不變式(即必須一直爲true)。一些編程語言(如Eiffel)直接提供了對指定並檢測不變式的支持,大多數其他的語言則不支持,但這只是意味着不變式不被顯式支持,其實還是存在的。舉個例子,迭代器有有如下的不變式(以Java爲例子):

hasMoreElements() == true

//implies that:

nextElement()            

//will return a value
 

也就是說,如果 hasMoreElements() true,則能夠成功獲得下一個元素,否則將會發生一些麻煩的意想不到的錯誤。如果你正在運行沒有正確使用同步(加鎖)的多線程代碼,上面的不變式可能不成立,因爲其他的線程已經在你之前取出了最後一個元素。 

根據‘按契約設計’(Design by Contract),只要方法(查詢與命令)可以自由地混合在一起,並且不會違反類的不變式,那麼這是可行的。但是,在你維護類不變式時可能已經有意無意地增加了調用者和被調用者之間的耦合度,而這耦合度依你暴露出來的類的狀態而定。 

例如,你持有一個容器對象 C,可以將它的迭代器暴露給容器內持有對象(JDK類庫大多這樣做),或者你可以提供一個方法,該方法會執行集合內的一些成員函數來操作所有元素。在Java中你可能會這樣聲明:

public   interface Applyable {

    public   void each(Object anObject);

}

...

public   class SomeClass {

    void apply(Applyable);

}

//Called as:

SomeClass foo;

...

foo.apply( new Applyable() {

   public   void each(Object anObject) {

           // do what you want to anObject

 }

});

 

       這在支持函數指針的語言中很容易實現,PerlSmalltalk中內建了函數指針這種概念的語言中會更簡單。但是對於上面的代碼,你應該有這樣一種想法:運行這個函數來遍歷容器中的所有元素,我不關心代碼怎麼做。 

       不管是通過 apply方式還是迭代器方式,你都能夠獲得同樣的結果。最主要的抉擇在於耦合度:最小化耦合度,只暴露最少必需的狀態。對於上面這個例子,apply方式比迭代器方式暴露更少的狀態。

 
Law of Demeter:迪米特法則

       我們決定儘可能少地暴露類的狀態,以此來達到我們的目的。現在我們開始在類內部對另一個對象發送命令和查詢,而不管系統是否支持。是的,你可以這樣做,但依據Demeter原則(迪米特法則)這不是好的做法。Demeter原則限制類間的交互,以此來減少類間的耦合。(這裏有更多相關討論 

       Demeter法則指的是,一個對象與越多對象交流,就得承擔由這些對象變化時所帶來的越大的風險。因此,不僅僅說儘可能減少,而且不應該跟超過不必要的對象有關係。實際上,根據Demeter法則,對象中任一方法應該只調用以下的方法:

       自身的;

       方法中傳入的參數;

    該對象所創建的對象;

    組合的對象。   

    不在上面列出的方法則屬於從其他調用返回的對象。例如(這裏使用Java語法):

    SortedList thingy = someObject.getEmployeeList();

thingy.addElementWithKey(foo.getKey(), foo);
 

這是我們應該避免的(上面的foo.getKey()也是要避免的例子)。類似的這一類直接訪問擴大了調用者與其所需對象的耦合度。這些調用者依賴於以下事實:

SortedList 中持有employees 對象;

SortedList 的 add 方法是addElementWithKey();

foo 查詢其 key的方法是 getKey() 

上面的代碼應該替換爲:

someObject.addToThingy(foo); 

現在調用者只是依賴於將foo添加(add)到thingy當中去,這意味着高層依賴於一種職責,而不是依賴於實現。 

當然,這種做法的不足之處是你必須寫大量小型的包裝器方法來委託容器元素的遍歷(或其他操作)。這需要在效率和類的緊耦合之間做出權衡。 

類之間的耦合度越高,你所做的修改會引起其他地方被破壞的機率也就越高,這會導致代碼很脆弱。 

相比應用程序運行時的低效率,很多情況下開發與維護緊耦合的類更容易使你陷入泥潭。

 
Command/Query Seperation:分離命令與查詢

現在回到ask與tell,ask就是查詢,tell是命令。我贊成將這二者分離爲多個方法開來維護,爲什麼呢?

如果你考慮到按照命令來執行指定的、定義良好的動作,這有利於維護;

如果你的類完全基於命令,這能夠幫助你考慮類不變式。(如果你只是處理數據,那麼沒必要對不變式考慮太多)

如果你能假定查詢的執行不會有副作用,那麼你可以:

    在調試器中使用查詢,並且不會影響到測試的進行;

    創建內建的、自動的迴歸測試;

    評測類不變式、前置條件與後置條件。

 

上面最後一點也就是爲什麼Eiffel要求在斷言(Assertion)中只能調用無副作用的方法。但是在C++或者Java中,如果你想要在某個代碼點手工檢測一個對象的狀態,只有當你知道某個查詢不會引起其他某些地方被修改,那麼你纔可以自信地調用這個方法。 

 
後記:
翻譯非常非常耗時!這才更加明白,自己稍微看得懂的內容,不一定能夠依據原文的闡述以及自己的理解來將其意思表達出來。我想了一下爲什麼會出現這種情況,大概是因爲平時自己在理解這一類英文文章時無意識地被自己已有的知識“欺騙”(忽略一些其實不怎麼正確理解的內容),因爲我們總想着能夠順暢地閱讀下去。對嗎?
另外,這才知道我們平時讀的一些中譯本的書籍是經過多麼艱難的過程才能夠來到我們手上啊!很多事情都不簡單,嘗試過了才知道其痛苦!但是,很多事情其實很值得,嘗試過來才知道其價值!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章