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

框架

浏览数:31

2019-6-11

单一职责原则    定义:一个类只负责一个领域的相应职责。
开闭原则 定义:软件实体应对扩展开放,而对修改关闭。
里氏替换原则   定义:所有引用基类的对象能够透明的使用其子类的对象。
依赖倒转原则   定义:抽象不应该依赖于细节,细节依赖于抽象。
接口隔离原则   定义:使用多个专门的接口,而不是使用单一总接口。
合成复用原则   定义:尽量使用对象组合,而不是继承来达到复合目的。
迪米特法则   定义:一个软件实体应当尽可能少的与其它实体发生相互作用。

1.单一职责原则 : 每个类型(包括接口和抽象)功能要求单一,只对外负责一件事情,应该仅有一个原因引起类的变更。不要让一个类存在多个改变的理由。只应该做和一个任务相关的业务,不应该把过多的业务放在一个类中完成。单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。

文件:

把不同类型的文件放在不同的文件夹里做区分,不同功能的文件的划分,文件之间相互引用

代码:

    /**
     * @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)
        })
    },

按照最小单位,拆分不同功能的发法,方法之间项目调用

原因:这也是灵活的前提,类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题。
核心:拆分到最小单位,解决复用和组合问题,封装的优良体现,即解耦和增强内聚性(高内聚,低耦合)。
优点: 降低了类的复杂度,明确了对应的职责、可读性和维护性变高、如果接口单一职责做得好,修改接口影响的仅仅是相应的实现类。

2.开闭原则:一个软件实体应该对扩展开发,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。开闭原则是设计原则的核心原则,其他的设计原则都是开闭原则表现和补充。实现开闭原则的方法就是抽象。 

问题:

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)继承和组合聚合复用原则的“冲突”:继承也能实现复用,那这个原则是不是要抛弃继承了?不是的。继承更注重的是“血统”,也就是什么类型的。而组合聚合更注重的是借用“技能”。并且,组合聚合中,两个类是部分与整体的关系,组合聚合可以由多个类的技能组成。这个原则不是告诉我们不用继承了都用组合聚合,而是在“复用”这个点上,我们优先使用组合聚合。
 

总结:

所以可以看出前辈们给定我们这些原则,实际上是为了1、降低耦合度;2、提高稳定性;3、增加可读和可维护性;4、提高扩展性。同时发现,上面的原则的根本就是让我们尽量在定义好核心类之后用相应的接口去实现核心类的其他方法,在引入时尽量引入接口。所以面向对象变成的精髓之一就是面向接口编程。

作者:俗的太不一样