65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的服务定位器模式

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2022年7月13日

CPOL

12分钟阅读

viewsIcon

37347

downloadIcon

176

服务定位器模式入门教程, 附带示例。

1 简短教程

本文的目的是提供一个关于“服务定位器设计模式”的简洁教程,并附带 C# 示例。虽然这个设计模式已被“依赖注入模式”和“依赖注入容器”的使用所取代,但由于学术原因和实际原因(例如遗留代码可能仍依赖此模式),它仍然可能引起读者的兴趣。如今,在新的代码中不推荐使用服务定位器模式,甚至一些作者认为它是一种反模式。

提供的代码是教程级别的,“概念验证”,为了简洁起见,没有处理异常等。

2 依赖倒置原则 – DIP

因此,“依赖倒置原则(DIP)”是一个软件设计原则。它被称为“原则”是因为它提供了关于如何设计软件产品的高级建议。

DIP 是由 Robert C. Martin [5] 推广的 SOLID [3] 首字母缩略词所知的五项设计原则之一。DIP 原则规定

  1. 高层模块不应依赖于低层模块。两者都应依赖于抽象。
  2. 抽象不应依赖于细节。细节应依赖于抽象。

解释是

虽然高层原则谈论的是“抽象”,但我们需要将其转化为我们特定编程环境(在本例中是 C#/.NET)的术语。C# 中的抽象是通过接口和抽象类实现的。当谈论“细节”时,该原则意味着“具体实现”。
所以,基本上,这意味着 DIP 提倡在 C# 中使用接口,而具体实现(低层模块)应依赖于接口。

传统的模块依赖关系如下所示

DIP 提出了新的设计

正如您所见,一些依赖关系(箭头)的方向已反转,这就是“倒置”名称的由来。

DIP 的目标是创建“松散耦合”的软件模块。传统上,高层模块依赖于低层模块。DIP 的目标是使高层模块独立于低层模块的实现细节。它通过在它们之间引入一个“抽象层”(以接口的形式)来实现这一点。

DIP 原则是一个广泛的概念,并对其他设计模式产生影响。例如,当应用于工厂设计模式或单例设计模式时,它建议这些模式应返回对接口的引用,而不是对对象的引用。“依赖注入模式”遵循此原则。“服务定位器模式”也遵循此原则。

3 服务定位器模式 – 静态版本

首先,“服务定位器模式”是一个软件设计模式。它被称为“模式”是因为它为特定问题提供了低级特定实现的建议。

该模式旨在解决的主要问题是如何创建“松散耦合”的组件。目标是通过消除客户端和服务实现之间的依赖来提高应用程序的模块化。该模式使用一个称为“服务定位器”的中央注册表,该注册表在客户端请求时,为其提供它所依赖的服务。

此模式中有四个主要角色(类)

  1. ClientClient 是一个想要使用另一个名为 Service 的组件提供的服务的组件/类。
  2. ServiceInterfaceService 接口是描述 Service 组件提供的服务类型的抽象。
  3. ServiceService 组件/类根据 Service-Interface 描述提供服务。
  4. ServiceLocator:是一个组件/类,它封装了如何获取 Client 所需/依赖的服务知识。它是 Client 获取服务的唯一联系点。它是所有客户端使用的服务的单例注册表。ServiceLocator 负责在 Client 请求时返回服务实例。

其工作原理是 Client 依赖于 ServiceInterfaceClient 依赖于 ServiceInterface 接口,但本身不依赖于 ServiceService 实现 ServiceInterface 接口,并提供 Client 所需的某些服务。Client 还依赖于 ServiceLocatorClient **显式**地从 ServiceLocator 请求它所依赖的 Service 实例。一旦 Client 获取到它需要的 Service 实例,它就可以执行工作。

这是该模式的类图

这是该模式的示例代码

internal interface IServiceA
{
    void UsefulMethod();
}

internal interface IServiceB
{
    void UsefulMethod();
}

internal class ServiceA : IServiceA
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceA-UsefulMethod");
    }
}

internal class ServiceB : IServiceB
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceB-UsefulMethod");
    }
}

internal class Client
{
    public IServiceA serviceA = null;
    public IServiceB serviceB = null;

    public void DoWork()
    {
        serviceA?.UsefulMethod();
        serviceB?.UsefulMethod();
    }
}

internal class ServiceLocator
{
    private static ServiceLocator locator = null;

    public static ServiceLocator Instance
    {
        get
        {
            // ServiceLocator itself is a Singleton
            if (locator == null)
            {
                locator = new ServiceLocator();
            }
            return locator;
        }
    }

    private ServiceLocator()
    {
    }

    private IServiceA serviceA = null;
    private IServiceB serviceB = null;

