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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了依赖注入和抽象之间的平衡在哪里?相关的知识,希望对你有一定的参考价值。

许多建筑师和工程师推荐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,给它客户正在寻找的产品的名称,以及我要检查的商店,它会告诉我产品是否在股票。”?它不应该对Databases或InventoryCatalogs一无所知,从它的角度来看,这是一个它不需要关注的实现细节吗?

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

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

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

答案

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

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

UPDATE

我不想将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

也许情况是那些注入的依赖关系是跨领域的问题。通常情况下,更好地应用跨领域问题,而不是将其注入数十或数百名消费者(这违反了开放/封闭原则)。您可以使用装饰器或拦截器。

以上是关于依赖注入和抽象之间的平衡在哪里?的主要内容,如果未能解决你的问题,请参考以下文章

Android 片段和依赖注入

Android片段和依赖注入

接口和抽象之间有啥区别以及依赖注入如何[重复]

Spring依赖注入(DI)的理解

依赖注入[3]: 依赖注入模式

向依赖关系宣战[转]