天天看点

SOLID设计原则--依赖倒置原则背景SOLID设计原则依赖倒置原则(DIP)总结

SOLID设计原则--依赖倒置原则

  • 背景
  • SOLID设计原则
  • 依赖倒置原则(DIP)
    • 什么是依赖倒置原则
      • 定义
      • 解释说明
    • 依赖倒置原则的使用
      • 需求描述
      • 解决方案一
        • 优缺点分析
      • 解决方案二
    • 谁和谁的依赖被倒置了?
  • 总结

背景

  • 设计原则–>设计模式–>程序语言语法机制,是编程思考和实施的三个层次。由左向右抽象层次越来越低,工作内容越来越具体。语法机制提供了机制和实施的可能性,设计模式是如何操作这些机制,设计模式可以看做是设计原则的具现化,设计模式遵循了设计原则,提供针对重复问题的最佳解决方案。设计原则指导设计模式的产生。
  • 设计原则是心法,设计模式是招式,程序语言语法机制是基本能力(能动、能跳、能推)。
  • 违背程序语言语法机制的代码段是不能正确工作的,不违背程序语言语法机制是作为程序员的

    基本格

    ;违背设计原则而写出的程序,会使程序的质量属性[可扩展,可维护,可测试,可重用,…]急剧下降。理解设计原则,并会使用设计模式的程序员是成为

    中高级工程师

    的必要条件。
  • 设计原则影响的范围最广,影响效果最长久,也最潜移默化。不经历一个较长时间的软件生长和演化过程,设计原则的影响很难体现出来。
  • 比设计原则更抽象的思维层次是设计思想,比如

    高内聚低耦合

    设计思想、面向过程、面向对象、函数式编程思想。

SOLID设计原则

  • 经过面向对象程序设计领域几十年的发展和总结,诞生了五个重要的设计原则(单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则),后来这几个原则被Bob大叔调整了下顺序,其首字母刚好是单词

    solid

    ,因此后来就被被称为

    SOLID

    原则,

    SOLID

    原则起初主要是在OOP中针对接口设计的指导原则,其实这些原则还可以作为函数、类以及模块的设计原则。遵循这几个原则可以编写出扩展性好,可维护性好,透明性好,可读性好的软件系统。
  • 总的来说

    SOLID

    设计原则还是程序设计方面的具体原则。在软件工程中针对的

    level

    phase

    大概是处于架构设计和编码实现的中间阶段 ------- 程序详细设计阶段。

依赖倒置原则(DIP)

什么是依赖倒置原则

定义

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
  • 抽象不能依赖于实现细节,实现细节应该依赖于抽象。

解释说明

  • 依赖倒置原则(DIP)虽然是位于

    SOLID

    最后一个,但其实,DIP是非常基础和重要的设计原则。其余四个原则几乎都或多或少有DIP的影子,比如违反了DIP,也大概率会违反开闭原则(OCP)。

依赖倒置原则的使用

我们通过对一个具体需求的分析、设计和实现来说明如何遵循依赖倒置原则。违背和不违背DIP原则分别对扩展性有什么影响。比如有一个业务需求:

需求描述

我期待通过一个二值自锁开关类来控制一个两状态的台灯。按下开关,台灯打开;抬起开关,台灯关闭。

解决方案一

  • 这里我们通过最直接的做法来满足需求,作为做原始的迭代版本,保留程序的演化和成长过程。
  • 通过一个二值自锁开关类来控制一个两状态的台灯,我们对这个需求经过简单的面向对象分析后,把关键名词挑出来作为类名,把动词作为方法命。于是有了

    class Switch

    class Lamp

    两个类。

    我们先定义客户端的使用程序:

#include "lamp.hpp"
#include "switch.hpp"

// wrapper 类,组装两个功能类对象
class Client {
public:
  Client() {
    // 创建一个位于卧室的台灯对象
    lamp_ = Lamp("bed room");
    // 将创建到好的台灯对象交给Swich对象来管理,继而实现控制
    switch_ = Switch(lamp_); // 注意这里用的是引用或指针而不是值拷贝
  }

  // 开灯
  bool openLamp() { return switch_.open(); }

  // 关灯
  bool closeLamp() { return switch_.close(); }

private:
  Lamp lamp_;
  Switch switch_;
};

// Application main entry point
void main() {
  Client client;
  client.openLamp();
  client.closeLamp();
}
           
  • 下面给出

    class Switch

    class Lamp

    两个类实现。
  • Lamp

