'Where is the Balance Between Dependency Injection and Abstraction?
Many Architects and Engineers recommend 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 in general?
I feel conflicted! I wrote an example to illustrate this; it's not super-realistic and I wouldn't design it this way, but I needed a quick and simple example of a class structure with multiple dependencies. The first example is without Dependency Injection, and the second uses Injected Dependencies.
Non-DI Example
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;
}
}
Dependency-Injected Example
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;
}
}
(Please feel to make my example better if you have any ideas! It might not be the best example.)
In my example, I feel that Abstraction has been completely violated by EmployeeInventoryAnswerer
having knowledge of underlying implementation details of StoreInventoryManager
.
Shouldn't EmployeeInventoryAnswerer
have the perspective of, "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."? Shouldn't it not know a single thing about Database
s or InventoryCatalog
s, as from its perspective, that's an implementation detail it need not concern itself with?
So, where's the balance between testable code with injected dependencies, and information-hiding as a principal of abstraction? Even if the middle-classes are merely passing-through dependencies, the constructor signature alone reveals irrelevant details, right?
More realistically, let's say this a long-running background application processing data from a DBMS; at what "layer" of the call-graph is it appropriate to create and pass around a database connector, while still making your code testable without a running DBMS?
I'm very interested in learning about both OOP theory and practicality here, as well as clarifying what seems to be a paradox between DI and Information Hiding/Abstraction.
Solution 1:[1]
The 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. A volatile dependency is a dependency that, among other things, communicates with out-of-process resources, is non-deterministic, or needs to be replaceable. Tightly coupling to volatile dependencies hinders testability, but also limits the maintainability and flexibility of your application.
But no matter what you do, and no matter how many abstractions you introduce, somewhere in your application you need to take a dependency on a concrete type. So you can’t get rid of this coupling completely—but this shouldn't be a problem: An application that is 100% abstract is also 100% useless.
What this means is that you want to reduce the amount of coupling between classes and modules in your application, and the best way of doing that is to have a single place in the application that depends on all concrete types and will instantiate that for you. This is most beneficial because:
- You will only have one place in the application that knows about the composition of object graphs, instead of having this knowledge scattered throughout the application
- You will only have one place to change if you want to change implementations, or intercept/decorate instances to apply cross-cutting concerns.
This place where you wire everything up should be in your entry-point assembly. It should be the entry-point assembly, because this assembly already depends on all other assemblies anyway, making it already the most volatile part of your application.
According to the Stable-Dependencies Principle (2) dependencies should point in direction of stability, and since the part of the application where you compose your object graphs will be the most volatile part, nothing should depend on it. That’s why this place where you compose your object graphs should be in your entry point assembly.
This entry point in the application where you compose your object graphs is commonly referred to as the Composition Root.
If you feel that EmployeeInventoryAnswerer
should not know anything about databases and InventoryCatalogs
, it might be the case that the EmployeeInventoryAnswerer
is mixing infrastructural logic (to build up the object graphs) and application logic. Iin other words, it might be violating the Single Responsibility Principle. In that case your EmployeeInventoryAnswerer
should not be the entry point. Instead you should have a different entry point and the EmployeeInventoryAnswerer
should only get a StoreInventoryManager
injected. Your new entry point can than build up the object graph starting with EmployeeInventoryAnswerer
and call its AnswerInventoryQuestion
method (or whatever you decide to call it).
where's the balance between testable code with injected dependencies, and information-hiding as a principal of abstraction?
The constructor is an implementation detail. Only the Composition Root knows about concrete types and it is, therefore, the only one calling those constructors. When a consuming class depends on abstractions as its incoming/injected dependencies (e.g. by specifying its constructor arguments as abstractions), the consumer knows nothing about the implementation and that makes it easier to prevent leaking implementation details onto the consumer. If the abstraction itself would leak implementation details, on the other hand, it would violate the 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. Both violations should be prevented.
But even if you would have a consumer that depends on a concrete component, that component can still do information-hiding—it doesn't have to expose its own dependencies (or other values) through public properties. And the fact that this component has a constructor that takes in the component's dependencies, does not make it violate information-hiding, because it is impossible to retrieve the component's dependencies through its constructor (you can only insert dependencies through the constructor; not receive them). And you can't change the component's dependencies, because that component itself will be injected into the consumer, and you can't call a constructor on an already created instance.
As I see it, when talking about a "balance," you are providing a false choice. Instead, it's a matter of applying the SOLID principles correctly, because without applying the SOLID principles, you'll be in a bad place (from a maintainability perspective) anyway—and application of the SOLID principles undoubtedly leads to Dependency Injection.
at what "layer" of the call-graph is it appropriate to create and pass around a database connector
At the very least, the entry point knows about the database connection, because it is only the entry point that should read from the configuration file. Reading from the configuration file should be done up front and in one single place. This allows the application to fail fast if it is misconfigured and prevents you from having reads to the config file scattered throughout your application.
But whether the entry point should be responsible of creating the database connection, that can depend on a lot of factors. I usually have some sort of ConnectionFactory
abstraction for this, but YMMV.
UPDATE
I don't want to pass around a Context or an AppConfig to everything and end up passing dependencies classes don't need
Passing dependencies a class itself doesn't need is typically not the best solution, and might indicate that you are violating the Dependency Inversion Principle and applying the Control Freak anti-pattern. Here's an example of such problem:
public class Service : ServiceAbs
{
private IOtherService otherService;
public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) {
this.otherService = new OtherService(dep1, dep2, dep3);
}
}
Here you see a class Service
that takes in 3 dependencies, but it doesn't use them at all. It only forwards them to the OtherService
's constructor which it creates. When OtherService
is not local to Service
(i.e. lives in a different module or layer), it means that Service
violates the Dependency Inversion Principle—Service
is now tightly coupled with OtherService
. Instead, this is how Service
should look like:
public class Service : IService
{
private IOtherService otherService;
public Service(IOtherService otherService) {
this.otherService = otherService;
}
}
Here Service
only takes in what it really needs and doesn't depend on any concrete types.
but I also don't want to pass the same 4 things to several different classes
If you have a group of dependencies that are often all injected together into a consumer, changes are that you are violating the Single Responsibility Principle: the consumer might do too much—know too much.
There are several solutions for this, depending on the problem at hand. One thing that comes to mind is refactoring to Facade Services.
It might also be the case that those injected dependencies are cross-cutting concerns. It's often much better to apply cross-cutting concerns transparently, instead of injecting it into dozens or hundreds of consumers (which is a violation of the Open/Closed principle). You can use the Decorator design pattern, Chain-of-Responsibility design pattern, or dynamic interception for this.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |