依赖注入和抽象之间的平衡点在哪里?
Where is the Balance Between Dependency Injection and Abstraction?
许多建筑师和工程师总体上推荐 Dependency Injection and other Inversion of Control patterns as a way to improve the testability of your code. There's no denying that Dependency Injection makes code more testable, however, isn't it also a completing goal to Abstraction?
我很矛盾!我写了一个例子来说明这一点;它不是超现实的,我不会以这种方式设计它,但我需要一个具有多个依赖项的 class 结构的快速简单示例。第一个示例没有依赖注入,第二个示例使用了注入依赖。
非 DI 示例
package com.Whosebug.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;
}
}
依赖注入示例
package com.Whosebug.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;
}
}
(如果有什么想法,请把我的例子做得更好!这可能不是最好的例子。)
在我的示例中,我觉得抽象已被 EmployeeInventoryAnswerer
完全违反 StoreInventoryManager
的底层实现细节。
不应该EmployeeInventoryAnswerer
有"Okay, I'll just grab a StoreInventoryManager
, give it the name of the product the customer is looking for, and what store I want to check, and it will tell me if the product is in stock."的视角吗?它不应该不知道关于 Database
s 或 InventoryCatalog
s 的任何事情吗,从它的角度来看,这是一个它不需要关心的实现细节?
那么,注入依赖项的可测试代码与作为抽象原则的信息隐藏之间的平衡点在哪里?即使中间 classes 只是传递依赖项,构造函数签名本身也会揭示不相关的细节,对吗?
更现实,假设这是一个长期运行后台应用程序处理来自 DBMS 的数据;在调用图的 "layer" 处创建和传递数据库连接器是否合适,同时仍然使您的代码无需 运行 DBMS 即可测试?
我非常有兴趣在这里学习 OOP 理论和实践,以及澄清 DI 和 Information 之间看似矛盾的东西 Hiding/Abstraction。
Dependency Inversion Principle and, more specifically, Dependency Injection tackle the problem of how make application code loosely coupled. This means that in many cases you want to prevent the classes in your application from depending on other concrete types, in case those dependent types contain volatile behavior。易失性依赖项是一种依赖项,除其他外,它与进程外资源进行通信,是不确定的或需要可替换的。与易变依赖项的紧密耦合会阻碍可测试性,但也会限制应用程序的可维护性和灵活性。
但是无论您做什么,也无论您引入了多少抽象,您都需要在应用程序的某处依赖具体类型。所以你不能完全摆脱这种耦合——但这应该不是问题:一个 100% 抽象的应用程序也是 100% 无用的。
这意味着你想减少应用程序中 classes 和模块之间的耦合量,最好的方法是在应用程序中有一个地方依赖于所有具体类型,并将为您实例化。这是最有益的,因为:
- 您将在应用程序中只有一个位置知道对象图的组成,而不是将这些知识分散在整个应用程序中
- 如果您想更改实现或 intercept/decorate 个实例以应用横切关注点,您将只有一个地方可以更改。
连接所有内容的地方应该在入口程序集中。它应该是入口点程序集,因为这个程序集已经依赖于所有其他程序集,使其已经成为您应用程序中最不稳定的部分。
根据 Stable-Dependencies Principle (2) 依赖项应指向稳定性方向,并且由于您编写对象图的应用程序部分将是最易变的部分,因此不应依赖于它。这就是为什么您编写对象图的地方应该在您的入口点程序集中。
在您编写对象图的应用程序中,这个入口点通常称为 Composition Root。
如果您认为 EmployeeInventoryAnswerer
和 InventoryCatalogs
不应该对数据库一无所知,那么 EmployeeInventoryAnswerer
可能正在混合基础架构逻辑(以构建对象图) 和应用程序逻辑。换句话说,它可能违反了 Single Responsibility Principle。在那种情况下,您的 EmployeeInventoryAnswerer
不应该是入口点。相反,您应该有一个不同的入口点,并且 EmployeeInventoryAnswerer
应该只注入 StoreInventoryManager
。您的新入口点可以构建以 EmployeeInventoryAnswerer
开头的对象图并调用其 AnswerInventoryQuestion
方法(或您决定调用它的任何内容)。
where's the balance between testable code with injected dependencies,
and information-hiding as a principal of abstraction?
构造函数是一个实现细节。只有 Composition Root 知道具体类型,因此,它是唯一调用这些构造函数的。当消费 class 依赖抽象作为其 incoming/injected 依赖项(例如,通过将其构造函数参数指定为抽象)时,消费者对实现一无所知,这更容易防止将实现细节泄露给消费者.如果抽象本身会泄露实现细节,另一方面,它会违反 Dependency Inversion Principle. And if the consumer would decide to cast the dependency back to the implementation, it would in turn violate the Liskov Substitition Principle。两种违规行为都应避免。
但即使您有一个依赖于具体组件的消费者,该组件仍然可以进行信息隐藏——它不必通过 public 公开自己的依赖项(或其他值)特性。事实上,这个组件有一个接受组件依赖的构造函数,并没有违反信息隐藏,因为不可能通过它的构造函数检索组件的依赖(你只能通过构造函数插入依赖;而不是接收它们).而且您不能更改组件的依赖项,因为该组件本身将被注入到消费者中,并且您不能在已经创建的实例上调用构造函数。
在我看来,在谈论“平衡”时,您提供了一个错误的选择。相反,这是正确应用 SOLID 原则的问题,因为如果不应用 SOLID 原则,无论如何(从可维护性的角度来看)你都会处于一个糟糕的地方——并且 SOLID 原则的应用无疑会导致依赖注入.
at what "layer" of the call-graph is it appropriate to create and pass around a database connector
至少,入口点知道数据库连接,因为它只是应该从配置文件中读取的入口点。从配置文件中读取应该预先在一个地方完成。这允许应用程序在配置错误时快速失败,并防止您读取分散在整个应用程序中的配置文件。
但是入口点是否应该负责创建数据库连接,这取决于很多因素。我通常对此有某种 ConnectionFactory
抽象,但是 YMMV。
更新
I don't want to pass around a Context or an AppConfig to everything and end up passing dependencies classes don't need
传递 class 本身不需要的依赖项通常不是最佳解决方案,并且可能表明您违反了依赖倒置原则并应用了 Control Freak anti-pattern。这是此类问题的示例:
public class Service : ServiceAbs
{
private IOtherService otherService;
public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) {
this.otherService = new OtherService(dep1, dep2, dep3);
}
}
在这里你看到一个 class Service
接受了 3 个依赖项,但它 根本没有使用它们 。它只将它们转发给它创建的 OtherService
的构造函数。当 OtherService
不是 local 到 Service
(即位于不同的模块或层)时,这意味着 Service
违反了依赖倒置原则—— Service
现在与 OtherService
紧密耦合。相反,Service
应该是这样的:
public class Service : IService
{
private IOtherService otherService;
public Service(IOtherService otherService) {
this.otherService = otherService;
}
}
这里Service
只接受它真正需要的,不依赖于任何具体类型。
but I also don't want to pass the same 4 things to several different classes
如果您有一组依赖项,这些依赖项通常一起注入到一个消费者中,则改变是您违反了单一职责原则:消费者可能做的太多了——知道的太多了。
根据手头的问题,有多种解决方案。我想到的一件事是 refactoring to Facade Services.
也可能是那些注入的依赖项是横切关注点。透明地应用横切关注点通常要好得多,而不是将其注入数十或数百个消费者(这违反了 Open/Closed 原则)。您可以为此使用 Decorator design pattern, Chain-of-Responsibility design pattern 或动态拦截。
许多建筑师和工程师总体上推荐 Dependency Injection and other Inversion of Control patterns as a way to improve the testability of your code. There's no denying that Dependency Injection makes code more testable, however, isn't it also a completing goal to Abstraction?
我很矛盾!我写了一个例子来说明这一点;它不是超现实的,我不会以这种方式设计它,但我需要一个具有多个依赖项的 class 结构的快速简单示例。第一个示例没有依赖注入,第二个示例使用了注入依赖。
非 DI 示例
package com.Whosebug.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;
}
}
依赖注入示例
package com.Whosebug.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;
}
}
(如果有什么想法,请把我的例子做得更好!这可能不是最好的例子。)
在我的示例中,我觉得抽象已被 EmployeeInventoryAnswerer
完全违反 StoreInventoryManager
的底层实现细节。
不应该EmployeeInventoryAnswerer
有"Okay, I'll just grab a StoreInventoryManager
, give it the name of the product the customer is looking for, and what store I want to check, and it will tell me if the product is in stock."的视角吗?它不应该不知道关于 Database
s 或 InventoryCatalog
s 的任何事情吗,从它的角度来看,这是一个它不需要关心的实现细节?
那么,注入依赖项的可测试代码与作为抽象原则的信息隐藏之间的平衡点在哪里?即使中间 classes 只是传递依赖项,构造函数签名本身也会揭示不相关的细节,对吗?
更现实,假设这是一个长期运行后台应用程序处理来自 DBMS 的数据;在调用图的 "layer" 处创建和传递数据库连接器是否合适,同时仍然使您的代码无需 运行 DBMS 即可测试?
我非常有兴趣在这里学习 OOP 理论和实践,以及澄清 DI 和 Information 之间看似矛盾的东西 Hiding/Abstraction。
Dependency Inversion Principle and, more specifically, Dependency Injection tackle the problem of how make application code loosely coupled. This means that in many cases you want to prevent the classes in your application from depending on other concrete types, in case those dependent types contain volatile behavior。易失性依赖项是一种依赖项,除其他外,它与进程外资源进行通信,是不确定的或需要可替换的。与易变依赖项的紧密耦合会阻碍可测试性,但也会限制应用程序的可维护性和灵活性。
但是无论您做什么,也无论您引入了多少抽象,您都需要在应用程序的某处依赖具体类型。所以你不能完全摆脱这种耦合——但这应该不是问题:一个 100% 抽象的应用程序也是 100% 无用的。
这意味着你想减少应用程序中 classes 和模块之间的耦合量,最好的方法是在应用程序中有一个地方依赖于所有具体类型,并将为您实例化。这是最有益的,因为:
- 您将在应用程序中只有一个位置知道对象图的组成,而不是将这些知识分散在整个应用程序中
- 如果您想更改实现或 intercept/decorate 个实例以应用横切关注点,您将只有一个地方可以更改。
连接所有内容的地方应该在入口程序集中。它应该是入口点程序集,因为这个程序集已经依赖于所有其他程序集,使其已经成为您应用程序中最不稳定的部分。
根据 Stable-Dependencies Principle (2) 依赖项应指向稳定性方向,并且由于您编写对象图的应用程序部分将是最易变的部分,因此不应依赖于它。这就是为什么您编写对象图的地方应该在您的入口点程序集中。
在您编写对象图的应用程序中,这个入口点通常称为 Composition Root。
如果您认为 EmployeeInventoryAnswerer
和 InventoryCatalogs
不应该对数据库一无所知,那么 EmployeeInventoryAnswerer
可能正在混合基础架构逻辑(以构建对象图) 和应用程序逻辑。换句话说,它可能违反了 Single Responsibility Principle。在那种情况下,您的 EmployeeInventoryAnswerer
不应该是入口点。相反,您应该有一个不同的入口点,并且 EmployeeInventoryAnswerer
应该只注入 StoreInventoryManager
。您的新入口点可以构建以 EmployeeInventoryAnswerer
开头的对象图并调用其 AnswerInventoryQuestion
方法(或您决定调用它的任何内容)。
where's the balance between testable code with injected dependencies, and information-hiding as a principal of abstraction?
构造函数是一个实现细节。只有 Composition Root 知道具体类型,因此,它是唯一调用这些构造函数的。当消费 class 依赖抽象作为其 incoming/injected 依赖项(例如,通过将其构造函数参数指定为抽象)时,消费者对实现一无所知,这更容易防止将实现细节泄露给消费者.如果抽象本身会泄露实现细节,另一方面,它会违反 Dependency Inversion Principle. And if the consumer would decide to cast the dependency back to the implementation, it would in turn violate the Liskov Substitition Principle。两种违规行为都应避免。
但即使您有一个依赖于具体组件的消费者,该组件仍然可以进行信息隐藏——它不必通过 public 公开自己的依赖项(或其他值)特性。事实上,这个组件有一个接受组件依赖的构造函数,并没有违反信息隐藏,因为不可能通过它的构造函数检索组件的依赖(你只能通过构造函数插入依赖;而不是接收它们).而且您不能更改组件的依赖项,因为该组件本身将被注入到消费者中,并且您不能在已经创建的实例上调用构造函数。
在我看来,在谈论“平衡”时,您提供了一个错误的选择。相反,这是正确应用 SOLID 原则的问题,因为如果不应用 SOLID 原则,无论如何(从可维护性的角度来看)你都会处于一个糟糕的地方——并且 SOLID 原则的应用无疑会导致依赖注入.
at what "layer" of the call-graph is it appropriate to create and pass around a database connector
至少,入口点知道数据库连接,因为它只是应该从配置文件中读取的入口点。从配置文件中读取应该预先在一个地方完成。这允许应用程序在配置错误时快速失败,并防止您读取分散在整个应用程序中的配置文件。
但是入口点是否应该负责创建数据库连接,这取决于很多因素。我通常对此有某种 ConnectionFactory
抽象,但是 YMMV。
更新
I don't want to pass around a Context or an AppConfig to everything and end up passing dependencies classes don't need
传递 class 本身不需要的依赖项通常不是最佳解决方案,并且可能表明您违反了依赖倒置原则并应用了 Control Freak anti-pattern。这是此类问题的示例:
public class Service : ServiceAbs
{
private IOtherService otherService;
public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) {
this.otherService = new OtherService(dep1, dep2, dep3);
}
}
在这里你看到一个 class Service
接受了 3 个依赖项,但它 根本没有使用它们 。它只将它们转发给它创建的 OtherService
的构造函数。当 OtherService
不是 local 到 Service
(即位于不同的模块或层)时,这意味着 Service
违反了依赖倒置原则—— Service
现在与 OtherService
紧密耦合。相反,Service
应该是这样的:
public class Service : IService
{
private IOtherService otherService;
public Service(IOtherService otherService) {
this.otherService = otherService;
}
}
这里Service
只接受它真正需要的,不依赖于任何具体类型。
but I also don't want to pass the same 4 things to several different classes
如果您有一组依赖项,这些依赖项通常一起注入到一个消费者中,则改变是您违反了单一职责原则:消费者可能做的太多了——知道的太多了。
根据手头的问题,有多种解决方案。我想到的一件事是 refactoring to Facade Services.
也可能是那些注入的依赖项是横切关注点。透明地应用横切关注点通常要好得多,而不是将其注入数十或数百个消费者(这违反了 Open/Closed 原则)。您可以为此使用 Decorator design pattern, Chain-of-Responsibility design pattern 或动态拦截。