細(xì)胞是構(gòu)成生物體不可分割的基本組成單位。細(xì)胞通過(guò)細(xì)胞膜(細(xì)胞壁),劃出了自己清晰的邊界。在邊界內(nèi)部,細(xì)胞有自己的各種物質(zhì)。而細(xì)胞膜則控制著允許外界通過(guò)的物質(zhì)。

而class
在OOP
的地位,像細(xì)胞一樣,構(gòu)成了一個(gè)OO
程序的基本組成單位,并具有不可分割性。一個(gè)類內(nèi)部由多種元素組成。其中一部分元素,設(shè)計(jì)者是不希望外部能夠訪問(wèn)的;因而類則必須具備細(xì)胞膜同樣的性質(zhì):對(duì)包含的各種元素進(jìn)行訪問(wèn)控制,由此定義明確而清晰的邊界。這就是類的封裝(encapsulation
)。
封裝的目的
一提起封裝,程序員們首先想到的是對(duì)類成員的private
,public
控制。但這些都是手段。相對(duì)于手段,目的才更為重要。
應(yīng)用OO
的正確姿勢(shì),是將類當(dāng)做一種模塊化手段。而好的模塊劃分原則是高內(nèi)聚,低耦合。(參見(jiàn)《正交設(shè)計(jì),OO與SOLID》)因而,如果你不能很好的理解內(nèi)聚與耦合,你就很難通過(guò)OO
方法學(xué)得到好的設(shè)計(jì)(事實(shí)上,如果不懂內(nèi)聚與耦合,你用任何方法學(xué)都無(wú)法針對(duì)一個(gè)復(fù)雜系統(tǒng)給出良好的設(shè)計(jì))。
而高內(nèi)聚強(qiáng)調(diào)的是:關(guān)聯(lián)緊密的事物應(yīng)該被放到一起。這樣有助于將不穩(wěn)定的、易于一起變化的元素都控制在一個(gè)模塊內(nèi)部。從而做到局部化影響。
而低耦合則強(qiáng)調(diào):API
的定義要讓客戶與之的耦合盡可能小,讓客戶代碼難以受到變化影響。
而封裝的目的正是為了隔離變化。通過(guò)對(duì)客戶提供抽象接口,隱藏實(shí)現(xiàn)細(xì)節(jié),讓客戶代碼只能依賴于更穩(wěn)定的抽象,與易于變化的實(shí)現(xiàn)細(xì)節(jié)進(jìn)行隔離,從而讓客戶代碼更加穩(wěn)定(向著穩(wěn)定方向依賴)。
如果不能做到這一點(diǎn),你即便使用了再多的private
權(quán)限控制,也無(wú)法改善軟件的內(nèi)部質(zhì)量。
數(shù)據(jù)與算法
我們都知道,無(wú)論你采取何種范式,從宏觀角度,程序最終都是由如下兩部分構(gòu)成:
- 數(shù)據(jù)
- 算法
可是,如果我們對(duì)于程序的理解僅僅停留在這個(gè)層面,那么就很難設(shè)計(jì)出易于應(yīng)對(duì)變化的軟件。
不幸的是,從我多年做咨詢的經(jīng)歷來(lái)看,絕大部分系統(tǒng)的工程師和設(shè)計(jì)師,對(duì)于設(shè)計(jì)的理解都還限于這個(gè)層面,不管他們講的多么天花亂墜,圖畫(huà)的有多漂亮,但最終的代碼會(huì)揭開(kāi)謊言,告訴你一切真相。
讓我們看看,如果僅僅將程序看做數(shù)據(jù)和算法,并將它們清晰分離,系統(tǒng)會(huì)呈現(xiàn)的局面:

而具體到一個(gè)數(shù)據(jù)結(jié)構(gòu),需要訪問(wèn)數(shù)據(jù)的客戶代碼與其之間的依賴關(guān)系則如下:

數(shù)據(jù)結(jié)構(gòu)在很多時(shí)候,都是一種不穩(wěn)定的實(shí)現(xiàn)細(xì)節(jié),而客戶代碼對(duì)于不穩(wěn)定實(shí)現(xiàn)細(xì)節(jié)的大面積依賴,會(huì)導(dǎo)致系統(tǒng)耦合度的急劇上升。從而讓系統(tǒng)在變化面前極其脆弱。
而為了提升內(nèi)聚度,降低耦合度,我們需要采取向著穩(wěn)定方向依賴的策略,從而得到:

