设计模式之美学习笔记19: 依赖反转原则(DIP)
2019-12-16
今天继续打卡学习极客时间上的专栏“设计模式之美”, 本篇是专栏第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,而不是低层模块数据库驱动。可以从这个角度理解依赖倒置原则,即高层模块不直接依赖底层模块,而是依赖抽象,抽象是属于高层模块的,底层模块实现了抽象,这就是依赖的反转。