3. 设计模式之原则

在最先接触设计模式的时候,我就看到了对「开闭原则」的介绍,后续又陆陆续续接触了「单一职责原则」、「迪米特法则」等等,今天在这里对设计模式的各原则进行一个统一的记录。

  • 单一职责原则
  • 开放-封闭原则
  • 里式代换原则
  • 依赖倒转原则
  • 迪米特法则
  • 接口隔离原则

单一职责原则

单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。

这个原则说的就是一个类只关注一件事情,我们经常会看到这个原则,比如我们开始学习编程的时候,会说到怎么优化代码,其中一项就是一个函数只做一件事情这样的,那时候「只做一件事情」就在我脑海中留下了比较深刻的印象。

现在随着前端组件化的发展,我们将页面也细分为大大小小的组件来进行拼接。在这个时候,「单一职责原则」就和前端的工作息息相关了,我们在拆分组件的时候,也应该保证每一个组件只做一件事情,只关注一个重点。这样我们的页面在后期的扩展维护上就会节省很多的功夫,这也是优秀的组件应该做到的。

「单一职责原则」的有点也是显而易见的:

  • 代码复杂度变低
  • 可维护性提高
  • 需求变更引起的影响减小

开放-封闭原则

开放-封闭原则,是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。[ASD]

我们在开发系统的时候,经常会有这样的感受,在产品最开始设计的时候,我们就尽可能把准备工作多做一些,希望最开始的框架能够满足未来的一些需求。但是随着开发工作的逐渐深入,我们最头疼的情况就发生了:不管我们开始的时候想的有多么完善,都无法满足未来的需求,总是需要去扩展我们的框架。这种时候,「开放-封闭原则」就是我们的指导。

其实我们需要面对的问题就是:但原框架无法适应新需求的时候,我们是修改原来的代码,还是扩展原来的代码?我们经常会发现一个需求只要修改一小段代码就可以完成,然后我们就用了很快的时间完成了任务,之后却发现引起了许多的问题。所以我们在处理新需求的时候,应该更多地在源代码的基础之上去加东西,而不要去改动原有的代码,以防止影响原来的功能。

「开放封闭原则」的特征就是:

  • 对于扩展是开放的
  • 对于更改是封闭的

它的优点也显而易见:

  • 提高了系统的稳定性

开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、复用性好、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。

里式代换原则

里氏代换原则:子类型必须能够替换掉他们的父类型。

我的理解是「里式代换原则」保留了父类的可扩展性,即我们设计的时候在父类的基础上添加特性,在具体实现的时候利用子类来实现。好处就是:

  • 可以随时随地地扩展父类,因为父类的扩展不影响具体功能
  • 所有具体功能的实现都由子类去做,系统是感知不到父类的存在的
  • 父类可以真正的被复用

依赖倒转原则

依赖倒转原则

A. 高层模块不应该依赖低层模块。两个都应该依赖抽象。

B. 抽象不应该依赖细节。细节应该依赖抽象。

说的清楚一些应该就是我们要「针对接口编程,而不是针对实现编程」。

假如当下我们要做一个关于考试分数的柱状图标,首先我们就会想到用echarts来做这个图表。

// echarts类
class Echarts {
    constructor() {}

    // 绘制柱状图
    renderBar(options) {}

    // 绘制折线图图
    renderLine(options) {}

    // 绘制饼图
    renderPie(options) {}
}

// 绘制图表类
class Render {
    tool = new Echarts();
    constructor() {}

    // 绘制柱状图
    renderBar(options) {
        this.tool.renderBar(options);
    }

    // 绘制折线图图
    renderLine(options) {
        this.tool.renderLine(options);
    }

    // 绘制饼图
    renderPie(options) {
        this.tool.renderLine(options);
    }
}

const render = new Render();
render.renderBar();
render.renderLine();
render.renderLine();

但是这时客户觉得更喜欢阿里的G2图表,这时我们修改代码就发现很难去修改,因为我们的业务绘制代码直接依赖了Echarts,如果我们需要改图表依赖,我们就要改很多东西,那怎么做才好一些呢?

