【问题标题】:Is Service Locator a good pattern for a framework?服务定位器是框架的好模式吗?
【发布时间】:2015-09-10 10:10:10
【问题描述】:

我正在构建一个 GUI 框架,我目前有很多行(通常在构造函数中)

var renderer = locator.GetService<IRenderer>();
var input = locator.GetService<IMouseInput>();

我获得的那些服务是核心服务,我的意思是,这些服务本质上是被认为是基础的一部分。

我曾考虑过使用构造函数注入,但出现了一些烦恼。

  • 我将不得不修改几乎每个构造函数以注入其依赖项,这意味着它甚至可以接收最基本的“服务”。我害怕每个构造函数中有 3 或 4 个参数。
  • 但更糟糕的另一件事是,如果框架的最终用户找不到无参数构造函数,他们将不会高兴。您是否认为创建 TextBox 需要您传递它真正需要的所有依赖项?

最终用户可能会被 new TextBox(dep1, dep2, dep3) 淹没。

那么,有没有一种很好的方法可以完全删除服务定位器而不会使其过于复杂?

编辑

在我们正在讨论的团队中,他们仍然希望保持服务定位器争论以下内容。这是对话的片段:

我们不关心 TextBox 实现中的单一职责。我们 不是'写应用程序。这是一个UI框架!它应该 隐藏最终用户的东西和细节,而不是暴露它们。一个流利的文本框构建器?严重地?我们不是在写一些网络请求 处理管道。

我同意他 (Stack Overflow 用户) 写的所有内容,但关于 应用程序。您希望东西尽可能可扩展和可重用的地方。我们有 10 项服务。 到处都在使用它们。如果 我们有 50 个依赖关系和复杂的对象图,我们做不到 任何没有 DI 和东西的东西。关于DI的事情是你有薪水 为了它。所以对我们来说 Service Locator 是免费的 DI 不是 它只是 不可维护 - 通过拥有更复杂的 API,通过拥有落后的 API/ABI 兼容性等。我们在不知道的情况下为服务定位器付费 什么类需要作为它的依赖项以及提供的一些问题 不同实例的不同依赖实现。但我们 也不需要,所以服务定位器是免费的。 DI 不是。

你同意吗?为什么?

【问题讨论】:

  • 什么是IRenderer,为什么它是一个接口?它的寿命是多少?作为一个框架,我实际上认为您希望避免引入 IOC 框架。从性能的角度来看,您确实应该在构造函数中对所有内容进行硬编码。
  • 假设您的定位器是全局对象,那么当前的定位器方法有什么问题?
  • 定位器错误,因为它隐藏了依赖关系,这是一件坏事。
  • @Aron 对象的生命周期基本上是从应用程序开始到结束。这只是他们每个人的一个实例。它是一个接口,因为它将是跨平台的,并且需要有不同的实现。
  • 当你在构造函数中传递所有依赖项时,我看不出构造函数注入方法有什么问题——它比使用服务定位器解决它们要好得多,服务定位器本质上是anti-pattern。使用构造函数注入,您的 API 不会对它们的依赖关系撒谎并使它们显而易见。

标签: c# design-patterns dependency-injection service-locator


【解决方案1】:

我将不得不修改几乎每个构造函数以注入其依赖项,这意味着它甚至可以接收最基本的“服务”。我害怕每个构造函数中有 4 或 5 个参数。

如果您在构造函数中有 4 或 5 个服务,则表明您违反了Single Responsibility Principle。届时,是时候重构为Aggregate Service (otherwise known as a facade service)了。

但更糟糕的另一件事是框架的最终用户将没有无参数的构造函数。您是否认为创建 TextBox 需要您传递它真正需要的所有依赖项?

嗯,首先应该清楚你是在设计一个框架还是事实上它是一个

但无论哪种方式,您都应该能够注入服务以进行覆盖,但要提供符合逻辑的默认行为。实现这一目标的最佳方法是使用流利的构建器来组合您的服务。也就是说,公共 API 将由构建器组成,这些构建器将完成底层服务的所有配置。然后,您可以根据最终用户的要求公开允许注入自定义服务的重载(即一个重载用于使用构建器构建一组默认服务,一个重载仅接受抽象)。

