依赖注入和抽象之间的平衡在哪里?

时间:2015-05-10 05:03:07

标签: java oop design-patterns dependency-injection language-agnostic

许多建筑师和工程师推荐Dependency Injection和其他Inversion of Control模式作为improve the testability of your code的一种方式。不可否认,依赖注入使代码更易于测试,然而,它也不是Abstraction的完整目标吗?

我感到矛盾!我写了一个例子来说明这一点;它不是超现实的,我不会这样设计它,但我需要一个快速而简单的具有多个依赖关系的类结构的例子。第一个示例没有依赖注入,第二个示例使用Injected依赖项。

非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;
    }
}

依赖关系注入的示例

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;
    }
}

(如果您有任何想法,请感觉让我的榜样更好!这可能不是最好的例子。)

在我的示例中,我认为EmployeeInventoryAnswerer了解了StoreInventoryManager的基本实现细节,完全违反了抽象。

不应该EmployeeInventoryAnswerer拥有"的观点,好吧,我只是抓住StoreInventoryManager,给它一个客户产品的名称正在寻找,我想检查什么商店,它会告诉我产品是否有货。" ?对于DatabaseInventoryCatalog s,它不应该知道一件事,从它的角度来看,这是一个它不需要关注的实现细节吗?

那么,可测试代码与注入依赖关系和信息隐藏作为抽象主体之间的平衡是什么?即使中间类只是传递依赖项,构造函数签名也只能显示无关的细节,对吗?

更现实,让我们说这是一个长期运行的后台应用程序处理来自DBMS的数据;在什么"层"调用图是否适合创建和传递数据库连接器,同时仍然可以在没有运行DBMS的情况下使代码可测试?

我非常有兴趣在这里学习OOP理论和实用性,以及澄清DI和信息隐藏/抽象之间似乎是一个悖论。

1 个答案:

答案 0 :(得分:4)

依赖注入,更具体地说Dependency Inversion Principle的要点是您希望应用程序代码松散耦合。这意味着在许多情况下,您希望应用程序中的类不依赖于具体类型,以防这些依赖类型包含 volatile 行为(即与进程外资源通信的行为,是非确定性的,或需要可替换的)。这不仅会妨碍可测试性,还会影响应用程序的可维护性和灵活性。

但无论你做什么,无论你介绍多少抽象,在你的应用程序的某个地方你都需要依赖于具体的类型。所以你不能完全摆脱这种耦合 - 但这不应该是一个问题:100%抽象的应用程序也是100%无用的。

这意味着您希望减少应用程序中类和模块之间的耦合量,最好的方法是在应用程序中有一个位置依赖于所有具体类型并将实例化为了你。这是最有益的,因为:

  • 在应用程序中只有一个地方知道对象图的组成,而不是将这些知识分散在整个应用程序中
  • 如果您想要更改实施,或者拦截/装饰实例以应用横切关注点,您将只有一个地方可以更改。

这个你连接所有内容的地方应该在你的入口点组装中。它应该是入口点组件,因为无论如何这个程序集已经依赖于所有其他程序集,使它成为应用程序中最不稳定的部分。

根据Stable-Dependencies Principle2)依赖关系应该指向稳定性的方向,并且因为组成对象图的应用程序部分将是最不稳定的部分,所以不应该依赖于它。这就是为什么构建对象图的位置应该在入口点汇编中。

组成对象图的应用程序中的此入口点通常称为Composition Root

如果您认为EmployeeInventoryAnswerer不应该对数据库和InventoryCatalogs有任何了解,那么可能是EmployeeInventoryAnswerer混合基础结构逻辑(构建对象图)和应用程序逻辑(换句话说,它违反了Single Responsibility Principle)。在这种情况下,您的EmployeeInventoryAnswerer不应该是切入点。相反,您应该有一个不同的入口点,EmployeeInventoryAnswerer应该只注入StoreInventoryManager。您的新入口点可以构建从EmployeeInventoryAnswerer开始的对象图,并调用其AnswerInventoryQuestion方法(或您决定调用它的任何方法)。

  

其中可测试代码与注入依赖项之间的平衡,   和信息隐藏作为抽象的原则?

构造函数是一个实现细节。只有组合根知道具体类型,因此它是唯一一个调用这些构造函数的方法。由于注入消费者的依赖应该是抽象的,因此消费者对实现一无所知,因此实现不可能将任何信息泄露给消费者。如果抽象本身会泄漏实现细节,则会违反Dependency Inversion Principle,如果使用者将依赖关系强制转换为实现,则会违反Liskov Substitition Principle

但即使你有一个依赖于具体组件的消费者,该组件仍然可以进行信息隐藏 - 它不必通过公共属性公开自己的依赖关系(或其他值)。事实上,这个组件有一个接受组件依赖关系的构造函数,并不会使它违反信息隐藏,因为它不可能通过它的构造函数检索组件的依赖关系(你只能插入通过构造函数的依赖关系;不接收它们)。并且您无法更改组件的依赖项,因为该组件本身将被注入到使用者中,并且您无法在已创建的实例上调用构造函数。

我认为,这里没有平衡。这只是正确应用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);
    } 
}

在这里,您会看到一个类Service,它接受​​3个依赖项,但它根本不会使用 。它只会将它们转发给它创建的OtherService构造函数。这违反了依赖性倒置原则,因为Service现在与OtherService紧密耦合。相反,这就是Service应该是这样的:

public class Service : IService
{
    private IOtherService otherService;

    public Service(IOtherService otherService) {
        this.otherService = otherService;
    } 
}

这里Service只接受它真正需要的东西,并不依赖于任何具体的类型。

  

但我也不想将相同的4件事传递给几个不同的班级

如果您有一组通常全部注入消费者的依赖项,那么您违反单一责任原则的变化很大:消费者可能做得太多 - 了解太多。

根据设计的不同,有几种解决方案。我想到的一件事是refactoring to Facade Services

这些注入的依赖关系也可能是跨领域的问题。将交叉问题透明地应用,而不是将其注入数十或数百个消费者(这违反了开放/封闭原则)通常要好得多。你可以使用装饰器或拦截器。