面向對象的七大基本原則和實例詳解

單一職責原則    定義:一個類只負責一個領域的相應職責。
開閉原則 定義:軟件實體應對擴展開放,而對修改關閉。
里氏替換原則   定義:全部引用基類的對象可以透明的使用其子類的對象。
依賴倒轉原則   定義:抽象不該該依賴於細節,細節依賴於抽象。
接口隔離原則   定義:使用多個專門的接口,而不是使用單一總接口。
合成複用原則   定義:儘可能使用對象組合,而不是繼承來達到複合目的。
迪米特法則   定義:一個軟件實體應當儘量少的與其它實體發生相互做用。vue


1.單一職責原則 : 每一個類型(包括接口和抽象)功能要求單一,只對外負責一件事情,應該僅有一個緣由引發類的變動。不要讓一個類存在多個改變的理由。只應該作和一個任務相關的業務,不該該把過多的業務放在一個類中完成。單一職責原則不僅是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用單一職責原則。編程

文件:element-ui

把不一樣類型的文件放在不一樣的文件夾裏作區分,不一樣功能的文件的劃分,文件之間相互引用canvas

代碼:設計模式

    /**
     * @author 劉貴生
     * @date:2018-11-2
     * @information:頁面初始化數據
     * @param: model 搜索條件
     */
    pageInit: function ({dispatch},model) {
        // 返回一個promise,先去請求表頭數據,成功以後去請求表格數據和三個考勤率
       return new Promise ((resolve,reject) => {
            dispatch('getHeaderData')
            resolve()
        }).then (() => {
            let p1 = new Promise ((resolve,reject) => {
                dispatch('getTableData',model)
                resolve()
            })        
            let p2 = new Promise ((resolve,reject) => {
                dispatch('searchPercentage',model)
                resolve()
            })
            Promise.all([p1,p2])
        })
    },
  /**
     * @author 劉貴生
     * @date:2018-11-8
     * @infromation: 請求表頭數據
     */
    getHeaderData: function ({commit}) {
        request.searchTableHeader().then(res => {
            commit(type.TABLEHEADER,res.data.result)
        })
    },
/**
     * @author 劉貴生
     * @date:2018-11-2
     * @information: 請求表格數據
     * @param: model 查詢的條件
     */
    getTableData: function ({state,commit},model) {
        // 打開正在加載
        state.loading = true
        let obj = {
            query:model,
            pages:state.pages,
            sort: state.sort
        }
        return request.searchTableData(obj).then(res => {
            // 表格數據和總條數
            let { data, totalSize } = res.data.result
            // 獲取每頁請求多少條
            let { size } = state.pages
            // 保存數據的總長度
            let num = data.length
            // 若是數據大於0條而且小於每頁顯示條數,用空數據補齊
            if(num > 0 && num < size) {
                for(let i = 0;i<size-num;i++) data.push({})
            }     
            // 向mutation提交狀態
            commit(type.TABLEDATA, data)
            commit(type.TOTALSIZE, totalSize)
            // 關閉正在加載
            state.loading = false
        })
    },

/**
     * @author 劉貴生
     * @date:2018-11-06
     * @information: 查詢三個考勤率
    */
    searchPercentage: function ({ commit },model) {
        request.searchPercentage(model).then(res => {
            commit(type.PERCENTAGE,res.data.result)
        })
    },

按照最小單位,拆分不一樣功能的發法,方法之間項目調用promise

 

緣由:這也是靈活的前提,類被修改的概率很大,所以應該專一於單一的功能。若是你把多個功能放在同一個類中,功能之間就造成了關聯,改變其中一個功能,有可能停止另外一個功能,這時就須要新一輪的測試來避免可能出現的問題。
核心:拆分到最小單位,解決複用和組合問題,封裝的優良體現,即解耦和加強內聚性(高內聚,低耦合)。
優勢: 下降了類的複雜度,明確了對應的職責、可讀性和維護性變高、若是接口單一職責作得好,修改接口影響的僅僅是相應的實現類。架構


2.開閉原則:一個軟件實體應該對擴展開發,對修改關閉。即在設計一個模塊的時候,應當使這個模塊能夠在不被修改的前提下被擴展。開閉原則是設計原則的核心原則,其餘的設計原則都是開閉原則表現和補充。實現開閉原則的方法就是抽象。 app

問題:框架

import { CanvasFun } from './canvas.js'
export class CanvasFun {
  constructor (ctx) {
    this.ctx = ctx
  }
  // 畫圖片
  drawImg(param) {
    let { url, left, top, width, height} = param
     this.ctx.drawImage(url, left, top, width, height)
  }

