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

服务定位器模式教程及实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (20投票s)

2013年6月3日

CPOL

7分钟阅读

viewsIcon

114861

downloadIcon

1825

本文通过一个示例实现提供了服务定位器模式的教程。

引言

本文档提供了一个关于 Service Locator 模式的教程,以及一个服务定位器的示例实现。该服务定位器使用激活器(activators)来创建本地或远程服务,并使用发现器(discoverers)在运行时发现服务。

背景

从根本上讲,定位器模式提供了一种通过封装获取依赖服务的过程来减少类与其依赖项之间耦合的方法。

让我们从一个使用 Logger 类实现的日志服务类的简单示例开始

class ClientClass
{
    private Logger _logger;
    public ClientClass()
    {
         _logger = new Logger();
        _logger.Log("Logger was created");
    }
}

在上面的代码片段中:  

  • ClientClassLogger 的特定实现紧密耦合。
  • ClientClass 知道如何创建 Logger 的实例(它拥有 Logger 所在程序集的引用)。
  • 要测试 ClientClass,您需要 Logger 的特定实现。当然,您可以模拟 Logger。  

ClientClass 及其与 Logger 的紧密耦合并没有错,但如果您有以下需求:

  • 与 Logger 松耦合,以便在 ClientClass 更改很少或不更改的情况下更改其实现。
  • 消除定位和创建依赖服务的重复逻辑。
  • 独立于日志服务的​​位置以及实例化 Logger 服务的方式。

那么您可能需要考虑 Service Locator 模式。关于 Service Locator 模式,MSDN 上有一个很好的参考资料 这里。我们不会直接跳到 Service Locator 模式,而是逐步介绍潜在的解决方案,看看我们是否能自己得出该模式。  

COM 用户快速通道 

如果您熟悉 COM,您可能想阅读本节以快速了解 Service Locator 模式。如果不熟悉,可以跳过本节。

还记得以前可以直接使用 CoCreateInstance/Ex 来获取某个类实现的契约(contract)的引用的日子吗?下面是一个非常简单的伪代码,说明您可以怎么做:

CoCreateInstance “类 ID”,“进程内或进程外”,“契约/接口 ID”

您不必关心实际的可执行文件在系统上的位置(或哪个系统)。COM 会以某种方式返回您请求的类的实例,该实例实现了您所需的契约。那么问题来了,内部发生了什么?我将以进程内组件为例,但对于进程外或远程组件,原理是相同的。请注意,这是实际发生情况的简化版本。

当您调用 CoCreateInstance

  • COM 会查找实现您所需接口的类的位置(DLL)(在注册表中 HKEY_CLASSES_ROOT\CLSID 下)。
  • COM 会加载 DLL。
  • COM 会调用 DllGetClassObject 来创建该类的工厂
  • COM 会调用工厂的 CreateInstance 来获取您请求的类的实际实例。
  • COM 会返回您请求的类。

因此,客户端类不必担心其依赖组件的位置和激活。这正是 Service Locator 模式的意图。您可以调用 ServiceLocator::GetComponent(type, contract) 来获取一个对象,服务定位器会以某种方式返回您请求的类的实例。与 COM 实现类似,Service Locator 应该在后台使用工厂来创建类的实例。

走向 Service Locator 模式

为了满足上面“背景”部分中提到的一些需求,您可能会重构您的类,使其如下所示:

public class ClientClass
{
    private ILogger _logger;

    public ClientClass()
    {
        _logger = LoggerFactory.GetLogger("context");
        _logger.Log("Logger was created");
    }
}

您甚至可以通过配置文件、反射等方式扩展设计,将各种日志记录器注册到 LoggerFactory 。例如:

public class ObjectFactory
{
    public readonly static ObjectFactory=  new ObjectFactory();

    public T GetInstance<T>(string context) where T : class
    {

        if(typeof(T).Equals(typeof(ILogger)))
        {
            return new Logger() as T; //simplistically. Context is not used here in this example
        }
        //other types....
        return null;
    }
}

ClientClass 将使用 ObjectFactory 来访问各种服务。

ILogger logger = GeneralObjectCreator.Instance.GetInstance<ILogger>();
ICalc calculator = GeneralObjectCreator.Instance.GetInstance<ICalc>(); 

