面向对象设计之依赖反转原则

设计模式专栏:http://t.csdnimg.cn/4Mt4u

目录

1.引言

2.控制反转(loC)

3.依赖注入(DI)

4.依赖注入框架(DlFramework)

5.依赖反转原则(DIP)

6.总结


1.引言

前面讲到,单一职责原则和开闭原则的原理比较简单,但在实践中用好比较难,而本节要讲的依赖反转原则正好相反。依赖反转原则的使用简单,但理解较难。在进行详细介绍之前,读者可以尝试回答下列向题。

1)"依赖反转"指的是"谁与谁"的"什么依赖"被反转了?如何理解"反转"?

2)我们经常听到另外两个概念:"控制反转"和"依赖注入"。它们和"依赖反转"是一回事吗?若不是,这两个概念与"依赖反转"的区别和联系是什么?

3)如果读者熟悉 Java 语言,那么 Spring 框架中的IoC 与上述3个概念有什么关系?

2.控制反转(IoC)

首先介绍控制反转(Inversion of Control,IoC)。此处强调一下,如果读者是 Java 工程师,那么暂时不要把这里提到的IoC与 Spring 框架的 IoC 联系在一起。关于 Spring 框架的IoC,我们会在下文介绍。

我们借助一个代码示例介绍什么是控制反转。

java 复制代码
public class UserServiceTest {
    public static boolean doTest(){
        ...
    }
    public static void main(String[] args){ //这部分逻辑可以放到框架中
         if(doTest())    {
            System.out.println("Test succeed.");
         }else{
            System.out.println("Test failed.");
        }
    }
}

上面这段代码是一段没有依赖任何测试框架的测试代码,测试代码的执行流程由程序员写和控制。实际上,我们可以从中抽象出一个测试框架,代码如下所示。

java 复制代码
public abstract class TestCase {
    public void run(){
        if(doTest()){
            System.out.println("Test succeed.");
        }else{
            System.out.println("Test failed,");
        }
    }
    public abstract boolean doTest();
}

public class JunitApplication {
    private static final list<TestCase> testCases = new Arraylist<>();
    public static void register(TestCase testCase){
        testCases.add(testCase);
    }
    public static final void main(stringl] args){
        for(TestCase testCase : testCases){
            testCase.run();
        }
    }
}

在把上述简化版的测试框架引入工程中之后,程序员要想测试某个类,只需要在框架预留的扩展点,也就是TestCase 类的抽象函数 doTes0 中,填充具体的测试代码,不需要亲自负责执行流程的 main()函数。示例代码如下所示。

java 复制代码
public class UserServiceTest extends TestCase {
    @Override
    public boolean doTest(){
        ...
    }
}
//注册操作还可以通过配置的方式实现,不需要程序员显式调用        
JunitApplication.register(new UserServiceTest());

上述代码示例是通过框架实现了"控制反转"的典型用法。框架提供了一个可扩展的代码"骨架",用来组装对象和管理整个执行流程。程序员利用框架进行开发时,只需要向框架预留的扩展点中添加与自己业务相关的代码,这样就可以利用框架驱动整个程序流程的执行。这里的"控制"是指对程序执行流程的控制,而"反转"是指在没有使用框架之前,程序员自己编写代码控制整个程序流程的执行。在使用框架之后,整个程序的执行流程由框架控制,流程的控制权从程序员"反转"给了框架。

实际上,实现控制反转的方法有很多,除上面的例子中类似模板设计模式的方法以外,还有依赖注入等方法。因此,控制反转并不是一种具体的实现技巧,而是一种比较笼统的设计思想,一般用来指导框架的设计。

3.依赖注入(DI)

与控制反转相反,依赖注入(DependencyInjection,DI)是一种具体的编程技巧。依赖注入容易理解、应用简单,并且非常有用。什么是依赖注入?用一句话来概括:不通过new的方式在类内部创建依赖的类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或称为注入)给类使用。示例代码如下。

java 复制代码
public class Notification{
    private MessageSender messageSender;
    public Notification (MessageSender messageSender){
        this.messageSender = messagesender; //依赖注入,而非通过new创建
    }
    public void sendMessage(String cellphone, String message) {            
         this.messageSender.send(cellphone, message);
    }
}

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

//短信发送类
pblic 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){
        ...
    }
}
//使用Notifcation
MessageSender messageSender = new SmsSender();
Notifcation notifcation = new Notification (messageSender);