    public IServiceA GetIServiceA()
    {
        //we will make ServiceA a singleton
        //for this example, but does not need
        //to be in a general case
        if (serviceA == null)
        {
            serviceA = new ServiceA();
        }
        return serviceA;
    }

    public IServiceB GetIServiceB()
    {
        //we will make ServiceB a singleton
        //for this example, but does not need
        //to be in a general case
        if (serviceB == null)
        {
            serviceB = new ServiceB();
        }
        return serviceB;
    }
}

static void Main(string[] args)
{
    Client client = new Client();
    client.serviceA = ServiceLocator.Instance.GetIServiceA();
    client.serviceB = ServiceLocator.Instance.GetIServiceB();
    client.DoWork();

    Console.ReadLine();
}

此模式的这个版本称为“静态版本”,因为它为每个服务使用一个字段来存储对象引用,并且每个服务都有一个专用的“Get”方法名。无法动态地向 ServiceLocator 添加不同类型的服务。所有内容都是静态硬编码的。

4 服务定位器模式 – 动态版本 – 字符串服务名

在这里,我们将展示此模式的动态版本,一个使用 string 作为 Service 名称的版本。原则与上述相同,只是 ServiceLocator 的实现不同。

这是类图

这是该模式的示例代码。

internal interface IServiceA
{
    void UsefulMethod();
}

internal interface IServiceB
{
    void UsefulMethod();
}

internal class ServiceA : IServiceA
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceA-UsefulMethod");
    }
}

internal class ServiceB : IServiceB
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceB-UsefulMethod");
    }
}

internal class Client
{
    public IServiceA serviceA = null;
    public IServiceB serviceB = null;

    public void DoWork()
    {
        serviceA?.UsefulMethod();
        serviceB?.UsefulMethod();
    }
}

internal class ServiceLocator
{
    private static ServiceLocator locator = null;

    public static ServiceLocator Instance
    {
        get
        {
            // ServiceLocator itself is a Singleton
            if (locator == null)
            {
                locator = new ServiceLocator();
            }
            return locator;
        }
    }

    private ServiceLocator()
    {
    }

    private Dictionary<String, object> registry =
        new Dictionary<string, object>();

    public void Register(string serviceName, object serviceInstance)
    {
        registry[serviceName] = serviceInstance;
    }

    public object GetService(string serviceName)
    {
        object serviceInstance = registry[serviceName];
        return serviceInstance;
    }
}

static void Main(string[] args)
{
    // register services with ServiceLocator
    ServiceLocator.Instance.Register("ServiceA", new ServiceA());
    ServiceLocator.Instance.Register("ServiceB", new ServiceB());

    //create client and get services 
    Client client = new Client();
    client.serviceA = (IServiceA)ServiceLocator.Instance.GetService("ServiceA");
    client.serviceB = (IServiceB)ServiceLocator.Instance.GetService("ServiceB");
    client.DoWork();

    Console.ReadLine();
}

请注意,在此版本中,我们有一个“初始化阶段”,在此阶段我们将服务注册到 ServiceLocator。这可以从代码动态完成,也可以从配置文件完成。

5 服务定位器模式 – 动态版本 – 泛型

在这里,我们将展示此模式的另一个动态版本,一个基于泛型方法版本。原则与上述相同,只是 ServiceLocator 的实现不同。此版本在文献 [6] 中非常流行。

这是类图

这是该模式的示例代码

internal interface IServiceA
{
    void UsefulMethod();
}

internal interface IServiceB
{
    void UsefulMethod();
}

internal class ServiceA : IServiceA
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceA-UsefulMethod");
    }
}

internal class ServiceB : IServiceB
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceB-UsefulMethod");
    }
}

internal class Client
{
    public IServiceA serviceA = null;
    public IServiceB serviceB = null;

    public void DoWork()
    {
        serviceA?.UsefulMethod();
        serviceB?.UsefulMethod();
    }
}

internal class ServiceLocator
{
    private static ServiceLocator locator = null;

    public static ServiceLocator Instance
    {
        get
        {
            // ServiceLocator itself is a Singleton
            if (locator == null)
            {
                locator = new ServiceLocator();
            }
            return locator;
        }
    }

    private ServiceLocator()
    {
    }

    private Dictionary<Type, object> registry =
        new Dictionary<Type, object>();

    public void Register<T>(T serviceInstance)
    {
        registry[typeof(T)] = serviceInstance;
    }

    public T GetService<T>()
    {
        T serviceInstance = (T)registry[typeof(T)];
        return serviceInstance;
    }
}

static void Main(string[] args)
{
    // register services with ServiceLocator
    ServiceLocator.Instance.Register<IServiceA>(new ServiceA());
    ServiceLocator.Instance.Register<IServiceB>(new ServiceB());

    //create client and get services 
    Client client = new Client();
    client.serviceA = ServiceLocator.Instance.GetService<IServiceA>();
    client.serviceB = ServiceLocator.Instance.GetService<IServiceB>();
    client.DoWork();

    Console.ReadLine();
}

