設計原則(SOLID原則)
在程式設計領域, SOLID(單一功能、開閉原則、里氏替換、介面隔離以及依賴反轉)是由羅伯特·C·馬丁在21世紀早期引入,指代了物件導向程式設計和麵向物件設計的五個基本原則。當這些原則被一起應用時,它們使得一個程式設計師開發一個容易進行軟體維護和擴充套件的系統變得更加可能。
1. 單一職責原則(SRP)
就一個類而言,應該僅有一個引起它變化的原因。在JavaScript中,需要用到類的場景並不太多,單一職責原則更多地是被運用在物件或者方法級別上。
如果我們有兩個動機去改寫一個方法,那麼這個方法就具有兩個職責。每個職責都是變化的一個軸線,如果一個方法承擔了過多的職責,那麼在需求的變遷過程中,需要改寫這個方法的可能性就越大。
簡單說就是:一個物件(方法)只做一件事。
1.1 優缺點
- 優點: 降低了單個類或物件複雜度,按照職責把物件劃分為更小粒度,有利於程式碼複用和單元測試。
- 缺點: 增加編寫程式碼複雜度,物件劃分為更小粒度,物件直接的聯絡也變得更復雜。
2. 開放-封閉原則(OCP)
開放-封閉原則最早由Eiffel語言的設計者Bertrand Meyer在其著作Object-Oriented Software Construction中提出。它的定義如下:
軟體實體(類、模組、函式)等應該對擴充套件開放,但是修改封閉。
擴充window.onload函式
Function.prototype.after = function(afterfn) {
var _self = this;
return function() {
var ret = _slef.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
}
window.onload = (window.onload || function() {}).after(function(){
consolo.log("擴充的程式碼")
})
通過動態裝飾函式的方式,直接擴充了新的函式,而不是直接修改之前的onload相關程式碼。
3. 里氏替換原則(The Liskov Substitution Principle,LSP)
定義如下:
子類的設計要保證在替換父類的時候,不改變原有程式的邏輯以及不破壞原有程式
的正確性
矩形例子:
// 考慮我們有一個程式用到下面這樣的一個矩形物件:
var rectangle = {
length: 0,
width: 0
};
// 過後,程式有需要一個正方形,由於正方形就是一個長(length)和寬(width)都一樣的特殊矩形,所以我們覺得建立一個正方形代替矩形。
var square = {};
(function() {
var length = 0, width = 0;
Object.defineProperty(square, "length", {
get: function() { return length; },
set: function(value) { length = width = value; }
});
Object.defineProperty(square, "width", {
get: function() { return width; },
set: function(value) { length = width = value; }
});
})();
不幸的是,當我們使用正方形代替矩形執行程式碼的時候發現了問題,其中一個計算矩形面積的方法如下:
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
該方法在呼叫的時候,結果是16,而不是期望的12,我們的正方形square物件違反了LSP原則,square的長度和寬度屬性暗示著並不是和矩形100%相容,但我們並不總是這樣明確的暗示。
里氏替換原則(LSP)表達的意思不是繼承的關係,而是任何方法(只要該方法的行為能體會另外的行為就行)。
4. 介面隔離原則(ISP)
定義:
Clients should not be forced to depend on methods they do not use.
客戶端不應該依賴它不需要的介面。用多個細粒度的介面來替代由多個方法組成的複雜介面,每個介面服務於一個子模組
類A通過介面interface依賴類C,類B通過介面interface依賴類D,如果介面interface對於類A和類B來說不是最小介面(胖介面),則類C和類D必須去實現他們不需要的方法。
簡單說就,建立單一專業介面,按照功能職責細化介面,介面中的方法儘量少。
例子:
var rectangle = {
area: function() {
/* 程式碼 */
},
draw: function() {
/* 程式碼 */
}
};
var geometryApplication = {
getLargestRectangle: function(rectangles) {
/* 程式碼 */
}
};
var drawingApplication = {
drawRectangles: function(rectangles) {
/* 程式碼 */
}
};
當一個rectangle替代品為了滿足新物件geometryApplication的getLargestRectangle 的時候,它僅僅需要rectangle的area()方法,但它卻違反了LSP(因為他根本用不到其中drawRectangles方法才能用到的draw方法)。
5. 依賴倒置原則(Dependence Inversion Principle, DIP)
高層模組不應該依賴底層模組,二者都該依賴其抽象。抽象不應該依賴細節,細節應該依賴抽象。
依賴倒置原則的最重要問題就是確保應用程式或框架的主要元件從非重要的底層元件實現細節解耦出來,這將確保程式的最重要的部分不會因為低層次元件的變化修改而受影響。
在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,相反可以任意替換其它的地圖提供商,那就是說可以按照程式的需求去適配任何地圖提供商。