如果读者理解了上述示例代码,那么就算掌握了依赖注入这一编程技巧。在下一节中,我们会提到,依赖注入是编写可测试性代码的有效手段。

4.依赖注入框架(DIFramework)

理解了什么是"依赖注入",我们再介绍什么是"依赖注入框架"。在上面的Notifcation 类的例子中,尽管我们采用依赖注入之后,不需要以类似hardcode(爱编码)的方式在 Notifcation 类内部通过new来创建 MessageSender 对象,但是,对象创建组装(或依赖注入)的代码逻辑仍然需要程序员自己实现,只不过是被移动到了上层代码。创建、组装的代码如下所示。

java 复制代码
public class Demo {
    Public static final void main(string args[]){
        MessageSender sender = new SmsSender();//创建对象
        Notification notifcation = new Notification(sender); //依赖注入
        notifcation.sendMessage("1391894****","短信验证码:2346");
    }
}

在实际的软件开发中,有些项目可能包含几十个类、上百个类,甚至几百个类、类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是依靠程序员自己编写代码来完成,那么容易出错且开发成本较高。而对象的创建和依赖注入本身与具体的业务无关。这部分逻辑完全可以抽象成框架,由框架自动完成。实际上,这个框架就是"依赖注入框架"。

我们通过依赖注入框架提供的扩展点,简单配置所有需要创建的类对象、类之间的依赖关系,就可以实现由框架自动创建对象、管理对象的生命周期、依赖注入等。

目前,依赖注入框架有很多,如 Google Guice、Spring、PicoContainer、Butterfly Container等,有人把Sping框架称为控制反转容器(Iinversion of Contol Conainer),也有人想Spring框架称为依赖注入框架。实际上,这两种说法都没错,"控制反转容器"是一种宽泛的描述,而"依赖注入框架"这种表述更加具体。上文提到,实现控制反转的方式有很多,除依赖注入外,还有模板设计模式等,而 Spring 框架的控制反转主要是通过依赖注入实现的, 因此Spring 归为依赖注入框架更确切。

5.依赖反转原则(DIP)

最后,我们来看一下本节的主角:依赖反转原则(DependencyInversion 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. Detail depend on abstractions."对应的中文翻译为:高层模块(high-level modules)不要依赖低层模块(low-evel modules)。高层模块和低层模块应该通过抽象(abstractions)互相依赖。除此之外,抽象不要依赖具体实现细节(details),具体实现细节依赖抽象。

如何划分高层模块和低层模块?简单来说,调用者属于高层,被调用者属于低层。依反转原则主要用来指导框架的设计,与前面讲到的控制反转类似。我们以Tomcat为例,对此进行进一步解释。

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在Tomcat容器下,便可以被 Tomcat 容器调用并执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,二者都依赖同一个"抽象",也就是Servlet规范。Servlet规范不依赖具体的Tomcat容器和应用程序的实现细节,而Tomcat容器和应用程序依赖 Servlet 规范。

6.总结

依赖反转是面向对象设计的基本原则之一。这个原则强调高层模块不应该依赖于低层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这有助于降低模块间的耦合度,提高系统的可维护性和可扩展性。

依赖反转原则通常通过以下两种方式实现:

1.接口和抽象类:通过定义接口或抽象类来隐藏具体实现细节,高层模块依赖于这些接口或抽象类,而不是具体的实现类。这样,当需要更换底层实现时,只需要修改接口或抽象类的实现,而不需要修改高层模块的代码。
2.依赖注入:通过构造函数、setter方法或接口注入等方式,将依赖的对象传递给需要它的对象。这样,对象之间的依赖关系可以在运行时动态地确定,而不是在编译时硬编码在代码中。

相关推荐
拉里小猪的迷弟7 分钟前
设计模式-创建型-常用:单例模式、工厂模式、建造者模式
单例模式·设计模式·建造者模式·工厂模式
哎呦没21 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
小飞猪Jay26 分钟前
C++面试速通宝典——13
jvm·c++·面试
Kalika0-038 分钟前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
_.Switch40 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
编程、小哥哥1 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
代码雕刻家1 小时前
课设实验-数据结构-单链表-文教文化用品品牌
c语言·开发语言·数据结构
一个闪现必杀技1 小时前
Python入门--函数
开发语言·python·青少年编程·pycharm
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
龙图:会赢的1 小时前
[C语言]--编译和链接
c语言·开发语言