// file lamp.hpp
class Lamp {
public:
  Lamp(const std::string &location) : location_(location) {}

public:
  bool open() {
    bool flag = false;
    // 具体实现细节
    // ...
    return flag;
  }
  bool close() {
    bool flag = false;
    // 具体实现细节
    // ...
    return flag;
  }

private:
  std::string location_;
};
           
  • Switch

// file switch.
#include <lamp.hpp>

class Switch {
public:
  // 构造域
  Switch(Lamp *lamp) : lamp_(lamp) {}

  Switch(Lamp &lamp) : lamp_(&lamp) {}

  // 接口域
public:
  bool open() { return lamp_->open(); }
  bool close() { return lamp_->close(); }

private:
  Lamp *lamp_;
};

           

优缺点分析

  • 优点是简单直接,没有思维负担,也满足了功能。我们画下当前的类图来看下当前依赖关系。
  • 这里我们重点关注类

    Switch

    和类

    Lamp

    。类

    Lamp

    聚合(Aggregation)到类

    Switch

    。类

    Swith

    是高层模块,类

    Lamp

    是低层模块。依赖关系为类

    Switch

    Lamp

    ,任何导致

    Lamp

    变化的原因都会导致

    Switch

    的变动,这种依赖关系是静态的,编译时的依赖。大部分情况下高层模块

    Swith

    的代码相对稳定,只具有

    open

    close

    两种操作,而这两种操作都是转调用底层模块的具体实现

    {open(); close()}

    。而低层模块

    Lamp

    相对不稳定,目前的依赖关系违背了依赖导致原则,高层模块

    Switch

    直接依赖的低层模块,导致高层

    Switch

    不易变化的模块的代码和低层

    Lamp

    容易变化的代码静态绑定到一起,继而导致高层模块

    Switch

    的代码不可复用。为了解决

    Switch

    可复用性问题,我们引入解决方案二。

解决方案二

  • 为了复用

    Switch

    代码,我们将引入一个抽象的中间类(引入一个中间层[分层]是计算机领域解决问题的好方法),名称为

    IControlable

    来抽象

    Switch

    可以操作的所有对象,包括

    Lamp

    对象,甚至未来的

    Motor

    Vehicle

    等所有具有二元[0-1]操作的可控制对象。
// file icontrolable.hpp

// 抽象类 或 接口类
class IControlable {
public:
  virtual ~IControlable() {}

public:
  virtual bool open() = 0;
  virtual bool close() = 0;
};
           
  • 我们让

    Switch

    依赖接口类

    IControlable

    ,让类

    Lamp

    实现接口类

    IControlable

    ,解耦

    Switch

    Lamp

    的直接依赖关系,解除

    Switch

    对象只能控制

    Lamp

    对象的尴尬局面(😃-😃-😃)。此时类图如下
  • 现在的依赖关系变成了

    Switch

    依赖接口类

    IControlable

    ;同时

    Lamp

    也依赖接口类

    IControlable

    Switch

    不在依赖

    Lamp

    了,这就是DIP原则的两种定义所描述的完整内涵----高层模块不应该依赖低层模块,二者都应该依赖于抽象;抽象不应该依赖实现细节,实现细节应该依赖于抽象。这里

    Switch

    是高层模块,

    Lamp

    是低层模块也是实现细节,而

    IControlable

    是抽象。至此,该设计完全遵循DIP原则了。
  • 对于C++语言来说,此时在编译时,类

    Switch

    文件

    switch.hpp

    也只依赖

    IControlable.hpp

    ,不会有

    Lamp

    lamp.hpp

    的影子。

谁和谁的依赖被倒置了?

  • 一般来说控制器

    Switch

    的能力由

    IControlable

    来描述和限制,我们会把

    IControlable

    也看做高层模块的一部分,即

    IControlable

    属于高层模块。类图如下右边所示:
    SOLID设计原则--依赖倒置原则背景SOLID设计原则依赖倒置原则(DIP)总结
  • 依赖关系由之前的 高层

    Switch

    模块直接依赖低层和实现细节模块

    Lamp

    , 进化为 低层

    Lamp

    模块依赖了高层

    Switch

    模块,由之前的稳定依赖于不稳定进化为不稳定依赖于稳定。这就是依赖倒置中倒置的深刻含义。

总结

  • 今天分析了

    SOLID

    原则 ----依赖倒置原则(

    DIP

    )的含义,并从复用性和可扩展性方面给出了一个例子,希望读者能认真思考,举一反三,将设计原则转化为自己的编程内功,修炼和提高自己的编程素养。

继续阅读