OO為什么被人吹捧?
可以說, 是它開創(chuàng)了一個(gè)歷史,人們開始普遍采用 "對(duì)抽象的、多態(tài)的事物編程"。
在那之前, 無論是OP還是OB, 都只能完成模塊化, 方便分工開發(fā)與測試。很少使用多態(tài)技術(shù)。
OB編程(OP就省了…… OP->OB依然能完成一定復(fù)用,但方式不同):
void f_ob(T v) { v.f(); }
f是針對(duì)一個(gè)具體的T進(jìn)行編程。
f的行為和T的行為緊緊綁定在一起。
f想要表現(xiàn)出不同行為, 必須要T表現(xiàn)出不同行為。
但無論T如何改變,T和f都只具有一種行為,最后一次改變后具有的行為。
OO編程:
void f_oo(I& i) { i.f(); }
f現(xiàn)在的行為, 僅僅與i所表現(xiàn)出的契約綁定在一起。
而i可以有各種各樣的滿足契約的方式。
這樣的一大好處就是, 對(duì)不同的D : I, f可以被復(fù)用。
得到這一好處的前提就是, D 滿足 I與f之間的契約。
GP編程:
template<typename T>
void f_gp(T v) { v.f(); }
同樣提供了多態(tài)行為:以不同的T帶入f, 就能得到不同的行為。
使得f能被復(fù)用。STL中的組件, 大部分是通過這種方式被復(fù)用的。
并且,STL組件之間, 也大部分是通過這種方式復(fù)用的。
1.
無論是GP還是OOP都是組合組件的方式。
它們(典型地)通過concepts 和 interface來抽象出組件多態(tài)行為。
對(duì)這種抽象事物編程得到的結(jié)果(函數(shù)/類模板,包),可以被復(fù)用(軟件工程一大目標(biāo))。
-------- -------- -------- -------- -------- -------- -------- --------
上面提到契約。
無論是OP、OB、OO、GP, 組件間要能協(xié)同工作, 都是需要契約的。
OP:
free 可以接受空指針, 而printf 不行。
前者是free對(duì)調(diào)用者定下的約束, 后者也是printf對(duì)調(diào)用者定下的約束
—— 要使用我就必須這樣做。
malloc 失敗返回空指針, 而new 拋異常, 否則返回可用動(dòng)態(tài)內(nèi)存。
這是它們對(duì)調(diào)用者的承諾。
—— 使用我, 你能得到什么。
T* p = new T;
if (!p) 這種代碼只在古董編譯器上才可能有意義。
"加上NULL判斷更好" , 也只有"C++方言"的古董學(xué)者才說得出來。
new[] 要 delete[] , new 要 delete, 這也是契約。
"因?yàn)閏har是基礎(chǔ)類型,所以可以new[] , delete", 只有不懂軟件契約的白癡學(xué)者才說得出來。
OB:
要將CString s 轉(zhuǎn)換為 TCHAR*, 一定要用 s.GetBuffer 而不是 (TCHAR*)&s[0]
CString 的約束。
basic_string.c_str() 一定以'\0'結(jié)尾, 而data() 則不是。
basic_string 的承諾。
OOP:
我用的OOP庫、框架不多…… 舉不出什么例子。
但它的契約和通常OB"非常類似" : 完成什么、需要調(diào)用什么、調(diào)用順序、 參數(shù)合法性。
GP:
GP總共有哪些契約形式, 我總結(jié)不出來。
但至少有一條 —— 它不再對(duì)T有完全限定, 而只作最小限定。
還是上面的代碼:
void f_oo(I& i ) { i.f(); }
D d;
f(d); // 要求 D : I
template<typename T>
void f_gp(T v) { v.f(); }
要求 v.f(); 合乎語法 :比如, 它既可以是non-static member function, 也可以是static member function。
并且僅僅要求這一點(diǎn)。
2.
契約是普遍存在的, 不僅僅是GP、 其他范式都有。
它是合作與復(fù)用的前提。
-------- -------- -------- -------- -------- -------- -------- --------
3.
為什么GP飽受爭議, 而OO沒有?
我覺得, 是因?yàn)閺腛B到OO過度比較自然。
要求一個(gè)I需要怎樣, 一個(gè)I需要使用者怎樣的時(shí)候,
通常也會(huì)有類似的需求 —— 要求一個(gè)C怎樣, 一個(gè)C需要使用者怎樣。
注意, 這只是 client 與 I, 以及 client 與 C之間的契約。
在這個(gè)層面上, 不需要學(xué)習(xí)新的思想。
而下面會(huì)提到OO引入的新的契約形式。
而GP的契約形式, 都是一些全新的形式。
其中最主要的形式是: T 是否具有一個(gè)叫f的函數(shù)(操作符也可以理解為一種調(diào)用函數(shù)的方式)。
在C++中, 還有必須有某個(gè)名字的嵌套類型, 通過T::f強(qiáng)制static member function等形式。
OO比較流行、支持OO的語言比較多,是目前主流。
C++,Java,C#的開發(fā)者總共占了多少比例?
GP支持的語言其實(shí)也多。
但拋開C++(或者算上C++用戶中理解GP的人)怎么都比不上上面3個(gè)巨頭。
批評(píng)自己不熟悉的事物是不嚴(yán)謹(jǐn)?shù)摹?br>遇見STL編譯錯(cuò)誤, 要么就去學(xué)習(xí)GP的方式, 要么就拋棄STL。
抱怨STL是種傻逼行為 —— 明明是自己不會(huì)用, 要怪只能怪自己。
-------- -------- -------- -------- -------- -------- -------- --------
4.
如同GP、 OO同樣需要學(xué)習(xí)、 需要文檔、 否則會(huì)充滿陷阱。
上面提到的client 與 I的契約形式通常和 clinet 與 C之間的形式相同。
使得OO的一個(gè)方面可以"溫固"。
而client 與 D之間的契約? 這個(gè)層面不"知新"是不行的。
并且這個(gè)層面上的契約常常是出bug的地方 —— 因?yàn)檫@是語法檢查不了的, 必須有程序員自己去滿足語意。
舉個(gè)例子 :
看見一個(gè)虛函數(shù),它是否可以被覆蓋? 覆蓋它的實(shí)現(xiàn)是否需要同時(shí)調(diào)用基類實(shí)現(xiàn)?
除非是約定俗成的一些情況, 比如 OnNotify、OnXXXEvent這種名字比較明顯。
其他情況, 不去看文檔, 依然是不知道的。
我剛剛就犯了一個(gè)錯(cuò)。
python 中 繼承HTMLParser , 覆蓋__init__ 時(shí), 必須調(diào)用基類實(shí)現(xiàn)。
我確實(shí)不知道構(gòu)造函數(shù)也可以被完全覆蓋……(原諒我…… 第1次使用python……)
在C++中, 基類的構(gòu)造函數(shù)是無論如何都會(huì)被調(diào)用的。
我沒有調(diào)用基類的__init__, 然后報(bào)了一個(gè)錯(cuò)。
好在報(bào)錯(cuò)清晰, 源代碼的位置都有, 源代碼也可見, 源代碼也清晰。問題很容易就找出來了。
如果
4.1.
它不是構(gòu)造函數(shù), 只是一個(gè)普通的虛函數(shù)?
名字也看不出什么蹊蹺?
4.2.
文檔也沒有記載是否需要調(diào)用基類?
(HTMLParser中的文檔沒有說要調(diào)用__init__, 這應(yīng)該是python的慣例, 所以就沒有記載了)
沒有嚴(yán)格的錯(cuò)誤檢查?
沒有完整的、信息豐富的call stack trace?
等發(fā)現(xiàn)錯(cuò)誤時(shí), 也許都離題萬里了。
4.3.
沒有清晰的源代碼(其實(shí)到了要查看源代碼的時(shí)候, 已經(jīng)是……)?
這些問題在OO中同樣是存在的, 只是它沒引起編譯錯(cuò)誤, 或者說編譯錯(cuò)誤比較明顯, 容易修改。
而更多的語意檢查, OO中 client 與 D之間的層次, 依然要靠程序員的學(xué)識(shí) —— 主要是查閱文檔的習(xí)慣。
對(duì)比GP
4.1之前的4.0(OnNotify, OnXXXEvent之流), 同樣存在一些約定俗成。
對(duì)4.1, 同樣是查文檔。
對(duì)4.2, 沒有文檔同樣犯錯(cuò)。
C++中, 一部分錯(cuò)誤可以提前到編譯時(shí)(C++只在持編譯時(shí)支持ducking type)
對(duì)4.3
同樣需要去查看源代碼。
GP的代碼比OOP難看?
同上面, 只是因?yàn)?strong>不熟悉、不信任、不需要這種抽象方法, 這些契約的形式。
STL中的契約形式其實(shí)并不多。
[first,last) 左閉右開區(qū)間;
iterator的幾個(gè)種類;
(adaptable)binary(unary)_function;boost只是將其提升到N-nary,還省去了adaptable的概念;
相等、等價(jià)、嚴(yán)格弱序。
多嗎?
而且, 我不覺得為了比較要去實(shí)現(xiàn)一個(gè)IComparable 有何美感可言……
-------- -------- -------- -------- -------- -------- -------- --------
5. 這些新的契約形式、 抽象方式, 有沒有其優(yōu)勢?
當(dāng)然有 —— 最小依賴帶來的靈活性。
上面也提到, GP只依賴于其需要的契約, 對(duì)其不需要的, 通通不關(guān)心。
現(xiàn)在詳細(xì)解釋上面
D d;
f_oop(d); // D : I
T v;
f_gp(v); // v.f
為什么后者比前者靈活。因?yàn)楹笳咧灰蕾囁枰臇|西。
5.1
template<typename T>
void f123_gp(T v) { v.f1(); v.f2(); v.f3(); }
可以使用
struct T123_1 { void f1(); void f2(); void f3(); }
struct T123_2 { void f1(); void f2(); void f3(); }
void f123_oop(I& i) { i.f1(); i.f2(); i.f3(); }
必須有一個(gè)
struct I { virtual void f1(); virtual void f2(); virtual void f3(); };
然后是:
D1 : I; D2 : I; ...
5.2
如果現(xiàn)在需要實(shí)現(xiàn)另一個(gè)函數(shù), 它對(duì)T的需求更少 :
template<typename T>
void f12_gp(T v) { v.f1(); v.f2(); }
T123_1, T123_2 可以使用
新增一個(gè):
struct T12_1 { void f1(); void f2(); }
依然可以使用
OOP就必須重構(gòu)了:
struct I12 { virtual void f1(); virtual void f2(); };
struct I123 : I12 { virtual void f3(); }
struct D12 : I12 {};
5.3
再看 :
template<typename T>
void f23_gp(T v) { v.f2(); v.f3(); }
T123_1, T123_2 依然可以使用
T12_1 不行。
但新增的
struct T23_1 { void f2(); void f3(); }; 可以使用
OOP又必須重構(gòu):
總體趨勢是這樣的, OOP需要極端靈活的時(shí)候, 就會(huì)變成這樣:
一個(gè)接口, 一個(gè)函數(shù)。
struct I1 { virutal void f1(); };
struct I2 { virutal void f2(); };
struct I3 { virutal void f3(); };
現(xiàn)在接口設(shè)計(jì)是極端靈活了。
但使用接口時(shí), 依然逃不過2種都不太優(yōu)雅的作法:
1. 接口組合
struct I12 : I1, I2;
struct I23 : I2, I3;
struct I31 : I3, I1;
2. 接口查詢
不組合出那些中間接口, 但運(yùn)行時(shí)作接口查詢:
void f12(I1* i1) {
i1->f1();
if (I2* i2 = dynamic_cast<I2*>(i1) {
i2->f2();
}
else { 將一部分編譯時(shí)錯(cuò)誤留到了運(yùn)行時(shí)。 }
}
這不是故意找茬, 而是將STL中的iterator換個(gè)簡單的形式來說明而已。
也許絕大部分情況下, 是不需要靈活到每個(gè)接口一個(gè)函數(shù), 而是一個(gè)接口3、4個(gè)相關(guān)的函數(shù)。通常它們會(huì)被一起使用。
即使沒有上面如此極端, 假設(shè)IPerfect1、IPerfect2都是設(shè)計(jì)得十分合理的, 3、4個(gè)函數(shù)的接口, 通常這3、4個(gè)函數(shù)要么必須一起提供, 要么都不提供, 單獨(dú)提供是不符合語意的, 提供太多又是不夠靈活的。
這需要經(jīng)驗(yàn), 相當(dāng)多的經(jīng)驗(yàn)。 但總是可以完成的事情。
但組合接口, 依然是OOP的痛處。
我記不清C#和Java中的interface是否繼承自多個(gè)interface。
如果不行, 它們就可能需要運(yùn)行時(shí)接口查詢。
而C++, 要在這種"組合接口"與接口查詢之前作一個(gè)選擇。
反觀GP, 它一開始就不是以接口為單位來提供抽象,而是按需而定。
所以, 它既不需要仔細(xì)的拆分接口, 也不需要組合接口。
STL中數(shù)據(jù)、容器、算法相互無關(guān)、可任意組合。
應(yīng)該是前無古人的突破。
后面有沒有來者? 上面已經(jīng)說了, OOP要達(dá)到這種靈活性, 同樣也有其代價(jià)。
并且, OOP代價(jià)體現(xiàn)在丑陋, 而不是難以理解。
靈活的事物肯定比不那么靈活的事物難理解,抽象總比具體難理解。
所以抽象出一個(gè)合理的、廣泛接受的語意很重要。
* 就是解引用, ++ 就是前迭代, -- 就是后迭代。
支持--就是雙向, 支持 + n 就是隨機(jī)。
GP也不會(huì)胡亂發(fā)明一些語意不清晰的概念。
window w;
control c;
w += c; 這種代碼在GP界同樣是收到批評(píng)的。
最后, 軟件工程中, 是否真正需要靈活到如此程度, 以至于大部分人難以(或者不愿意去)理解的事物, 我就不知道了……
回復(fù) 更多評(píng)論