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

在 C# 中分层实现 Bloch 的建造者模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (18投票s)

2011年8月15日

CPOL

8分钟阅读

viewsIcon

47080

downloadIcon

305

利用 C# 泛型创建构建器并行层次结构,从而减少总工作量。

引言

Bloch 构建器模式涉及创建一个构建器类,用于方便地创建目标类。构建器包含设置目标对象特定属性的方法,并返回构建器本身,以便可以链接后续调用。构建器的示例用法可能是

Window window = new WindowBuilder()
    .Width(500)
    .Height(300)
    .Build();

为了简要说明如何实现这一点,WindowBuilder 类大致具有以下成员定义

class WindowBuilder
{
    Window window;
    public WindowBuilder()
    {
        window = new Window();
    }
    public WindowBuilder Width(int width)
    {
        window.Width = width;
        return this;
    }
    public WindowBuilder Height(int height)
    {
        window.Height = height;
        return this;
    }
    public Window Build()
    {
        Window result = window;
        window = null; //prevent multiple invocations of Build()
        return result;
    }
}

如您所见,每个返回构建器本身的方法都支持链式调用。

(本文介绍了一种新的构建器创建模式。由于这是一种技术而不是一个库,我建议您阅读整篇文章以充分理解这个概念。当然,您可以跳到最后阅读完整的示例代码或下载它,但这样代码可能有点难理解。)

此设计的缺点

假设我上面使用的类有两个属性:WidthHeight。现在假设我有一个类 Window 的子类,名为 DialogDialog 类有一个名为 Message 的新属性,用于存储对话框显示的消息。

问题出现在我尝试创建类似的 DialogBuilder 时。我将不得不重写构建器方法 Width()Height()。虽然这听起来不算太糟,但想象一下为 WinForms 组件(如 Button)创建构建器,它有很多属性。每次有子类时,我们肯定不希望为每个属性编写一个构建器方法。

并行层次结构

“并行层次结构”一词指的是构建器与其目标类之间的类似继承结构。具体来说,如果 Dialog 继承自 Window,那么 DialogBuilder 应该继承自 WindowBuilder。这还表明 DialogBuilder 应该利用 WindowBuilder 中的现有方法,即 Width()Height(),这样 DialogBuilder 就无需重新实现它们。

第一个问题出现了:Width()Height() 的返回类型。它们返回 WindowBuilder。这意味着在我调用 Width() 之后,我将无法再构建 Dialog,因为我返回的是 WindowBuilder。但是,WindowBuilder 无法知道它应该返回 DialogBuilder

这里的解决方案是 **泛型**。让我们修改 WindowBuilder 如下

class WindowBuilder<T> where T : WindowBuilder<T>
{
    public T Width(int width)
    {
        return (T)this;
    }
    public T Height(int height)
    {
        return (T)this;
    }
}

让我解释一下这是什么意思。WindowBuilder 现在接受一个类型参数 T,它将引用我们将要使用的实际构建器的类型。然后,属性设置方法将返回 T,从而在实际构建器上启用链式调用。我们对 T 进行了限制,它必须是 WindowBuilder 的子类(或自身)。此限制很重要,因为它可以在我们返回时将 this 转换为 T 时防止编译器报错。(这里可能存在下转换的性能问题,但考虑到构建器的使用场景,这种情况很少见。但如果您担心,我们将在本文后面讨论这一点。)

请注意,为了清晰起见,我们省略了实际的构建器逻辑。

现在我们来看 DialogBuilder,看看构建器应该如何继承另一个

class DialogBuilder<T> : WindowBuilder<T> where T : DialogBuilder<T>
{
    public T Message(string message)
    {
        return (T)this;
    }
}

同样,为了强调继承模式,构建器逻辑代码被省略了。这里第一行的类定义看起来非常复杂。让我来解释一下。

除了“: WindowBuilder<T>”这些标记之外,其余的一切都与 WindowBuilder 声明类似。我们希望 DialogBuilder 成为 WindowBuilder 的子类,我们将类型参数 T 传递给基类,以告知基类它应该返回哪个构建器。我们还限制 T 必须是 DialogBuilder 的子类,以支持 (T)this 中的转换,并确保此类被正确使用。

