物件導向之七大基本原則(javaScript)

Aaron發表於2019-09-08

物件導向程式設計有自己的特性與原則,如果對於物件導向有一些瞭解的話,物件導向三大特徵,封裝、繼承、多型,如果對面向對這三個概念不太瞭解,請參考物件導向之三個基本特徵(javaScript)

單一職責

如果我們在編寫程式的時候,一類或者一個方法裡面包含了太多方法,對於程式碼的可讀性來說,無非是一場災難,對於我們來說。所以為了解決這個問題,出現了單一職責。

什麼是單一職責

單一職責:又稱單一功能原則,物件導向五個基本原則(SOLID)之一。它規定一個類應該只有一個發生變化的原因。(節選自百度百科)

按照上面說的,就是對一個類而言,應該僅有一個引起它變化的原因。換句話說,一個類的功能要單一,只做與它相關的事情。在類的設計過程中要按職責進行設計,彼此保持正交,互不干涉。

單一職責的好處
  1. 類的複雜性降低,實現什麼職責都有清晰明確的定義
  2. 可讀性提高,複雜性降低,那當然可讀性提高了
  3. 可維護性提高,可讀性提高,那當然更容易維護了
  4. 變更引起的風險降低,變更是必不可少的,如果介面的單一職責做得好,一個介面修改只對相應的實現類有影響,對其他的介面無影響,這對系統的擴充套件性、維護性都有非常大的幫助。
例項
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類存在兩個方法addGoodsgetGoodsList,分別是新增商品和獲取商品列表。Settlement類中存在兩個方法calculatePricegetAllPrice分別做的事情是計算價錢與獲取總價錢。ShoppinCarSettlement都是在做自己的事情。新增商品與計算價格,雖然在業務上是相互依賴的,但是在程式碼中分別用兩個類,然他們自己做自己的事情。其中任何一個類更改不會對另一個類進行更改。

開閉原則

在一個類中暴露出去的方法,若這個方法變更了,則會產生很大的後果,可能導致其他依賴於這個方法且有不需要變更的業務造成大面積癱瘓。為了解決這個問題,可以單獨再寫一個方法,若這個方法與這個類中的其他方法相互依賴。

解決辦法:

  1. 把其中依賴的程式碼copy一份到新的類中。
  2. 在新類中引用舊類中的方法。

兩種方法都不是最好的解決方案。

第一種方法會導致程式碼大量的重複,第二種方法會導致類與類之間互相依賴。

什麼是開閉原則

開閉原則:“軟體中的物件(類,模組,函式等等)應該對於擴充套件是開放的,但是對於修改是封閉的”,這意味著一個實體是允許在不改變它的原始碼的前提下變更它的行為。(節選自百度百科)

開閉原則對擴充套件開放,對修改關閉,並不意味著不做任何修改,底層模組的變更,必然要有高層模組進行耦合,否則就是一個孤立無意義的程式碼片段。開閉原則是一個最基本的原則,另外六個原則都是開閉原則的具體形態,是指導設計的工具和方法,而開閉原則才是精神領袖.

開閉原則好處
  1. 開閉原則有利於進行單元測試
  2. 開閉原則可以提高複用性
  3. 開閉原則可以提高可維護性
  4. 物件導向開發的要求
例項
class Drag {
    down(){
        //  ...
    }   
    move(){
        //  ...
        // 對拖拽沒有做任何限制可以隨意拖拽
    }   
    up(){
        //  ...
    }  
}
class LimitDrag extends Drag {
    move(){
        //  ...
        //  重寫該方法對拖拽進行限制處理
    }
}

LimitDrag中重寫了move方法,若修改了可以滿足兩種需求,一種是限制型拖拽,一種是不限制型拖拽,任何一個更改了另外一個還是可以正常執行。

里氏替換

每個開發人員在使用別人的元件時,只需知道元件的對外裸露的介面,那就是它全部行為的集合,至於內部到底是怎麼實現的,無法知道,也無須知道。所以,對於使用者而言,它只能通過介面實現自己的預期,如果元件介面提供的行為與使用者的預期不符,錯誤便產生了。里氏替換原則就是在設計時避免出現派生類與基類不一致的行為。

什麼是里氏替換

