怎樣理解「組合優於繼承」以及「OO的反模塊化」,在這些方面FP具體來說有什麼優勢?

傳統的CS教育往往重OO而忽視FP在編程思想上的重要作用。而目前在互聯網企業即便是BAT,函數式語言諸如 Haskell,erlang用的也往往較少。但是個人認為Java或者PHP在某些特定場合是有其局限性的,不知道大家如何理解。


OO是面向介面的,而繼承只是實現介面的一種代碼復用的辦法。所以不要把繼承跟OO等價。組合也可以是實現介面的一種辦法(如COM),這些東西跟OO是完全互相兼容的,所以跟OO沒什麼緊密的關係,跟FP也是。

OO反模塊化什麼的也是無稽之談,只是他們沒想清楚怎麼做就拍腦袋開始寫代碼的結果。


我感覺面向對象很有價值的。。。不過不應該被局限(當然過於自由會增大錯誤可能性)

Effiel, Ocaml, Common Lisp的「面向對象」就和Java的挺不一樣,還有Haskell的type class神馬的

說法之一就是,面向對象是「類型-模塊」的合併物,一個模塊不再是「基本軟體元素」(操作,類型,變數等)的簡單結合體。這樣的後果之一,就像《名詞王國的死刑》里寫的,"動詞"太依賴於"名詞"。

繼承和組合無疑會導致對象間的耦合(組合只是比繼承「稍好」的替代方式)。所以面向對象設計,有一條講「組合優於繼承」,部分設計模式就是為了符合「組合優與繼承」的原則。這樣可以方便復用操作,添加類型,添加動作。

代理/委託 和mix-in等使得一些設計模式不再必要,可以看做是對面向對象的「改進」。

關於函數式語言的模塊化策略,一時也說不清(而且各個函數式語言的方案也有所不同),推薦看一篇論文,Composing Contracts: An adventure in financial Engineering。

SPJ等人寫的,用金融的一個業務舉例子,展示了函數式的解決方案。我想也揭示了一些金融公司使用FP語言(Ocaml, Haskell等)的原因吧

軟體工程,語言架構的一些東西很多只是在爭執。

自己用起來,悶聲發大財


為什麼有「組合優於繼承」的說法

首先需要注意, 廣為流傳的「組合優於繼承」 的說法是一種不嚴謹的翻譯, 其來源如下

眾多設計模式包含的2個最核心原則(引自參考書籍 《Design Patterns: Elements of Reusable Object-Oriented Software》)

Program to an interface, not an implementation. (面向介面編程,而不是具體的實現)
Favor object composition over class inheritance.(如果某個場景的代碼復用既可以通過類繼承實現, 也可以通過對象組合實現, 盡量選擇對象組合的設計方式)

第一個原則的好處非常明顯: 可以極大程度地減少子系統具體實現之間的相互依賴。
第二個原則則不那麼容易理解, 下面展開敘述 。

對象組合與類繼承的對比

面向對象設計的過程中, 兩個最常用的技巧就是類繼承對象組合,同一個場景下的代碼復用,這兩個技巧基本上都可以完成。 但是他們有如下的區別:

  • 通過繼承實現的代碼復用常常是一種「白盒復用」, 這裡的白盒指的是可見性: 對於繼承來說,父類的內部實現對於子類來說是不透明的(實現一個子類時, 你需要了解父類的實現細節, 以此決定是否需要重寫某個方法)
  • 對象組合實現的代碼復用則是一種「黑盒復用」「: 對象的內部細節不可見,對象僅僅是以「黑盒」的方式出現(可以通過改變對象引用來改變其行為方式)

這裡通過汽車的剎車邏輯進行說明。 對於汽車來說, 存在多種不同的型號, 我們會很自然的希望定義一個類 Car 來描述所有汽車通用的剎車行為 brake(), 然後通過某種方式(繼承/組合)來為不同的型號的汽車提供不同的剎車行為。

  • 如果通過繼承來實現, 思路就是定義一個Car, 實現不同子類 CarModelA, CarModelB 來重寫父類的 brake() 方法以體現不同型號車的剎車行為區別。