请注意,在此版本中,我们同样有一个“初始化阶段”,在此阶段我们将服务注册到 ServiceLocator。这可以从代码动态完成,也可以从配置文件完成。

6 服务定位器模式的优缺点

6.1 优点

此模式的优点是

  • 它支持运行时绑定和在运行时添加组件。
  • 在运行时,可以更换组件,例如,用 ServiceA2 替换 ServiceA,而无需重新启动应用程序。
  • 由于模块有清晰的边界(即接口),因此可以实现并行代码开发。
  • 由于 DIP 原则和按接口分离模块,因此可以随意替换模块。
  • 可测试性好,因为您可以在注册 ServiceLocator 时将 Services 替换为 MockServices

6.2 缺点

此模式的缺点是

  • ClientServiceLocator 具有额外的依赖关系。没有 ServiceLocatorClient 代码就无法重用。
  • ServiceLocator 隐藏了 Client 的依赖关系,导致在依赖项丢失时出现运行时错误,而不是编译时错误。
  • 所有组件都需要对服务定位器(一个单例)有引用。
  • 将服务定位器实现为单例也可能在高度并发的环境中导致可伸缩性问题。
  • 服务定位器更容易引入接口实现中的破坏性更改。
  • 可测试性问题可能会出现,因为所有测试都需要使用相同的全局 ServiceLocator(单例)。
  • 在单元测试期间,您需要模拟 ServiceLocator 及其定位的服务。

6.3 有些人认为它是反模式

首先,稍微离题一下。让我们看看 [8] 中的这个定义:“反模式是一种常用结构或模式,尽管最初看来是解决问题的恰当有效的方法,但其不良后果多于良好后果。”

因此,一些人 [6] 认为服务定位器模式,虽然解决了一些问题,但由于其不良后果太多,他们称之为“反模式”。换句话说,他们认为引入的缺点大于优点。主要反对意见是它隐藏了依赖关系,并使模块更难测试。

6.4 服务定位器模式 (SLP) vs 依赖注入容器 (DIC)

文献通常倾向于在可能的情况下使用 DIC 而不是 SLP。以下是对这两种方法的常见比较。

  • 这两种模式的目标都是通过抽象层(接口)将 Client 与其依赖的服务解耦。主要区别在于,在 SLP 中,类依赖于充当装配器的 ServiceLocator,而在 DIC 中,类是自动装配的,而这些类独立于装配器(即 DI 容器)。此外,在 SLP 中,客户端类显式请求服务,而在 DIC 中,没有显式请求。
  • SLP 和 DIC 都引入了依赖项丢失时容易出现运行时错误的问题。这仅仅是因为它们都试图将应用程序解耦为依赖于“抽象层”(即接口)的模块。因此,当依赖项丢失时,会在运行时以运行时错误的形式发现。
  • 使用 DIC,通过查看注入机制可以更容易地看到组件依赖关系。使用 SLP,您需要搜索源代码中对服务定位器的调用。([11])
  • 一些有影响力的作者([11])认为,在某些情况下,当隐藏依赖关系不是大问题时,他们看不到 DIC 比 SLP 提供任何更多的好处。
  • SLP 的一个大问题是所有组件都需要引用 ServiceLocator,而 ServiceLocator 是一个单例。Singleton 模式对于 ServiceLocator 来说,在高并发应用程序中可能是一个可伸缩性问题。使用 DIC 而不是 SLP 可以避免这些问题。这两种模式具有相同的目标。
  • 人们普遍认为,使用 DIC 比使用 SLP 提供更好的可测试性。一些作者([11])不认同这一观点,并认为这两种方法都易于将真实的服务实现替换为模拟实现。

7 IServiceLocator 接口(又名 CommonServiceLocator)

为了解耦应用程序对特定服务定位器实现的依赖,从而使代码/组件更具可重用性,发明了 IServiceLocator 接口。IServiceLocator 接口是对服务定位器的抽象。这样就创建了一个可插拔的架构,因此应用程序不再依赖于任何特定的服务定位器实现,任何实现该接口的具体服务定位器模块都可以插入到应用程序中。

IServiceLocator 接口最初位于 Microsoft.Practices.ServiceLocation 命名空间 [12],但该模块已被弃用。似乎现在它的后继者是 CommonServiceLocator 命名空间 [13],但该项目也不再维护。无论如何,您可以在 [14] 找到该接口的描述,它看起来像这样

namespace CommonServiceLocator
{
    // The generic Service Locator interface. This interface is used
    // to retrieve services (instances identified by type and optional
    // name) from a container.