这现在非常类似于 Locator 模式的实现。此时可能会出现一个问题:Locator 和 Factory 之间有什么区别?引用 MSDN 这里 的说法:

Service Locator 模式不描述如何实例化服务。它描述了一种注册和定位服务的方式。通常,Service Locator 模式结合了 Factory 模式和/或 Dependency Injection 模式。这种组合允许服务定位器创建服务实例。 

另一方面,Factory 模式是一种创建型模式,即它负责直接或间接地构建相关对象。

但是,外面有很多不同的观点。您可以找到一些服务定位器的实现,它们也负责创建服务实例。本文中的实现将采用以下范例:

  • 定位器主要负责提供服务的中央存储库。  
  • 定位器将使用“激活器”来创建实际类型的实例。这将使定位器与创建实例的各种方式解耦,例如,从本地程序集创建实例或通过 remoting 或 WCF 连接到远程服务。  
  • 可以通过编程方式或通过配置文件将服务添加到定位器。
  • 可以使用发现器动态地将发现的服务添加到定位器。

请注意,这是一个示例实现,不打算直接在生产环境中使用。

有些人可能会问 Service Locator 与 DI 有何不同?DI 是另一种以不同方式解决相同问题的模式。主要区别在于,如果您使用 Service Locator 模式,您的类会明确依赖于 Service Locator,而 DI 会执行自动装配,以使您的各个类独立于 Service Locator(类由“装配器”“提供/注入”依赖项)。DI 在某些方面也可以充当定位器。定位器可以被 DI 用作容器,在自动装配期间查找要使用的各种服务。

哪个更好?Service Locator 还是 DI?这取决于上下文、需求和回答问题的人。两者都有各自的优缺点,网络上有足够多的资料可供查阅。

实现

下图展示了本文教程中 Service Locator 模式实现的总体概览。

让我们详细看看上面的每个参与者: 

客户端

客户端使用服务定位器来访问各种服务。

下面是客户端使用定位器的简单示例。

IServiceLocator locator = ServiceLocator.instance();
ILogger logger = locator.GetInstance<ILogger>();
//or locator.GetInstance<ILogger>("logger") for named access
logger.Log("this is a log message");

下面是客户端使用定位器的更详细示例。

public void TestLocator()
{
    //add manual entry to locator
    LocatorEntry entry = new LocatorEntry();
    entry.ContractTypeName = "LocatorExample.Contracts.ILogger, LocatorExample.Contracts";
    entry.IsSingleton = false;
    entry.LocationName = "ManuallyEnteredLoger";
    entry.ActivatorTypeName = typeof(DefaultAssemblyActivator).AssemblyQualifiedName;
    var activatorConfig = new DefaultAssemblyActivatorConfig();
    activatorConfig.TypeToCreate = "ExampleComponents.Logger, ExampleComponents";
    entry.ActivatorConfig = activatorConfig;
    ServiceLocator.Instance.RegisterLocatorEntry(entry);

    ILogger logger = ServiceLocator.Instance.GetReference<ILogger>("ManuallyEnteredLoger");
    logger.Log("Manually added logger called");

    //get logger for configuration file
    logger = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    logger.Log("Logger added from config file called");

    //Try remoting activator (specified in config file, transparent to client)
    ICalc calc = ServiceLocator.Instance.GetReference<ICalc>("Calculator");
    int sum = calc.Add(10, 20);
    logger.Log(" Result of addition using remoting activator is " + sum);

    //Try dynamically discovered type
    IAuthenticator authenticator = ServiceLocator.Instance.GetReference<IAuthenticator>();
    bool authenticated = authenticator.Authenticate("user", "pass");
    ServiceLocator.Instance.GetReference<ILogger>("Logger").Log(
            "Dynamically discovered Authenticator returned " + authenticated);

    //Get all services which implement ILogger
    IList<ILogger> loggers = ServiceLocator.Instance.GetAllReferences<ILogger>();
    foreach (var item in loggers)
    {
        item.Log("Calling a service which impements ILogger");
    }

    //make sure entries which are marked as singletons are singletons
    ILogger logger1 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
    ILogger logger2 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
    if( object.ReferenceEquals(logger1, logger2))
    {
        logger.Log("Got the same reference for SingletonLogger");   
    }

    //try with non singletons
    logger1 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    logger2 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    if (! object.ReferenceEquals(logger1, logger2))
    {
        logger.Log("Got different references for non Singletons");
    }
}

