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

使用工厂模式进行动态绑定

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2009年11月1日

CPOL

11分钟阅读

viewsIcon

27104

downloadIcon

196

使用工厂设计模式来隐藏动态绑定,并使用配置文件字符串来确定应该实例化哪些类。

目录

引言

我需要使用一个暴露对象的服务,该服务通过一个接口(例如 ITheService 接口)公开。对于单元测试,我希望在单元测试进程中使用我自己的接口简单实现,这样我的单元测试就不会依赖于实际的服务实现。对于正式测试和生产,客户端将通过 .NET TCP 远程处理连接到服务。将来,我们可能会希望通过 HTTP 连接。我想通过一个配置文件字符串在实现之间切换。在本文中,我将分享一种使用工厂设计模式实现此类动态绑定的方法。

背景

动态地将类加载到当前进程空间通常需要结合使用 Assembly.LoadAssembly.CreateInstance 方法。这需要知道程序集名称和类名。Assembly.CreateInstance 方法能够将值传递给类构造函数。

动态建立 .NET 远程处理连接的最直接机制是使用 Activator.GetObject 方法。这需要知道远程对象的类型和 URI。只能使用默认构造函数。没有内置的机制可以将值传递给构造函数。

此时,我将大致忽略 HTTP 的可能性,只是记住我们的配置文件字符串需要允许这种可能性。只使用默认构造函数。

为了实现可扩展设计,如果解决方案能够允许添加新的激活类型而无需重新编译现有程序集,那将是很好的。

基本解决方案

