慚愧,一個系列第二篇能跟第一篇隔兩年之久,我還真是……
對象與類
上篇文章談到了"什么是對象"問題。而事實上,我們所見過和學習的大多數面向對象語言,迎面而來的一個概念是:類。
遺憾的是,大部分程序語言的書籍,都是直接開始講解類的概念,並沒有着重強調類和對象的關系。所以,面向"對象"的語言,為何引入了這樣一個"類"的概念呢?最簡單的回答是,你不能夠一個一個地去描述對象,那樣太愚蠢了。
類對於一般的人類而言,同樣是一個朴素的概念,在比對象認知稍晚些時候,人類開始具有抽象能力:小孩子不再說“我要那個”,而是開始表達“我要蘋果”。
再更大一些時候(也許五六歲?),小孩子開始能夠把蘋果、梨子、香蕉等概念抽象成“水果”。這個時候,類層次關系開始出現在人的認識中。
面向對象編程的最重要意義在於它提供了一種接近人類思維的表達方式,只要人類的思維模式不發生根本性改變,面向對象絕對不會過時——它的表達形式可能多種多樣,但是任何從根本上否認面向對象的所謂“反思”,皆不可相信。
分類與歸類
對於類層次關系,還有個有趣的問題:分類與歸類。
一種建立類層次關系的方法是,分類。就是,把所有對象放在一起,稱作一個類Object,接下來,根據特征看看Object能分成哪些類,這些類又能分成哪些子類……以次類推。這樣的方法得出的結果,類與類之間是不存在交集的,並且所有類最終都會繼承一個基類Object。使用這樣的邏輯的語言典型代表有Java和C#。
另一種方法是,歸類。就是對於每一個對象,根據它的特征,看看它屬於哪些類。這時,一個類可能有多個父類,這種方法,就會產生所謂的“多繼承”關系。使用這樣的邏輯的語言典型代表就是C++。
所以,實際上流傳甚廣的說法“C#和Java不能多繼承類,只能提供多繼承接口”是不恰當的(不能說不對),如果你使用C#或者Java這樣的語言,從設計開始,就完全不可能出現需要多繼承的情況。只有理解了語言背后采用的哲學,才能夠正確使用語言。
面向對象設計
《C++程序設計語言》一書中,講到了一系列設計的步驟:
- 發現類
- 描述操作
- 描述依賴性
- 描述接口
發現類最簡單和行之有效的方法就是從需求描述中尋找。一些名詞往往對應着一個類。而TC++PL中還提到了幾種情況:
- 動詞可能意味着對象上的操作、全局函數或者類
- “重復”、“將……作用於”往往意味着迭代器對象
- 形容詞“可存儲的”、“並行的”、“注冊的”、“約束的”可能成為類(winter注:C#或者Java中,它們更可能作為接口或者attribute)
有趣的是,自然語言非常自由,所以程序未必應該完全對應於需求描述,比如著名的“狗咬人”問題:
A dog bites a person. A person is bited by a dog.
寫成程序,就是:
Person person = new Person;
Dog dog = new Dog;
dog.bite(person);
person.isBitedBy(dog);
兩種設計哪個更好呢?
我們在寫程序的時候,不可能受到需求文檔使用句型的影響,這個時候,我們必須回到對象的本質上面:標識、狀態、行為。而對象的行為,必定是改變對象自身狀態或者對外輸出對象狀態的。
這樣,在這個場景里面,答案就是顯而易見的了:
人的狀態改變,所以人應該有hurt方法。
狗的狀態未改變,但是咬這個動作必須根據它的內部狀態輸出傷害,所以dog應該有biteDamage方法。
最后,一個良好的設計是:
person.hurt(dog.biteDamage());
所以,不要陷入“面向對象語言描述要盡量跟需求描述一致”,正確的抽象才是根本。
設計實踐
一個常見的錯誤是把類與模塊相混淆。比如以下類就很可疑:
Login類
DBHelper類
BusinessLogic類
模塊是一個相對獨立的功能單元,一般來說,模塊可能包含很多類。如何避免這樣的錯誤呢?
我自己喜歡使用先對象、后類的設計方法。也就是說,先完全不考慮類的問題,將系統中的具體對象識別出來。我在OOD階段做的第一件事,就是在設計圖中間畫一條線,線上面的對象是可見的,線下面是不可見的邏輯對象。
下圖是我編寫的一個黑白棋游戲(shaofei.name/othelloAI/othello.html)時畫的對象圖:
這種方法可以幫助我們發現一些壞味道,比如,每一條跨過分割線的依賴線條,都應該是同一個方向的。
應用了MVC模式以后,Controller位於線的中間,它阻止了UI對象直接控制業務邏輯:
因為是one man project我使用的圖形比較簡單,在正式的項目中,UML的對象圖和時序圖都是非常強有力的工具。