  // 畫文字
  setFont (param) {
    let { color,size,words,x,y } = param
    this.ctx.setFillStyle(color)
    this.ctx.setFontSize(size)
    this.ctx.fillText(words,x,y)
  }
  
}


// 執行命令

export class Commond {
  constructor (ctx) {
    this.ctx = ctx
    this.list = []
  }
  // 將全部的命令添加到一個列表
  addStep (step) {
    this.list.push(step)
  }
  // 根據不一樣的類型執行不一樣的命令
  run () {
    let { ctx, list} = this
    let canvas = new CanvasFun (ctx)
    list.map (el => {
      canvas[el.type](el.param)
    })
  }
}
commond.addStep({type: "drawImg",param: {url: that.data.peoplePhote,left: 0,top: 0,width: 600,height: 880}})
 

 解決:模塊化

export class CanvasFun {
  constructor (ctx) {
    this.ctx = ctx
  }
}
  // 畫圖片
class DrawImg extends CanvasFun {
  constructor (url,left,top,width,height) {
    this.url = url
    this.left = left 
    this.top = top
    this.width = width
    this.height = height
  }
  draw (ctx) {
    ctx.drawImage(url, left, top, width, height)
  }
}
  // 畫文字
class DrawText extends CanvasFun {
  constructor(color, size, words, x, y) {
    this.color = color
    this.size = size
    this.words = words
    this.x = x
    this.y = y
  }
  draw(ctx) {
    ctx.setFillStyle(color)
    ctx.setFontSize(size)
    ctx.fillText(words, x, y)
  }
}

export class Commond {
  constructor (ctx) {
  this.ctx = ctx
  this.list = []
}
  // 將全部的命令添加到一個列表
  addStep (step) {
    this.list.push(step)
}
  // 根據不一樣的類型執行不一樣的命令
  run () {
    let { ctx, list} = this
    list.map (el => {
    el.draw(ctx)
 
  })
    ctx.draw()
  }
}


commond.addStep(new DrawImg('../../assets/line_two.png', 0, 0, 750, 550))
 

 

 

 

緣由:軟件系統的功能上的可擴展性要求模塊是擴展開放的,軟件系統的功能上的穩定性,持續性要求是修改關閉的。根本控制需求變更風險,縮小維護成本。
核心:用抽象構建框架,用實現類實現擴展,在不修改原有模塊的基礎上能擴展其功能。
優勢: 增長穩定性、可擴展性高。
 


3.替換原則(里氏代換原則):子類可以替換父類,出如今父類可以出現的任何地方,子類必須徹底實現父類的方法。在類中調用其餘類是務必要使用父類或接口,若是不能使用父類或接口,則說明類的設計已經違背了原則。覆蓋或實現父類的方法時輸入參數能夠被放大。即子類能夠重載父類的方法,但輸入參數應比父類方法中的大,這樣在子類代替父類的時候,調用的仍然是父類的方法。里氏替換原則是針對繼承而言的,若是繼承是爲了實現代碼重用,也就是爲了共享方法,那麼共享的方法應該保持不變,不被子類從新定義。若是繼承是爲了多態那麼,而多態的前提是子類覆蓋父類的方法因此將父類定義爲抽象類,抽象類不可以實例化對象也就不存在替換這一說。

問題:

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor() {
    super();
  }

  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach((rectangle) => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    let area = rectangle.getArea();
    rectangle.render(area);
  })
}

let rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

 

解決:

class Shape {
  constructor() {}

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }
}

