用代碼來說明,為什么需要面向擴(kuò)展的設(shè)計(jì)
在基本的面向?qū)ο缶幊讨?,你只能直接調(diào)用一個(gè)類的方法,而這些方法是由這個(gè)類的作者定義的,這對于面向用戶設(shè)計(jì)的類來說是沒有問題的。此外,在 20 - 30 年前,在大型標(biāo)準(zhǔn)庫和開源庫被大量復(fù)用之前,大部分代碼通常是跟自己的代碼中的類來一起工作的 —— 也就是你自己的團(tuán)隊(duì)或公司維護(hù)的代碼。然而,在現(xiàn)代代碼世界中,我們經(jīng)常會使用其他人編寫的類。
業(yè)務(wù)邏輯通常大量使用包括字符串和集合等標(biāo)準(zhǔn)庫功能、以及第三方庫中的一些類,我們受到這些類提供的操作的限制。例如,當(dāng)我們需要用破折號替換字符串中的空格時(shí),我們會這樣編寫代碼:
- string.replace(' ', '-')
但是當(dāng)我們需要將左邊的字符串對齊到指定的長度時(shí),我們可能沒有現(xiàn)成的方法可用,在這些舊的語言(如 Objective-C、C++、Java 或 JS)中,你需要強(qiáng)制寫成這種形式:
- leftPad(string, ' ', length)
這個(gè) leftPad 可能來自一個(gè)單獨(dú)的庫¹,也可能來自第三方的工具函數(shù)集合(比如 Apache Commons),或者在你自己的項(xiàng)目中自行編寫??傊?,它的調(diào)用看起來和字符串類上的內(nèi)置方法是非常不同的。
為什么會有這樣的問題呢?我引用 Java 的作者之一 Guy Steele,他在 1998 年的《成長的語言》論文²中的一段話。
在大多數(shù)語言中,用戶至少可以定義一些新語法來代表另外一段代碼,然后可以很方便地調(diào)用這些代碼,這種方式可以讓新語法看起來像原生調(diào)用一樣。通過這種方式,用戶可以構(gòu)建一個(gè)更大的語言來滿足他的需求。
Guy Steele, Growing a Language
他是在批評 APL 缺乏這樣的設(shè)施,但同樣的批評也適用于現(xiàn)代環(huán)境下的舊的面向?qū)ο笳Z言。你被困在一個(gè)類的操作詞匯表上,而這個(gè)詞匯表是原始庫的設(shè)計(jì)者們所設(shè)想的,它不能由你來擴(kuò)展。此外,它也沒法被廣泛使用的庫的維護(hù)者隨意地?cái)U(kuò)展,再次引用同一篇論文中的內(nèi)容作為原因。
編程詞匯的一部分適合所有程序員使用,但其他部分僅適合少數(shù)幾個(gè)人。 程序員需要了解學(xué)習(xí)其所有詞匯用法,這并不公平。
現(xiàn)代語言(如 C#、Scala、Rust、Kotlin 和 Swift)通過支持?jǐn)U展方法解決了這個(gè)問題。你可以在不是你控制的類中添加特定領(lǐng)域的擴(kuò)展方法,這樣,你自己的函數(shù)可以用類似于內(nèi)置方法來調(diào)用,而你的代碼仍然可以像散文一樣,流暢的按從左到右的順序閱讀。
- string.padLeft(' ', length)
這個(gè) padLeft 擴(kuò)展可以在任何地方定義,它是一個(gè)很好的編程語言進(jìn)化的故事。但是,它的意義還不止于此。
一旦一種編程語言支持?jǐn)U展函數(shù),它就改變了經(jīng)典面向?qū)ο蟮?API 設(shè)計(jì)方法。這對于一個(gè)從 Java 這樣的舊語言,切換到 Kotlin 這樣的現(xiàn)代語言的程序員來說,是一個(gè)不小的啟示,因?yàn)閿U(kuò)展函數(shù)通常只是作為方便的語法糖³呈現(xiàn)出來。我們還是先看一個(gè)帶有一堆屬性(或 getter 方法)的接口。
interface Obscure { val foo: Int val bar: Int val sum: Int val max: Int val min: Int}
它和你在一個(gè)典型的商業(yè)應(yīng)用程序中找到的接口或類并無大的區(qū)別 —— 有一堆屬性和方法。
你能快速掌握這個(gè)接口代表了一個(gè)什么樣的實(shí)體嗎?它的狀態(tài)空間是由哪些屬性構(gòu)成的?如果沒有額外的文檔,要弄清楚這一點(diǎn)并不容易。但是,讓我們把這個(gè)接口重構(gòu)成一個(gè)核心實(shí)體和方便的擴(kuò)展函數(shù)。
- interface NotObscure {
- val foo: Int
- val bar: Int
- }
- val NotObscure.sum: Int
- val NotObscure.max: Int
- val NotObscure.min: Int
現(xiàn)在,很明顯,這個(gè)接口的核心功能是由兩個(gè)整數(shù)屬性 foo 和 bar 組成的,而其余的 sum、max 和 min 屬性只是為了方便起見而提供的,并在這些核心屬性的基礎(chǔ)上進(jìn)行計(jì)算。不需要再明確地寫文檔描述這種區(qū)別了 —— 從代碼的結(jié)構(gòu)中就可以直接看出。
這種面向擴(kuò)展的設(shè)計(jì)在 Kotlin 標(biāo)準(zhǔn)庫和第三方庫中得到了廣泛的應(yīng)用。它是一種強(qiáng)大的設(shè)計(jì)技術(shù),使用它會有非常好的效果。
這種設(shè)計(jì)方法有一個(gè)副作用。你可能會注意到,Kotlin 代碼通常會使用通配符 import,比如 import com.examplease.*。這在 Kotlin 中很方便,因?yàn)樵?Kotlin 中僅導(dǎo)入一個(gè)類是非常少見的。所有有用的、方便的、實(shí)用的函數(shù)通常都定義在同一個(gè)包中,但在類外作為擴(kuò)展函數(shù)定義。
文中鏈接:
https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/ How one developer just broke Node, Babel and thousands of projects in 11 lines of JavaScript, Chris Williams, 2016
https://www.cs.virginia.edu/~evans/cs655/readings/steele.pdf Growing a Language, Guy Steele, 1998
https://kotlinlang.org/docs/reference/extensions.html Extensions in Kotlin Programming Language
英文原文:
https://medium.com/@elizarov/extension-oriented-design-13f4f27deaee
本文轉(zhuǎn)載自微信公眾號「 高可用架構(gòu)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 高可用架構(gòu)公眾號。