原文:Goodbye, Object Oriented Programming
原作者:Charles Scalfani
原文介紹
作為使用面向?qū)ο缶幊桃延惺畮啄甑淖髡撸?jīng)也非常熱衷于繼承睛藻、封裝启上、多態(tài)的優(yōu)點。它們是范式的三大支柱店印。但是隨著對面向?qū)ο缶幊痰纳钊肜斫飧栽冢Y(jié)合實際遇到的問題,帶著挑剔的眼光按摘,作者看到了面向?qū)ο缶幊痰囊恍﹩栴}包券。
*繼承,墮落的第一支柱
**香蕉 猴 叢林問題
你想要一個香蕉炫贤,但是你得到的卻是一只拿著香蕉的大猩猩和整個叢林溅固。
由于繼承原因,當你想要復用其他項目某一個已經(jīng)存在的類時兰珍,你不得不需要這個類的父類侍郭,然后可能它父類的父類……最后會發(fā)現(xiàn)我需要它的祖宗十八代=。=。你以為解決了這個就可以了嗎亮元?少年你還是太天真了猛计,你會發(fā)現(xiàn),它編譯不過爆捞,為什么奉瘤? 這個對象包含了這個其他對象。 所以你也需要它煮甥,這沒問題毛好,但問題是你不只是需要那個對象,你需要對象的父對象及其父對象的父對象苛秕,依此類推肌访,每個包含的對象以及包含父對象,父對象艇劫,父對象的所有父對象......
**香蕉猴叢林解決方案
我們可以通過不創(chuàng)建太深的層次結(jié)構(gòu)來解決這個問題吼驶。哦……似乎沒什么不對,但如果繼承是重用的關(guān)鍵店煞,那么我對該機制的任何限制肯定會限制重用的好處蟹演。
**鉆石問題(菱形繼承問題)
早晚你會遇到下面這種惡心的問題,有些語言甚至根本解決不了顷蟀。
大多數(shù)面向?qū)ο笳Z言都不支持這種情況酒请,盡管看上去似乎很符合邏輯。為什么面向?qū)ο笳Z言支持這種情況如此困難鸣个?
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier inherits from Scanner, Printer {
}
請注意羞反,Scanner類和Printer類都實現(xiàn)了一個名為start的函數(shù)。那么Copier類繼承了哪個啟動函數(shù)囤萤? 掃描儀在昼窗? 打印機一個? 它不可能兩者兼而有之涛舍。
**鉆石(菱形繼承問題)解決方案
解決方案很簡單:不要這樣做澄惊。沒錯。大多數(shù)面向?qū)ο蠖疾蛔屇氵@么干富雅。但是掸驱,但是……要是必須這樣建模該怎么辦?我需要重用没佑!那就必須使用包含和委托毕贼。
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier {
Scanner scanner
Printer printer
function start() {
printer.start()
}
}
這里注意Copier類現(xiàn)在包含了Printer類和Scanner類的實例。Copier委派Printer類的start方法去實現(xiàn)自己的start方法图筹,它也可以簡單的委派給Scanner類的start方法帅刀。
在C++我們可以采用虛繼承让腹,這個網(wǎng)上搜索下很多講解。
**脆弱的基類問題
我們盡量使用較淺的類層次結(jié)構(gòu)扣溺,并保證里面沒有環(huán)骇窍,這樣就不會出現(xiàn)菱形繼承了。
似乎一切都解決了锥余。直到我們發(fā)現(xiàn)……
我前一天工作得好好的代碼今天出錯了腹纳!關(guān)鍵是,我沒有改任何代碼驱犹!
嗯也許是個 bug……但等等……的確有些改動……
但改動的不是我的代碼嘲恍。似乎改動來自我繼承的那個類。
基類的改動怎么讓我的代碼掛了呢雄驹?
看看下面這個基類
import java.util.ArrayList;
public class Array
{
private ArrayList<Object> a = new ArrayList<Object>();
public void add(Object element)
{
a.add(element);
}
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
a.add(elements[i]); // this line is going to be changed
}
}
注意被注釋的那一行佃牛,那一行改動會讓代碼掛掉。
下面是派生類
public class ArrayCount extends Array
{
private int count = 0;
@Override
public void add(Object element)
{
super.add(element);
++count;
}
@Override
public void addAll(Object elements[])
{
super.addAll(elements);
count += elements.length;
}
}
Array的add()添加一個元素到本地的ArrayList
Array的addAll()調(diào)用本地的ArrayList的add()方法添加循環(huán)的每一個元素医舆。
ArrayCount的add()方法調(diào)用了父類的add()然后count變量+1俘侠。
ArrayCount的addAll()方法調(diào)用父類的addAll()然后加上元素數(shù)組的長度。
基類中加注釋的那行代碼現(xiàn)在改成這樣,問題就出現(xiàn)了:
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
add(elements[i]); // this line was changed
}
從基類的作者的角度來看蔬将,這個類實現(xiàn)的功能完全沒有變化爷速。而且所有自動化測試也都通過來了。但是基類的作者忘記了繼承的類霞怀。而繼承類的作者就被坑了T_T惫东。
現(xiàn)在ArrayCount的addAll()調(diào)用父類的addAll(),后者在內(nèi)部調(diào)用add()毙石,而add()被繼承類重載了廉沮。
因此,每次繼承類的add()被調(diào)用時胁黑,count都會增加废封,然后在繼承類的addAll()被調(diào)用時再次增加。
count被增加了兩次丧蘸。
**脆弱的基類的解決方法
這個問題還得要包含和委托來解決。使用包含和委托遥皂,可以從白盒編程轉(zhuǎn)到黑盒編程力喷。白盒編程的意思是說,寫繼承類時必須要了解基類的實現(xiàn)演训。而黑盒編程可以完全無視基類的實現(xiàn)弟孟,因為不可能通過重載函數(shù)的方式向基類注入代碼。只需要關(guān)注接口即可样悟。
*封裝拂募,倒塌的第二根支柱
封裝似乎是面向?qū)ο缶幊痰牡诙蠛锰幫バ伞ο鬆顟B(tài)變量被保護起來防止外部訪問,即它們被封裝在對象內(nèi)部陈症。我們不需要再操心那些可能被不知道誰訪問的全局變量蔼水。但是:
**引用問題
為了提高效率,對象傳遞給函數(shù)時傳遞的是引用录肯,而不是值趴腋。也就是說,函數(shù)不會傳遞對象本身论咏,而是傳遞指向?qū)ο蟮囊粋€引用或指針优炬。
如果一個對象的引用被傳遞給另一個對象的構(gòu)造函數(shù),構(gòu)造函數(shù)就能將這個對象引用放到私有變量中厅贪,用封裝保護起來蠢护。
但這個傳遞的對象不是安全的!因為其他代碼也可能擁有指向該對象的指針养涮,比如調(diào)用構(gòu)造函數(shù)的那段代碼糊余。它必須有指向?qū)ο蟮囊茫駝t沒辦法傳遞給構(gòu)造函數(shù)单寂。
**引用的解決
構(gòu)造函數(shù)必須要復制傳遞過來的對象贬芥。而且不能是淺復制,必須是深復制宣决,即傳入的對象內(nèi)包含的所有對象和所有對象中包含的所有對象……都必須要復制蘸劈。但這卻沒有效率。更糟糕的是尊沸,并非所有對象都能復制的威沫。一些擁有操作系統(tǒng)資源的對象,最好的情況是復制無效洼专,最糟糕的情況是根本不可能復制棒掠。
*多態(tài),倒塌的第三根支柱
并不是因為多態(tài)不好屁商,而是因為實現(xiàn)多態(tài)并不需要面向?qū)ο笳Z言烟很。接口也能實現(xiàn)多態(tài),而且不需要面向?qū)ο蟮呢摀狻6椅砀ぃ涌谝膊粫拗颇隳芑烊氲牟煌袨榈臄?shù)目」倩梗可以告別面向?qū)ο蟮亩鄳B(tài)芹橡,使用基于接口的多態(tài)。
個人觀點:
作者羅列了面向?qū)ο缶幊痰亩鄠€問題:香蕉 猴 叢林問題望伦、鉆石問題(菱形繼承問題)林说、脆弱的基類問題煎殷、引用問題。并且可以使用基于接口的多態(tài)而非面向?qū)ο蟮亩鄳B(tài)腿箩。這是作者幾十年面向?qū)ο缶幊痰慕?jīng)驗之談豪直,但是 說 再見,面向?qū)ο缶幊?肯定是夸大了度秘,不過其問題與不足我們也該謹慎處理避免掉坑里顶伞。也許隨著歷史的發(fā)展會出現(xiàn)更好的更NB的不同理念的編程語言,即使那時我相信面向?qū)ο缶幊桃矔幸幌亍?/p>
知乎個人首頁:
https://www.zhihu.com/people/lichangke/
個人Blog:
https://lichangke.github.io/
歡迎大家來一起交流學習