通过公共API设计平衡依赖项注入


13

我一直在考虑如何通过提供简单的固定公共API来平衡使用依赖注入的可测试设计。我的困境是:人们希望做类似的事情var server = new Server(){ ... },而不必担心创建a Server(,,,,,,)可能具有的许多依赖关系和依赖关系图。在开发过程中,我不必担心太多,因为我使用IoC / DI框架来处理所有这些事情(我没有使用任何容器的生命周期管理方面,这会使事情进一步复杂化)。

现在,依赖关系不太可能重新实现。在这种情况下,组件化几乎纯粹是为了可测试性(和体面的设计!),而不是为了扩展等而创建接缝。人们将有99.999%的时间希望使用默认配置。所以。我可以对依赖项进行硬编码。不想这样做,我们会丢失测试!我可以提供一个带有硬编码依赖关系的默认构造函数,以及一个带有依赖关系的构造函数。那是...杂乱无章,可能令人困惑,但可行。我可以将接收构造函数的依赖项设置为内部,并使我的单元测试成为一个朋友程序集(假定为C#),它整理了公共API,但留下了一个隐藏的陷阱供维护。在我的书中,具有两个隐式连接而不是显式连接的构造函数通常是不好的设计。

目前,这是我能想到的最少的邪恶。意见?智慧?

Answers:


11

如果使用得当,依赖注入是一种强大的模式,但是它的从业人员常常会依赖于某些外部框架。对于那些不想将我们的应用程序与XML胶带轻松地捆绑在一起的人来说,生成的API会让他们感到非常痛苦。不要忘记普通的旧对象(PO​​O)。

首先考虑一下在不涉及框架的情况下,您希望别人如何使用您的API。

  • 您如何实例化服务器?
  • 如何扩展服务器?
  • 您想在外部API中公开什么?
  • 您想在外部API中隐藏什么?

我喜欢为您的组件提供默认实现的想法,以及您拥有这些组件的事实。以下方法是一种完全有效且体面的OO实践:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

只要您在API文档中记录了组件的默认实现是什么,并提供一个执行所有设置的构造函数,就可以了。只要设置代码发生在一个主构造函数中,级联构造函数就不会脆弱。

本质上,所有面向公众的构造函数都具有您要公开的组件的不同组合。在内部,它们将填充默认值,并遵循私有的构造函数来完成设置。本质上,这为您提供了一些DRY,并且非常易于测试。

如果您需要提供friend对测试的访问权限以设置Server对象以将其隔离以进行测试,请继续使用它。它不会成为公共API的一部分,这是您要小心的。

只需对您的API使用者好一点,并且不需要 IoC / DI框架即可使用您的库。为了感觉到自己遭受的痛苦,请确保您的单元测试也不依赖IoC / DI框架。(这是个人喜怒无常的做法,但是一旦您引入了该框架,它就不再是单元测试了,它变成了集成测试)。


我同意,我当然也不喜欢IoC配置汤-在涉及IoC时,我根本不使用XML配置。当然,避免使用IoC正是我要避免的事情-这是公共API设计人员永远无法做出的假设。感谢您的评论,这是我一直在考虑的思路,并且可以放心地一次又一次地进行健全性检查!
kolektiv 2011年

-1这个答案基本上说“不要使用依赖注入”,并且包含不正确的假设。在您的示例中,如果HttpListener构造函数发生更改,则Server该类也必须更改。他们是一对。更好的解决方案是下面的@Winston Ewert所说的,它是在库的API之外提供具有预先指定的依赖关系的默认类。这样,自从您为他做出决定以来,程序员就不必强行设置所有依赖关系,但是他仍然可以灵活地稍后更改它们。当您具有第三方依赖项时,这很重要。
M. Dudley


1
@MichaelDudley,您是否阅读了OP的问题。所有“假设”均基于OP提供的信息。有一时间,您所称的“ Bastard注入”是受青睐的依赖项注入格式,尤其是对于其组成在运行时不会改变的系统。setter和getter也是依赖注入的另一种形式。我只是根据OP提供的内容使用了示例。
Berin Loritsch 2013年

4

在这种情况下,在Java中,通常由工厂构建“默认”配置,使其与实际的“适当”行为服务器保持独立。因此,您的用户写道:

var server = DefaultServer.create();

Server的构造函数仍接受其所有依赖关系,并可用于深度定制。


+ 1,SRP。牙医办公室的责任不是建立牙医办公室。服务器的责任不是构建服务器。
R. Schmitz

2

如何提供提供“公共API”的子类

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

用户可以new StandardServer()并且正在途中。如果希望对服务器功能进行更多控制,他们也可以使用Server基类。这或多或少是我使用的方法。(我没有使用框架,因为我还没有明白要点。)

这仍然会公开内部api,但我认为您应该这样做。您已将对象拆分为不同的有用组件,这些组件应独立工作。没有告诉第三方何时可能想单独使用这些组件之一。


1

我可以将接收构造函数的依赖项内部化,并让我的单元测试成为一个朋友程序集(假定为C#),它整理了公共API,但留下了一个隐藏的隐藏陷阱以供维护。

在我看来,这似乎不是“讨厌的隐藏陷阱”。公共构造函数应该只调用具有“默认”依赖项的内部构造函数。只要很明显,不应该修改公共构造函数,一切就可以了。


0

我完全同意你的意见。我们不想仅仅为了单元测试而污染组件的使用范围。这是基于DI的测试解决方案的全部问题。

我建议使用InjectableFactory / InjectableInstance模式。InjectableInstance是一个简单的可重用的实用程序类,是可变的值持有者。组件接口包含对此值持有者的引用,该引用使用默认实现初始化。该接口将提供一个委托给值持有者的单例方法get()。组件客户端将调用get()方法而不是new方法来获取实现的实例,以避免直接依赖。在测试期间,我们可以选择用模拟替换默认实现。接下来,我将使用一个示例TimeProvider类,它是Date()的抽象,因此允许单元测试注入和模拟以模拟不同的时刻。抱歉,我将在这里使用Java,因为我对Java更加熟悉。C#应该相似。

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance非常容易实现,我在Java中有一个参考实现。有关详细信息,请参阅我的博客文章Injectable Factory的Dependency Indirect。


那么...用可变的单例替换依赖项注入?这听起来很可怕,您将如何运行独立测试?您是否为每个测试打开新流程或将其强制为特定顺序?Java不是我的强项,我误会了什么吗?
nvoigt

在Java Maven项目中,共享实例不是问题,因为junit引擎默认会在不同的类加载器中运行每个测试,但是我在某些IDE中确实遇到了此问题,因为它可能会重用同一类加载器。为解决此问题,该类提供了一个reset方法,该方法将在每次测试后调用以重置为原始状态。实际上,不同的测试想要使用不同的模拟是一种罕见的情况。多年来,我们已经将此模式应用于大型项目,一切都运行顺利,我们享受了可读,干净的代码,而没有牺牲可移植性和可测试性。
陈建武
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.