让我们从配置文件字符串开始,然后逐步展开。我该如何在单个字符串中表示所需的信息?URI 提供了一种很好的格式,可以表示所有这些条件。

  • 程序集名称、类名和构造函数的字符串可以编码到 URI 风格的字符串中(*local:///AssemblyName/ClassName?ConsructorString*)。
  • .NET 远程处理使用 URI 风格的字符串(*tcp://:9000/TheClass*)。.NET 远程处理始终使用默认构造函数,但如果我们能开发一种约定来允许类初始化,那将是很好的。
  • 我们可以合理地假设 HTTP 也可以使用某种形式的 URI。

在上面的配置文件字符串中,URI 的 scheme(协议)部分决定了字符串的解释方式。其余部分提供了关于如何实例化类的有关信息。一种通用的创建设计模式,用于将某种信息映射到类创建,是工厂模式。这似乎是这个解决方案的一个很好的候选者,它将 scheme 映射到一个专门的类,该类可以根据字符串中的信息创建一个所需类的实例(或代理)。

我们的解决方案将包含三个基本部分:

  1. 一个单例类,它将充当工厂。我们将这个类称为 Locator。当我们想要创建一个由配置指定的对象时,我们将把配置字符串传递给 Locator。Locator 将选择知道如何创建该类型对象的专门类,并要求该专门对象来创建它。
  2. 一个所有专门类都将派生的接口。我们将这个接口称为 ILocatorActivator。它将指定工厂创建对象所必需的方法和签名。
  3. 至少一个 ILocatorActivator 接口的具体实现(一个专门类的实现)。在这种情况下,我们将从两个实现开始。一个用于 Assembly.CreateInstance(进程内),另一个用于 Activator.GetObject(.NET 远程处理)。

注意:我选择将 ILocatorActivator 接口嵌套在 Locator 类实现中。我这样做的原因有两个。首先,它减少了解决方案中的文件数量。这不一定是个好理由,但在上传示例时很有帮助。其次,它只在工厂的上下文中才有意义。它仅用于创建该工厂的专门类。某些编码标准会建议不要嵌套。遵循您的编码标准(它们是为了提供帮助)。

工厂

接下来,我们考虑工厂。工厂负责将 URI 字符串转换为对象。在此示例中,工厂是一个名为 Locator 的类。我们将从类的基本结构开始,并逐步填充方法的内部。Locator 类应该

  • 具有单例行为,
  • 有一个创建对象的方法(Activate),
  • 有一个注册 ILocatorActivator 实现的方法(RegisterActivator)。

这个工厂依赖于所有激活器类(将从 URI 字符串实际创建对象的类)拥有一个共同的接口(ILocatorActivator),所以我们在这里也定义它。

public class Locator
{
    public interface ILocatorActivator
    {
        object Activate(Type type, Uri uri);
    }

    private static Dictionary<string,> _activators
        = new Dictionary<string,>();

    static Locator()
    {
    }

    public static void RegisterActivator(string scheme, 
                  ILocatorActivator activator)
    {
    }

    public static object Activate(Type type, string locator)
    {
    }
}

所有静态方法和静态构造函数都用于实现单例行为。该类有一个集合,用于将 scheme 映射到 ILocatorActivator 接口的实现。由于此集合将由静态方法使用,因此它也是静态的。

我更希望不将 type 作为 Activate 方法的参数,但 Activator.GetObject 方法需要它,所以我们必须为所有方法添加它。长远来看,它可能对其他 ILocatorActivator 实现有帮助。ILocatorActivator 实现不必使用它。

让我们继续向 RegisterActivatorActivate 方法添加内容。RegisterActivator 将给定 scheme 的 ILocatorActivator 实现的实例添加到我们的集合中。

public static void RegisterActivator(string scheme, ILocatorActivator activator)
{
    if (_activators.Keys.Contains(scheme.ToLower()))
    {
        throw new ArgumentException(scheme);
    }
    else
    {
        _activators.Add(scheme.ToLower(), activator);
    }
}

对于这个简单的实现,我们不允许您重复添加相同的 scheme。Activate 方法负责查找 scheme 的合适 ILocatorActivator 实现,并要求映射的对象创建所需的对象。

public static object Activate(Type type, string locator)
{
    object response = null;

    Uri uri = new Uri(locator);
    if (_activators.Keys.Contains(uri.Scheme.ToLower()))
    {
        ILocatorActivator activator = _activators[uri.Scheme.ToLower()];
        response = activator.Activate(type, uri);
    }

    return response;
}

如果找不到该 scheme 的 ILocatorActivator 实现,则返回 null。否则,将使用 ILocatorActivator 实例来创建对象。

使用 Assembly.CreateInstance 进行进程内激活

现在,让我们看一下 Assembly.CreateInstance(进程内)ILocatorActivator 实现。我们将这个类称为 InProcessActivator。这个类只会从配置文件字符串(格式为 *“local://host/AssemblyName/ClassName?ConsructorString”*)使用 Assembly.CreateInstance 创建一个对象(在当前进程空间中)。

这个类的实例将使用 Locator.RegisterActivator 方法在 Locator 集合中与 scheme “local”相关联,因此 URI 字符串必须以 “local”开头。我们将忽略 host 值,但它在 URI 字符串中是必需的占位符。URI.LocalPath 属性用于确定程序集名称和类名。根据是否提供了构造函数字符串值,将选择正确版本的 Assembly.CreateInstance

public class InProcessActivator : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        // Uri must be in the format
        // local://host/AssemblyName/ClassName or
        // local://host/AssemblyName/ClassName?ConstructorString
        string[] pathSegments = uri.LocalPath.Split('/');
        if (pathSegments.Length != 3)
        {
            throw new ArgumentException(uri.ToString());
        }

        // path starts with slash, so element zero is empty
        string assemblyName = pathSegments[1];
        Assembly assembly = Assembly.Load(assemblyName);
        if (null == assembly)
        {
            throw new ArgumentException(uri.ToString());
        }

        string className = pathSegments[2];

        object response = null;

        //Do we have a constructor string?
        if (uri.Query.Length > 0)
        {
            string initString = Uri.UnescapeDataString(uri.Query);
            if ('?' == initString[0])
            {
                initString = initString.Remove(0, 1);
            }
            object[] initParam = { initString };

            response = assembly.CreateInstance(className, 
                false,
                BindingFlags.CreateInstance,
                null,
                initParam,
                null,
                null);
        }
        else
        {
            response = assembly.CreateInstance(className);
        }

        return response;
    }

    #endregion
}

除了 URI 解析代码之外,这只是普通的 System.Reflection 代码。

使用 Activator.GetObject 进行远程激活

接下来是 ILocatorActivatorActivator.GetObject 实现(.NET 远程处理)。我们将这个类称为 SaoActivator,代表 Server Activated Object Activator。这个类只会调用 Activator.GetObject 来从 URI 字符串(格式为 *“tcp://:9000/TheClass”*)创建一个代理对象。

这个类的实例将使用 Locator.RegisterActivator 方法在 Locator 集合中与 scheme “tcp”相关联,因此 URI 字符串必须以 “tcp”开头。有关此字符串的更多信息,请参阅 .NET 远程处理文档。

public class SaoActivator : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        return Activator.GetObject(type, uri.ToString());
    }

    #endregion
}

