深入理解JavaScript系列(8) S.O.L.I.D五大原則之里氏替換原則LSP
更新時(shí)間:2012年01月15日 23:27:39 作者:
本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實(shí)現(xiàn)的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。
前言
本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實(shí)現(xiàn)的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。
英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/
復(fù)制代碼
開閉原則的描述是:
Subtypes must be substitutable for their base types.
派生類型必須可以替換它的基類型。
復(fù)制代碼
在面向?qū)ο缶幊汤?,繼承提供了一個(gè)機(jī)制讓子類和共享基類的代碼,這是通過在基類型里封裝通用的數(shù)據(jù)和行為來實(shí)現(xiàn)的,然后已經(jīng)及類型來聲明更詳細(xì)的子類型,為了應(yīng)用里氏替換原則,繼承子類型需要在語義上等價(jià)于基類型里的期望行為。
為了來更好的理解,請(qǐng)參考如下代碼:
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";
}
};
}
上述代碼我們定義了一個(gè)Vehicle函數(shù),其構(gòu)造函數(shù)為vehicle對(duì)象提供了一些基本的操作,我們來想想如果當(dāng)前函數(shù)當(dāng)前正運(yùn)行在服務(wù)客戶的產(chǎn)品環(huán)境上,如果現(xiàn)在需要添加一個(gè)新的構(gòu)造函數(shù)來實(shí)現(xiàn)加快移動(dòng)的vehicle。思考以后,我們寫出了如下代碼:
function FastVehicle(my) {
var my = my || {};
var that = new Vehicle(my);
that.accelerate = function() {
my.speed += 3;
};
return that;
}
在瀏覽器的控制臺(tái)我們都測(cè)試了,所有的功能都是我們的預(yù)期,沒有問題,F(xiàn)astVehicle的速度增快了3倍,而且繼承他的方法也是按照我們的預(yù)期工作。此后,我們開始部署這個(gè)新版本的類庫到產(chǎn)品環(huán)境上,可是我們卻接到了新的構(gòu)造函數(shù)導(dǎo)致現(xiàn)有的代碼不能支持執(zhí)行了,下面的代碼段揭示了這個(gè)問題:
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());
};
根據(jù)上面的代碼,我們看到拋出的異常是“The vehicle is still moving!”,這是因?yàn)閷戇@段代碼的作者一直認(rèn)為加速(accelerate)和減速(decelerate)的數(shù)字是一樣的。但FastVehicle的代碼和Vehicle的代碼并不是完全能夠替換掉的。因此,F(xiàn)astVehicle違反了里氏替換原則。
在這點(diǎn)上,你可能會(huì)想:“但,客戶端不能老假定vehicle都是按照這樣的規(guī)則來做”,里氏替換原則(LSP)的妨礙(譯者注:就是妨礙實(shí)現(xiàn)LSP的代碼)不是基于我們所想的繼承子類應(yīng)該在行為里確保更新代碼,而是這樣的更新是否能在當(dāng)前的期望中得到實(shí)現(xiàn)。
上述代碼這個(gè)case,解決這個(gè)不兼容的問題需要在vehicle類庫或者客戶端調(diào)用代碼上進(jìn)行一點(diǎn)重新設(shè)計(jì),或者兩者都要改。
減少LSP妨礙
那么,我們?nèi)绾伪苊釲SP妨礙?不幸的話,并不是一直都是可以做到的。我們這里有幾個(gè)策略我們處理這個(gè)事情。
契約(Contracts)
處理LSP過分妨礙的一個(gè)策略是使用契約,契約清單有2種形式:執(zhí)行說明書(executable specifications)和錯(cuò)誤處理,在執(zhí)行說明書里,一個(gè)詳細(xì)類庫的契約也包括一組自動(dòng)化測(cè)試,而錯(cuò)誤處理是在代碼里直接處理的,例如在前置條件,后置條件,常量檢查等,可以從Bertrand Miller的大作《契約設(shè)計(jì)》中查看這個(gè)技術(shù)。雖然自動(dòng)化測(cè)試和契約設(shè)計(jì)不在本篇文字的范圍內(nèi),但當(dāng)我們用的時(shí)候我還是推薦如下內(nèi)容:
檢查使用測(cè)試驅(qū)動(dòng)開發(fā)(Test-Driven Development)來指導(dǎo)你代碼的設(shè)計(jì)
設(shè)計(jì)可重用類庫的時(shí)候可隨意使用契約設(shè)計(jì)技術(shù)
對(duì)于你自己要維護(hù)和實(shí)現(xiàn)的代碼,使用契約設(shè)計(jì)趨向于添加很多不必要的代碼,如果你要控制輸入,添加測(cè)試是非常有必要的,如果你是類庫作者,使用契約設(shè)計(jì),你要注意不正確的使用方法以及讓你的用戶使之作為一個(gè)測(cè)試工具。
避免繼承
避免LSP妨礙的另外一個(gè)測(cè)試是:如果可能的話,盡量不用繼承,在Gamma的大作《Design Patterns – Elements of Reusable Object-Orineted Software》中,我們可以看到如下建議:
Favor object composition over class inheritance
盡量使用對(duì)象組合而不是類繼承
復(fù)制代碼
有些書里討論了組合比繼承好的唯一作用是靜態(tài)類型,基于類的語言(例如,在運(yùn)行時(shí)可以改變行為),與JavaScript相關(guān)的一個(gè)問題是耦合,當(dāng)使用繼承的時(shí)候,繼承子類型和他們的基類型耦合在一起了,就是說及類型的改變會(huì)影響到繼承子類型。組合傾向于對(duì)象更小化,更容易想靜態(tài)和動(dòng)態(tài)語言語言維護(hù)。
與行為有關(guān),而不是繼承
到現(xiàn)在,我們討論了和繼承上下文在內(nèi)的里氏替換原則,指示出JavaScript的面向?qū)ο髮?shí)。不過,里氏替換原則(LSP)的本質(zhì)不是真的和繼承有關(guān),而是行為兼容性。JavaScript是一個(gè)動(dòng)態(tài)語言,一個(gè)對(duì)象的契約行為不是對(duì)象的類型決定的,而是對(duì)象期望的功能決定的。里氏替換原則的初始構(gòu)想是作為繼承的一個(gè)原則指南,等價(jià)于對(duì)象設(shè)計(jì)中的隱式接口。
舉例來說,讓我們來看一下Robert C. Martin的大作《敏捷軟件開發(fā) 原則、模式與實(shí)踐》中的一個(gè)矩形類型:
矩形例子
考慮我們有一個(gè)程序用到下面這樣的一個(gè)矩形對(duì)象:
var rectangle = {
length: 0,
width: 0
};
[code]
過后,程序有需要一個(gè)正方形,由于正方形就是一個(gè)長(zhǎng)(length)和寬(width)都一樣的特殊矩形,所以我們覺得創(chuàng)建一個(gè)正方形代替矩形。我們添加了length和width屬性來匹配矩形的聲明,但我們覺得使用屬性的getters/setters一般我們可以讓length和width保存同步,確保聲明的是一個(gè)正方形:
[code]
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; }
});
})();
不幸的是,當(dāng)我們使用正方形代替矩形執(zhí)行代碼的時(shí)候發(fā)現(xiàn)了問題,其中一個(gè)計(jì)算矩形面積的方法如下:
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
該方法在調(diào)用的時(shí)候,結(jié)果是16,而不是期望的12,我們的正方形square對(duì)象違反了LSP原則,square的長(zhǎng)度和寬度屬性暗示著并不是和矩形100%兼容,但我們并不總是這樣明確的暗示。解決這個(gè)問題,我們可以重新設(shè)計(jì)一個(gè)shape對(duì)象來實(shí)現(xiàn)程序,依據(jù)多邊形的概念,我們聲明rectangle和square,relevant。不管怎么說,我們的目的是要說里氏替換原則并不只是繼承,而是任何方法(其中的行為可以另外的行為)。
總結(jié)
里氏替換原則(LSP)表達(dá)的意思不是繼承的關(guān)系,而是任何方法(只要該方法的行為能體會(huì)另外的行為就行)。
本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實(shí)現(xiàn)的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。
英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/
復(fù)制代碼
開閉原則的描述是:
Subtypes must be substitutable for their base types.
派生類型必須可以替換它的基類型。
復(fù)制代碼
在面向?qū)ο缶幊汤?,繼承提供了一個(gè)機(jī)制讓子類和共享基類的代碼,這是通過在基類型里封裝通用的數(shù)據(jù)和行為來實(shí)現(xiàn)的,然后已經(jīng)及類型來聲明更詳細(xì)的子類型,為了應(yīng)用里氏替換原則,繼承子類型需要在語義上等價(jià)于基類型里的期望行為。
為了來更好的理解,請(qǐng)參考如下代碼:
復(fù)制代碼 代碼如下:
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";
}
};
}
上述代碼我們定義了一個(gè)Vehicle函數(shù),其構(gòu)造函數(shù)為vehicle對(duì)象提供了一些基本的操作,我們來想想如果當(dāng)前函數(shù)當(dāng)前正運(yùn)行在服務(wù)客戶的產(chǎn)品環(huán)境上,如果現(xiàn)在需要添加一個(gè)新的構(gòu)造函數(shù)來實(shí)現(xiàn)加快移動(dòng)的vehicle。思考以后,我們寫出了如下代碼:
復(fù)制代碼 代碼如下:
function FastVehicle(my) {
var my = my || {};
var that = new Vehicle(my);
that.accelerate = function() {
my.speed += 3;
};
return that;
}
在瀏覽器的控制臺(tái)我們都測(cè)試了,所有的功能都是我們的預(yù)期,沒有問題,F(xiàn)astVehicle的速度增快了3倍,而且繼承他的方法也是按照我們的預(yù)期工作。此后,我們開始部署這個(gè)新版本的類庫到產(chǎn)品環(huán)境上,可是我們卻接到了新的構(gòu)造函數(shù)導(dǎo)致現(xiàn)有的代碼不能支持執(zhí)行了,下面的代碼段揭示了這個(gè)問題:
復(fù)制代碼 代碼如下:
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());
};
根據(jù)上面的代碼,我們看到拋出的異常是“The vehicle is still moving!”,這是因?yàn)閷戇@段代碼的作者一直認(rèn)為加速(accelerate)和減速(decelerate)的數(shù)字是一樣的。但FastVehicle的代碼和Vehicle的代碼并不是完全能夠替換掉的。因此,F(xiàn)astVehicle違反了里氏替換原則。
在這點(diǎn)上,你可能會(huì)想:“但,客戶端不能老假定vehicle都是按照這樣的規(guī)則來做”,里氏替換原則(LSP)的妨礙(譯者注:就是妨礙實(shí)現(xiàn)LSP的代碼)不是基于我們所想的繼承子類應(yīng)該在行為里確保更新代碼,而是這樣的更新是否能在當(dāng)前的期望中得到實(shí)現(xiàn)。
上述代碼這個(gè)case,解決這個(gè)不兼容的問題需要在vehicle類庫或者客戶端調(diào)用代碼上進(jìn)行一點(diǎn)重新設(shè)計(jì),或者兩者都要改。
減少LSP妨礙
那么,我們?nèi)绾伪苊釲SP妨礙?不幸的話,并不是一直都是可以做到的。我們這里有幾個(gè)策略我們處理這個(gè)事情。
契約(Contracts)
處理LSP過分妨礙的一個(gè)策略是使用契約,契約清單有2種形式:執(zhí)行說明書(executable specifications)和錯(cuò)誤處理,在執(zhí)行說明書里,一個(gè)詳細(xì)類庫的契約也包括一組自動(dòng)化測(cè)試,而錯(cuò)誤處理是在代碼里直接處理的,例如在前置條件,后置條件,常量檢查等,可以從Bertrand Miller的大作《契約設(shè)計(jì)》中查看這個(gè)技術(shù)。雖然自動(dòng)化測(cè)試和契約設(shè)計(jì)不在本篇文字的范圍內(nèi),但當(dāng)我們用的時(shí)候我還是推薦如下內(nèi)容:
檢查使用測(cè)試驅(qū)動(dòng)開發(fā)(Test-Driven Development)來指導(dǎo)你代碼的設(shè)計(jì)
設(shè)計(jì)可重用類庫的時(shí)候可隨意使用契約設(shè)計(jì)技術(shù)
對(duì)于你自己要維護(hù)和實(shí)現(xiàn)的代碼,使用契約設(shè)計(jì)趨向于添加很多不必要的代碼,如果你要控制輸入,添加測(cè)試是非常有必要的,如果你是類庫作者,使用契約設(shè)計(jì),你要注意不正確的使用方法以及讓你的用戶使之作為一個(gè)測(cè)試工具。
避免繼承
避免LSP妨礙的另外一個(gè)測(cè)試是:如果可能的話,盡量不用繼承,在Gamma的大作《Design Patterns – Elements of Reusable Object-Orineted Software》中,我們可以看到如下建議:
Favor object composition over class inheritance
盡量使用對(duì)象組合而不是類繼承
復(fù)制代碼
有些書里討論了組合比繼承好的唯一作用是靜態(tài)類型,基于類的語言(例如,在運(yùn)行時(shí)可以改變行為),與JavaScript相關(guān)的一個(gè)問題是耦合,當(dāng)使用繼承的時(shí)候,繼承子類型和他們的基類型耦合在一起了,就是說及類型的改變會(huì)影響到繼承子類型。組合傾向于對(duì)象更小化,更容易想靜態(tài)和動(dòng)態(tài)語言語言維護(hù)。
與行為有關(guān),而不是繼承
到現(xiàn)在,我們討論了和繼承上下文在內(nèi)的里氏替換原則,指示出JavaScript的面向?qū)ο髮?shí)。不過,里氏替換原則(LSP)的本質(zhì)不是真的和繼承有關(guān),而是行為兼容性。JavaScript是一個(gè)動(dòng)態(tài)語言,一個(gè)對(duì)象的契約行為不是對(duì)象的類型決定的,而是對(duì)象期望的功能決定的。里氏替換原則的初始構(gòu)想是作為繼承的一個(gè)原則指南,等價(jià)于對(duì)象設(shè)計(jì)中的隱式接口。
舉例來說,讓我們來看一下Robert C. Martin的大作《敏捷軟件開發(fā) 原則、模式與實(shí)踐》中的一個(gè)矩形類型:
矩形例子
考慮我們有一個(gè)程序用到下面這樣的一個(gè)矩形對(duì)象:
復(fù)制代碼 代碼如下:
var rectangle = {
length: 0,
width: 0
};
[code]
過后,程序有需要一個(gè)正方形,由于正方形就是一個(gè)長(zhǎng)(length)和寬(width)都一樣的特殊矩形,所以我們覺得創(chuàng)建一個(gè)正方形代替矩形。我們添加了length和width屬性來匹配矩形的聲明,但我們覺得使用屬性的getters/setters一般我們可以讓length和width保存同步,確保聲明的是一個(gè)正方形:
[code]
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; }
});
})();
不幸的是,當(dāng)我們使用正方形代替矩形執(zhí)行代碼的時(shí)候發(fā)現(xiàn)了問題,其中一個(gè)計(jì)算矩形面積的方法如下:
復(fù)制代碼 代碼如下:
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
該方法在調(diào)用的時(shí)候,結(jié)果是16,而不是期望的12,我們的正方形square對(duì)象違反了LSP原則,square的長(zhǎng)度和寬度屬性暗示著并不是和矩形100%兼容,但我們并不總是這樣明確的暗示。解決這個(gè)問題,我們可以重新設(shè)計(jì)一個(gè)shape對(duì)象來實(shí)現(xiàn)程序,依據(jù)多邊形的概念,我們聲明rectangle和square,relevant。不管怎么說,我們的目的是要說里氏替換原則并不只是繼承,而是任何方法(其中的行為可以另外的行為)。
總結(jié)
里氏替換原則(LSP)表達(dá)的意思不是繼承的關(guān)系,而是任何方法(只要該方法的行為能體會(huì)另外的行為就行)。
您可能感興趣的文章:
- 深入理解JavaScript系列(6):S.O.L.I.D五大原則之單一職責(zé)SRP
- JavaScript最少知識(shí)原則介紹與體現(xiàn)
- JavaScript面向?qū)ο笾叽蠡驹瓌t實(shí)例詳解
- JavaScript的數(shù)據(jù)類型轉(zhuǎn)換原則(干貨)
- 深入淺析JavaScript的API設(shè)計(jì)原則
- 深入理解JavaScript系列(22):S.O.L.I.D五大原則之依賴倒置原則DIP詳解
- 深入理解JavaScript系列(21):S.O.L.I.D五大原則之接口隔離原則ISP詳解
- 深入理解JavaScript系列(7) S.O.L.I.D五大原則之開閉原則OCP
- JavaScript單一職責(zé)原則深入分析
相關(guān)文章
BootStrap 下拉菜單點(diǎn)擊之后不會(huì)出現(xiàn)下拉菜單(下拉菜單不彈出)的解決方案
最近學(xué)到Bootstrap下拉菜單,學(xué)懂了教程內(nèi)容之后自己敲一個(gè)點(diǎn)擊按鈕底下彈出下拉菜單的小demo,寫完代碼發(fā)現(xiàn)運(yùn)行之后點(diǎn)擊按鈕沒反應(yīng),下拉菜單彈不出來,下面給大家分享下解決方案2016-12-12javaScript的函數(shù)對(duì)象的聲明詳解
這篇文章主要介紹了javaScript的函數(shù)對(duì)象的聲明詳解,需要的朋友可以參考下2015-02-02Ajax,UTF-8還是GB2312 eval 還是execScript
討厭的東西。 關(guān)于Ajax獲取HTML內(nèi)容編碼,與JavaScript載入腳本的動(dòng)態(tài)執(zhí)行問題。2008-11-11ES6的函數(shù)rest參數(shù)使用小結(jié)
這篇文章主要介紹了ES6的函數(shù)rest參數(shù)用法,通過一個(gè)rest參數(shù)代替arguments變量的例子,對(duì)ES6?rest參數(shù)用法講解的非常詳細(xì),需要的朋友可以參考下2023-08-08JS小功能(onmouseover實(shí)現(xiàn)選擇月份)實(shí)例代碼
這篇文章主要介紹了onmouseover實(shí)現(xiàn)選擇月份實(shí)例代碼,有需要的朋友可以參考一下2013-11-11淺析javascript中函數(shù)聲明和函數(shù)表達(dá)式的區(qū)別
這篇文章主要介紹了淺析javascript中函數(shù)聲明和函數(shù)表達(dá)式的區(qū)別,需要的朋友可以參考下2015-02-02一個(gè)簡(jiǎn)單的JS時(shí)間控件示例代碼(JS時(shí)分秒時(shí)間控件)
這篇文章主要介紹了一個(gè)簡(jiǎn)單的JS時(shí)間控件示例代碼(JS時(shí)分秒時(shí)間控件)。需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-11-11