class Rectangle extends Shape {
  constructor() {
    super();
    this.width = 0;
    this.height = 0;
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor() {
    super();
    this.length = 0;
  }

  setLength(length) {
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach((shape) => {
    switch (shape.constructor.name) {
      case 'Square':
        shape.setLength(5);
      case 'Rectangle':
        shape.setWidth(4);
        shape.setHeight(5);
    }

    let area = shape.getArea();
    shape.render(area);
  })
}

let shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeShapes(shapes);

 

 

緣由:這是多態的前提,要保證父類的方法不被覆蓋。
核心:從開閉原則能夠看出,設計模式一個重要的部分是抽象化,里氏代換原則從另外一個角度描述了抽象(父類)和具體(子類)之間的關係
優勢:總感受只要是面向對象這個原則是默認遵照的。
 


4.依賴倒轉原則:具體依賴抽象,上層不依賴於下層。兩個模塊之間依賴的應該是抽象(接口或抽象類)而不是細節(實現類)。細節(實現類)依賴於抽象(接口或抽象類)。相對於實現類的多變性,抽象的東西要穩定得多,基於抽象的構架也比基於實現的架構更加穩定,且擴展性更高。經過構造函數、setter方法傳遞依賴對象,接口聲明實現依賴對象。要根據接口隔離原則分拆接口時,必須知足單一職責原則。想要理解依賴倒置原則,必須先理解傳統的解決方案。面相對象的初期的程序,被調用者依賴於調用者。也就是調用者決定被調用者有什麼方法,有什麼樣的實現方式,這種結構在需求變動的時候,會付出很大的代價,甚至推翻重寫。依賴倒置原則就是要求調用者和被調用者都依賴抽象,這樣二者沒有直接的關聯和接觸,在變更的時候,一方的變更不會影響另外一方的變更。

下面看一個實例:

import ElementUI from 'element-ui' // vue的ui組件-(餓了麼-ui)element-ui
Vue.use(ElementUI)

其實就是咱們的須要的模塊依賴vue模塊,main.js就是vue模塊抽象出來的接口,這裏使用Vue.use(),把咱們須要的模塊vue注入進來,而後咱們就能夠用它了。

 

實例2:

class Tracker {
        constructor (item) {
            this.item = item
            this.requester = new request ()
        }
        requestItems () {
            this.item.forEach(el => {
                this.requester.requestItem(item)
            });
        }
    }

    class Request {
        constructor () {
            this.type = ["HTTP"]
        }
        requestItem () {
            // ...
        }
    }
let useTracker = new Tracker(['apples',"banans"])
useTracker.requestItems()
 

很顯然Tracker的構造器中有一段錯誤的代碼,this.requester只實現了對特定的請求,咱們再來改造一下:

    class Tracker {
        constructor (item,res) {
            this.item = item
            this.requester = res
        }
        requestItems () {
            this.item.forEach(el => {
                this.requester.requestItem(item)
            });
        }
    }

    class Request1 {
        constructor () {
            this.type = ["HTTP"]
        }
        requestItem () {
            // ...
        }
    }

    class Request2 {
        constructor () {
            this.type = ['ws']
        }
        requestItem () {
            // ...
        }
    }
    let useTracker = new Tracker (['apples','banbans'],new Request2)
    useTracker.requestItems()

 

 

緣由:依賴抽象的接口能夠適應變化的需求。防止需求變化時對被依賴者的改變過大。
核心:要依賴於抽象,面向抽象編程,不要依賴於具體的實現,思想是面向接口編程。高層模塊不該該依賴低層模塊,二者都應該依賴其抽象(抽象類或接口)。解耦調用和被調用者。
優勢:採用依賴倒置原則能夠減小類間的耦合性,提升系統的穩定性,減小並行開發引發的風險,提升代碼的可讀性和可維護性。從大局看Java的多態就屬於這個原則。
 

5.接口隔離原則:模塊間要經過具體接口分離開,而不是經過類強耦合。一個接口不須要提供太多的行爲,一個接口應該只提供一種對外的功能,不該該把全部的操做都封裝到一個接口當中。分離接口的兩種實現方法:使用委託分離接口和使用多重繼承分離接口。例如A類對B類的依賴,能夠抽象接口I,B實現I,A類依賴I來實現。可是抽象接口必須功能最小化(與單一功能原則有點不謀而合)。創建單一接口,不要創建龐大的接口,儘可能細化接口,接口中的方法儘可能少。也就是要爲各個類創建專用的接口,而不要試圖去創建一個很龐大的接口供全部依賴它的類去調用。依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的約定,經過分散定義多個接口,能夠預防外來變動的擴散,提升系統的靈活性和可維護性。

緣由:是單一職責的必要手段,儘可能使用職能單一的接口,而不使用職能複雜、全面的接口。接口是爲了讓子類實現的,若是想達到職能單一那麼接口也必須知足職能單一。若是接口融合了多個不相關的方法,那子類就被迫實現全部方法,儘管有些方法是根本用不到的。這就是接口污染。
核心:拆分,從接口開始。不該該強迫客戶程序依賴他們不須要使用的接口,一個類對另外一個類的依賴應該創建在最小的接口上。 使用專門的接口,比用統一的接口要好。
優勢:下降耦合性、提高代碼可讀性、影藏實現細節。
 

6.合成複用原則:複用的種類: 繼承、合成聚合,在複用時應優先考慮使用合成聚合而不是繼承。儘可能使用對象組合,而不是繼承來達到複用的目的。該原則就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分:新的對象經過向這些對象的委派達到複用已有功能的目的。爲了達到代碼複用的目的,儘可能使用組合與聚合,而不是繼承。組合聚合只是引用其餘的類的方法,而不會受引用的類的繼承而改變血統。

緣由:繼承的耦合性更大,好比一個父類後來添加實現一個接口或者去掉一個接口,那子類可能會遭到毀滅性的編譯錯誤,但若是隻是組合聚合,只是引用類的方法,就不會有這種巨大的風險,同時也實現了複用。
核心:多使用聚合/組合達到代碼的重用,少使用繼承複用。 
優勢:組合/聚合複用原則可使系統更加靈活,類與類之間的耦合度下降,一個類的變化對其餘類形成的影響相對較少,所以通常首選使用組合/聚合來實現複用。


7.迪米特原則:最小依賴原則又叫最少知識原則,一個類對其餘類儘量少的瞭解。在模塊之間只經過接口來通訊,而不理會模塊的內部工做原理,可使各個模塊的耦合成都降到最低,促進軟件的複用在類的劃分上,應該建立有弱耦合的類;在類的結構設計上,每個類都應當儘可能下降成員的訪問權限;在類的設計上,只要有可能,一個類應當設計成不變;在對其餘類的引用上,一個對象對其它對象的引用應當降到最低;儘可能下降類的訪問權限;謹慎使用序列化功能;不要暴露類成員,而應該提供相應的訪問器(屬性)。要求類之間的直接聯繫儘可能的少,兩個類的訪問,經過第三個中介類來實現。每一個對象都會與其餘對象有耦合關係,出現成員變量、方法參數、方法返回值中的類爲直接的耦合依賴,而出如今局部變量中的類則不是直接耦合依賴,也就是說不是直接耦合依賴的類最好不要做爲局部變量的形式出如今類的內部。

緣由:一個類若是暴露太多私用的方法和字段,會讓調用者很茫然。而且會給類形成沒必要要的判斷代碼。因此,咱們使用盡可能低的訪問修飾符,讓外界不知道咱們的內部。這也是面向對象的基本思路。這是迪米特原則的一個特性,沒法瞭解類更多的私有信息。
核心:一個對象應當對其餘對象有儘量少的瞭解,軟件實體應當儘量少的與其餘實體發生相互做用。意思就是下降各個對象之間的耦合,要求儘可能的封裝,儘可能的獨立,儘可能的使用低級別的訪問修飾符以提升系統的可維護性。
優勢:下降耦合度、增長穩定性。
 

特別說明:

1) 高內聚、低耦合和單一職能的「衝突」:實際上,這二者是一回事。內聚,要求一個類把全部相關的方法放在一塊兒,初看是職能多,但有個「高」,就是要求把聯繫很是緊密的功能放在一塊兒,從總體看是一個職能的才能放在一塊兒,因此二者是不一樣的表述而已。
 
2)多個單一職能接口的靈活性和聲明類型問題:若是一個類實現多個接口,那麼這個類應該用哪一個接口類型聲明呢?應該是用一個抽象類來繼承多個接口,而實現類來繼承這個接口。聲明的時候,類型是抽象類。
 
