這是理解
SOLID
原則,介紹什麼是
開閉原則以及它爲何可以在對已有的軟件系統或者模塊提供新功能時,避免沒必要要的更改(重複勞動)。
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.軟件實體(類、模塊、函數等)都應當對擴展具備開放性,可是對於修改具備封閉性。前端
首先,咱們假設在代碼中,咱們已經有了若干抽象層代碼,好比類、模塊、高階函數,它們都僅作一件事(還記得單一職責原則嗎?),而且都作的十分出色,因此咱們想讓它們始終處於簡潔、高內聚而且好用的狀態。node
可是另外一方面,咱們仍是會面臨改變,這些改變包含範圍(譯者注:應當是指抽象模塊的職責範圍)的改變,新功能的增長請求還有新的業務邏輯需求。webpack
因此對於上面咱們所擁有的抽象層代碼,在長期想讓它處於一成不變的狀態是不現實的,你不可避免的會針對以上的須要做出改變的需求,增長更多的功能,增長更多的邏輯和交互。在上一篇文章,咱們知道,改變會使系統複雜,複雜會促使模塊間的耦合性上升,因此咱們迫切地須要尋找一種方法可以使咱們的抽象模塊不只能夠擴大它的職責範圍,同時還可以保持當前良好的狀態(簡潔、高內聚、好用)。git
這即是開閉原則存在的意義,它可以幫助咱們完美地實現這一切。github
當你須要對已有代碼做出一些修改時,請切記如下兩點:web
這裏關於繼承,咱們特地增長了一個註釋,在這種狀況下使用繼承可能會使模塊之間耦合在一塊兒,同時這種耦合是可避免的,咱們一般在一些預先有着良好定義的結構上使用繼承。(譯者注:這裏應該是指,對於咱們預先設計好的功能,推薦使用繼承方式,對於後續新增的變動需求,推薦使用組合方式)express
舉個例子(譯者注:我對這裏的例子作了一些修改,原文中並無詳細的說明)編程
interface IRunner { run: () => void; } class Runner implements IRunner { run(): void { console.log("9.78s"); } } interface IJumper { jump: () => void; } class Jumper implements IJumper { jump(): void { console.log("8.95,"); } }
例子中,咱們首先聲明瞭一個IRunner
接口,以後又聲明瞭IJumper
,並分別實現了它們,而且實現類的職能都是單一的。redux
假如如今咱們須要提供一個既會跑又會跳的對象,若是咱們使用繼承的方式,能夠這麼寫後端
class RunnerAndJumper extends Runner { jump: () => void }
或者
class RunnerAndJumper extends Jumper { run: () => void }
可是使用繼承的方式會使這個RunnerAndJumper
與Runner
(或者Jumper
)耦合在一塊兒(耦合在一塊兒的緣由是由於它會因它的父類改變而改變),咱們再來用組合的方式試試看,以下:
class RunnerAndJumper { private runnerClass: IRunner; private jumperClass: IJumper; constructor(runner: IRunner, jumper: IJumper) { this.runnerClass = new runner(); this.jumperClass = new jumper(); } run() { this.runnerClass.run(); } jump() { this.jumperClass.jump(); } }
咱們在RunnerAndJumper
的構造函數中聲明兩個依賴,一個是IRunner
類型,一個是IJumper
類型。
最終的代碼其實和依賴倒置原則中的例子很像,並且你會發現,RunnerAndJumper
類自己並無與任何別的類耦合在一塊兒,它的職能一樣是單一的,它是對一個即會跑又會跳的實體的抽象,而且這裏咱們還可使用DI(依賴注入)
技術進一步的優化咱們的代碼,下降它的耦合度。
開閉原則所帶來最有用的好處就是,當咱們在實現咱們的抽象層代碼時,咱們就能夠對將來可能須要做出改變的地方擁有一個比較完整的設想,這樣當咱們真正面臨改變時,咱們所對原有代碼的修改,更貼近於改變自己,而不是一味的修改咱們已有的抽象代碼。
在這種狀況下,因爲咱們節省了沒必要要的勞動和時間,咱們就能夠將更多的精力投入到關於更加長遠的事宜計劃上面,並且能夠針對這些事宜須要做出的改變,提早和團隊溝通,最終給予一套更加健壯、更符合系統模塊自己的解決方案。
在整個軟件開發週期中(好比一個敏捷開發週期),你對於整個週期中的事情瞭解的越透徹、越多,則越好。身爲一個工程師,在一個開發衝刺中,爲了在衝刺截止日期結束前,實現一個高效的、可靠的系統,你不會指望做出太多的改變,所以每每你可能會「偷工減料」。
從另外一個角度來說,咱們也應當致力於在每一次面臨需求變動的狀況下,不須要一而再,再而三的更改咱們已有的代碼。全部新的功能都應當經過增長一個新的組合類或方法實現,或者經過複用已有的代碼來實現。
充分貫徹開閉原則的另外一個例子,即是插件與中間件架構,咱們能夠從三個角度來簡單分析這種架構是如何運做的:
Chrome
。Redux
、express
還有不少框架都支持這樣的功能。但願這篇文章可以幫助你學會如何應用開閉原則而且從中收益。設計一個具備可組合性的系統,同時提供具備良好定義的擴展接口,是一種很是有用的技術,這種技術最關鍵的地方在於,它使咱們的系統可以在保持強健的同時,提供新功能、新特性,可是卻不會影響它當前的狀態。
開閉原則是面向對象編程中最重要的原則之一,有多重要呢?這麼說吧,不少的設計原則和設計模式所但願達成的最終狀態,每每符合開閉原則,所以許多原則均可以做爲實現開閉原則的一種手段,在原文的例子中,咱們能夠很明顯的體會到,在實現開閉原則所提倡的理念的過程當中,咱們不經意地使用以前兩篇文章中涉及的原則,好比:
我以前一直是作後端相關工做的,因此對於開閉原則接觸較早,這兩年轉行作了前端,隨着nodejs
的發展,框架技術突飛猛進,可是其中脫穎而出的優秀框架每每是充分貫徹了開閉原則,好比express
、webpack
還有狀態管理容器redux
,它們均是開閉原則的最佳實踐。
另一方面,在這兩年的工做也感覺到,適當的使用函數式編程的思想,每每是貫徹開閉原則一個比較好的開始,由於函數式的編程中的核心概念之一即是compose(組合)
。以函數式描述業務每每是原子級的指令,以後在須要描述更復雜的業務時,咱們複用並組合以前已經存在的指令以達到目的,這偏偏符合開閉原則所提倡的可組合性。
最後再分享一些前端工做中,常常須要使用開閉原則的最佳業務場景,
事件驅動模型:對於一些複雜的事件驅動模型,好比拖拽,每每使用開閉原則會達到意想不到的效果。最近有一個比較火的拖拽庫draggable,提供的拖拽體驗相比其餘同類型的庫簡直不是一個級別。我前段時間去讀它的源碼,發現它之因此強大,是由於在它內部,針對多種拖拽事件,封裝了獨立的事件發射器(其內部稱做Sensor
),以後根據這些發射器指定了一套獨立的抽象事件驅動模型,在這個模型基礎上,針對不一樣的業務場景提供不一樣的插件,好比:
能想到的大概就這麼多,但願能夠拋磚引玉,若有錯誤,還望指正。
關注公衆號 全棧101,只談技術,不談人生