面向对象的七大基本原则和实例详解

单一职责原则    定义:一个类只负责一个领域的相应职责。
开闭原则 定义:软件实体应对扩展开放,而对修改关闭。
里氏替换原则   定义:全部引用基类的对象可以透明的使用其子类的对象。
依赖倒转原则   定义:抽象不该该依赖于细节,细节依赖于抽象。
接口隔离原则   定义:使用多个专门的接口,而不是使用单一总接口。
合成复用原则   定义:尽可能使用对象组合,而不是继承来达到复合目的。
迪米特法则   定义:一个软件实体应当尽量少的与其它实体发生相互做用。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)继承和组合聚合复用原则的“冲突”:继承也能实现复用,那这个原则是否是要抛弃继承了?不是的。继承更注重的是“血统”,也就是什么类型的。而组合聚合更注重的是借用“技能”。而且,组合聚合中,两个类是部分与总体的关系,组合聚合能够由多个类的技能组成。这个原则不是告诉咱们不用继承了都用组合聚合,而是在“复用”这个点上,咱们优先使用组合聚合。
 

总结:

因此能够看出前辈们给定咱们这些原则,其实是为了一、下降耦合度;二、提升稳定性;三、增长可读和可维护性;四、提升扩展性。同时发现,上面的原则的根本就是让咱们尽可能在定义好核心类以后用相应的接口去实现核心类的其余方法,在引入时尽可能引入接口。因此面向对象变成的精髓之一就是面向接口编程。

相关文章
相关标签/搜索