依赖倒置原则的理解
7 min read

依赖倒置原则的理解

依赖倒置原则的理解以及在 Go 中的运用。
依赖倒置原则的理解
Photo by Elsa Gonzalez / Unsplash

DIP

依赖倒置原则(Dependency Inversion Principle, DIP):高层模块不应该依赖于低层模块,二者应该依赖于其抽象。抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

DIP 理解

  • 针对接口编程,而不是针对实现编程
  • 尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现
  • 关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于上层模块,逻辑层和数据层自然归为低层。

DIP 控制反转(Inversion of Control, IoC)

反转是指:在没有使用框架之前,程序员自己控制整个程序的执行,当使用框架之后,整个程序的执行可以通过框架来控制; 框架提供了可扩展的代码骨架,用来组装对象/管理整个执行流程,程序只需关注扩展点,就可以利用框架来驱动整个程序流程的执行。

控制反转跟依赖倒置是如出一辙的两个概念,当存在依赖倒置的时候往往也存在着控制反转。但是控制反转也有自己的独特内涵。


首先我们要区分两个角色,server 和 client,也就是服务方和客户方,我们最熟悉的例子就是分布式应用的 C/S 架构,服务端和客户端。其实除此之外,C/S 关系处处可见。比如在 TCP/IP 协议栈中,我们知道,每层协议为上一层提供服务,那么这里就是一个 C/S 关系。当我们使用开发框架时,开发框架就是作为服务方,而我们自己编写的业务应用就是客户方。当 client 调用 server 时,这个叫做一般的控制;而当 server 调用 client 时,就是我们所说的控制反转,同时我们也将这个调用称为“回调”。控制反转跟依赖倒置都是一种编程思想,依赖倒置着眼于调用的形式,而控制反转则着眼于程序流程的控制权。一般来说,程序的控制权属于 client,而一旦控制权交到 server,就叫控制反转。比如你去下馆子,你是 client 餐馆是 server。你点菜,餐馆负责做菜,程序流程的控制权属于 client;而如果你去自助餐厅,程序流程的控制权就转到 server 了,也就是控制反转。

控制反转的思想体现在诸多领域。比如事件的发布/订阅就是一种控制反转,GOF设计模式中也多处体现了控制反转,比如典型的模板方法模式等。而开发框架则是控制反转思想应用的集中体现。

DIP 依赖注入(Dependency Injection, DI)

依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。

依赖注入与依赖倒置、控制反转的关系仍旧是一本万殊。依赖注入,就其广义而言,即是通过“注入”的方式,来获得依赖。我们知道,A 对象依赖于 B 对象,等价于 A 对象内部存在对 B 对象的“调用”,而前提是 A 对象内部拿到了 B 对象的引用。B 对象的引用的来源无非有以下几种:A 对象内部创建(无论是作为字段还是作为临时变量)、构造器注入、属性注入、方法注入。后面三种方式统称为“依赖注入”,而第一种方式我也生造了一个名词,称为“依赖内生”,二者根本的差异即在于,我所依赖的对象的创建工作是否由我自己来完成。当然,这个是广义的依赖注入的概念,而我们一般不会这样来使用。我们通常使用的,是依赖注入的狭义的概念。不过,直接陈述其定义可能会过于诘屈聱牙,我们还是从具体的例子来看:

Golang Demo

Demo 场景:

  • 线上学校有一系列课程
  • 用户可选择若干门课程进行学习
  • 如果把学习课程的过程直接实现为用户的方法, 则每增加一门课程, 就需要增加一个学习方法
  • 根据依赖倒置原则, 可以把学习过程抽象为学习接口, 由不同的课程实例各自实现

Bad DIP

BadUser 以不同方法实现各种课程的学习过程, 课程的增加导致BadUser代码越来越臃肿。

package dependence_inversion

import "fmt"

type BadUser struct {
    iID int
    sName string
}

func NewBadUser(id int, name string) *BadUser {
    return &BadUser{
        iID: id,
        sName: name,
    }
}

func (me *BadUser) StudyJavaCourse() {
    fmt.Printf("%v is learning %v\n", me.sName, "java")
}

func (me *BadUser) StudyGolangCourse() {
    fmt.Printf("%v is learning %v\n", me.sName, "golang")
}

Good DIP

User 抽象

GoodUser 通过实现 IUser 接口提供用户基本信息, 并把不同课程的学习过程, 委托给 ICourse 接口去实现。

package dependence_inversion


type IUser interface {
    ID() int
    Name() string
    Study(ICourse)
}


type GoodUser struct {
    iID int
    sName string
}

func NewGoodUser(id int, name string) IUser {
    return &GoodUser{
        iID: id,
        sName: name,
    }
}

func (me *GoodUser) ID() int {
    return me.iID
}

func (me *GoodUser) Name() string {
    return me.sName
}

func (me *GoodUser) Study(course ICourse) {
    course.SetUser(me)
    course.Study()
}

Course 抽象

通过setter方法注入IUser, ICourse接口封装了具体课程的学习过程。

package dependence_inversion

import "fmt"

type ICourse interface {
    ID() int
    Name() string
    SetUser(IUser)
    Study()
}

type GolangCourse struct {
    iID int
    sName string
    xCurrentUser IUser
}

func NewGolangCourse() ICourse {
    return &GolangCourse{
        iID: 11,
        sName: "golang",
        xCurrentUser: nil,
    }
}

func (me *GolangCourse) ID() int {
    return me.iID
}


func (me *GolangCourse) Name() string {
    return me.sName
}

func (me *GolangCourse) SetUser(user IUser) {
    me.xCurrentUser = user
}

func (me *GolangCourse) Study() {
    fmt.Printf("%v is learning %v\n", me.xCurrentUser.Name(), me.Name())
}

Good DIP test

package main

import "testing"
import (
    dip "learning/gooop/principles/dependence_inversion"
)

func TestDIP(t *testing.T) {
    bu := dip.NewBadUser(1, "Tom")
    bu.StudyGolangCourse()

    gu := dip.NewGoodUser(2, "Mike")
    gu.Study(dip.NewGolangCourse())
}
$ go test -v main/dependence_inversion_test.go 
=== RUN   TestDIP
Tom is learning golang
Mike is learning golang
--- PASS: TestDIP (0.00s)
PASS
ok      command-line-arguments  0.002s

References

  1. 那些年搞不懂的高深术语——依赖倒置•控制反转•依赖注入•面向接口编程
  2. 手撸golang 架构设计原则 依赖倒置原则
  3. Go设计模式之依赖倒置原则
  4. Go-Design-Pattern/DesighPrinciple

Public discussion