Wil*_*ill 2 java language-agnostic oop design-patterns dependency-injection
许多架构师和工程师推荐使用依赖注入和其他控制反转模式来提高代码的可测试性。不可否认,依赖注入使代码更具可测试性,但是,一般来说,它不也是抽象的一个完整目标吗?
我觉得很矛盾!我写了一个例子来说明这一点;它不是超现实的,我不会这样设计,但我需要一个具有多个依赖项的类结构的快速而简单的示例。第一个例子没有依赖注入,第二个例子使用注入依赖。
非 DI 示例
package com.stackoverflow.di;
public class EmployeeInventoryAnswerer()
{
/* In reality, at least the store name and product name would be
* passed in, but this example can't be 8 pages long or the point
* may be lost.
*/
public void myEntryPoint()
{
Store oaklandStore = new Store('Oakland, CA');
StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore);
Product fancyNewProduct = new Product('My Awesome Product');
if (inventoryManager.isProductInStock(fancyNewProduct))
{
System.out.println("Product is in stock.");
}
}
}
public class StoreInventoryManager
{
protected Store store;
protected InventoryCatalog catalog;
public StoreInventoryManager(Store store)
{
this.store = store;
this.catalog = new InventoryCatalog();
}
public void addProduct(Product product, int quantity)
{
this.catalog.addProduct(this.store, product, quantity);
}
public boolean isProductInStock(Product product)
{
return this.catalog.isInStock(this.store, this.product);
}
}
public class InventoryCatalog
{
protected Database db;
public InventoryCatalog()
{
this.db = new Database('productReadWrite');
}
public void addProduct(Store store, Product product, int initialQuantity)
{
this.db.query(
'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d'
).format(
store.id, product.id, initialQuantity
);
}
public boolean isInStock(Store store, Product product)
{
QueryResult qr;
qr = this.db.query(
'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d'
).format(
store.id, product.id
);
if (qr.quantity.toInt() > 0)
{
return true;
}
return false;
}
}
Run Code Online (Sandbox Code Playgroud)
依赖注入示例
package com.stackoverflow.di;
public class EmployeeInventoryAnswerer()
{
public void myEntryPoint()
{
Database db = new Database('productReadWrite');
InventoryCatalog catalog = new InventoryCatalog(db);
Store oaklandStore = new Store('Oakland, CA');
StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore, catalog);
Product fancyNewProduct = new Product('My Awesome Product');
if (inventoryManager.isProductInStock(fancyNewProduct))
{
System.out.println("Product is in stock.");
}
}
}
public class StoreInventoryManager
{
protected Store store;
protected InventoryCatalog catalog;
public StoreInventoryManager(Store store, InventoryCatalog catalog)
{
this.store = store;
this.catalog = catalog;
}
public void addProduct(Product product, int quantity)
{
this.catalog.addProduct(this.store, product, quantity);
}
public boolean isProductInStock(Product product)
{
return this.catalog.isInStock(this.store, this.product);
}
}
public class InventoryCatalog
{
protected Database db;
public InventoryCatalog(Database db)
{
this.db = db;
}
public void addProduct(Store store, Product product, int initialQuantity)
{
this.db.query(
'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d'
).format(
store.id, product.id, initialQuantity
);
}
public boolean isInStock(Store store, Product product)
{
QueryResult qr;
qr = this.db.query(
'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d'
).format(
store.id, product.id
);
if (qr.quantity.toInt() > 0)
{
return true;
}
return false;
}
}
Run Code Online (Sandbox Code Playgroud)
(如果您有任何想法,请让我的示例变得更好!这可能不是最好的示例。)
在我的示例中,我觉得通过EmployeeInventoryAnswerer了解StoreInventoryManager.
不应该EmployeeInventoryAnswerer有这样的观点,“好吧,我就拿一个StoreInventoryManager,给它客户正在寻找的产品的名称,以及我想查看的商店,它会告诉我该产品是否有库存.” ? 难道它不应该对Databases 或InventoryCatalogs一无所知,从它的角度来看,这是一个它不需要关心的实现细节吗?
那么,具有注入依赖项的可测试代码与作为抽象原则的信息隐藏之间的平衡在哪里?即使中间类只是传递依赖,仅构造函数签名就揭示了不相关的细节,对吧?
更现实地说,假设这是一个长期运行的后台应用程序处理来自 DBMS 的数据;在调用图的哪个“层”创建和传递数据库连接器是合适的,同时仍然使您的代码在没有运行 DBMS 的情况下可测试?
我非常有兴趣在这里学习 OOP 理论和实用性,以及澄清 DI 和信息隐藏/抽象之间似乎存在的悖论。
Ste*_*ven 11
该依赖倒置原则,更具体地说,依赖注入解决的化妆应用程序代码问题如何松耦合。这意味着在许多情况下,您希望防止应用程序中的类依赖于其他具体类型,以防这些依赖类型包含volatile行为。易失性依赖是一种依赖,除其他外,与进程外资源通信、不确定或需要可替换。与 volatile 依赖关系的紧密耦合会阻碍可测试性,但也会限制应用程序的可维护性和灵活性。
但是无论你做什么,无论你引入了多少抽象,在你的应用程序的某个地方你需要依赖于一个具体的类型。所以你不能完全摆脱这种耦合——但这应该不是问题:100% 抽象的应用程序也是 100% 无用的。
这意味着您希望减少应用程序中类和模块之间的耦合量,最好的方法是在应用程序中拥有一个依赖所有具体类型的地方,并为您实例化它。这是最有益的,因为:
您连接所有东西的地方应该在您的入口点程序集中。它应该是入口点程序集,因为无论如何该程序集已经依赖于所有其他程序集,因此它已经成为应用程序中最不稳定的部分。
根据稳定依赖原则( 2 ),依赖关系应该指向稳定性的方向,并且由于应用程序中组成对象图的部分将是最不稳定的部分,因此不应依赖于它。这就是为什么您编写对象图的这个地方应该在您的入口点程序集中的原因。
您在其中组合对象图的应用程序中的这个入口点通常称为Composition Root。
如果您觉得不EmployeeInventoryAnswerer应该了解数据库和InventoryCatalogs,则可能EmployeeInventoryAnswerer是混合基础结构逻辑(以构建对象图)和应用程序逻辑的情况。换言之,它可能违反了单一职责原则。在这种情况下,您EmployeeInventoryAnswerer不应该成为切入点。相反,您应该有一个不同的入口点,并且EmployeeInventoryAnswerer应该只StoreInventoryManager注入一个。您的新入口点可以从构建对象图开始EmployeeInventoryAnswerer并调用其AnswerInventoryQuestion方法(或您决定调用的任何方法)。
具有注入依赖项的可测试代码与作为抽象原则的信息隐藏之间的平衡在哪里?
构造函数是一个实现细节。只有 Composition Root 知道具体类型,因此,它是唯一调用这些构造函数的人。由于注入消费者的依赖应该是抽象的,消费者对实现一无所知,因此实现不可能向消费者泄露任何信息。另一方面,如果抽象本身会泄漏实现细节,则会违反依赖倒置原则。如果消费者将依赖强制转换回实现,它反过来会违反Liskov 替换原则。
但是,即使您有一个依赖于具体组件的使用者,该组件仍然可以进行信息隐藏——它不必通过公共属性公开自己的依赖项(或其他值)。并且这个组件有一个构造函数来接收组件的依赖,这并没有使它违反信息隐藏,因为不可能通过它的构造函数检索组件的依赖(只能通过构造函数插入依赖;不能接收它们) )。并且您无法更改组件的依赖项,因为该组件本身将被注入到使用者中,并且您无法在已创建的实例上调用构造函数。
在我看来,这里没有平衡。这是正确应用SOLID原则的问题,因为如果不应用 SOLID 原则,无论如何您都会处于糟糕的境地(从可维护性的角度来看)——而 SOLID 原则的应用无疑会导致依赖注入。
在调用图的哪个“层”适合创建和传递数据库连接器
至少,入口点知道数据库连接,因为它只是应该从配置文件中读取的入口点。从配置文件中读取应该预先在一个地方完成。这允许应用程序在配置错误时快速失败,并防止您读取分散在整个应用程序中的配置文件。
但是入口点是否应该负责创建数据库连接,这取决于很多因素。我通常ConnectionFactory对此有某种抽象,但 YMMV。
更新
我不想将 Context 或 AppConfig 传递给所有内容并最终传递不需要的依赖项类
传递类本身不需要的依赖项通常不是最佳解决方案,并且可能表明您违反了依赖倒置原则并应用了Control Freak 反模式。这是此类问题的示例:
public class Service : IService
{
private IOtherService otherService;
public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) {
this.otherService = new OtherService(dep1, dep2, dep3);
}
}
Run Code Online (Sandbox Code Playgroud)
这里你可以看到一类Service是发生在3依赖性,但它不使用它们的全部。它只将它们转发给OtherService它创建的 的构造函数。当OtherService不是本地的Service(即位于不同的模块或层中)时,这意味着Service违反了依赖倒置原则——Service现在与 紧密耦合OtherService。相反,这Service应该是这样的:
public class Service : IService
{
private IOtherService otherService;
public Service(IOtherService otherService) {
this.otherService = otherService;
}
}
Run Code Online (Sandbox Code Playgroud)
这里Service只考虑它真正需要的东西,不依赖于任何具体的类型。
但我也不想将相同的 4 件事传递给几个不同的类
如果您有一组通常一起注入到消费者中的依赖项,那么变化就是您违反了单一职责原则:消费者可能做的太多——知道的太多。
有几种解决方案,具体取决于手头的问题。我想到的一件事是重构为 Facade Services。
也可能是那些注入的依赖项是横切关注点。透明地应用横切关注点通常要好得多,而不是将其注入数十或数百个消费者(这违反了开放/封闭原则)。您可以为此使用装饰器设计模式、责任链设计模式或动态拦截。