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

Posted

技术标签:

【中文标题】依赖注入和抽象之间的平衡点在哪里?【英文标题】:Where is the Balance Between Dependency Injection and Abstraction? 【发布时间】:2015-07-20 18:59:50 【问题描述】:

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

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

非直接投资示例

package com.***.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.***.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 和信息隐藏/抽象之间似乎存在的悖论。

【问题讨论】:

任何人都可以否决或近距离投票请评论如何改进这个问题? 【参考方案1】:

Dependency Inversion Principle,更具体地说,依赖注入解决了如何使应用程序代码松散耦合的问题。这意味着在许多情况下,您希望防止应用程序中的类依赖于其他具体类型,以防这些依赖类型包含volatile behavior。易失性依赖是一种依赖,除其他外,它与进程外资源通信、不确定或需要可替换。与易失性依赖项紧密耦合会阻碍可测试性,但也会限制应用程序的可维护性和灵活性。

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

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

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

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

根据Stable-Dependencies Principle (2) 的依赖关系应该指向稳定的方向,并且由于您构成对象图的应用程序部分将是最不稳定的部分,因此不应依赖它。这就是为什么您编写对象图的地方应该在您的入口点程序集中。

应用程序中用于构成对象图的入口点通常称为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 anti-pattern。以下是此类问题的示例:

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 的构造函数。当OtherService 不是localService(即位于不同的模块或层中)时,这意味着Service 违反了依赖倒置原则——Service 现在与@ 紧密耦合987654350@。相反,Service 应该是这样的:

public class Service : IService

    private IOtherService otherService;

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

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

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

如果您有一组通常一起注入到消费者中的依赖项,则变化是您违反了单一职责原则:消费者可能做的太多——知道的太多。

对此有多种解决方案,具体取决于手头的问题。想到的一件事是refactoring to Facade Services。

这些注入的依赖项也可能是横切关注点。透明地应用横切关注点通常要好得多,而不是将其注入数十个或数百个消费者(这违反了开放/封闭原则)。您可以为此使用Decorator design pattern、Chain-of-Responsibility design pattern 或动态拦截。

【讨论】:

感谢您提供详细周到的答案以及您提供的资源。我的问题似乎是“I”和“D”之间的平衡——我不想将ContextAppConfig 传递给所有东西并最终传递不需要的依赖项,但我也不想将相同的 4 件事传递给几个不同的类。我认为这些共享抽象是平衡的所在……对于我的数据库示例,我选择了一个在入口点附近实例化的DbConfig 容器,感觉这是一个很好的平衡。这很棒,其他人应该阅读它,但我被 3 倍否决了 :( 谢谢! 接受这个答案,只是想给它一些时间,以防其他人有更多的补充。再次感谢@Steven! @Will,我根据您的回复更新了我的问题。 这个答案是 Stack Overflow 的一个例子。太棒了。 非常感谢@Steven。我有完全相同的问题。

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

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

Spring依赖注入(DI)的理解

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

向依赖关系宣战[转]

依赖注入的本质与里氏替换原则

什么是依赖注入 IoC