public abstract class Car {

// 也可以將該方法設置成抽象方法, 強迫子類來實現該方法
public void brake() {
// 提供一個默認的剎車實現
...
}
}

public class CarModelA extends Car {

public void brake() {
aStyleBrake();// A 風格的剎車行為
}
}

public class CarModelB extends Car {

public void brake() {
bStyleBrake(); // B 風格的剎車行為
}
}

上述的例子展現了如何通過繼承來完成不同型號車輛剎車行為的變化。但是可以注意到, 每一個型號的車的剎車行為是在編譯時就確定好的 , 沒有辦法在運行時刻將 CarModelB 的剎車行為賦予 CarModelA 。

  • 如果通過對象組合的實現方式, 則需要為 Car 定義一個引用, 該引用的類型是一個為剎車行為定義的介面。

public interface IBrakeBehavior {
public void brake();
}

public class AStyleBrake implements IBrakeBehavior {
public void brake() {
aStyleBrake(); // A 風格的剎車行為
}
}

public class BStyleBrake implements IBrakeBehavior {
public void brake() {
bStyleBrake(); // B 風格的剎車行為
}
}

//通過給下面的類賦予 AStyleBrake 或 BStyleBrake 可以完成不同 Model 的剎車行為的切換

// 同理, 汽車其他的行為(如啟動 launch) 也可以用類似的方法實現
// 不同型號的汽車實現, 可以通過賦予不同風格的行為實例來 「組裝」 出來的, 也就不需要為 Car 定義不同的子類了
public class Car{
protected IBrakeBehavior brakeBehavior;

public void brake() {
brakeBehavior.brake();
}

public void setBrakeBehavior(final IBrakeBehavior brakeType) {
this.brakeBehavior = brakeType;
}
}

值得注意的是, 上面的剎車行為不一定需要通過介面來實現, 定義一個 BrakeBehaviour 的父類, 然後再定義AStyleBrake , BStyleBrake 來繼承該類, 實現不同的行為, 同樣是組合方式的應用。

所以不難發現, 當我們拿類繼承組合在一起進行對比時, 並不是以實現方式中是否有用到類繼承而區分的。

我們真正關注的是行為的繼承行為的組合 :需要變化的行為是通過 繼承後重寫的方式 實現, 還是通過 賦予不同的行為實例 實現。

繼承與組合的優缺點對比

類繼承優點:

  • 類之間的繼承關係時在編譯時刻靜態地定義好的, 因此使用起來也非常直觀, 畢竟繼承是被編程語言本身所支持的功能。
  • 類繼承也使得修改要重用的代碼變得相對容易, 因為可以僅僅重寫要更改的父類方法。

類繼承缺點:

  • 第一個缺點是伴隨第一個優點而生的: 沒有辦法在運行時刻改變繼承了父類的子類行為。
    • 這一點在之前汽車的例子中已經進行了說明
  • 第二個缺點與第一個缺點相比往往更嚴重: 通過繼承實現的代碼復用,本質上把父類的內部實現細節暴露給了子類, 子類的實現會和父類的實現緊密的綁定在一起, 結果是父類實現的改動,會導致子類也必須得改變。
    • 以之前的例子進行說明, 如果是通過繼承的方式來實現不同型號汽車的剎車行為變化, 假設現在我們基於 Car 這個父類實現了 10 種不同型號的汽車 CarModel( A, B, C, D, E, F, G,H ,I , J ), 其中前 5 個型號( A、B、C、D、E) 都沒有重寫父類的剎車方法, 直接使用了父類 Car 提供的默認方法, 後 5 個型號均提供了自己獨特的 brake 實現 。 現假設, 我們希望對 Car 中的 brake 方法進行升級改造, 然而,升級改造後的 brake 行為只適用於C,D , 最早的兩種型號A, B 並不兼容升級後的剎車行為。 這樣, 我們為了保證 A, B 依舊能正常工作, 就不得不把舊的 brake 實現挪到 A、B 中。 或者, 分別去升級 C、 D、E 中的 brake 方法。