3)最少知識原則和中介類氾濫兩種極端狀況:這是另外一種設計的失誤。迪米特原則要求類之間要用中介來通信,但類多了之後,會形成中介類氾濫的狀況,這種狀況,咱們能夠考慮中介模式,用一個總的中介類來實現。固然,設計模式都有本身的缺陷,迪米特原則也不是十全十美,交互類很是繁多的狀況下,要適當的犧牲設計原則。
 
4)繼承和組合聚合複用原則的「衝突」:繼承也能實現複用,那這個原則是否是要拋棄繼承了?不是的。繼承更注重的是「血統」,也就是什麼類型的。而組合聚合更注重的是借用「技能」。而且,組合聚合中,兩個類是部分與總體的關係,組合聚合能夠由多個類的技能組成。這個原則不是告訴咱們不用繼承了都用組合聚合,而是在「複用」這個點上,咱們優先使用組合聚合。
 

總結:

因此能夠看出前輩們給定咱們這些原則,其實是爲了一、下降耦合度;二、提升穩定性;三、增長可讀和可維護性;四、提升擴展性。同時發現,上面的原則的根本就是讓咱們儘可能在定義好核心類以後用相應的接口去實現核心類的其餘方法,在引入時儘可能引入接口。因此面向對象變成的精髓之一就是面向接口編程。

相關文章
相關標籤/搜索