    public interface IServiceLocator : IServiceProvider
    {
        // Get an instance of the given serviceType
        object GetInstance(Type serviceType);
        // Get an instance of the given named serviceType
        object GetInstance(Type serviceType, string key);
        // Get all instances of the given serviceType currently
        // registered in the container.
        IEnumerable<object> GetAllInstances(Type serviceType);
        // Get an instance of the given TService
        TService GetInstance<TService>();
        // Get an instance of the given named TService
        TService GetInstance<TService>(string key);
        // Get all instances of the given TService currently
        // registered in the container.
        IEnumerable<TService> GetAllInstances<TService>();
    }
}

8 作为服务定位器 (SL) 的依赖注入容器 (DIC)

有趣的是,依赖注入容器 (DIC) 提供了服务定位器 (SL) 的一种超集服务,并且在需要时(有意或不当使用时)可以充当 SL。

如果您查询依赖项,即使它是一个 DI 容器,它也会成为服务定位器模式([6])。当应用程序(而不是框架 [7])主动查询 DI 容器以获取所需的依赖项时,DI 容器实际上充当服务定位器。因此,如果您不正确地使用 DI 容器作为框架,而是创建对 DI 容器的显式依赖,那么您实际上将使用服务定位器模式。这不仅仅是容器的品牌,还在于您如何使用它。

一个有趣的有意滥用上述事实是,当 DI 容器通过 IServiceLocator 接口公开自身时(适配器设计模式的应用)。

8.1 Autofac DI 容器充当服务定位器

我们将以 Autofac [15] DI 容器为例进行说明。它提供了 Autofac.Extras.CommonServiceLocator 适配器([16]、[17]),使其看起来像 IServiceLocator。在这种情况下,Autofac.Extras.CommonServiceLocator 充当适配器设计模式,并将 Autofac DI 容器公开为服务定位器。发生的情况是,Autofac DI 容器用作服务定位器模式的后端。

这是类图

这是代码示例

internal interface IServiceA
{
    void UsefulMethod();
}

internal interface IServiceB
{
    void UsefulMethod();
}

internal class ServiceA : IServiceA
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceA-UsefulMethod");
    }
}

internal class ServiceB : IServiceB
{
    public void UsefulMethod()
    {
        //some useful work
        Console.WriteLine("ServiceB-UsefulMethod");
    }
}

internal class Client
{
    public IServiceA serviceA = null;
    public IServiceB serviceB = null;

    public void DoWork()
    {
        serviceA?.UsefulMethod();
        serviceB?.UsefulMethod();
    }
}

static void Main(string[] args)
{
    // Register services with Autofac 
    Autofac.ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<ServiceA>().As<IServiceA>();
    builder.RegisterType<ServiceB>().As<IServiceB>();
    Autofac.IContainer container = builder.Build();

    // create Service Locator
    CommonServiceLocator.IServiceLocator locator = new AutofacServiceLocator(container);
    CommonServiceLocator.ServiceLocator.SetLocatorProvider(() => locator);
    CommonServiceLocator.IServiceLocator myServiceLocator = ServiceLocator.Current;

    //create client and get services 
    Client client = new Client();
    client.serviceA = myServiceLocator.GetInstance<IServiceA>();
    client.serviceB = myServiceLocator.GetInstance<IServiceB>();
    client.DoWork();

    Console.ReadLine();
}

在上面的示例中,不一定是接口 IServiceLocator 的使用使其成为服务定位器模式,而是我们**显式**请求解析依赖项并隐式创建对 IServiceLocator 的依赖。

有趣的是,您可能可以向 Autofac 添加分层依赖关系链,Autofac 将通过依赖注入解析它们。因此,您将获得一种混合解决方案,即深入依赖的 DI 解析和顶层依赖的显式解析。但这更多是由于您将 DI 容器用作服务定位器的后端而导致的异常,而不是有效模式。服务定位器解析通常深入一层,而 DI 容器解析则递归深入任何深度。

9 结论

虽然服务定位器模式通常被依赖注入模式/容器 [7] 所忽视,并且如今被视为一种反模式,但了解它是如何工作的仍然很有趣。我认为研究那些未能成功的模式并了解它们失败原因很有教育意义。

了解为什么服务定位器模式被认为不如依赖注入容器(DIC)是一个重要的方面,而不仅仅是依赖于“它在互联网上不再流行”这一事实。

看到曾经被软件工程架构思想认为是具有前景的设计模式,如今却被称为反模式,并在不同的互联网论坛上几乎被鄙视,这很有趣。

一些作者 [6] 公开承认,他们从服务定位器模式的支持者和实现库开发者转变为该模式的反对者和批评者。

在时尚界,许多时尚单品和潮流会不断重复。我们将拭目以待,软件设计模式的世界是否也会发生类似的事情。

10 参考文献

历史

  • 2022年7月13日:初始版本
© . All rights reserved.