目录
7 IServiceLocator Interface (又名 CommonServiceLocator)
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组件/类正在根据服务接口描述提供服务。
- ServiceLocator:是一个组件/类,它封装了有关如何获取Client需要/依赖的服务的知识。它是Client获得服务的单点联系。它是客户端使用的所有服务的单一实例注册表。ServiceLocator负责在Client请求服务实例时返回服务实例。
它的工作方式是Client依赖于ServiceInterface。Client依赖于ServiceInterface接口,但不依赖于Service自身。Service实现ServiceInterface接口并提供Client所需的某些服务。Client对ServiceLocator也有依赖性。Client显式请求它所依赖的Service的ServiceLocator实例。一旦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注册时使用MockServices替换Services。
6.2 缺点
此模式的缺点是:
- Client对ServiceLocator有额外的依赖性。没有ServiceLocator就不可能重用Client代码。
- ServiceLocator掩盖Client依赖项,当依赖项丢失时,会导致运行时错误而不是编译时错误。
- 所有组件都需要具有对服务定位器的引用,该服务定位器是单一实例。
- 将服务定位器实现为单一实例也会在高并发环境中产生可伸缩性问题。
- 使用服务定位器可以更轻松地在接口实现中引入重大更改。
- 由于所有测试都需要使用相同的全局ServiceLocator(单例),因此可能会出现可测试性问题。
- 在单元测试期间,您需要模拟ServiceLocator及其定位的服务。
6.3 有些人认为这是一种反模式
首先,一个小题外话。让我们看看[8]中的这个定义:“反模式是一种常用的结构或模式,尽管最初似乎是对问题的适当和有效的反应,但坏的后果多于好的后果”。
因此,有些人[6]认为服务定位器模式虽然解决了一些问题,但会产生许多不良后果,以至于他们将其称为“反模式”。换句话说,他们认为引入的缺点大于好处。主要的反对意见是它掩盖了依赖关系并使模块更难测试。
6.4 服务定位器模式(SLP)与依赖注入容器(DIC)
文献通常倾向于尽可能使用DIC而不是SLP。以下是这两种方法的频繁比较。
- 这两种模式都有一个目标,即使用抽象层(接口)将客户端与其所依赖的服务分离。主要区别在于,在SLP中,类依赖于充当汇编程序的ServiceLocator ,而在DIC中,您可以自动连接独立于汇编程序的类,在本例中为DI Container。此外,在SLP客户端类中显式请求服务,而在 DIC 中,没有显式请求。
- SLP和DIC都引入了一个问题,即当缺少依赖项时,它们容易出现运行时错误。这只是因为它们都试图将应用程序解耦为依赖于“抽象层”(即接口)的模块的结果。因此,当缺少依赖项时,会在运行时以运行时错误的形式发现。
- 使用DIC,只需查看注入机制,就更容易看到什么是组件依赖关系。使用SLP时,您需要在源代码中搜索对服务定位器的调用。([11])
- 一些有影响力的作者([11])认为,在某些情况下,当隐藏依赖项不是这样的问题时,他们不认为DIC提供的不仅仅是SLP。
- SLP有一个大问题,因为所有组件都需要引用ServiceLocator,这是一个单例。ServiceLocator的Singleton模式可能是高并发应用程序中的可伸缩性问题。这些问题可以通过使用DIC而不是SLP来避免。两种模式具有相同的目标。
- 人们普遍认为,DIC的使用比SLP的使用提供了更多的可测试性。一些作者([11])不同意这种观点,并认为这两种方法都可以很容易地用模拟实现替换真正的服务实现。
7 IServiceLocator Interface (又名 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)的超集服务,如果需要,有意或未正确使用,可以像一个超集一样运行。
如果要查询依赖项,即使它是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]忽略,并且今天被认为是一种反模式,但看看它是如何工作的仍然很有趣。我认为研究那些没有成功的模式,并了解是什么让它们失败是有教育意义的。
重要的是要知道为什么服务定位器模式被认为是依赖注入容器的劣质解决方案,而不仅仅是依赖于“它在互联网上不再流行”的事实。
有趣的是,软件工程架构思维曾经被认为是透视的设计模式现在被称为反模式,并且在互联网上的不同论坛上几乎被鄙视。
一些作者[6]公开承认,他们从服务定位器模式的支持者和实现库开发人员变成了该模式的反对者和批评者。
在时尚界,许多时尚作品和趋势最终会重演。我们将看看软件设计模式的世界是否会发生类似的事情。
10 参考资料
- [1] https://en.wikipedia.org/wiki/Service_locator_pattern
- [2] Design Patterns Explained - Service Locator Pattern with Code Examples
- [3] Inversion of Control Containers and the Dependency Injectionpattern
- [4] Design Pattern - Service Locator Pattern
- [5] Service Locator Pattern - GeeksforGeeks
- [6] Mark Seemann, Steven van Deursen - Dependency Injection Principles, Practices, and Patterns, Manning Publications, 2019
- [7] Dependency Injection Pattern in C# – Short Tutorial - CodeProject
- [8] https://en.wikipedia.org/wiki/Anti-pattern
- [9] Service Locator is an Anti-Pattern
- [10] A tutorial on Service locator pattern with implementation - CodeProject
- [11] Inversion of Control Containers and the Dependency Injectionpattern
- [12]https://docs.microsoft.com/en-us/previous-versions/msp-n-p/hh860410(v=pandp.51)
- [13] NuGet Gallery | CommonServiceLocator 2.0.6
- [14] commonservicelocator/IServiceLocator.cs at master · unitycontainer/commonservicelocator · GitHub
- [15] Autofac: Home
- [16] AutofacServiceLocator Class
- [17] NuGet Gallery | Autofac.Extras.CommonServiceLocator 6.0.1
https://www.codeproject.com/Articles/5337102/Service-Locator-Pattern-in-Csharp