一文搞懂依赖翻转原则

115 阅读2分钟

在系统架构中有一个设计原则叫做「依赖翻转原则」,这里可能有些同学就有点疑惑了,依赖的关系难道不是在引入时就确定了吗,怎么还能翻转?其实这里有两个层面的关系,一个是一个是源码级别的依赖关系,一个是控制流的依赖关系

image.png 从上图可以看出,User 依赖 Permission,同时控制流和源码的依赖方向相同,但是如果引入依赖翻转,即在 User 中定义一个接口然后让 Permission 来实现,虽然此时源码的依赖方向还没变,但是控制流的方向已经改变,现在 User 开始控制 Permission 至此,我们可以引入这个原则的完整定义了

  1. 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
  2. 抽象不应该依赖细节,细节应该依赖抽象

依赖翻转的核心思想其实是「依赖抽象而不是依赖实现」这样做的好处也是显而易见的,因为实现是大概率会变动的,而抽象大概率是稳定的,所以这对系统的稳定性也是一个很大的提升

这里给出一个 Golang 的示例,我们定义一个消息发送的接口,各个业务可以根据需要自己实现,然后在上层我们也是依赖这个接口,当调用的时候传入实现接口的 struct 就可以了,上层不用关心下层的具体实现,下层也不需要关心上层的具体调用,这样就把上下层解耦了

// 定义发送消息的抽象接口
type Sender interface {
  Send(string) error
}

// 实现接口的邮件发送器
type EmailSender struct{}

func (e *EmailSender) Send(msg string) error {
  // 发送邮件的实现
  return nil 
}

// 实现接口的短信发送器  
type SMSSender struct{}

func (s *SMSSender) Send(msg string) error {
  // 发送短信的实现
  return nil
}

// 服务层依赖抽象接口发送消息
type Service struct {
  sender Sender 
}

func (s *Service) Process() {
  err := s.sender.Send("Hello World")
  // ...
}

// 主函数中注入依赖
func main() {
  emailSender := &EmailSender{}
  
  service := &Service{sender: emailSender}
  service.Process()

  smsSender := &SMSSender{}
  
  service.sender = smsSender
  service.Process()
}

综上,如果你的项目遇到了下面这些情形,那么就可以尝试使用

  1. 模块间存在紧密耦合的系统。使用依赖翻转可以降低它们之间的耦合。
  2. 需要频繁扩展或更改系统功能的项目。依赖翻转可以降低扩展带来的影响。
  3. 需要复用模块的系统。面向接口编程可以提高复用性。