C# 中的服务定位器模式
服务定位器模式入门教程,
1 简短教程
本文的目的是提供一个关于“服务定位器设计模式”的简洁教程,并附带 C# 示例。虽然这个设计模式已被“依赖注入模式”和“依赖注入容器”的使用所取代,但由于学术原因和实际原因(例如遗留代码可能仍依赖此模式),它仍然可能引起读者的兴趣。如今,在新的代码中不推荐使用服务定位器模式,甚至一些作者认为它是一种反模式。
提供的代码是教程级别的,“概念验证”,为了简洁起见,没有处理异常等。
2 依赖倒置原则 – DIP
因此,“依赖倒置原则(DIP)”是一个软件设计原则。它被称为“原则”是因为它提供了关于如何设计软件产品的高级建议。
DIP 是由 Robert C. Martin [5] 推广的 SOLID [3] 首字母缩略词所知的五项设计原则之一。DIP 原则规定
- 高层模块不应依赖于低层模块。两者都应依赖于抽象。
- 抽象不应依赖于细节。细节应依赖于抽象。
解释是
虽然高层原则谈论的是“抽象”,但我们需要将其转化为我们特定编程环境(在本例中是 C#/.NET)的术语。C# 中的抽象是通过接口和抽象类实现的。当谈论“细节”时,该原则意味着“具体实现”。
所以,基本上,这意味着 DIP 提倡在 C# 中使用接口,而具体实现(低层模块)应依赖于接口。
传统的模块依赖关系如下所示
DIP 提出了新的设计
正如您所见,一些依赖关系(箭头)的方向已反转,这就是“倒置”名称的由来。
DIP 的目标是创建“松散耦合”的软件模块。传统上,高层模块依赖于低层模块。DIP 的目标是使高层模块独立于低层模块的实现细节。它通过在它们之间引入一个“抽象层”(以接口的形式)来实现这一点。
DIP 原则是一个广泛的概念,并对其他设计模式产生影响。例如,当应用于工厂设计模式或单例设计模式时,它建议这些模式应返回对接口的引用,而不是对对象的引用。“依赖注入模式”遵循此原则。“服务定位器模式”也遵循此原则。
3 服务定位器模式 – 静态版本
首先,“服务定位器模式”是一个软件设计模式。它被称为“模式”是因为它为特定问题提供了低级特定实现的建议。
该模式旨在解决的主要问题是如何创建“松散耦合”的组件。目标是通过消除客户端和服务实现之间的依赖来提高应用程序的模块化。该模式使用一个称为“服务定位器”的中央注册表,该注册表在客户端请求时,为其提供它所依赖的服务。
此模式中有四个主要角色(类)
Client
:Client
是一个想要使用另一个名为Service
的组件提供的服务的组件/类。ServiceInterface
:Service
接口是描述Service
组件提供的服务类型的抽象。Service
:Service
组件/类根据 Service-Interface 描述提供服务。ServiceLocator
:是一个组件/类,它封装了如何获取Client
所需/依赖的服务知识。它是Client
获取服务的唯一联系点。它是所有客户端使用的服务的单例注册表。ServiceLocator
负责在Client
请求时返回服务实例。
其工作原理是 Client
依赖于 ServiceInterface
。Client
依赖于 ServiceInterface
接口,但本身不依赖于 Service
。Service
实现 ServiceInterface
接口,并提供 Client
所需的某些服务。Client
还依赖于 ServiceLocator
。Client
**显式**地从 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 缺点
此模式的缺点是
Client
对ServiceLocator
具有额外的依赖关系。没有ServiceLocator
,Client
代码就无法重用。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 参考文献
- [1] https://en.wikipedia.org/wiki/Service_locator_pattern
- [2] https://stackify.com/service-locator-pattern/
- [3] https://martinfowler.com.cn/articles/injection.html#UsingAServiceLocator
- [4] https://tutorialspoint.org.cn/design_pattern/service_locator_pattern.htm
- [5] https://www.geeksforgeeks.org/service-locator-pattern/
- [6] Mark Seemann, Steven van Deursen - Dependency Injection Principles, Practices, and Patterns, Manning Publications, 2019
- [7] https://codeproject.org.cn/Articles/5333947/Dependency-Injection-Pattern-in-Csharp-Short-Tutor
- [8] https://en.wikipedia.org/wiki/Anti-pattern
- [9] https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
- [10] https://codeproject.org.cn/Articles/597787/A-tutorial-on-Service-locator-pattern-with-impleme
- [11] https://martinfowler.com.cn/articles/injection.html#ServiceLocatorVsDependencyInjection
- [12] https://docs.microsoft.com/en-us/previous-versions/msp-n-p/hh860410(v=pandp.51)
- [13] https://nuget.net.cn/packages/CommonServiceLocator/
- [14] https://github.com/unitycontainer/commonservicelocator/blob/master/src/IServiceLocator.cs
- [15] https://autofac.org/
- [16] https://autofac.org/apidoc/html/CE503C57.htm
- [17] https://nuget.net.cn/packages/Autofac.Extras.CommonServiceLocator/
历史
- 2022年7月13日:初始版本