之前,我指出 Activator.GetObject 只使用默认构造函数。如果我们能将其扩展并允许在将对象返回给调用者之前进行初始化,那就更好了。这只能通过向我们正在创建的对象添加某种 Initialize 方法来实现,但这不属于 ILocatorActivator 接口的一部分,而且我们不希望将此方法强制应用于所有 ILocatorActivator 实现。为了实现这一点,我们需要开发一个约定,该约定仅可选地应用于将通过 SoaActivator 创建的对象。此约定将在一个名为 IInitializer 的接口中编码,该接口具有一个 Initialize 方法。可以通过 SoaActivator 创建并希望支持初始化的类可以实现此接口。对于支持该接口的对象,可以调用 Initialize 方法。对于不支持此接口的对象,并且提供了初始化参数,我们将抛出异常。这将使 SaoActivator 类如下所示:

public class SaoActivator : Locator.ILocatorActivator
{
    public interface IInitializer
    {
        void Intialize(string arg);
    }

    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        // if an initializatin parameter has been provided, it must be
        // stripped off before calling Activator.GetObject
        string realUri = (uri.Query.Length == 0) ?
            uri.AbsoluteUri :
            uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - uri.Query.Length);

        // Create the object
        object response = Activator.GetObject(type, realUri);

        // If an initiaization parameter has been provided, process it.
        if (uri.Query.Length > 0)
        {
            // if necessary, remove the "?"
            string initString = Uri.UnescapeDataString(uri.Query);
            if ('?' == initString[0])
            {
                initString = initString.Remove(0, 1);
            }

            IInitializer initializer = response as IInitializer;

            if (null == initializer)
            {
                // the user expected IInitializer support
                throw new ArgumentException(initString);
            }
            else
            {
                initializer.Intialize(initString);
            }
        }

        return response;
    }

    #endregion
}

SoaActivator 现在提供初始化支持,这是 .NET Remoting 不直接支持的。

为了方便起见

由于我将 IProcessActivatorSoaActivator 打包在与 Locator 相同的程序集中,所以我更希望将它们作为默认支持的类型。为了实现这一点,我将在 Locator 的静态构造函数中自动注册这些类型。这样,构造函数看起来就像这样:

static Locator()
{
    RegisterActivator("local", new InProcessActivator());
    RegisterActivator("tcp", new SaoActivator());
}

演示代码

附加的示例解决方案包含一个共享程序集,该程序集定义了我们的服务必须实现的接口。

public interface ITheService
{
    string Hello(string name);
}

SoaHost 项目通过 SoaTheService 类实现了 ITheService 接口和 IInitializer 接口(以支持初始化)。然后它使此类可远程。

public class SoaTheService : MarshalByRefObject, 
                             ITheService, SaoActivator.IInitializer
{
    private string _salutation = "Hello ";
    #region ITheService Members

    public string Hello(string name)
    {
        return string.Format("{0} {1}", _salutation, name);
    }

    #endregion

    #region IInitializer Members

    public void Intialize(string arg)
    {
        _salutation = arg;
    }

    #endregion
}

客户端项目通过 TheInProcessService 类实现了 ITheService 接口。然后,它使用 Locator 创建 TheInProcessServiceSoaTheService 类的实例。为了方便阅读和调试,URI 字符串直接写在代码中,而不是放在配置文件中。

要在当前进程中创建 TheInProcessService 对象,客户端调用:

ITheService theClass = Locator.Activate(typeof(ITheService), 
    "local:///Wycoff.Client/Wycoff.Client.TheInProcessService") 
    as ITheService;

theClass = Locator.Activate(typeof(ITheService), 
    "local:///Wycoff.Client/Wycoff.Client.TheInProcessService?Hola") 
    as ITheService;

要创建 SoaTheService 代理对象,客户端调用:

theClass = Locator.Activate(typeof(ITheService), 
    "tcp://:9000/TheService?xyz") 
    as ITheService;