對象組合優點:

  • 對象的組合是在運行時刻通過對象之間獲取引用關係定義的,所以對象組合要求不同的對象遵從對方所實現的介面來實現引用傳遞, 這樣反過來會要求更加用心設計的介面,以此支持你在使用一個對象時, 可以把它和很多其他的對象組合在一起使用而不會出現問題。
  • 對象的組合由於是通過介面實現的, 這樣在復用的過程中是不會打破其封裝的。 任意一個對象都可以在運行時刻被替換成另外一個實現了相同介面且類型相同對象, 更重要的是,由於一個對象的實現是針對介面而編寫的, 具體實現之間的依賴會更少。
  • 對象組合的方式可以幫助你保持每個類的內聚性,讓每個類專註實現一個任務。 類的層次會保持的很小,不會增長到一種無法管理的恐怖數量。 (這也是為什麼Java語言支持單繼承的原因

對象組合缺點:

  • 不具備之前所羅列的類繼承的優點


不是繼承優與組合, 而是多用組合少用繼承,因為繼承使的不到家代碼容易出問題。

使用繼承是有語義的的依賴的, 當子類繼承父類時, 意味著子類和父類在抽相層面必須是要相同的,也是就所謂的IS - A關係。這種關係很難把控, 尤其層次多且複雜的繼承結構 ,要保持結構中所有類之間的語意清楚一致是相當困難的。 而且如果要對位於繼承結構上端的類作較大的改動,如改變和擴展類本身的語義,那結構中下層的類的意義也會一併改變,而這改變並非寫代碼時的本意。 當然可以可以實行介面隔離的方法再次分解繼承結構 ,然而這是危險且耗費時間的工作, 並且會使整個繼隨結構變的更複雜。 因此, 在面向對象程序設計有一點原則是:多用結合少用繼承 。 因為繼承不但依賴目標類的功能, 而且還依賴它的語義, 而組合是依賴功能不依賴語義。 為什麼Java語言在設計時去除了c++的多重繼承功能, 規定一個字類只能繼承一個父類, 因為濫用繼承會使代碼變的複雜,單繼承已經能使問題變的不可控, 更何況多繼承。 如果使用繼承光只是奔著復用代碼去的, 那絕對是一個錯誤的設計決策。

至於OO的的反模塊化, 似乎從來沒有聽到過這樣的理論。類是實現面向對象中第一要素, 是很好的模塊化工具, OO的反模塊化又從何說起?

FP程序設計和面象對象程序設計理念上雖然有所不同, 但絕對不會起衝突的。

FP編程有幾個要點

  • 聲明性編程,也就是功能都是以表達式形式呈現

  • 變數的值不可變,狀態不可變

  • 不關注代碼的執行順序

  • 控制流使用函數調用和遞歸來實現

  • 把函數當成程序設計的第一個素

而面向對象中主要的編程單元類和對象, 一些重要的原則如

  • 單一職責,包、類、函數、變數這些編程單元在自身的層面要保持功能的單一性

  • 開放封閉,對程序要的功能更新要以增加擴展的形式進行, 而非修改原來的舊的代碼

  • Liskov替換,少執行抽像類到具體類這種編程方式,保持同類形的類在相互替換時不出問題

  • 依賴倒轉, 盡量針對抽像編程

  • 介面隔離 ,不從父類繼承用的不到東西到子類

  • 還有如高內聚低耦合,封閉變化 ,多用組少用繼承等等

這些面象對象的要義都與FP編程方式沒有半分瓜葛, 兩者完全可以融合在一起使用,面象對象關注的是寫代碼的宏觀設計,而FP更關注的是微觀的細節實現。 如果非要說FP編程與傳統編程理念不一致,那隻能讓面向過程出來背鍋了。

現在主流平台上的高端編程語言如JVM中的scala, CLR的F#都是面向對象和FP結合的,或者說是FP為主, 面向對象為輔。 而C#和Java則是面向對象為主FP輔。順便吐槽一下Java,在FP編程上,8中的lambda和集合操作要多難用就有多難用,根本沒法和C#的linq比。

可能是由於FP編程比普通的面向過程來的難學, 且主流編程語言都是在發展的後期才開始支持FP編程的, 行業環境就那樣, FP編程市場佔有率低也在情理之中。


傳統的CS教育連OO是啥都講不清楚,哪還講得清楚FP。

要理解組合優於繼承和OO的反模塊化,

請看 @高博 翻譯的 元素模式 元素模式 (豆瓣)。


繼承是一種多態工具,而不是一種代碼復用工具。有些開發者喜歡用繼承的方式來實現代碼復用,即使是在沒有多態關係的情況下。是否使用繼承的規則是繼承只能用在類之間有「父子」關係的情況下。

不要僅僅為了代碼復用而繼承。當你使用組合來實現代碼復用的時候,是不會產生繼承關係的。過度使用繼承(通過「extends」關鍵字)的話,如果修改了父類,會損壞所有的子類。這是因為子類和父類的緊耦合關係是在編譯期產生的。

不要僅僅為了多態而繼承。如果你的類之間沒有繼承關係,並且你想要實現多態,那麼你可以通過介面和組合的方式來實現,這樣不僅可以實現代碼重用,同時也可以實現運行時的靈活性。

這就是為什麼四人幫(Gang of Four)的設計模式里更傾向於使用組合而不是繼承的原因。面試者會在你的答案里著重關注這幾個詞語——「耦合」,「靜態還是動態」,以及「發生在編譯期還是運行時」。運行時的靈活性可以通過組合來實現,因為類可以在運行時動態地根據一個結果有條件或者無條件地進行組合。但是繼承卻是靜態的。

組合和繼承,都能實現對類的擴展。

區別如下表所示

----------來自互聯網


繼承耦合度高,組合耦合度低,這是一個顯而易見的道理。組合各個組件是分離的,所以組合更加符合單一責任原則。而繼承會有導致功能代碼的不斷累積,組成一個龐大的複合功能組件。所以能用組合則用組合。


sicp 開篇就說了,編程語言需要提供抽象的工具。什麼是抽象,抽象就是提煉本質,定義共性,反應到編程上就是鴨子模型,就是介面。然而以一般人類的思維模式,直接的的抽象思考不來,所以他們給了你OO,好有物化的對照。

組合優於集成是針對 OO 說的,我不必先成為鴨子才可以游泳,仍舊是以介面定義的延伸。

模塊是什麼,模塊是功能的集合,我可以用OO,我也可以只給你毫無關聯的一組函數,所以 OO 反模塊我不知道如何談起。

至於 FP,太深奧,不懂。


個人理解,OO是為了提供統一介面,從來就不是以模塊化為目標的,更不是以反模塊化為目標(誰這麼無聊)


以下均為主觀瞎扯淡:

最近在看兩本書,挺不錯的。

編程原本

C#敏捷開發實踐

函數式就是說調用多少次f(x)結果都一樣。

值就是不變

對象就是有狀態。

所謂CS教育不重FP重OO應該是誤解。你用OO的語言也能寫出來不變的數據結構吧。

因為不變這個特性,函數式數據結構的刪除和修改都比較詭異,或者乾脆不支持?題主感興趣可以去看看purely functional data structures.

模塊化和(OO還是FP)應該是兩回事,模塊化就是說抽象實體怎麼轉化成具體實體的過程。OO就是繼承/介面/mixin/trait這些東西。FP就是structural typing/module/typeclass這些。反正都挺複雜的。具體的用途也不完全一樣。模塊化可以等到做出原型以後再開始。面對一片空白設計一通,很容易跑偏。

可以看看去編程原本第一章。

最後,糾結語言沒太大用處。表達力夠用就行。演算法和數據結構更重要一些。


推薦閱讀:

haskell中的類型類是相當於面向對象語言的介面嗎?
Lambda calculus引論(目錄)
愉♂悅的scheme之旅(6)-用宏構建DSL
如何編譯函數閉包

TAG:面向對象編程 | 函數式編程 | 設計模式 |