9.1 依赖倒置
请记住,我们曾经说过,在服务器应用程序中,您不能只通过创建流new Thread().start()
?只有容器应该创建线程。我们现在将进一步发展这个想法。
所有对象也应该只由容器创建。当然,我们不是在谈论所有对象,而是在谈论所谓的业务对象。它们通常也被称为垃圾箱。这种方法的支柱源于 SOLID 的第五条原则,该原则要求摆脱类并转向接口:
- 顶级模块不应该依赖于低级模块。那些和其他人都应该依赖于抽象。
- 抽象不应依赖于细节。实现必须依赖于抽象。
模块不应该包含对特定实现的引用,它们之间的所有依赖关系和交互都应该完全建立在抽象(即接口)的基础上。这条规则的精髓可以用一句话来表达:所有的依赖关系都必须是接口的形式。
尽管它的基本性质和表面上的简单性,这条规则最常被违反。即,每次我们在程序/模块的代码中使用new操作符,创建一个特定类型的新对象时,就不再依赖于接口,而是形成了对实现的依赖。
很明显,这是无法避免的,必须在某处创建对象。但是,至少,您需要尽量减少执行此操作的地方以及明确指定类的地方的数量,并本地化和隔离这些地方,以免它们分散在整个程序代码中。
一个很好的解决方案是在专门的对象和模块中集中创建新对象的疯狂想法——工厂、服务定位器、IoC 容器。
从某种意义上说,这样的决定遵循单一选择原则,即:“当软件系统必须支持多种选择时,它们的完整列表应该只为系统的一个模块所知”。
因此,如果将来有必要添加新的选项(或新的实现,就像我们正在考虑的创建新对象的情况一样),那么只更新包含此信息的模块就足够了,而所有其他模块将保持不受影响,并能够像往常一样继续他们的工作。
示例 1
JDK 为您提供叶的正确实现而不是new ArrayList
编写类似的东西是有意义的:ArrayList、LinkedList,甚至是 ConcurrentList。List.new()
例如,编译器发现有来自不同线程的对对象的调用,并在那里放置一个线程安全的实现。或者sheet中间insert太多,那么会基于LinkedList来实现。
示例 2
例如,这已经发生在各种情况下。您上次编写排序算法来对集合进行排序是什么时候?取而代之的是,现在大家都使用方法Collections.sort()
,而且集合的元素必须支持 Comparable 接口(comparable)。
如果sort()
你将少于 10 个元素的集合传递给该方法,则很有可能使用冒泡排序(Bubble sort)对其进行排序,而不是 Quicksort。
示例 3
编译器已经在观察您如何连接字符串并将您的代码替换为StringBuilder.append()
.
9.2 实践中的依赖倒置
现在最有趣的是:让我们考虑一下如何将理论与实践结合起来。模块如何正确地创建和接收它们的“依赖关系”而不违反依赖倒置?
为此,在设计模块时,您必须自己决定:
- 模块做什么,执行什么功能;
- 然后模块需要从它的环境中获取,也就是说,它必须处理哪些对象/模块;
- 他将如何得到它?
为了遵守依赖倒置的原则,您肯定需要决定您的模块使用哪些外部对象以及它将如何获取对它们的引用。
这里有以下选项:
- 模块本身创建对象;
- 模块从容器中获取对象;
- 该模块不知道对象来自哪里。
问题在于创建对象需要调用特定类型的构造函数,这样一来,模块将不依赖于接口,而是依赖于具体的实现。但是如果我们不想在模块代码中显式地创建对象,那么我们可以使用工厂方法模式。
“最重要的是,我们不是通过 new 直接实例化一个对象,而是为客户端类提供一些接口来创建对象。由于这样的接口总是可以被正确的设计覆盖,我们在使用低级模块时获得了一些灵活性在高级模块中”。
在需要创建相关对象组或系列的情况下,使用抽象工厂代替工厂方法。
9.3 使用服务定位器
该模块从已经拥有它们的人那里获取必要的对象。假设系统有一些对象存储库,模块可以在其中“放置”它们的对象并从存储库中“取出”对象。
这种方法是通过服务定位器模式实现的,其主要思想是程序有一个对象,它知道如何获取所有可能需要的依赖项(服务)。
与工厂的主要区别在于服务定位器不创建对象,但实际上已经包含实例化对象(或者知道在哪里/如何获取它们,如果它创建,那么在第一次调用时只创建一次)。每次调用时,工厂都会创建一个新对象,您拥有该对象的全部所有权,您可以随心所欲地使用它。
重要!服务定位器生成对相同的已存在对象的引用。因此,您需要非常小心服务定位器发布的对象,因为其他人可以与您同时使用它们。
Service Locator 中的对象可以直接通过配置文件来添加,确实以任何方便程序员的方式添加。服务定位器本身可以是具有一组静态方法的静态类、单例或接口,并且可以通过构造函数或方法传递给所需的类。
服务定位器有时被称为反模式并且不被鼓励(因为它创建隐式连接并且只给出良好设计的外观)。您可以从 Mark Seaman 那里了解更多信息:
9.4 依赖注入
该模块根本不关心“挖掘”依赖项。它只决定它需要做什么,而所有必要的依赖项都是由其他人从外部提供(引入)的。
这就是所谓的 -依赖注入。通常,所需的依赖项作为构造函数参数(构造函数注入)或通过类方法(Setter 注入)传递。
这种方法颠倒了创建依赖项的过程——而不是模块本身,依赖项的创建是由外部人员控制的。来自对象主动发射器的模块变为被动 - 不是他创造,而是其他人为他创造。
这种方向的改变被称为控制反转,或好莱坞原则——“不要打电话给我们,我们会打电话给你。”
这是最灵活的解决方案,给了模块最大的自主权。可以说,只有它完全贯彻了“单一职责原则”——模块应该完全专注于做好它的工作,而不用担心其他任何事情。
为模块提供工作所需的一切是一个单独的任务,应该由适当的“专家”处理(通常是某个容器,一个 IoC 容器,负责管理依赖项及其实现)。
其实这里的一切都和生活一样:组织严密的公司里,程序员编程,办公桌、电脑等工作所需的一切都是由办公室经理购买和提供的。或者,如果您使用程序作为构造函数的比喻,那么模块不应该考虑电线,其他人参与组装构造函数,而不是零件本身。
不夸张地说,使用接口来描述模块之间的依赖关系(Dependency Inversion)+这些依赖关系的正确创建和注入(主要是Dependency Injection)是解耦的关键技术。
它们是代码松散耦合、灵活性、抗更改性、重用性的基础,没有它们,所有其他技术都毫无意义。这是松散耦合和良好架构的基础。
Martin Fowler 详细讨论了控制反转的原理(连同依赖注入和服务定位器)。他的两篇文章都有翻译:“Inversion of Control Containers and the Dependency Injection pattern”和“Inversion of Control”。
GO TO FULL VERSION