通常,后一种情况是通过构造函数参数或 setter 方法处理的。然而,这并不一定正确——我知道 Java 中 Spring DI 框架的最新版本,例如,允许您注释私有字段(例如使用 @Autowired) ,并且依赖关系将通过反射设置,而不需要通过任何类公共方法/构造函数公开依赖关系。这可能就是你想要的解决方案。
在理想的情况下,当处理对象时,无论如何都要通过接口与它们交互,而且这样做的次数越多(并且通过 DI 连接了依赖关系) ,实际上就越少需要自己处理构造函数。在理想的情况下,你的代码不会处理或者甚至不会创建类的具体实例; 所以它只是通过 DI 获得一个 IFoo,而不用担心 FooImpl的构造函数指示它需要做什么,事实上甚至不需要知道 FooImpl的存在。从这个角度来看,封装是完美的。
这当然是一种观点,但在我看来 DI 并不一定违反封装,事实上它可以通过将所有必要的内部知识集中到一个地方来帮助它。这不仅本身是一件好事,而且更好的是,这个地方在您自己的代码库之外,所以您编写的代码都不需要了解类的依赖关系。
我也在纠结这个想法。起初,使用 DI 容器(如 Spring)来实例化一个对象的“需求”让人感觉像是跳过了一个圈。但在现实中,它真的不是一个箍-它只是另一个“发布”的方式来创建我需要的对象。当然,封装是“破碎的”,因为“类之外”的人知道它需要什么,但真正知道这一点的并不是系统的其他部分——而是 DI 容器。没有什么神奇的事情会因为 DI“知道”一个对象需要另一个而发生不同。
但是如果你正在建造一辆需要某种引擎对象的汽车,那么你就有了一个外部依赖项。您可以在 Car 对象的构造函数内部实例化这个引擎——例如 new GMOverHeadCamEngine ()——保留封装,但是创建一个更加隐蔽的耦合到具体类 GMOverHeadCamEngine,或者您可以注入它,允许 Car 对象以不可知的方式(并且更加健壮地)操作,例如没有具体依赖关系的接口 IEngine。无论你是使用 IOC 容器还是简单的 DI 来实现这一点都不重要——重要的是你的车可以使用多种引擎而不需要与其中任何一种耦合,从而使你的代码库更加灵活,不容易产生副作用。
DI 并没有违反封装,它是在几乎每个 OOP 项目中,当封装必然被打破时,将耦合最小化的一种方法。在外部将依赖项注入到接口中可以最大限度地减少耦合副作用,并允许您的类对实现保持不可知性。
这使得代码更难重用。客户端可以使用的模块无需显式提供依赖项,显然比客户端必须以某种方式发现组件的依赖项是什么,然后以某种方式使它们可用的模块更容易使用。例如,最初创建用于 ASP 应用程序的组件可能期望由 DI 容器提供其依赖项,DI 容器为对象实例提供与客户端 http 请求相关的生存期。这可能不容易在另一个客户机中重现,因为它没有与原始 ASP 应用程序相同的内置 DI 容器。
根据我的经验,使用 DI 的主要问题是,无论你是从内置 DI 的应用框架开始,还是在代码库中添加 DI 支持,出于某种原因,人们认为既然你有 DI 支持,那么实例化 一切的正确方式就一定是 DI 支持。他们甚至从来没有问过这样的问题: “这种依赖性需要外部指定吗?”.更糟糕的是,他们还开始试图强迫其他人也使用 一切的 DI 支持。
这样做的结果是,你的代码库开始不可避免地进入这样一种状态: 在你的代码库中创建任何东西的实例都需要大量的钝 DI 容器配置,而调试任何东西都是两倍的难度,因为你有额外的工作量去尝试确定任何东西是如何以及在哪里被实例化的。
DI 违反了对非共享对象的封装-句号。共享对象在所创建的对象之外具有生命周期,因此必须聚合到所创建的对象中。对于正在创建的对象来说是私有的对象应该被组合到创建的对象中——当创建的对象被破坏时,它会带走组合的对象。
让我们以人体为例。什么是组成的,什么是聚合的。如果我们使用 DI,人体构造函数将有100个对象。例如,许多器官(可能)是可替换的。但是,他们仍然组成了身体。血细胞每天在体内产生(并被破坏) ,不需要外界的影响(蛋白质除外)。因此,血细胞是由身体内部产生的-新的血细胞()。
DI 的支持者认为对象应该永远不要使用新的操作符。
这种“纯粹主义”的方法不仅违反了封装,而且也违反了创建对象的人的 Liskov代换原则。