面向對象之七大基本原則(JavaScript)
面向對象編程有自己的特性與原則,如果對于面向對象有一些了解的話,面向對象三大特征,封裝、繼承、多態,如果對面向對這三個概念不太了解,請參考面向對象之三個基本特征(JavaScript)
單一職責
如果我們在編寫程序的時候,一類或者一個方法里面包含了太多方法,對于代碼的可讀性來說,無非是一場災難,對于我們來說。所以為了解決這個問題,出現了單一職責。
什么是單一職責
單一職責:又稱單一功能原則,面向對象五個基本原則(SOLID)之一。它規定一個類應該只有一個發生變化的原因。(節選自百度百科)
按照上面說的,就是對一個類而言,應該僅有一個引起它變化的原因。換句話說,一個類的功能要單一,只做與它相關的事情。在類的設計過程中要按職責進行設計,彼此保持正交,互不干涉。
單一職責的好處
- 類的復雜性降低,實現什么職責都有清晰明確的定義
- 可讀性提高,復雜性降低,那當然可讀性提高了
- 可維護性提高,可讀性提高,那當然更容易維護了
- 變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,這對系統的擴展性、維護性都有非常大的幫助。
實例
- class ShoppinCar {
- constructor(){
- this.goods = [];
- }
- addGoods(good){
- this.goods = [good];
- }
- getGoodsList(){
- return this.goods;
- }
- }
- class Settlement {
- constructor(){
- this.result = 0;
- }
- calculatePrice(list,key){
- let allPrice = 0;
- list.forEach((el) => {
- allPrice += el[key];
- })
- this.result = allPrice;
- }
- getAllPrice(){
- return this.result;
- }
- }
用上面的代碼來說ShoppinCar類存在兩個方法addGoods和getGoodsList,分別是添加商品和獲取商品列表。Settlement類中存在兩個方法calculatePrice和getAllPrice分別做的事情是計算價錢與獲取總價錢。ShoppinCar與Settlement都是在做自己的事情。添加商品與計算價格,雖然在業務上是相互依賴的,但是在代碼中分別用兩個類,然他們自己做自己的事情。其中任何一個類更改不會對另一個類進行更改。
開閉原則
在一個類中暴露出去的方法,若這個方法變更了,則會產生很大的后果,可能導致其他依賴于這個方法且有不需要變更的業務造成大面積癱瘓。為了解決這個問題,可以單獨再寫一個方法,若這個方法與這個類中的其他方法相互依賴。
解決辦法:
- 把其中依賴的代碼copy一份到新的類中。
- 在新類中引用舊類中的方法。
兩種方法都不是最好的解決方案。
第一種方法會導致代碼大量的重復,第二種方法會導致類與類之間互相依賴。
什么是開閉原則
開閉原則:“軟件中的對象(類,模塊,函數等等)應該對于擴展是開放的,但是對于修改是封閉的”,這意味著一個實體是允許在不改變它的源代碼的前提下變更它的行為。(節選自百度百科)
開閉原則對擴展開放,對修改關閉,并不意味著不做任何修改,底層模塊的變更,必然要有高層模塊進行耦合,否則就是一個孤立無意義的代碼片段。開閉原則是一個最基本的原則,另外六個原則都是開閉原則的具體形態,是指導設計的工具和方法,而開閉原則才是精神領袖.
開閉原則好處
- 開閉原則有利于進行單元測試
- 開閉原則可以提高復用性
- 開閉原則可以提高可維護性
- 面向對象開發的要求
實例
- class Drag {
- down(){
- // ...
- }
- move(){
- // ...
- // 對拖拽沒有做任何限制可以隨意拖拽
- }
- up(){
- // ...
- }
- }
- class LimitDrag extends Drag {
- move(){
- // ...
- // 重寫該方法對拖拽進行限制處理
- }
- }
在LimitDrag中重寫了move方法,若修改了可以滿足兩種需求,一種是限制型拖拽,一種是不限制型拖拽,任何一個更改了另外一個還是可以正常運行。
里氏替換
每個開發人員在使用別人的組件時,只需知道組件的對外裸露的接口,那就是它全部行為的集合,至于內部到底是怎么實現的,無法知道,也無須知道。所以,對于使用者而言,它只能通過接口實現自己的預期,如果組件接口提供的行為與使用者的預期不符,錯誤便產生了。里氏替換原則就是在設計時避免出現派生類與基類不一致的行為。
什么是里氏替換
里氏替換原則:OCP作為OO的高層原則,主張使用“抽象(Abstraction)”和“多態(Polymorphism)”將設計中的靜態結構改為動態結構,維持設計的封閉性。“抽象”是語言提供的功能。“多態”由繼承語義實現。(節選自百度百科)
里氏替換好處
- 代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性
- 提高代碼的重用性
- 子類可以形似父類,但是又異于父類。
- 提高代碼的可擴展性,實現父類的方法就可以了。許多開源框架的擴展接口都是通過繼承父類來完成。
- 提高產品或項目的開放性
實例
- // 抽象槍類
- class AbstractGun {
- shoot(){
- throw "Abstract methods cannot be called";
- }
- }
- // 步槍
- class Rifle extends AbstractGun {
- shoot(){
- console.log("步槍射擊...");
- }
- }
- // 狙擊槍
- class AUG extends Rifle {
- zoomOut(){
- console.log("通過放大鏡觀察");
- }
- shoot(){
- console.log("AUG射擊...");
- }
- }
- // 士兵
- class Soldier {
- constructor(){
- this.gun = null;
- }
- setGun(gun){
- this.gun = gun;
- }
- killEnemy(){
- if(!this.gun){
- throw "需要給我一把槍";
- return;
- }
- console.log("士兵開始射擊...");
- this.gun.shoot();
- }
- }
- // 狙擊手
- class Snipper extends Soldier {
- killEnemy(aug){
- if(!this.gun){
- throw "需要給我一把槍";
- return;
- }
- this.gun.zoomOut();
- this.gun.shoot();
- }
- }
- let soldier = new Soldier();
- soldier.setGun(new Rifle());
- soldier.killEnemy();
- let snipper = new Snipper();
- // 分配狙擊槍
- snipper.setGun(new AUG());
- snipper.killEnemy();
- snipper.setGun(new Rifle());
- // snipper.killEnemy(); // this.gun.zoomOut is not a function
從上述代碼中可以看出,子類和父類之間關系,子類方法一定是等于或大于父類的方法。子類能夠出現的父類不一定能出現,但是父類出現的地方子類一定能夠出現。
依賴倒置
如果方法與方法之間或類與類之間,存在太多的依賴關系會導致代碼可讀性以及可維護性很差。依賴倒置原則能夠很好的解決這些問題。
什么是依賴倒置
依賴倒置原則:程序要依賴于抽象接口,不要依賴于具體實現。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。(節選自百度百科)
- 高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象
- 抽象不應該依賴細節
- 細節應該依賴抽象
依賴倒置好處
- 通過依賴于接口,隔離了具體實現類
- 低一層的變動并不會導致高一層的變動
- 提高了代碼的容錯性、擴展性和易于維護
實例
- // 抽象槍類
- class AbstractGun {
- shoot(){
- throw "Abstract methods cannot be called";
- }
- }
- // 步槍
- class Rifle extends AbstractGun {
- shoot(){
- console.log("步槍射擊...");
- }
- }
- // 狙擊槍
- class AUG extends AbstractGun {
- shoot(){
- console.log("AUG射擊...");
- }
- }
從上面的代碼可以看出,步槍與狙擊槍的shoot全部都是依賴于AbstractGun抽象的槍類,上述編程滿足了依賴倒置原則。
接口隔離
什么是接口隔離
接口隔離:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。(節選自百度百科)
接口隔離原則與單一職責原則的審視角度不相同。單一職責原則要求是類和接口的職責單一,注重的職責,這是業務邏輯上的劃分。接口隔離原則要求接口的方法盡量少。
接口隔離好處
- 避免接口污染
- 提高靈活性
- 提供定制服務
- 實現高內聚
實例
- function mix(...mixins) {
- class Mix {}
- for (let mixin of mixins) {
- copyProperties(Mix, mixin);
- copyProperties(Mix.prototype, mixin.prototype);
- }
- return Mix;
- }
- function copyProperties(target, source) {
- for (let key of Reflect.ownKeys(source)) {
- if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
- let desc = Object.getOwnPropertyDescriptor(source, key);
- Object.defineProperty(target, key, desc);
- }
- }
- }
- class Behavior {
- eat(){
- throw "Abstract methods cannot be used";
- }
- call(){
- throw "Abstract methods cannot be used";
- }
- }
- class Action {
- climbTree(){
- throw "Abstract methods cannot be used";
- }
- }
- class Dog extends Behavior{
- eat(food){
- console.log(`狗正在吃${food}`);
- }
- hungry(){
- console.log("汪汪汪,我餓了")
- }
- }
- const CatMin = mix(Behavior,Action);
- class Cat extends CatMin{
- eat(food){
- console.log(`貓正在吃${food}`);
- }
- hungry(){
- console.log("喵喵喵,我餓了")
- }
- climbTree(){
- console.log("爬樹很開心哦~")
- }
- }
- let dog = new Dog();
- dog.eat("骨頭");
- dog.hungry();
- let cat = new Cat();
- cat.eat("魚");
- cat.hungry();
- cat.climbTree();
大家一定要好好分析一下上面的代碼,共有兩個抽象類,分別對應不同的行為,Cat與Dog類擁有共同的行為,但是Cat又擁有其自己單獨的行為,使用抽象(即接口)繼承其方法,使用接口隔離使其完成各自的工作,各司其職。
迪米特法則
迪米特法則:最少知識原則(Least Knowledge Principle 簡寫LKP),就是說一個對象應當對其他對象有盡可能少的了解,不和陌生人說話。英文簡寫為: LoD.(節選自百度百科)
迪米特法則的做法觀念就是類間解耦,弱耦合,只有弱耦合了以后,類的復用率才可以提高。一個類應該對其他對象保持最少的了解。通俗來講,就是一個類對自己依賴的類知道的越少越好。因為類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
迪米特法則好處
- 減少對象之間的耦合性
實例
- class ISystem {
- close(){
- throw "Abstract methods cannot be used";
- }
- }
- class System extends ISystem{
- saveCurrentTask(){
- console.log("saveCurrentTask")
- }
- closeService(){
- console.log("closeService")
- }
- closeScreen(){
- console.log("closeScreen")
- }
- closePower(){
- console.log("closePower")
- }
- close(){
- this.saveCurrentTask();
- this.closeService();
- this.closeScreen();
- this.closePower();
- }
- }
- class IContainer{
- sendCloseCommand(){
- throw "Abstract methods cannot be used";
- }
- }
- class Container extends IContainer{
- constructor(){
- super()
- this.system = new System();
- }
- sendCloseCommand(){
- this.system.close();
- }
- }
- class Person extends IContainer{
- constructor(){
- super();
- this.container = new Container();
- }
- clickCloseButton(){
- this.container.sendCloseCommand();
- }
- }
- let person = new Person();
- person.clickCloseButton();
上面代碼中Container作為媒介,其調用類不知道其內部是如何實現,用戶去觸發按鈕,Container把消息通知給計算機,計算機去執行相對應的命令。
組合/聚合復用原則
聚合(Aggregation)表示一種弱的‘擁有’關系,體現的是A對象可以包含B對象但B對象不是A對象的一部分。
合成(Composition)則是一種強的'擁有'關系,體現了嚴格的部分和整體關系,部分和整體的生命周期一樣。
組合/聚合:是通過獲得其他對象的引用,在運行時刻動態定義的,也就是在一個對象中保存其他對象的屬性,這種方式要求對象有良好定義的接口,并且這個接口也不經常發生改變,而且對象只能通過接口來訪問,這樣我們并不破壞封裝性,所以只要類型一致,運行時還可以通過一個對象替換另外一個對象。
優先使用對象的合成/聚合將有助于你保持每個類被封裝,并被集中在單個任務上,這樣類和類繼承層次會保持較小規模,而且不太可能增長為不可控制的龐然大物。
組合/聚合復用原則好處
- 新的實現較為容易,因為超類的大部分功能可通過繼承關系自動進入子類;
- 修改或擴展繼承而來的實現較為容易。
實例
- function mix(...mixins) {
- class Mix {}
- for (let mixin of mixins) {
- copyProperties(Mix, mixin);
- copyProperties(Mix.prototype, mixin.prototype);
- }
- return Mix;
- }
- function copyProperties(target, source) {
- for (let key of Reflect.ownKeys(source)) {
- if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
- let desc = Object.getOwnPropertyDescriptor(source, key);
- Object.defineProperty(target, key, desc);
- }
- }
- }
- class Savings {
- saveMoney(){
- console.log("存錢");
- }
- withdrawMoney(){
- console.log("取錢");
- }
- }
- class Credit {
- overdraft(){
- console.log("透支")
- }
- }
- const CarMin = mix(Savings,Credit);
- class UserCar extends CarMin {
- constructor(num,carUserName){
- super();
- console.log()
- this.carNum = num;
- this.carUserName = carUserName;
- }
- getCarNum(){
- return this.carNum;
- }
- getCarUserName(){
- return this.carUserName;
- }
- }
- let myCar = new UserCar(123456798,"Aaron");
- console.log(myCar.getCarNum());
- console.log(myCar.getCarUserName());
- myCar.saveMoney();
- myCar.withdrawMoney();
- myCar.overdraft();
總結
這些原則在設計模式中體現的淋淋盡致,設計模式就是實現了這些原則,從而達到了代碼復用、增強了系統的擴展性。所以設計模式被很多人奉為經典。我們可以通過好好的研究設計模式,來慢慢的體會這些設計原則。