// 依赖倒置原则
// echarts类
class Echarts {
    constructor() {}

    // 绘制柱状图
    renderBar(options) {}

    // 绘制折线图图
    renderLine(options) {}

    // 绘制饼图
    renderPie(options) {}
}

// g2
class G2 {
    constructor() {}

    // 绘制柱状图
    renderBar(options) {}

    // 绘制折线图图
    renderLine(options) {}

    // 绘制饼图
    renderPie(options) {}
}

function getTool(type) {
    switch (type) {
        case 'echarts':
            return new Echarts();
            break;
        case 'g2':
            return new G2();
            break;
    }
}

// 绘制图表类
class Render {
    constructor(tool) {
        this.tool = tool;
    }

    // 绘制柱状图
    renderBar(options) {
        this.tool.renderBar(options);
    }

    // 绘制折线图图
    renderLine(options) {
        this.tool.renderLine(options);
    }

    // 绘制饼图
    renderPie(options) {
        this.tool.renderLine(options);
    }
}

const render = new Render(getTool('g2'));
render.renderBar();
render.renderLine();
render.renderLine();

由上可见,我们新增了一个getTool的方法,如果我们需要调整或者扩展图表类,我们只需要扩展底层方法即可,代价和难度都会大大降低,也减少了高层代码(绘制图表类)对底层代码(图表类)的依赖。

迪米特法则

迪米特法则(LoD):如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

举个例子,现在我们打车都用上了高德地图,但是还是有些地方地图定位的不是特别准确,这些时候就需要我们口头告诉司机应该怎么走。这种场景下我们就有了3个类:乘客司机汽车,其实我们最终的目的是希望指挥车子的运动(向左转、向右转等等),但是其实我们和车子的控制没有关系,我们就需要通过第三者(司机)来达成目的。

// 迪米特法则
class Car {
    constructor() {}

    // 左转
    turnLeft() {}

    // 右转
    turnRight() {}

    // 掉头
    turnRound() {}

    // 直行
    goStraight() {}
}

class Driver {
    constructor(car) {
        this.car = car;
    }

    // 左转
    turnLeft() {
        this.car.turnLeft();
    }

    // 右转
    turnRight() {
        this.car.turnRight();
    }

    // 掉头
    turnRound() {
        this.car.turnRound();
    }

    // 直行
    goStraight() {
        this.car.goStraight();
    }
}

class Customer {
    constructor() {}

    inform(driver, direction) {
        switch (direction) {
            case 'left':
                driver.turnLeft();
                break;
            case 'right':
                driver.turnRight();
                break;
            case 'round':
                driver.turnRound();
                break;
            case 'straight':
                driver.goStraight();
                break;
        }
    }
}

const car = new Car();
const driver = new Driver(car);
const customer = new Customer();

// 乘客告诉司机下个路口左转
customer.inform(driver, 'left');
// 乘客告诉司机下个路口右转
customer.inform(driver, 'right');
  1. 在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
  2. 迪米特法则其根本思想,是强调了类之间的松耦合;
  3. 类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。

接口隔离原则

一个类对另一个类的依赖应该建立在最小的接口上。

对于这个原则,我的理解是这样的:在当下我们写页面的时候,常用的UI框架有ElementUI和Ant Design UI,在这两个UI框架中,有三个组件:InputAutoCompleteInputNumber,我们会发现他们有很多共同的地方:

  • 都是输入框
  • 支持相同的事件(Focus、Blur等等)
  • 支持相同的属性(readOnly,autofocus等等)

除此之外他们也都有自己的特性:

  • AutoComplete支持自动搜索
  • InputNumber对数字有特殊的处理

其实我们能不能把这些特性全部集中在Input上呢?其实应该也是可以的,那样的话Input组件就会很臃肿,也提供了很多我们平时用不到的特性。

那最好的方式就是将「自动搜索」和「数字特殊处理」当做独立接口处理,以形成我们今天的3个组件,让我们在需要的地方用最适合的组件。

我想这是前端工程师对「接口隔离原则」的一个很棒的应用。


参考

大话设计模式 -- 程杰
Element-UI
Ant Design