盡管依賴范圍(數(shù)量)依然,但由于API
是站在站在客戶真正需要的角度定義的更為穩(wěn)定的抽象接口(參見(jiàn)《我們?cè)撊绾味xAPI》),其穩(wěn)定度大大提升,從而降低了系統(tǒng)的耦合度。
更何況,很多時(shí)候,經(jīng)過(guò)封裝,甚至連依賴范圍也會(huì)隨之縮小。
TELL, DON'T ASK
Greg Willits
在論文《OOP Principles in Action: Tell, Don't Ask》的開(kāi)篇就解釋了TDA
的思想:
Tell, Don't Ask is a paradigm or style of object oriented programming which prefers that objectA tells objectB to do something, rather than asking ob- jectB about its state so that objectA can make a decision.
這種思想對(duì)于一直以面向過(guò)程方式思考的人是顛覆性的。
很多程序員,即便開(kāi)始使用C++
,Java
等OO
語(yǔ)言,也會(huì)很習(xí)慣的一上來(lái)就為每個(gè)數(shù)據(jù)成員都添加上Setter
和Getter
接口。
當(dāng)然把數(shù)據(jù)全部都暴露出去,會(huì)讓程序員在系統(tǒng)各處使用時(shí)非常便利。但這種貪圖一時(shí)之爽的便利背后意味著對(duì)系統(tǒng)可維護(hù)性的長(zhǎng)期危害。
這種做法是典型的數(shù)據(jù)+
算法的程序設(shè)計(jì)思路,完全沒(méi)有任何高內(nèi)聚,低耦合的基本素養(yǎng)。對(duì)于這樣的設(shè)計(jì),OO
不僅不會(huì)給你帶來(lái)任何好處,反而增加了很多麻煩(更復(fù)雜的語(yǔ)法,類的原子不可分割性等等),這也是多年來(lái)OO
在一部分圈子里承受惡名的重要原因之一。但這究竟是誰(shuí)的錯(cuò)?
可是,這并非意味著所有的Getter
都是邪惡的。封裝的目的在于封裝變化。如果本來(lái)某種信息的獲取就是一種穩(wěn)定的本質(zhì)需要,而不是易于變化的實(shí)現(xiàn)細(xì)節(jié),那么這種Getter
就是合理的。
例如,每一個(gè)Entity
對(duì)象,都有一個(gè)身份證號(hào)(Identity
),通過(guò)它,可以從倉(cāng)庫(kù)(Repository
)中對(duì)Entity
進(jìn)行查找,因而一個(gè)Entity
暴露一個(gè)getId()
的接口是非常合理的。比如:
struct FooEntity
{
unsigned int getId() const;
// ...
};
但同時(shí),對(duì)于Id
的表示卻有必要進(jìn)行封裝,因?yàn)樗芸赡苁莻€(gè)不穩(wěn)定的實(shí)現(xiàn)細(xì)節(jié),今天或許是一個(gè)32
位整數(shù),明天就會(huì)變?yōu)?code>64位,后天干脆變成了一個(gè)128
位的字符串(你要是覺(jué)得這也是穩(wěn)定的,不妨想想我們身份證號(hào)的升位)。而當(dāng)變化沒(méi)有發(fā)生之前,你至少要先通過(guò)最簡(jiǎn)單的手段將這個(gè)信息進(jìn)行封裝(隱藏),比如:
typedef unsigned int FooId;
struct FooEntity
{
FooId getId() const;
// ...
};
這樣的做法,考慮其投入的成本,可獲得的收益,絕不屬于過(guò)度設(shè)計(jì)。
最后,即便回到最壞的局面,Getter
還是會(huì)比直接暴露數(shù)據(jù)略勝一籌。畢竟,Getter
是個(gè)函數(shù),而函數(shù)比數(shù)據(jù)更具備彈性。
避免重復(fù)
眾所周知,重復(fù)是軟件可維護(hù)性問(wèn)題的主要原因之一。
在一些以C
語(yǔ)言為主開(kāi)發(fā)的大系統(tǒng)中,類型的重復(fù)定義,同一算法的重復(fù)實(shí)現(xiàn)是一個(gè)非常普遍的現(xiàn)象。這種重復(fù)很多時(shí)候并非故意為之,而是在缺乏封裝的情況下,必然導(dǎo)致的結(jié)果。
比如,在如下代碼中,結(jié)構(gòu)體Rectangle
被傳遞給不同的函數(shù), 而每個(gè)函數(shù)都有基于Rectangle
的計(jì)算。
struct Rectangle
{
int width;
int height;
};
// 模塊的外部接口,rect 由其它模塊傳入
void f1(Rectangle* rect){
int area = rect->width * rect->height;
// ...
}
void f2(Rectangle* rect)
{
int perimeter = 2 * (rect->width + rect->height);
// ...
}
void f3(Rectangle* rect)
{
// 和 f1 一樣的計(jì)算
int area = rect->width * rect->height;
// ...
}
從中,我們能夠看到f1
和 f3
有著相同的計(jì)算。而類似于這樣的重復(fù),在大型系統(tǒng)中非常普遍。
我們可以站在一個(gè)開(kāi)發(fā)者的角度,來(lái)思考為何會(huì)產(chǎn)生這樣的結(jié)果。
由于C
的結(jié)構(gòu)體是不能有行為的,那么結(jié)構(gòu)體的所定義的數(shù)據(jù)和操作它們的數(shù)據(jù)行為就是分離的 (缺乏內(nèi)聚性)。那么對(duì)一個(gè)開(kāi)發(fā)者而言,既然他可以輕松拿到所需數(shù)據(jù),他所需要的計(jì)算就可以親自完成;至于別人是否已經(jīng)實(shí)現(xiàn)過(guò)類似的代碼,他并不知道。難道要讓他去龐大的代碼庫(kù)中四處搜索?這還不如自己親自寫(xiě)更方便一些。
其造成的結(jié)果必然是:相同的算法就被多個(gè)開(kāi)發(fā)者重復(fù)實(shí)現(xiàn)。只有極其有心的開(kāi)發(fā)者才可能會(huì)盡力的降低一些這類的重復(fù)。當(dāng)然,這是以付出額外精力為代價(jià)的。
但一旦把這些行為都放在類中,把數(shù)據(jù)隱藏起來(lái),就可以最大程度的避免重復(fù)。
首先開(kāi)發(fā)者們失去了自由訪問(wèn)數(shù)據(jù)的權(quán)力。如果一個(gè)開(kāi)發(fā)者需要某種計(jì)算,數(shù)據(jù)訪問(wèn)權(quán)的喪失會(huì)讓他首先考慮的不是在類的外部親自實(shí)現(xiàn)某個(gè)算法,而是首先尋求復(fù)用別人的實(shí)現(xiàn)。
那別人是否曾經(jīng)實(shí)現(xiàn)過(guò)呢?無(wú)須到系統(tǒng)中漫無(wú)邊際的搜索,只需要看看這個(gè)類有沒(méi)有相應(yīng)函數(shù)即可(內(nèi)聚性是多么重要)。如果沒(méi)有,則添加一個(gè),首先滿足了自己,別人如果需要,當(dāng)然也可以復(fù)用。
所以,我們可以其修改為如下的樣子:
struct Rectangle
{
// ...
unsigned int getArea() const
{
return width * height;
}
unsigned int getPerimeter() const
{
return 2 * (width + height);
}
private:
unsigned int width;
unsigned int height;
};
有了這樣的定義之后,此類的用戶甚至都不用到類的定義處去查看接口。在IDE
的幫助下,程序員可以迅速判斷自己所需的接口是否存在:

