標籤:javascript
前言
本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實現的第5篇,依賴倒置原則LSP(The Dependency Inversion Principle )。
英文原文:http://freshbrewedcode.com/derekgreer/2012/01/22/solid-javascript-the-dependency-inversion-principle/
依賴倒置原則
依賴倒置原則的描述是:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions. 高層模組不應該依賴於低層模組,二者都應該依賴於抽象B. Abstractions should not depend upon details. Details should depend upon abstractions. 抽象不應該依賴於細節,細節應該依賴於抽象
依賴倒置原則的最重要問題就是確保應用程式或架構的主要組件從非重要的底層組件實現細節解耦出來,這將確保程式的最重要的部分不會因為低層次組件的變化修改而受影響。
該原則的第一部分是關於高層模組和低層模組之間的耦合方式,在傳統的分成架構中,高層模組(封裝了程式的核心商務邏輯)總依賴於低層的一些模組(一些基礎點)。當應用依賴倒置原則的時候,關係就反過來了。和高層模組依賴於低層模組不同,依賴倒置是讓低層模組依賴於高層模組裡定義的介面。舉例來說,如果要給程式進行資料持久化,傳統的設計是核心模組依賴於一個持久化模組的API,而根據依賴倒置原則重構以後,則是核心模組需要定義持久化的API介面,然後持久化的實現執行個體需要實現核心模組定義的這個API介面。
該原則的第二部分描述的是抽象和細節之間的正確關係。理解這一部分,通過瞭解C++語言比較有協助,因為他的適用性比較明顯。
不像一些靜態類型的語言,C++沒有提供一個語言層級的概念來定義介面,那類定義和類實現之間到底是怎麼樣的呢,在C++裡,類通過標頭檔的形式來定義,其中定義了源檔案需要實現的類成員方法和變數。因為所有的變數和私人方法都定義在標頭檔裡,所以可以用來抽象以便和實現細節之前解耦出來。通過定只定義抽象方法來實現(C++裡是抽象基類)介面這個概念用於實作類別來實現。
DIP and JavaScript
因為JavaScript是動態語言,所以不需要去為瞭解耦而抽象。所以抽象不應依賴於細節這個改變在JavaScript裡沒有太大的影響,但高層模組不應依賴於低層模組卻有很大的影響。
在當靜態類型語言的上下文裡討論依賴倒置原則的時候,耦合的概念包括語義(semantic)和物理(physical)兩種。這就是說,如果一個高層模組依賴於一個低層模組,也就是不僅耦合了語義介面,也耦合了在底層模組裡定義的物理介面。也就是說高層模組不僅要從第三方類庫解耦出來,也需要從原生的低層模組裡解耦出來。
為瞭解釋這一點,想象一個.NET程式可能包含一個非常有用的高層模組,而該模組依賴於一個低層的持久化模組。當作者需要在持久化API裡增加一個類似的介面的時候,不管依賴倒置原則有沒有使用,高層模組在不重新實現這個低層模組的新介面之前是沒有辦法在其它的程式裡得到重用的。
在JavaScript裡,依賴倒置原則的適用性僅僅限於高層模組和低層模組之間的語義耦合,比如,DIP可以根據需要去增加介面而不是耦合低層模組定義的隱式介面。
為了來理解這個,我們看一下如下例子:
$.fn.trackMap = function(options) { var defaults = { /* defaults */ }; options = $.extend({}, defaults, options); var mapOptions = { center: new google.maps.LatLng(options.latitude,options.longitude), zoom: 12, mapTypeId: google.maps.MapTypeId.ROADMAP }, map = new google.maps.Map(this[0], mapOptions), pos = new google.maps.LatLng(options.latitude,options.longitude); var marker = new google.maps.Marker({ position: pos, title: options.title, icon: options.icon }); marker.setMap(map); options.feed.update(function(latitude, longitude) { marker.setMap(null); var newLatLng = new google.maps.LatLng(latitude, longitude); marker.position = newLatLng; marker.setMap(map); map.setCenter(newLatLng); }); return this;};var updater = (function() { // private properties return { update: function(callback) { updateMap = callback; } };})();$("#map_canvas").trackMap({ latitude: 35.044640193770725, longitude: -89.98193264007568, icon: ‘http://bit.ly/zjnGDe‘, title: ‘Tracking Number: 12345‘, feed: updater});
在上述代碼裡,有個小型的JS類庫將一個DIV轉化成Map以便顯示當前跟蹤的位置資訊。trackMap函數有2個依賴:第三方的Google Maps API和Location feed。該feed對象的職責是當icon位置更新的時候調用一個callback回調(在初始化的時候提供的)並且傳入緯度latitude和精度longitude。Google Maps API是用來渲染介面的。
feed對象的介面可能按照裝,也可能沒有照裝trackMap函數的要求去設計,事實上,他的角色很簡單,著重在簡單的不同實現,不需要和Google Maps這麼依賴。介於trackMap語義上耦合了Google Maps API,如果需要切換不同的地圖供應商的話那就不得不對trackMap函數進行重寫以便可以適配不同的provider。
為了將於Google maps類庫的語義耦合翻轉過來,我們需要重寫設計trackMap函數,以便對一個隱式介面(抽象出地圖供應商provider的介面)進行語義耦合,我們還需要一個適配Google Maps API的一個實現對象,如下是重構後的trackMap函數:
$.fn.trackMap = function(options) { var defaults = { /* defaults */ }; options = $.extend({}, defaults, options); options.provider.showMap( this[0], options.latitude, options.longitude, options.icon, options.title); options.feed.update(function(latitude, longitude) { options.provider.updateMap(latitude, longitude); }); return this;};$("#map_canvas").trackMap({ latitude: 35.044640193770725, longitude: -89.98193264007568, icon: ‘http://bit.ly/zjnGDe‘, title: ‘Tracking Number: 12345‘, feed: updater, provider: trackMap.googleMapsProvider});
在該版本裡,我們重新設計了trackMap函數以及需要的一個地圖供應商介面,然後將實現的細節挪到了一個單獨的googleMapsProvider組件,該組件可能獨立封裝成一個單獨的JavaScript模組。如下是我的googleMapsProvider實現:
trackMap.googleMapsProvider = (function() { var marker, map; return { showMap: function(element, latitude, longitude, icon, title) { var mapOptions = { center: new google.maps.LatLng(latitude, longitude), zoom: 12, mapTypeId: google.maps.MapTypeId.ROADMAP }, pos = new google.maps.LatLng(latitude, longitude); map = new google.maps.Map(element, mapOptions); marker = new google.maps.Marker({ position: pos, title: title, icon: icon }); marker.setMap(map); }, updateMap: function(latitude, longitude) { marker.setMap(null); var newLatLng = new google.maps.LatLng(latitude,longitude); marker.position = newLatLng; marker.setMap(map); map.setCenter(newLatLng); } };})();
做了上述這些改變以後,trackMap函數將變得非常有彈性了,不必依賴於Google Maps API,相反可以任意替換其它的地圖供應商,那就是說可以按照程式的需求去適配任何地圖供應商。
何時依賴注入?
有點不太相關,其實依賴注入的概念經常和依賴倒置原則混在一起,為了澄清這個不同,我們有必要來解釋一下:
依賴注入是控制反轉的一個特殊形式,反轉的意思一個組件如何擷取它的依賴。依賴注入的意思就是:依賴提供給組件,而不是組件去擷取依賴,意思是建立一個依賴的執行個體,通過工廠去請求這個依賴,通過Service Locator或組件自身的初始化去請求這個依賴。依賴倒置原則和依賴注入都是關注依賴,並且都是用於反轉。不過,依賴倒置原則沒有關注組件如何擷取依賴,而是只關注高層模組如何從低層模組裡解耦出來。某種意義上說,依賴倒置原則是控制反轉的另外一種形式,這裡反轉的是哪個模組定義介面(從低層裡定義,反轉到高層裡定義)。
總結
這是五大原則的最後一篇了,在這5篇文字裡我們看到了SOLID如何在JavaScript裡實現的,不同的原則在JavaScript裡通過不同的角度來說明的。(大叔註:其實大叔覺得雖然是有點不倫不類,但從另外一個層面上說,大體的原則在各種語言上其實還是一樣的。)
著作權聲明:本文為博主http://www.zuiniusn.com原創文章,未經博主允許不得轉載。
深入理解JavaScript系列(22):S.O.L.I.D五大原則之依賴倒置原則DIP