今天继续打卡学习极客时间上的专栏“设计模式之美”, 本篇是专栏第19节的学习笔记,介绍面向对象设计原则中的依赖反转原则(DIP)。

笔记

面向对象有很多经典的设计原则:

  • SOLID5原则分别是
    • 单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)、依赖反转原则(DIP)
  • KISS - Keep It Simple, Stupid
  • YAGNI - You aren't gonna need it
  • DRY - Don't Repeat Yourself
  • LOD - Law of Demeter(迪米特法则,又叫做最少知识原则)

依赖反转原则

依赖反转(DIP)与控制反转(IOC)、依赖注入(DI)有什么区别和联系呢?

控制反转(IOC): 控制指的是对程序执行流程的控制,而反转指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员"反转"到了框架。实现控制反转的方法有很多,如使用模板设计模式的框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行过程,也可以使用依赖注入实现。从这点可以看出控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

依赖注入(DI): 是一种具体的编程技巧,依赖注入是指不通过new()的方式在类内部创建依赖对象,而是将依赖的类对象在外部创建好后,通过构造函数、方法传参等方式传递(注入)给类使用。通过依赖注入的方式将依赖的对象注入到对象内部,可以提高代码的扩展性,我们可以灵活的替换依赖的类。可以在类的内部把它依赖的对象引用定义成接口,基于接口而非实现编程,这样可以通过依赖注入的方式轻松替换依赖具体的实现。

依赖注入框架(DI Framework): 依赖注入框架提供了配置所需创建对象、对象与对象之间依赖关系的扩展点,一般只需简单配置即可实现由框架自动创建对象、管理对象的生命周期、依赖注入等功能。Java里面比较流行的DI框架: Spring Framework、Google Guice。

依赖反转原则(DIP, Dependency Inversion Principle),也叫依赖倒置原则。 高层模块不要依赖底层模块。高层模块和底层模块应该通过抽象来实现依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

代码重写

用Go重写文中的Notification的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"fmt"
)

type MessageSender interface {
	send(cellphone string, message string) error
}

// SmsSender 短信发送
type SmsSender struct {
}

func (s *SmsSender) send(cellphone string, message string) error {
	fmt.Println("send sms")
	return nil
}

// InboxSender 站内消息发送
type InboxSender struct {
}

func (s *InboxSender) send(cellphone string, message string) error {
	fmt.Println("send inbox")
	return nil
}

type Notification struct {
	MessageSender MessageSender
}

func (n *Notification) sendMessage(cellphone string, message string) error {
	fmt.Println("notifycation user")
	return n.MessageSender.send(cellphone, message)
}

func main() {
	var messageSender MessageSender = &SmsSender{}
	notifaction := &Notification{
		MessageSender: messageSender,
	}
	notifaction.sendMessage("13888888888", "hello")
}

更进一步可以将依赖注入逻辑放到一个容器实例内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Container struct {
	messageSender MessageSender
	notification  *Notification
}

func (c *Container) GetMessageSender() MessageSender {
	if c.messageSender == nil {
		c.messageSender = &SmsSender{}
	}
	return c.messageSender
}

func (c *Container) GetNotification() *Notification {
	if c.notification == nil {
		c.notification = &Notification{
			MessageSender: c.GetMessageSender(),
		}
	}
	return c.notification
}

func main() {
	container := &Container{}
	notification := container.GetNotification()
	notification.sendMessage("13888888888", "hello")
}

再更进一步可以选型一些DI框架,如https://github.com/uber-go/fx

个人理解

依赖反转(倒置)原则DIP: 高层模块不应该依赖低层模块,高层模块和低层模块通过抽象来实现依赖,而抽象不应该依赖具体实现,具体实现应该依赖抽象。举个例子用Java开发的数据库访问层模块代码并不直接依赖于数据库驱动,而是依赖于JDBC这个抽象,不同数据库驱动都实现了JDBC,这里面数据访问层代码即为高层模块,数据库驱动为低层模块,高层模块依赖的是抽象JDBC,而不是低层模块数据库驱动。可以从这个角度理解依赖倒置原则,即高层模块不直接依赖底层模块,而是依赖抽象,抽象是属于高层模块的,底层模块实现了抽象,这就是依赖的反转。

参考