samedi 9 mai 2015

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 EmployeeInventoryAnswererhave 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 Databases or InventoryCatalogs, 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?

Additional, where is the balance between DI and Maintainability? If my LowLevelTemperatureSensorMaintenceScheduleChecker suddenly needs a new FancyMaintenanceServiceApiSession, how far should I be walking backward to inject the dependency?

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?

Aucun commentaire:

Enregistrer un commentaire