里氏替換原則:OCP作為OO的高層原則,主張使用“抽象(Abstraction)”和“多型(Polymorphism)”將設計中的靜態結構改為動態結構,維持設計的封閉性。“抽象”是語言提供的功能。“多型”由繼承語義實現。(節選自百度百科)

里氏替換好處
  1. 程式碼共享,減少建立類的工作量,每個子類都擁有父類的方法和屬性
  2. 提高程式碼的重用性
  3. 子類可以形似父類,但是又異於父類。
  4. 提高程式碼的可擴充套件性,實現父類的方法就可以了。許多開源框架的擴充套件介面都是通過繼承父類來完成。
  5. 提高產品或專案的開放性
例項
//  抽象槍類
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

從上述程式碼中可以看出,子類和父類之間關係,子類方法一定是等於或大於父類的方法。子類能夠出現的父類不一定能出現,但是父類出現的地方子類一定能夠出現。

依賴倒置

如果方法與方法之間或類與類之間,存在太多的依賴關係會導致程式碼可讀性以及可維護性很差。依賴倒置原則能夠很好的解決這些問題。

什麼是依賴倒置

依賴倒置原則:程式要依賴於抽象介面,不要依賴於具體實現。簡單的說就是要求對抽象進行程式設計,不要對實現進行程式設計,這樣就降低了客戶與實現模組間的耦合。(節選自百度百科)

  1. 高層模組不應該依賴低層模組,兩者都應該依賴其抽象
  2. 抽象不應該依賴細節
  3. 細節應該依賴抽象
依賴倒置好處
  1. 通過依賴於介面,隔離了具體實現類
  2. 低一層的變動並不會導致高一層的變動
  3. 提高了程式碼的容錯性、擴充套件性和易於維護
例項
//  抽象槍類
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抽象的槍類,上述程式設計滿足了依賴倒置原則。

介面隔離

什麼是介面隔離

介面隔離:客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。(節選自百度百科)

介面隔離原則與單一職責原則的審視角度不相同。單一職責原則要求是類和介面的職責單一,注重的職責,這是業務邏輯上的劃分。介面隔離原則要求介面的方法儘量少。

介面隔離好處
  1. 避免介面汙染
  2. 提高靈活性
  3. 提供定製服務
  4. 實現高內聚
例項
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();

大家一定要好好分析一下上面的程式碼,共有兩個抽象類,分別對應不同的行為,CatDog類擁有共同的行為,但是Cat又擁有其自己單獨的行為,使用抽象(即介面)繼承其方法,使用介面隔離使其完成各自的工作,各司其職。

迪米特法則

迪米特法則:最少知識原則(Least Knowledge Principle 簡寫LKP),就是說一個物件應當對其他物件有儘可能少的瞭解,不和陌生人說話。英文簡寫為: LoD.(節選自百度百科)

迪米特法則的做法觀念就是類間解耦,弱耦合,只有弱耦合了以後,類的複用率才可以提高。一個類應該對其他物件保持最少的瞭解。通俗來講,就是一個類對自己依賴的類知道的越少越好。因為類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

迪米特法則好處
  1. 減少物件之間的耦合性
例項
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)則是一種強的'擁有'關係,體現了嚴格的部分和整體關係,部分和整體的生命週期一樣。

組合/聚合:是通過獲得其他物件的引用,在執行時刻動態定義的,也就是在一個物件中儲存其他物件的屬性,這種方式要求物件有良好定義的介面,並且這個介面也不經常發生改變,而且物件只能通過介面來訪問,這樣我們並不破壞封裝性,所以只要型別一致,執行時還可以通過一個物件替換另外一個物件。

優先使用物件的合成/聚合將有助於你保持每個類被封裝,並被集中在單個任務上,這樣類和類繼承層次會保持較小規模,而且不太可能增長為不可控制的龐然大物。

組合/聚合複用原則好處
  1. 新的實現較為容易,因為超類的大部分功能可通過繼承關係自動進入子類;
  2. 修改或擴充套件繼承而來的實現較為容易。
例項
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();

總結

這些原則在設計模式中體現的淋淋盡致,設計模式就是實現了這些原則,從而達到了程式碼複用、增強了系統的擴充套件性。所以設計模式被很多人奉為經典。我們可以通過好好的研究設計模式,來慢慢的體會這些設計原則。

相關文章