「设计模式」六大原则之五:依赖倒置原则小结


「设计模式」六大原则系列链接:
「设计模式」六大原则之一:单一职责小结
「设计模式」六大原则之二:开闭职责小结
「设计模式」六大原则之三:里氏替换原则小结
「设计模式」六大原则之四:接口隔离原则小结
「设计模式」六大原则之五:依赖倒置原则小结
「设计模式」六大原则之六:最小知识原则小结

六大原则体现很多编程的底层逻辑:高内聚、低耦合、面向对象编程、面向接口编程、面向抽象编程,最终实现可读、可复用、可维护性。

设计模式的六大原则有:

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则(最少知道原则)
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则
    把这六个原则的首字母联合起来( L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。

本文介绍 SOLID 中的第五个原则:依赖倒置原则。

1.依赖倒置原则(DIP)定义

依赖倒置原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。

中文翻译有时候也叫依赖反转原则

为了追本溯源,我先给出这条原则最原汁原味的英文描述:

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

我们将它翻译成中文,大概意思就是:

  • 高层模块(high-level modules)不要依赖低层模块(low-level)。
  • 高层模块和低层模块应该通过抽象(abstractions)来互相依赖。
  • 除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。

扫描二维码关注公众号,回复: 14744334 查看本文章

在 Java 语言中,抽象指的是接口或抽象类,两都都是不能直接实例被化的,细节就是实现类,实现接口或继承抽象类而产生的类就是细节。其特点就是可以被直接实例化,也就是可以加上 new 关键字。

高层模块就是调用端,低层模块就是具体实现类。

依赖倒置原则在 Java中的体现就是:

模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系通过接口或抽象产生的。

简单点理解,我们要面向接口编程,或者是面向抽象编程

4.DIP举例说明

在项目中如何灵活的更换第三方图片加载库,或者同时共存,用户根据手机选择性使用三方库。

第一个版本,只使用一种 Picasso 加载库

private void loadImage(String url, ImageView imageView) {
    
    
    Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
}

第二个版本图片加载库,需要变化, Picasso 换成 Glide, 只需要修改这一个工具类即可

public class PicassoUtils {
    
    
    //从网络加载
    public static void loadByUrl(String url, IamgeView imageView){
    
    
        Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
    }

    //从文件加载
    public static void loadByPath(String path,ImageView imageView) {
    
    

    }
}

第三个版本,需求变化,要求两种加载库都需要,根据不同手机适配。(类似的:根据不同的手机选择不同的推送服务,方便提高消息的到达率)

此时就需要应用好面向接口编程思想了。

interface ImageLoader {
    
    
    //从网络加载
    void loadByUrl(String url, ImageView imageView);
    //从文件加载
    void loadByPath(String path, ImageView imageView);
}

//使用 Picasso
public class PicassoLoader implements ImageLoader {
    
    
    ...
}

//使用 Glide
public class GlideLoader implements ImageLoader {
    
    
    ...
}

//业务层使用,imageLoader是什么就看需求,随便创建
//也可以定义一个全局管理类,里面视需求自定义全局 PicassoLoader 或 GlideLoader
// 调用方法
ImageLoader imageLoader = new PicassoLoader() 
  //ImageLoader imageLoader = new GlideLoader()
imageLoader.loadByUrl(url, imageView);

可以看到面向接口的好处: 能够减少耦合,代码容易维护,容易拓展,因为接口是抽象的,所以不用纠结于具体的逻辑,想怎么实现都可以;又因为接口是顶层的,所以下层就多,就更容易扩展,就更灵活。

2. 如何理解控制反转(IOC)

控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

3. 如何理解依赖注入(DI)

依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。

依赖注入的英文翻译是 Dependency Injection,缩写为 DI。

那到底什么是依赖注入呢?

我们用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

在这个例子中,Notification 类负责消息推送,依赖 MessageSender 类实现推送商品促销、验证码等消息给用户。我们分别用依赖注入和非依赖注入两种方式来实现一下。具体的实现代码如下所示:

// 非依赖注入实现方式
public class Notification {
    
    
  private MessageSender messageSender;
  
  public Notification() {
    
    
    this.messageSender = new MessageSender(); //此处有点像hardcode
  }
  
  public void sendMessage(String cellphone, String message) {
    
    
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

public class MessageSender {
    
    
  public void send(String cellphone, String message) {
    
    
    //....
  }
}
// 使用Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
    
    
  private MessageSender messageSender;
  
  // 通过构造函数将messageSender传递进来
  public Notification(MessageSender messageSender) {
    
    
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    
    
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。这一点在我们之前讲“开闭原则”的时候也提到过。

当然,上面代码还有继续优化的空间,我们还可以把 MessageSender 定义成接口,基于接口而非实现编程。改造后的代码如下所示:

public class Notification {
    
    
  private MessageSender messageSender;
  
  public Notification(MessageSender messageSender) {
    
    
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    
    
    this.messageSender.send(cellphone, message);
  }
}

public interface MessageSender {
    
    
  void send(String cellphone, String message);
}

// 短信发送类
public class SmsSender implements MessageSender {
    
    
  @Override
  public void send(String cellphone, String message) {
    
    
    //....
  }
}

// 站内信发送类
public class InboxSender implements MessageSender {
    
    
  @Override
  public void send(String cellphone, String message) {
    
    
    //....
  }
}

//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

上面例子是依赖注入的典型用法。

4. 小结

“基于接口而非实现编程”与“依赖注入”的联系是二者都是从外部传入依赖对象而不是在内部去new一个出来。

区别是“基于接口而非实现编程”强调的是“接口”,强调依赖的对象是接口,而不是具体的实现类;

而“依赖注入”不强调这个,类或接口都可以,只要是从外部传入不是在内部new出来都可以称为依赖注入。

参考:

《设计模式之美》

《重学 Java 设计模式》

《Android 源码设计模式解析与实战》

设计模式之基-六大设计原则

猜你喜欢

转载自blog.csdn.net/jun5753/article/details/126878824
今日推荐