今天继续打卡学习极客时间上的专栏“设计模式之美”, 本篇是专栏第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的例子

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type MessageSender interface {
 8	send(cellphone string, message string) error
 9}
10
11// SmsSender 短信发送
12type SmsSender struct {
13}
14
15func (s *SmsSender) send(cellphone string, message string) error {
16	fmt.Println("send sms")
17	return nil
18}
19
20// InboxSender 站内消息发送
21type InboxSender struct {
22}
23
24func (s *InboxSender) send(cellphone string, message string) error {
25	fmt.Println("send inbox")
26	return nil
27}
28
29type Notification struct {
30	MessageSender MessageSender
31}
32
33func (n *Notification) sendMessage(cellphone string, message string) error {
34	fmt.Println("notifycation user")
35	return n.MessageSender.send(cellphone, message)
36}
37
38func main() {
39	var messageSender MessageSender = &SmsSender{}
40	notifaction := &Notification{
41		MessageSender: messageSender,
42	}
43	notifaction.sendMessage("13888888888", "hello")
44}

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

 1type Container struct {
 2	messageSender MessageSender
 3	notification  *Notification
 4}
 5
 6func (c *Container) GetMessageSender() MessageSender {
 7	if c.messageSender == nil {
 8		c.messageSender = &SmsSender{}
 9	}
10	return c.messageSender
11}
12
13func (c *Container) GetNotification() *Notification {
14	if c.notification == nil {
15		c.notification = &Notification{
16			MessageSender: c.GetMessageSender(),
17		}
18	}
19	return c.notification
20}
21
22func main() {
23	container := &Container{}
24	notification := container.GetNotification()
25	notification.sendMessage("13888888888", "hello")
26}

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

个人理解

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

参考