实例化构建器

我们遇到了一个有趣的问题:如何实例化构建器?我提到 T 指的是实际构建器的类型,但是要构建一个 Dialog,例如,实际构建器的类型是什么?它不能是 DialogBuilder,因为我们缺少一个参数。它也不能是 DialogBuilder<DialogBuilder>,因为我们仍然缺少一个参数。唉,看起来我们需要一个名为 DialogBuilder<DialogBuilder<DialogBuilder<...>>> 的类,但这不可能。所以这里有一个变通方法

class DialogBuilder : DialogBuilder<DialogBuilder>{ }

等等,等等,这是什么鬼东西?嗯,上面的代码利用了 C# 的一个特性,即 DialogBuilderDialogBuilder<T> 会引用两个完全不同的类。这里调用新类 DialogBuilder 只是为了整洁并避免命名空间污染。请注意,新的 DialogBuilder 类如何满足 T 的条件,即它是 DialogBuilder<T> 的子类!这意味着我们现在可以如下实例化构建器

new DialogBuilder()

这很巧妙,因为它向用户隐藏了所有复杂的泛型。接下来,我们将实现构建器逻辑。

构建器逻辑

到目前为止,我们一直专注于构建器的并行继承,但我们忽略了一个重要方面:实际让构建器构建东西。

本文最顶部的原始构建器在初始化构建器时创建了一个 Window 对象,并在用户调用构建器方法时修改 Window 对象。我无意模仿相同的风格,因为在实例化 DialogBuilder 时,我会创建一个 Window 对象 **和** 一个 Dialog 对象。或者,我可以创建一个 BuilderBase 类,其中包含一个指向正在构建的实际对象的 object,但这样每次调用修改此对象都会涉及强制类型转换。这两种方法都不太高效。

再次,解决方案是泛型。

我们将为构建器添加一个泛型参数,该参数指示我们正在构建的类。在这种情况下,是 WindowDialog。为了处理我们可能创建的其他构建器,我们创建一个 BuilderBase 类如下

class BuilderBase<T, P> where T : BuilderBase<T, P> where P : class, new()
{
    protected P obj;
    protected BuilderBase()
    {
        obj = new P();
    }
    public P Build()
    {
        P result = obj;
        obj = null;
        return result;
    }
}

添加的类型参数 P 指的是正在构建的类。它被限制为 class(支持 struct 的这种构建器没有意义,因为 struct 不能相互继承),并且它必须支持默认的无参构造函数。构建器通常使用默认构造函数实例化目标对象,因为备用构造函数应该在没有构建器的情况下使用。因此,我们可以安全地假设我们的目标类支持默认构造函数。目标类是抽象类的情况不是问题,因为 P 永远不会是抽象类。抽象类的构建器确实可以实现。(我们将在本文稍后看到如何实现。)

BuilderBase 类保存一个指向类型为 P 的正在构建的对象的引用。它有一个 Build() 方法,该方法返回对象并将对象设置为 null,以防止意外重复调用 Build()

现在让我们看看新的功能性 WindowBuilder

class WindowBuilder<T, P> : BuilderBase<T, P> 
    where T : WindowBuilder<T, P> where P : Window, new()
{
    public T Width(int width)
    {
        obj.Width = width;
        return (T)this;
    }
    public T Height(int height)
    {
        obj.Height = height;
        return (T)this;
    }
}

我们已将此类更改为继承自 BuilderBase<T, P>,并沿用了 PT 现在自然地限制为 WindowBuilder<T, P> 的子类,而 P 必须是 Window 的子类,并且可以使用默认构造函数进行实例化。

尽管类声明很复杂,但类的正文非常简单。我们只需设置对象上的相应属性,该对象必须是 Window 的子类,然后返回构建器本身。

现在让我们看看 DialogBuilder

class DialogBuilder<T, P> : WindowBuilder<T, P> 
      where T : DialogBuilder<T, P> where P : Dialog, new()
{
    public T Message(string message)
    {
        obj.Message = message;
        return (T)this;
    }
}

