在系统架构中有一个设计原则叫做「依赖翻转原则」,这里可能有些同学就有点疑惑了,依赖的关系难道不是在引入时就确定了吗,怎么还能翻转?其实这里有两个层面的关系,一个是一个是源码级别的依赖关系,一个是控制流的依赖关系
从上图可以看出,User 依赖 Permission,同时控制流和源码的依赖方向相同,但是如果引入依赖翻转,即在 User 中定义一个接口然后让 Permission 来实现,虽然此时源码的依赖方向还没变,但是控制流的方向已经改变,现在 User 开始控制 Permission
至此,我们可以引入这个原则的完整定义了
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象
依赖翻转的核心思想其实是「依赖抽象而不是依赖实现」这样做的好处也是显而易见的,因为实现是大概率会变动的,而抽象大概率是稳定的,所以这对系统的稳定性也是一个很大的提升
这里给出一个 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()
}
综上,如果你的项目遇到了下面这些情形,那么就可以尝试使用
- 模块间存在紧密耦合的系统。使用依赖翻转可以降低它们之间的耦合。
- 需要频繁扩展或更改系统功能的项目。依赖翻转可以降低扩展带来的影响。
- 需要复用模块的系统。面向接口编程可以提高复用性。