依赖注入在“链”的更下游

本文关键字:注入 依赖 | 更新日期: 2023-09-27 18:08:16

我一直在阅读如何编写可测试代码,偶然发现了依赖注入设计模式。

这个设计模式真的很容易理解,它真的没有什么,对象请求值,而不是自己创建它们。

然而,现在我正在考虑如何在我正在开发的应用程序中使用它,我意识到它有一些复杂性。想象一下下面的例子:

public class A{
   public string getValue(){
      return "abc";
   }
}

public class B{
   private A a;
   public B(A a){
      this.a=a;
   }
   public void someMethod(){ 
      String str = a.getValue();
   }
}

单元测试someMethod ()现在很容易,因为我可以创建a的模拟,并让getValue()返回我想要的任何内容。

类B对A的依赖是通过构造函数注入的,但这意味着A必须在类B之外实例化,因此该依赖已经转移到另一个类中。这将重复许多层,在某些点实例化必须完成。

现在的问题是,当使用依赖注入时,你是否一直通过所有这些层传递依赖?这会不会使代码的可读性降低,并且花费更多的时间来调试?当您到达"顶层"时,您将如何对该类进行单元测试?

依赖注入在“链”的更下游

希望我正确理解了你的问题。

注入依赖项

不,我们不传递依赖关系到所有的层。我们只将它们传递给直接与它们对话的图层。例如:

public class PaymentHandler {
    private customerRepository;
    public PaymentHandler(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }
    public void handlePayment(CustomerId customerId, Money amount) {
        Customer customer = customerRepository.findById(customerId);
        customer.charge(amount);
    }
}
public interface CustomerRepository {
    public Customer findById(CustomerId customerId);
}
public class DefaultCustomerRepository implements CustomerRepository {
    private Database database;    
    public CustomerRepository(Database database) {
        this.database = database;
    }
    public Customer findById(CustomerId customerId) {
        Result result = database.executeQuery(...);
        // do some logic here
        return customer;
    }
}
public interface Database {
    public Result executeQuery(Query query);
}

PaymentHandler不知道Database的存在,它只与CustomerRepository对话。Database的注入在存储库层停止。

代码可读性

在没有框架或库帮助的情况下进行手工注入时,我们最终可能会得到包含许多样板代码的工厂类,比如return new D(new C(new B(), new A());,这些代码在某种程度上可能会降低可读性。为了解决这个问题,我们倾向于使用像Guice这样的DI框架来避免编写这么多工厂。

然而,对于实际执行工作/业务逻辑的类,它们应该更具可读性和可理解性,因为它们只与直接协作者对话并完成它们需要做的工作。

单元测试

我假设你所说的"Top"层是指PaymentHandler类。在这个例子中,我们可以创建一个存根CustomerRepository类,并让它返回一个Customer对象,然后将存根传递给PaymentHandler来检查是否收取了正确的金额。

一般的想法是传入假协作器来控制它们的输出,这样我们就可以安全地断言被测试类的行为(在这个例子中是PaymentHandler类)。

<

为什么接口/strong>

正如在上面的评论中提到的,依赖接口而不是具体类更可取,它们提供了更好的可测试性(易于模拟/存根)和更容易调试。

嗯,是的,这意味着你必须传递所有层的依赖关系。然而,这正是控制反转容器派上用场的地方。它们允许您注册系统中的所有组件(类)。然后,您可以向IoC容器请求class B的实例(在您的示例中),它将自动为您调用正确的构造函数,自动创建构造函数所依赖的任何对象(在您的示例中是class A)。

可以在这里找到一个很好的讨论:为什么我需要一个IoC容器而不是直接的DI代码?

在我看来,你的问题表明你理解这个模式。

正确使用

,您将拥有一个组合根,其中所有依赖项都被解析和注入。在这里使用IoC容器可以解析依赖关系,并将它们传递到各个层。

这与许多人认为是反模式的服务位置模式直接相反。

使用组合根不应该使你的代码可读性/可理解性降低,因为设计良好的类具有清晰和相关的依赖关系,应该合理地自文档化。我不确定单元测试的组成根。