有了類的封裝,Rectangle
的客戶將不再可能編寫(xiě)重復(fù)的算法:
void f1(const Rectangle& rect)
{
unsigned int area = rect.getArea();
// ...
}
void f2(const Rectangle& rect)
{
unsigned int perimeter = rect.getPerimeter();
// ...
}
void f3(const Rectangle& rect)
{
unsigned int area = rect.getArea();
// ...
}
IDE對(duì)于效率的影響
多年前我剛剛加入ThoughtWorks
時(shí),有幸與PicoContainer的作者Jon Tirson
一個(gè)團(tuán)隊(duì),并與他結(jié)對(duì)編程。第一次結(jié)對(duì),我就被他神一般的編碼速度所震驚:大手一抹,整片的Java
代碼就出現(xiàn)在屏幕上。
震驚之余,我開(kāi)始仔細(xì)觀察他是如何做到的:除了他確實(shí)敲鍵盤(pán)的速度很快之外,他對(duì)IDE
的應(yīng)用極其熟練:各種熱鍵,代碼模版用的滾瓜爛熟,并非常善于利用IDE
的提示信息等等。
在靜態(tài)面向?qū)ο笳Z(yǔ)言里,class
作為行為的載體,會(huì)非常方便于IDE
給出程序員提示信息。這對(duì)于程序員的高效編碼極有幫助。(這也是我在進(jìn)行C
或者Haskell
編程時(shí),很多函數(shù)名都必須靠自己記憶,而不是靠IDE
幫助,感覺(jué)特別不爽的原因)。
提高內(nèi)聚性
我們通過(guò)一個(gè)例子:Maybe問(wèn)題,來(lái)看看封裝如何有利于提高包括數(shù)據(jù)自身的內(nèi)聚性的。
每個(gè)C
和C++
程序員都處理過(guò)空指針問(wèn)題。比如,我們?cè)谝粋€(gè)單向鏈表的典型實(shí)現(xiàn)中,我們會(huì)讓對(duì)象持有一個(gè)指向下一個(gè)對(duì)象的指針:
struct Object
{
void f();
void g();
Object* next; // 指向下一個(gè)節(jié)點(diǎn)的指針
// ...
};
其中,next
指針為空是一種合法的情況,它表示當(dāng)前對(duì)象是單向鏈表中的最后一個(gè)節(jié)點(diǎn)。所以,我們可能會(huì)編寫(xiě)如下的代碼:
void Object::f()
{
// ...
if(next != 0)
{
next->g();
}
// ...
}
所以,任何一個(gè)指針,事實(shí)上都包含了兩種語(yǔ)義:
- 指針是否有效(是否是一個(gè)空指針);
- 其二,如果有效,它的值是什么(指向哪個(gè)對(duì)象)。
指針天然就存在這樣的雙重語(yǔ)義,而其它類型則不然。但程序員們可以通過(guò)自定義的方式,為其它類型引入這種雙重語(yǔ)義。
比如,程序員們經(jīng)常選擇0xffffffff
當(dāng)作uint32_t
類型的“空指針”。比如:
const u32 INVALID_VALUE = 0xffffffff;
// ...
void f(const u32 value)
{
if(value != INVALID_VALUE)
{
// 使用 value 進(jìn)行進(jìn)一步的運(yùn)算
g(value * 5);
}
// ...
}
但無(wú)論是空指針,還是自定義無(wú)效數(shù)值,都并非在所有場(chǎng)景下都適用。
比如, 系統(tǒng)并不想通過(guò)動(dòng)態(tài)分配內(nèi)存的方式來(lái)分配對(duì)象,所以,你無(wú)法使用空指針來(lái)區(qū)分?jǐn)?shù)據(jù)的有效性。而對(duì)于整數(shù)來(lái)說(shuō),如果其取值范圍內(nèi)的所有數(shù)值都是有效數(shù)值,你也不可能找個(gè)一個(gè)數(shù)值來(lái)代表無(wú)效數(shù)值。
這種情況下,程序員們往往會(huì)選擇專門(mén)用一個(gè)bool
型的標(biāo)志來(lái)標(biāo)示數(shù)據(jù)的有效性。比如:
struct Object
{
void f();
// ...
Foo foo; // 不想動(dòng)態(tài)分配內(nèi)存
bool isFooEffective;
u32 value; // [0, 0xffffffff] 均為有效值
bool isValueEffective; // ...
};
對(duì)于這類數(shù)據(jù)的操作模式往往是:
void Object::f()
{
int result = 0;
if(isValueEffective)
{
result += value;
}
if(isFooEffective)
{
result += foo.getValue();
}
// ...
}
這種一分為二的方法,還原了事情的原貌:本來(lái)它們就是雙重語(yǔ)義。
但這種語(yǔ)義的分離卻帶來(lái)一個(gè)缺點(diǎn)。毫無(wú)疑問(wèn),這兩個(gè)語(yǔ)義是緊密關(guān)聯(lián)的,任何一個(gè)離開(kāi)另外一個(gè)都讓原有的語(yǔ)義不再完整。但從形式上,這種緊密的關(guān)聯(lián)性卻沒(méi)有得到任何體現(xiàn),只有通過(guò)仔細(xì)分析代碼,才可以確定這種關(guān)聯(lián)性。這無(wú)疑會(huì)傷害代碼的可理解性。
解決這個(gè)問(wèn)題的辦法,當(dāng)然是再將它們合在一起,形成一個(gè)完整的概念: Maybe
。
template <typename T>
struct Maybe
{
Maybe()
: effective(false)
{}
Maybe(const T& data)
: data(data), effective(true)
{}
bool isEffective() const
{
return effective;
}
T* operator->()
{
return effective ? &data : 0;
}
const T* operator->() const
{
return effective ? &data : 0;
}
T& operator*()
{
return *(operator->());
}
const T& operator*() const
{
return *(operator->());
}
void clear()
{
effective = false;
}
// ...
private:
T data;
bool effective;
};
借助這個(gè)定義,之前的那些定義就可以改變?yōu)?
struct Object
{
// ...
Maybe<Foo> foo;
Maybe<u32> value;
// ...
};
然后,對(duì)于Maybe
類型的數(shù)據(jù)訪問(wèn)方式也改變?yōu)?
void Object::f()
{
int result = 0;
if(value.isEffective())
{
result += (*value);
}
if(foo.isEffective())
{
result += foo->getValue();
}
// ...
}
如果你仔細(xì)觀察,就會(huì)發(fā)現(xiàn):通過(guò)Maybe
這樣的定義,我們又重新恢復(fù)了空指針語(yǔ)義。每一個(gè)Maybe
類型的數(shù)據(jù)都是一個(gè)指針,只有在有效的情況下,才可以對(duì)指針指向的內(nèi)容進(jìn)行訪問(wèn),否則,對(duì)其訪問(wèn)將會(huì)引起異常。
另外,這種實(shí)現(xiàn)方式,并沒(méi)有簡(jiǎn)化客戶代碼,它仍然需要使用if
結(jié)構(gòu)來(lái)確定數(shù)據(jù)的有效性,然后再對(duì)數(shù)據(jù)進(jìn)行訪問(wèn),這和原有方式的復(fù)雜度是一樣的。但這種方式的關(guān)注點(diǎn)在于,通過(guò)將兩個(gè)分散的變量定義(數(shù)據(jù)和有效性標(biāo)志)合二為一,形成一個(gè)更明確的概念,以提高代碼的表現(xiàn)力,增強(qiáng)可理解性。
另外,在原有方式下,程序員需要考慮每一個(gè)標(biāo)志的命名,并要確保在邏輯判斷處引用了對(duì)應(yīng)的標(biāo)志;并且,當(dāng)需要?jiǎng)h除一個(gè)數(shù)據(jù)時(shí),也需要同時(shí)考慮刪除其對(duì)應(yīng)的標(biāo)志,這都給可維護(hù)性帶來(lái)更多代價(jià)。而Maybe
則統(tǒng)一了這類問(wèn)題的操作方式,從而讓程序員在編寫(xiě),閱讀和維護(hù)這類代碼時(shí),免除了上述的麻煩。
Setter
一個(gè)類不管三七二十一,都直接提供對(duì)所有數(shù)據(jù)成員的Setter
接口,很多時(shí)候,像Getter
一樣,都是將不穩(wěn)定的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)進(jìn)行暴露的方式。
但Setter
問(wèn)題也并不都是邪惡的。為了說(shuō)明這個(gè)問(wèn)題,讓我們?cè)俅位氐?code>Rectangle的例子:
struct Rectangle
{
Rectangle
( unsigned int width
, unsigned int height)
: width(width)
, height(height)
{}
unsigned int getArea() const
{ return m_width * m_height; }
unsigned int getPerimeter() const
{ return m_width * m_height; }
private:
unsigned int width;
unsigned int height;
};
此類所構(gòu)造出來(lái)的對(duì)象,都是Immutable
的:其狀態(tài)自從構(gòu)造后,就再也不可修改。
如果Immutability
正是對(duì)Rectangle
的要求,那么你絕不應(yīng)該擅自增加對(duì)于width
和height
的Setter
接口。
但如果我們隨后從需求上確實(shí)要求能夠修改其長(zhǎng)度和寬度,那就不得不增加相應(yīng)的Setter
:
struct Rectangle
{
Rectangle
( unsigned int width
, unsigned int height)
: width(width)
, height(height)
{}
void setWidth(unsigned int width)
{ m_width = width; }
void setHeight (unsigned int height)
{ m_height = height; }
// ...
private:
unsigned int width;
unsigned int height;
};
這樣的Setter
在需求的驅(qū)動(dòng)下,并沒(méi)有什么危害性。最主要的原因在于,Width
和Height
本來(lái)就是一個(gè)Rectangle
的穩(wěn)定屬性。
另外,在本例中,構(gòu)造函數(shù)和Setter
方法,看起來(lái)都是為了給成員變量賦值。它們似乎是可以相互替代的賦值手段,其差異不過(guò)是:構(gòu)造看起來(lái)要比Setter更簡(jiǎn)潔。
這是一種完全錯(cuò)誤的看法。兩者從任何一個(gè)角度看,都是完全不同的兩類事物。
首先,兩者的客戶不同:構(gòu)造函數(shù)面向的用戶是Creator
,而其它成員函數(shù)的用戶則是類服務(wù)需要者,即類真正的Clients
。