上面示例的配置文件如下所示:

<configuration>
  <configSections>
    <section name="Locator" type="Locator.ServiceLocatorConfig, Locator" />
  </configSections>

  <Locator>
    <LocatorEntries>
      <!--Add a locator entry for a  logger using activation from assembly-->
      <Add LocationName="Logger" 
           ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
           ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
           ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator">
        <ActivatorConfig TypeToCreate="ExampleComponents.Logger, ExampleComponents" />

      </Add>

      <!--Add another locatory entry for a advanced logger using activation from assembly-->
      <Add LocationName="SingletonLogger" 
           ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
           ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
           ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator"
           IsSingleton="true">
        <ActivatorConfig TypeToCreate="ExampleComponents.AdvancedLogger, ExampleComponents" />

      </Add>

      <!--Add another locator entry for a calculator using remoting activation. -->
      <Add LocationName="Calculator" ContractTypeName="LocatorTest.ICalc, LocatorTest"
           ActivatorTypeName="Activators.RemotingServiceActivator, Activators"
           ActivatorConfigType="Activators.RemotingActivatorConfig, Activators">
        <ActivatorConfig Uri="https://:9871/Calc" />

      </Add>
    </LocatorEntries>

    <Discoverers>
      <!--Dynamically discover IAuthenticator types-->
      <Add DiscovererTypeName="Locator.DefaultAssemblyDiscoverer, Locator"
           DiscovererConfigType="Locator.DefaultAssemblyDiscovererConfig, Locator">
        <DiscovererConfig SearchPath=".;c:\temp;plugins;..\plugins"
          SearchForContract="LocatorExample.Contracts.IAuthenticator, LocatorExample.Contracts" />
      </Add>
    </Discoverers>
  </Locator>
</configuration>

定位器

定位器负责维护客户端可以请求的服务注册表。这些服务可以通过名称或服务实现的契约来访问,例如 Locator.GetInstance(ILogger.GetType())Locator.GetInstance("name")。此外,定位器还可以提供其他功能,例如维护服务的共享实例(单例)等。 

定位器本身不激活服务,而是使用激活器来执行此操作。这使定位器与创建服务实例的各种方式解耦,例如,从本地程序集创建服务实例或通过 remoting 或 WCF 连接到远程服务。  

定位器和激活器激活类型所需的信息是:

  • LocatorName:这允许客户端通过名称访问组件。可选。
  • ContractType:这允许客户端通过类型访问组件。
  • IsSingleton:这指定定位器应该返回共享实例还是新实例。
  • ActivatorTypeName:这是定位器用于激活类型的激活器。
  • ActivatorConfigType:这是激活器使用的特定于激活器的配置对象,例如,这可能是 C# 类的完全限定名称,如 Utils.Logger,或远程对象的 URI。定位器将此信息提供给激活器。请注意,我使用了一个动态配置部分来将激活器的配置与定位器条目保持在一起。

发现器

如上面的示例所示,可以通过编程方式或通过配置文件将服务注册到定位器。为了注册部署时未知的类型,可以使用发现范例将类型注册到定位器。我提供的示例在提供的路径列表中搜索程序集。可以创建其他发现器以从中央存储库检索服务列表。

下图是一个(简化的)整体工作流程的序列图。

附加解决方案的内容

附加的解决方案包含以下项目:

  1. Locator:这是主项目,包含 Service Locator 的实现,以及 DefaultAssemblyActivatorDefaultAssemblyDiscoverer
  2. Activators:包含一个 remoting 激活器的示例。
  3. ExampleComponents:包含日志记录器和身份验证器等服务的简单实现。
  4. LocatorExample.Contracts:包含日志记录器和身份验证器的契约(ILoggerIAuthenticator)。
  5. Locator.Utilities:包含定位器使用的一些实用程序类。
  6. LocatorTest:包含定位器使用示例以及一个示例计算器服务的实现。
© . All rights reserved.