构建器类声明每次都类似,即使是抽象类。抽象类构建器与非抽象类构建器之间的区别在于,抽象类构建器不(也 *不能*)实现以下功能

class WindowBuilder : WindowBuilder<WindowBuilder, Window> { }
class DialogBuilder : DialogBuilder<DialogBuilder, Dialog> { }

现在我们已经为 WindowDialog 类提供了一套完整且可用的构建器。示例调用如下

Window window = new WindowBuilder()
    .Width(500)
    .Height(400)
    .Build();
Dialog dialog = new DialogBuilder()
    .Width(500)
    .Message("Hello")
    .Height(400)
    .Build();

摆脱类型转换

让我们回顾一下这句话

return (T)this;

ILDASM 表明此语句涉及 unbox.any 指令,对于引用类型,它与 castclass 指令相同。此指令可能很慢。为了追求完美,让我们摆脱这种类型转换(或者至少,只转换一次)。

BuilderBase 添加一个名为 _this 的类型为 T 的引用,并在构造函数内为其赋值

class BuilderBase<T, P> where T : BuilderBase<T, P> where P : class, new()
{
    protected P obj;
    protected T _this;
    protected BuilderBase()
    {
        obj = new P();
        _this = (T)this;
    }
    public P Build()
    {
        P result = obj;
        obj = _this = null;
        return result;
    }
}

这似乎是我们能做的最好的了,因为我们不能继承类型参数 T,所以 this 永远不能隐式转换为 T

请注意,我在 Build() 时将 _this 设为 null;也许这是不必要的,但它消除了对构建器的所有引用,使垃圾回收器更容易处理。我对此不确定,但多一个 null 赋值并没有坏处。(如果您对此有所了解,请告诉我。谢谢!)

然后我们继续将每个 return 语句更改为以下内容

return _this;

完整代码

这是上面 WindowDialog 示例中的完整代码

using System;
using System.Collections.Generic;
using System.Text;

namespace HPMV.Builders
{
    class TestProgram
    {
        static void Main(string[] args)
        {
            Window window = new WindowBuilder()
                .Width(500)
                .Height(400)
                .Build();
            Dialog dialog = new DialogBuilder()
                .Width(500)
                .Message("Hello")
                .Height(400)
                .Build();
        }
    }


    class BuilderBase<T, P>
        where T : BuilderBase<T, P>
        where P : class, new()
    {
        protected P obj;
        protected T _this;
        protected BuilderBase()
        {
            obj = new P();
            _this = (T)this;
        }
        public P Build()
        {
            P result = obj;
            obj = null;
            return result;
        }
    }

    class Window
    {
        public int Width { get; set; }
        public int Height { get; set; }
    }

    class Dialog : Window
    {
        public string Message { get; set; }
    }

    class WindowBuilder<T, P> : BuilderBase<T, P>
        where T : WindowBuilder<T, P>
        where P : Window, new()
    {
        public T Width(int width)
        {
            obj.Width = width;
            return _this;
        }
        public T Height(int height)
        {
            obj.Height = height;
            return _this;
        }
    }
    class WindowBuilder : WindowBuilder<WindowBuilder, Window> { }

    class DialogBuilder<T, P> : WindowBuilder<T, P>
        where T : DialogBuilder<T, P>
        where P : Dialog, new()
    {
        public T Message(string message)
        {
            obj.Message = message;
            return _this;
        }
    }
    class DialogBuilder : DialogBuilder<DialogBuilder, Dialog> { }
}

更多构建器

通过遵循示例代码中的模式,我们可以轻松创建新的构建器。这里唯一复杂的工作是正确编写类声明,但尝试几次您应该会习惯的。当然,您也可以创建代码片段或其他类似的东西,但我不会在本文中深入探讨。

我希望您觉得这个构建器模式有趣且有用。

致谢

JavaFX 2.0 Beta 的一个近期版本中,在 javafx.builders 包中观察到了并行层次结构模式。由于 Java 和 C# 处理泛型的方式不同,无法将确切的技术应用于 C#。本文中的技术是原创的,灵感来自上述 JavaFX API。

历史

  • 2011 年 8 月 15 日:初稿。
© . All rights reserved.