本章咱們要講解的是S.O.L.I.D五大原則JavaScript語言實現的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。javascript
英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/
開閉原則的描述是:java
Subtypes must be substitutable for their base types.
派生類型必須能夠替換它的基類型。
在面向對象編程裏,繼承提供了一個機制讓子類和共享基類的代碼,這是經過在基類型裏封裝通用的數據和行爲來實現的,而後已經及類型來聲明更詳細的子類型,爲了應用里氏替換原則,繼承子類型須要在語義上等價於基類型裏的指望行爲。編程
爲了來更好的理解,請參考以下代碼:瀏覽器
function Vehicle(my) {
var my = my || {};
my.speed = 0;
my.running = false;
this.speed = function() {
return my.speed;
};
this.start = function() {
my.running = true;
};
this.stop = function() {
my.running = false;
};
this.accelerate = function() {
my.speed++;
};
this.decelerate = function() {
my.speed--;
}, this.state = function() {
if (!my.running) {
return "parked";
}
else if (my.running && my.speed) {
return "moving";
}
else if (my.running) {
return "idle";
}
};
}
上述代碼咱們定義了一個Vehicle函數,其構造函數爲vehicle對象提供了一些基本的操做,咱們來想一想若是當前函數當前正運行在服務客戶的產品環境上,若是如今須要添加一個新的構造函數來實現加快移動的vehicle。思考之後,咱們寫出了以下代碼:函數
function FastVehicle(my) {
var my = my || {};
var that = new Vehicle(my);
that.accelerate = function() {
my.speed += 3;
};
return that;
}
在瀏覽器的控制檯咱們都測試了,全部的功能都是咱們的預期,沒有問題,FastVehicle的速度增快了3倍,並且繼承他的方法也是按照咱們的預期工做。此後,咱們開始部署這個新版本的類庫到產品環境上,但是咱們卻接到了新的構造函數致使現有的代碼不能支持執行了,下面的代碼段揭示了這個問題:工具
var maneuver = function(vehicle) {
write(vehicle.state());
vehicle.start();
write(vehicle.state());
vehicle.accelerate();
write(vehicle.state());
write(vehicle.speed());
vehicle.decelerate();
write(vehicle.speed());
if (vehicle.state() != "idle") {
throw "The vehicle is still moving!";
}
vehicle.stop();
write(vehicle.state());
};
根據上面的代碼,咱們看到拋出的異常是「The vehicle is still moving!」,這是由於寫這段代碼的做者一直認爲加速(accelerate)和減速(decelerate)的數字是同樣的。但FastVehicle的代碼和Vehicle的代碼並非徹底可以替換掉的。所以,FastVehicle違反了里氏替換原則。 測試
在這點上,你可能會想:「但,客戶端不能老假定vehicle都是按照這樣的規則來作」,里氏替換原則(LSP)的妨礙(譯者注:就是妨礙實現LSP的代碼)不是基於咱們所想的繼承子類應該在行爲裏確保更新代碼,而是這樣的更新是否能在當前的指望中獲得實現。this
上述代碼這個case,解決這個不兼容的問題須要在vehicle類庫或者客戶端調用代碼上進行一點從新設計,或者二者都要改。設計
那麼,咱們如何避免LSP妨礙?不幸的話,並非一直都是能夠作到的。咱們這裏有幾個策略咱們處理這個事情。code
處理LSP過度妨礙的一個策略是使用契約,契約清單有2種形式:執行說明書(executable specifications)和錯誤處理,在執行說明書裏,一個詳細類庫的契約也包括一組自動化測試,而錯誤處理是在代碼裏直接處理的,例如在前置條件,後置條件,常量檢查等,能夠從Bertrand Miller的大做《契約設計》中查看這個技術。雖然自動化測試和契約設計不在本篇文字的範圍內,但當咱們用的時候我仍是推薦以下內容:
對於你本身要維護和實現的代碼,使用契約設計趨向於添加不少沒必要要的代碼,若是你要控制輸入,添加測試是很是有必要的,若是你是類庫做者,使用契約設計,你要注意不正確的使用方法以及讓你的用戶使之做爲一個測試工具。
避免LSP妨礙的另一個測試是:若是可能的話,儘可能不用繼承,在Gamma的大做《Design Patterns – Elements of Reusable Object-Orineted Software》中,咱們能夠看到以下建議:
Favor object composition over class inheritance
儘可能使用對象組合而不是類繼承
有些書裏討論了組合比繼承好的惟一做用是靜態類型,基於類的語言(例如,在運行時能夠改變行爲),與JavaScript相關的一個問題是耦合,當使用繼承的時候,繼承子類型和他們的基類型耦合在一塊兒了,就是說及類型的改變會影響到繼承子類型。組合傾向於對象更小化,更容易想靜態和動態語言語言維護。
到如今,咱們討論了和繼承上下文在內的里氏替換原則,指示出JavaScript的面向對象實。不過,里氏替換原則(LSP)的本質不是真的和繼承有關,而是行爲兼容性。JavaScript是一個動態語言,一個對象的契約行爲不是對象的類型決定的,而是對象指望的功能決定的。里氏替換原則的初始構想是做爲繼承的一個原則指南,等價於對象設計中的隱式接口。
舉例來講,讓咱們來看一下Robert C. Martin的大做《敏捷軟件開發 原則、模式與實踐》中的一個矩形類型:
考慮咱們有一個程序用到下面這樣的一個矩形對象:
var rectangle = {
length: 0,
width: 0
};
事後,程序有須要一個正方形,因爲正方形就是一個長(length)和寬(width)都同樣的特殊矩形,因此咱們以爲建立一個正方形代替矩形。咱們添加了length和width屬性來匹配矩形的聲明,但咱們以爲使用屬性的getters/setters通常咱們可讓length和width保存同步,確保聲明的是一個正方形:
var square = {};
(function() {
var length = 0, width = 0;
// 注意defineProperty方式是262-5版的新特性
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%兼容,但咱們並不老是這樣明確的暗示。解決這個問題,咱們能夠從新設計一個shape對象來實現程序,依據多邊形的概念,咱們聲明rectangle和square,relevant。無論怎麼說,咱們的目的是要說里氏替換原則並不僅是繼承,而是任何方法(其中的行爲能夠另外的行爲)。
里氏替換原則(LSP)表達的意思不是繼承的關係,而是任何方法(只要該方法的行爲能體會另外的行爲就行)。