完成了。代码行之间的唯一区别是字符串值。这些字符串本可以轻松地从配置文件中获取。

增强解决方案

WCF 为远程处理提供了不同的包装器。让我们创建一个 ILocatorActivator 实现来处理使用 WCF 的 HTTP 远程处理需求。为此,我必须对共享接口 ITheService 进行一个小更改。

[ServiceContract]
public interface ITheService
{
    [OperationContract]
    string Hello(string name);
}

虽然这将强制重新编译所有使用该接口的代码,但它不会强制更改任何与 Locator 相关的类。WCF 属性不会对任何不使用 WCF 的其他 ITheInterface 实现产生负面影响。

当我们尝试创建远程对象时,下一个挑战出现了。要使用 WCF 连接到远程对象,我们必须:

  1. 创建 Binding
  2. 创建 EndPoint
  3. 使用 ChannelFactory<>.CreateChannel 创建对象

请注意,ChannelFactory 是一个泛型类,因此它必须在编译时而不是运行时提供类型。这会将相同的要求传递给正在创建的 ILocatorActivator 实现。这意味着两件事:

  • 我们的 ILocatorActivator 实现将是一个泛型类,它捕获将由 ChannelFactory 泛型使用的类型。
  • 虽然我们其他的 ILocatorActivator 实现可以创建任何类型的对象,但这个 ILocatorActivator 的一个实例只能创建一个类型的对象。

让我们仔细看看第二个。如果我想使用 HTTP WCF 创建两种不同的对象,我必须有两个该类的实例。以前,URI scheme 足以确定使用哪个 ILocatorActivator 实现。URI 字符串的其余部分决定了对象的类型。由于 scheme 是映射的关键,因此我们需要为我们的泛型类的每个实例使用不同的 scheme。在内部,ILocatorActivator 实现可以更改 scheme 为其应有的值,因为此实现将只知道如何通过 WCF 进行 HTTP 通信。

考虑到这些因素,以下是代码:

public class WcfHttpActivator<t> : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        string correctedUri = null;
        if (uri.IsDefaultPort)
        {
            correctedUri = string.Format("http://{0}{1}",
             uri.Host, uri.PathAndQuery);
        }
        else
        {
            correctedUri = string.Format("http://{0}:{1}{2}",
             uri.Host, uri.Port, uri.PathAndQuery);
        }

        BasicHttpBinding binding = new BasicHttpBinding();
        EndpointAddress address = new EndpointAddress(correctedUri);
        object proxy = ChannelFactory<t>.CreateChannel(binding, address);

        return proxy;
    }

    #endregion
}

演示程序现在可以使用以下方式注册新的 ILocatorActivator 实现:

Locator.RegisterActivator("HttpTheService", 
    new WcfHttpActivator<itheservice>());

并使用以下方式调用它:

theClass = Locator.Activate(typeof(ITheService), 
        "HttpTheService://:8080/RemoteTheService") 
        as ITheService;

与 .NET Remoting 一样,WCF 只使用默认构造函数。我没有选择实现初始化,但我认为如果您需要,您可以自己弄清楚如何实现。

运行演示程序

演示程序实际上是为了在调试器中运行,以便您可以看到“s”变量的变化。要运行 *Client* 项目,您还必须运行 *SaoHost* 和 *WcfHttpHost* 项目。我发现完成此任务的最简单方法是右键单击解决方案(在解决方案资源管理器中),然后从上下文菜单中使用“设置启动项目”。这样,您就可以在按 F5 调试应用程序时自动启动所有三个项目。

总结

通过使用工厂设计模式,我可以将动态绑定隐藏在一些简单的类中,并使用配置来确定哪个接口的实现最适合我的需求。我只需要确保将 *Activation* 程序集包含在我的项目中,并使用 Locator 类来创建需要动态绑定的对象。这使我能够使用进程内且绝对可预测的实现进行单元测试,并通过更改配置来对 QA 进行测试并将其推广到生产环境。此外,只要我的远程对象支持提供的接口,我就可以初始化它们。

这种方法的另一个好处是,我可以推迟一些关于哪些对象应该包含在哪个进程中的决策。如果对哪些进程应该托管哪些对象存在疑问,开发可以继续进行,而无需明确的答案。

历史

  • 2009 年 10 月 31 日 - 首次发布。
© . All rights reserved.