盡管可以由單個(gè)類同時(shí)承擔(dān),但Creator
和Clients
本質(zhì)上是兩種完全不同的角色。在需要分離不同變化方向時(shí)的時(shí)候,Creator
和Client
經(jīng)常由不同的類來(lái)扮演。
其次,當(dāng)一個(gè)類實(shí)例被構(gòu)造完成時(shí),就應(yīng)該能夠表現(xiàn)它所表現(xiàn)的對(duì)象。比如,當(dāng)Rectangle rect(3,4)
執(zhí)行結(jié)束后,實(shí)例rect
就應(yīng)該能夠代表一個(gè)寬為3
,高為4
的Rectangle
。
而表達(dá)式Rectangle rect
構(gòu)造出的rect
其實(shí)什么都不是。它無(wú)法履行它的職責(zé),即無(wú)法表現(xiàn)一個(gè)合法的矩形。
所以,構(gòu)造函數(shù)的目的,是為了創(chuàng)造出一個(gè)合法的、能夠履行職責(zé)的、能夠代表它要表現(xiàn)的概念實(shí)例。
而Setter
函數(shù),則是提供給Client
的一個(gè)狀態(tài)變更接口。它的存在,完全取決于客戶是否需要。從這個(gè)意義上,Setter
和任何其它服務(wù)函數(shù)沒(méi)有任何差別。
所以,構(gòu)造和Setter
并非二選一的關(guān)系,而是為不同目的存在、完全沒(méi)有任何關(guān)系的兩套接口。它們可以同時(shí)存在,也可以根據(jù)需要選擇其中任何一種。
以Rectangle
為例,通過(guò)構(gòu)造函數(shù)來(lái)得到合法的Rectangle
實(shí)例是必須的。如果沒(méi)有任何狀態(tài)變更的需要,那么在構(gòu)造結(jié)束后,我們就得到了一個(gè)值對(duì)象(Value Object
)。而一個(gè)值對(duì)象的成員函數(shù),除了構(gòu)造和析構(gòu),都應(yīng)該有const
修飾。構(gòu)造時(shí)賦值是設(shè)置值對(duì)象狀態(tài)的唯一時(shí)機(jī)。
如果。隨后Client
要求一個(gè)狀態(tài)變更接口,無(wú)論是否是Setter
,只要可能改變實(shí)例的狀態(tài),那么它就是一個(gè)普通對(duì)象。
總結(jié)
面向?qū)ο蟮闹饕康氖菫榱?strong>模塊化。而封裝作為OO
的三大特征之一,其主要目的,是在模塊化的過(guò)程中通過(guò)信息隱藏,封裝變化,從而提高系統(tǒng)應(yīng)對(duì)變化的能力。
本文通過(guò)幾個(gè)例子,從不同側(cè)面講述了關(guān)于封裝的作用和方法。而關(guān)于如何做好封裝,總是可以回到高內(nèi)聚低耦合的角度來(lái)思考,通過(guò)正交策略來(lái)指導(dǎo)。
相關(guān)鏈接
其它與封裝有關(guān)的話題:《容器與封裝》
關(guān)于變化:《第一顆子彈》
關(guān)于OO
的目的: 《正交設(shè)計(jì): OO與SOLID》
關(guān)于如何避免過(guò)度設(shè)計(jì):《簡(jiǎn)單設(shè)計(jì)》