- 原文地址:The Hidden Treasures of Object Composition
- 原文做者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:yoyoyohamapi
- 校對者:IridescentMia PCAaron
(譯註:該圖是用 PS 將煙霧處理成方塊狀後獲得的效果,參見 flickr。)javascript
這是 「軟件編寫」 系列文章的第十三部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯註:關於軟件可組合性的概念,參見維基百科 < 上一篇 | << 返回第一篇前端
「經過對象的組合裝配或者組合對象來得到更復雜的行爲」 ~ Gang of Four,《設計模式:可複用面向對象軟件的基礎》java
「優先考慮對象組合而不是類繼承。」 ~ Gang of Four,《設計模式:可複用面向對象軟件的基礎》android
軟件開發中最多見的錯誤之一就是對於類繼承的過分使用。類繼承是一個代碼複用機制,實例對象和基類構成了 **是一個(is-a)**關係。若是你想要使用 is-a 關係來構建應用程序,你將陷入麻煩,由於在面向對象設計中,類繼承是最緊的耦合形式,這種耦合會引發下面這些常見問題:ios
類繼承是經過從基類中抽象出一個可供子類繼承或者重載的公共接口來實現複用的。抽象有兩個重要的方面:git
目前,有許多方式去完成泛化和具化。注入簡單函數、高階函數、以及對象組合都能很好地代替類繼承。github
不幸的是,對象組合很是容易被曲解,許多開發者都難於用對象組合的方式來思考問題。如今,是時候更深層次地探索這一主題了。編程
「在計算機科學中,一個組合數據類型或是複合數據類型是任意的一個能夠經過編程語言原始數據類型或者其餘數據類型構造而成的數據類型。構成一個複合類型的操做又稱爲組合。」 ~ Wikipedia後端
造成對象組合疑雲的緣由之一是,任何將原始數據類型組裝到一個複合對象的過程都是對象組合的一個形式,可是繼承技術卻常常與對象組合做對比,即使它們是全然不一樣的兩件事。這種二義性的產生是因爲對象組合的語法(grammer)和語義(semantic)間存在着一個差異。設計模式
當咱們談論到對象組合 vs 類繼承時,咱們並不是在談論一個具體的技術:咱們是在談論組件對象(component objects)間的語義關聯和耦合程度。咱們談論的是意義而非語法,人們一般一葉障目而不見泰山,沒法區別兩者,並陷入到語法細節中去。
GoF 建議道 「優先使用對象組合而不是類繼承」,這啓示了咱們將對象看做是更小,耦合更鬆的對象的組合,而不是大量從一個統一的基類繼承而來。GoF 將緊耦合對象描述爲 「它們造成了一個統一的系統,你沒法在對其餘類不知情或者不更改的狀況下修改或者刪除某個類。這讓系統結構變得緊密,從而難於認知、修改及維護。」
在《設計模式中》,GoF 聲稱:「你將一次又一次的在設計模式中看到對象組合」,而且描述了不一樣類型的組合關係,包括有聚合(aggregation)和委託(delegation)。
《設計模式》的做者最初是使用 C++ 和 Smalltalk(Java 的前身)進行工做的。相較於 JavaScript,它們在運行時構建和改變對象關係要更加複雜,因此,GoF 在敘述對象組合時沒用牽涉任何的實現細節也是能夠理解的。然而,在 JavaScript 中,脫離動態對象擴展(也稱爲 鏈接(concatenation))去討論對象組合是不可能的。
相較於《設計模式》中對象組合的定義,出於對 JavaScript 適用性以及構造一個更清晰的泛化的考慮,咱們會稍作發散。例如,咱們不會要求聚合須要隱式控制子類對象的生命期。對於動態對象擴展的語言來講,這並不正確。
若是選擇了一個錯誤的公理,會讓咱們在得出有用泛化時受到沒必要要的限制,強制咱們爲具備相同大意的特殊用例起一個名字。軟件開發者不喜歡重複作不須要的事兒。
jQuery.fn
上而構建。Array.prototype
上的方法,對象實例的方法則指向了 Object.prototype
上,等等。須要注意的是這三種對象組合形式並非彼此互斥的。咱們可以使用聚合來實現委託,在 JavaScript 中,類繼承也是經過委託實現的。許多軟件系統用了不止一種組合,例如 jQuery 插件使用了鏈接來擴展 jQuery 委託原型 —— jQuery.fn
。當客戶端代碼調用插件上的方法,請求將會被委託給鏈接到 jQuery.fn
上的方法。
後文的代碼實例中的將會共享下面這段初始化代碼:
const objs = [
{ a: 'a', b: 'ab' },
{ b: 'b' },
{ c: 'c', b: 'cb' }
];
複製代碼
聚合表示一個對象是由一個可枚舉的子對象集合構成。一個聚合對象就是包含了其餘對象的對象。聚合中的每個子對象都保留了各自的引用,所以可以輕易地從聚合中解構出來。聚合對象能夠表現爲不一樣類型的數據結構。
當集合中的成員須要共享相同的操做時(集合中的某個元素須要和其餘元素共享一樣的接口),能夠考慮使用聚合,例如可迭代對象(iterables)、棧、隊列、樹、圖、狀態機或者是它們的組合。
聚合適用於爲集合元素應用一個統一抽象,例如爲集合中的每一個成員應用一個將標量轉換爲向量的函數(如:array.map(fn)
)等等。可是,若是有成百上千或者成千上萬甚至上百萬個子對象,那麼流式處理更加高效。
數組聚合:
const collection = (a, e) => a.concat([e]);
const a = objs.reduce(collection, []);
console.log(
'collection aggregation',
a,
a[1].b,
a[2].c,
`enumerable keys: ${ Object.keys(a) }`
);
複製代碼
這將生成:
collection aggregation
[{"a":"a","b":"ab"},{"b":"b"},{"c":"c","b":"cb"}]
b
c
enumerable keys: 0,1,2
複製代碼
使用 pairs 進行的鏈表聚合:
const pair = (a, b) => [b, a];
const l = objs.reduceRight(pair, []);
console.log(
'linked list aggregation',
l,
`enumerable keys: ${ Object.keys(l) }`
);
/* linked list aggregation [ {"a":"a","b":"ab"}, [ {"b":"b"}, [ {"c":"c","b":"cb"}, [] ] ] ] enumerable keys: 0,1 */
複製代碼
鏈表構成了其餘數據結構或者聚合的基礎,例如數組、字符串以及各類形態的樹。可能還有其餘類型的聚合,但咱們在此不會對它們都進行深度探究。
鏈接表示一個對象經過向現有對象增長屬性而構成。
jQuery.fn
只要裝配數據對象的過程是在運行時,就考慮使用鏈接,例如,合併 JSON 對象、從多個源中合併應用狀態、以及不可變狀態的更新(經過將新的數據混合到前一步狀態)等等。
const c = objs.reduce(concatenate, {});
const concatenate = (a, o) => ({...a, ...o});
console.log(
'concatenation',
c,
`enumerable keys: ${ Object.keys(c) }`
);
// concatenation { a: 'a', b: 'cb', c: 'c' } enumerable keys: a,b,c
複製代碼
委託表示一個對象直接指向或者委託到另外一個對象。
Array.prototype
上的方法,對象實例則指向了 Object.prototype
,等等。Object.keys(instanceObj)
這樣公共枚舉機制時,委託屬性是不可枚舉的。const delegate = (a, b) => Object.assign(Object.create(a), b);
const d = objs.reduceRight(delegate, {});
console.log(
'delegation',
d,
`enumerable keys: ${ Object.keys(d) }`
);
// delegation { a: 'a', b: 'ab' } enumerable keys: a,b
console.log(d.b, d.c); // ab c
複製代碼
咱們已經學到了:
全部由其餘對象或者原始類型對象構成的對象都是複合對象。
建立複合對象的過程叫作組合。
存在不一樣形式的組合。
當咱們組合對象時,對象間關係和依賴的不一樣取決於對象是如何被組合的。
is-a 關係(由類繼承所構成的關係)在面向對象設計中是最緊的耦合,實踐中應當儘可能避免。
GoF 建議咱們經過組裝若干小的特性以造成一個更大的總體來進行對象組合,而不是從一個單一的基類或者基礎對象繼承。「優先考慮對象組合而不是類繼承」。
聚合將對象組合到一個可枚舉的集合中,該集合的每一個成員都保留有各自的引用,例如數組、DOM 樹等等。
委託經過將對象的委託鏈鏈接到一塊兒來進行對象組合,委託鏈上的對象直接指向另外一個對象,或者將屬性檢索委託到了另外一個對象,例如 [].map
委託到了 Array.prototype.map()
鏈接經過用新的屬性擴展示有對象來進行對象組合,例如 Object.assign(destination, a, b)
、{...a, ...b}
。
不一樣類型的對象組合不是彼此互斥的。委託是聚合的一個子集,鏈接則可用來構造委託和聚合等等。
目前不僅存在三種類型的對象組合。也能夠經過 相識(acquaintance)或聯合(association)來構建對象間鬆散、動態的關係,在這種關係下,對象被做爲參數傳遞給了另外一個對象(依賴注入)等等。
全部的軟件開發都是組合。可以經過輕鬆、靈活的方式來組合對象,也存在脆弱而不牢靠的方式來組合對象。一些對象組合的形式構成了對象間鬆耦合的關係,一些則構成了緊耦合。
竭力尋找一種變動小的程序需求時只須要變動小部分代碼實現的組合方式。代碼應當清楚且明練地描述你的意圖,而且記住:在你須要類繼承時,其實有更好的方式替代它。
DevAnyWhere 能幫助你最快進階你的 JavaScript 能力,如組合式軟件編寫,函數式編程一節 React:
Eric Elliott 是 「編寫 JavaScript 應用」 (O’Reilly) 以及 「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少機構的頂級藝術家,包括但不限於 Usher、Frank Ocean 以及 Metallica。
大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。_
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。