下面是一个简单的例子,展示了如何做到这一点:

public class VideoContentBuilder : IVideoContentBuilder
{
    private readonly string thumbnailLocation;
    private readonly string title;
    private readonly ICompressor compressor;

    public VideoContentBuilder()
        // Supply logical defaults
        : this(
            thumbnailLocation: string.Empty,
            title: string.Empty,
            compressor: new DefaultVideoCompressor(new Dependency())
        )
    {}

    private VideoContentBuilder(
        string thumbnailLocation,
        string title,
        ICompressor compressor)
    {
        this.thumbnailLocation = thumbnailLocation;
        this.title = title;
        this.compressor = compressor;
    }

    public IVideoContentBuilder WithThumbnailLocation(string thumbnailLocation)
    {
        return new VideoContentBuilder(thumbnailLocation, this.title, this.compressor);
    }

    public IVideoContentBuilder WithTitle(string title)
    {
        return new VideoContentBuilder(this.thumbnailLocation, title, this.compressor);
    }

    // Use a builder to configure defualt services
    //
    // Syntax:
    // .WithCompressor(compressor => compressor.WithLevel(Level.Maximum).WithEncryption(Encryption.None))
    public IVideoContentBuilder WithCompressor(Func<ICompressorBuilder, ICompressorBuilder> expression)
    {
        var starter = new CompressorBuilder(this.compressor);
        var builder = expression(starter);
        var compressor = builder.Create();

        return new VideoContentBuilder(this.thumbnailLocation, this.title, compressor);
    }

    // Allow a custom compressor to be injected.
    //
    // Syntax:
    // .WithCompressor(new CustomCompressor())
    public IVideoContentBuilder WithCompressor(ICompressor compressor)
    {
        return new VideoContentBuilder(this.thumbnailLocation, this.title, compressor);
    }

    // Create the configured service.
    public IVideoContent Create()
    {
        return new VideoContent(this.thumbnailLocation, this.title, compressor);
    }
)

用法

var videoContentBuilder = new VideoContentBuilder()
    .WithThumbnailLocation("http://www.example.com/thumb.jpg")
    .WithTitle("The greatest video")
    .WithCompressor(compressor => compressor
        .WithQuality(Level.High)
        .WithAlgorithm(Algorithm.ReallyCool)
        .WithDependentService(new DependentService())
    );

var videoContent = videoContentBuilder.Create();

参考资料:

【讨论】:

  • 您认为同样的建议适用于框架吗?在内部,我们有人争辩说(请参阅编辑后的问题)
  • 当然。一个框架不应该包含 DI,只为它提供支持。但是,您是采用构建器的想法(显然更复杂)还是像我链接到的文章中那样简单地使用多个构造函数重载取决于您。您可以轻松地将构建器隐藏在静态成员或其他外观后面,因此用户无需对其进行更新。请注意,一些现代 UI 框架使用 Builder 方法(例如 TwitterBootstrapMVC)。最终用户处理起来比寻找正确的构造函数或方法重载要快。
  • 服务定位器是一个选项。但在所有其他可能性都已用尽之后,它应该被视为最后的手段。它可能是您团队的最佳选择,但它可能不是您的用户的最佳选择。但这实际上取决于您希望用户多久自定义一次您的实现。如果其中 1% 需要定制支持,那么您最好使用服务定位器和一些文档来说明可以定制哪些类型。如果您的 25% 的用户需要定制支持,您应该更加认真地对待构建器方法。
  • 这还取决于您是构建自定义 UI 框架还是仅构建现有 UI 框架(如 WPF)的子框架。在 DI 友好的环境(如 WPF 或 MVC)中支持构建器方法比使用旧框架(如 Windows 窗体或 Web 窗体)要容易得多。
猜你喜欢
  • 1970-01-01
  • 2016-04-06
  • 2017-06-17
  • 2012-06-24
  • 2011-11-23
  • 2016-03-28
  • 2011-11-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多