继承的建造者设计模式:是否有更好的方法?

20

我正在创建一系列的构建器来清理语法,以创建领域类作为改进我们整体单元测试的一部分。我的构建器本质上是使用适当的WithXXX调用并将它们链接在一起来填充领域类(如Schedule)的某些值。

我在我的构建器中遇到了一些共性,想要将其抽象出来成为一个基类,以增加代码重用性。不幸的是,最终看起来像:

public abstract class BaseBuilder<T,BLDR> where BLDR : BaseBuilder<T,BLDR> 
                                          where T : new()
{
    public abstract T Build();

    protected int Id { get; private set; }
    protected abstract BLDR This { get; }

    public BLDR WithId(int id)
    {
        Id = id;
        return This;
    }
}

请特别注意protected abstract BLDR This { get; }

领域类构建器的示例实现如下:

public class ScheduleIntervalBuilder :
    BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
    private int _scheduleId;
    // ...

    // UG! here's the problem:
    protected override ScheduleIntervalBuilder This
    {
        get { return this; }
    }

    public override ScheduleInterval Build()
    {
        return new ScheduleInterval
        {
            Id = base.Id,
            ScheduleId = _scheduleId
                    // ...
        };
    }

    public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
    {
        _scheduleId = scheduleId;
        return this;
    }

    // ...
}

因为BLDR不是BaseBuilder类型,所以我不能在BaseBuilder的WithId(int)方法中使用return this。在此处使用属性abstract BLDR This { get; }来公开子类型是否是我的唯一选择,或者我错过了某些语法技巧?
更新(因为我可以更清楚地展示我为什么要这样做):
最终结果是拥有构建器来构建受配置文件支持的域类,人们期望以[程序员]可读格式从数据库中检索。 这没有任何问题...
mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new Schedule
    {
        ScheduleId = 1
        // ...
    }
);

因为这已经相当易读。另一种构建器语法是:

mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new ScheduleBuilder()
        .WithId(1)
        // ...
        .Build()
);

我希望使用构建器(并实现所有这些WithXXX方法)的优点是抽象出复杂的属性创建过程(自动扩展我们的数据库查找值与正确的Lookup.KnownValues匹配,而不会实际访问数据库),并且构建器为领域类提供常用的可重用测试配置文件...

mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new ScheduleBuilder()
        .AsOneDay()
        .Build()
);
3个回答

16

我只能说,如果有一种方法可以做到这一点,我也想知道 - 我在我的Protocol Buffers port中使用 完全相同的 模式。事实上,我很高兴看到其他人也采用了这种方法 - 这意味着我们至少有些可能是正确的!


看起来我们只能使用“abstract T This { get; }”了。我会接受并关闭,因为我认为这就是唯一可用的解决方案。 - cfeduke
我正在尝试找到一个相对清晰的方法来创建构建器,并发现了这篇文章(http://www.codeproject.com/Articles/240756/Hierarchically-Implementing-the-Bolchs-Builder-Pat)。它建议进行一次强制转换。(protected BLDR _this; _this = (BLDR)this; return _this;),但我无法理解为什么作者添加了类WindowBuilder:WindowBuilder<WindowBuilder,Window> {}(请看文章底部的完整示例)。在这篇文章中,我看到几乎相同的模式,但cfeduke使用的是class ScheduleIntervalBuilder:BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>... - Alex Dn
个人而言,我可能会在没有基类的情况下实现每个构建器。我总是尽量不使用这种自我泛型模式:http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx - Pellared
3
@Pellared说:但是这样做你就无法享受需要处理 "任何构建器" 或 "任何消息" 的情况的好处 - 是的,这些情况确实会发生。每种方法都有利弊。 - Jon Skeet

5

我知道这是一个老问题,但我认为你可以使用简单的转换来避免 abstract BLDR This { get; }

然后生成的代码将是:

public abstract class BaseBuilder<T, BLDR> where BLDR : BaseBuilder<T, BLDR>
                                           where T : new()
{
    public abstract T Build();

    protected int Id { get; private set; }

    public BLDR WithId(int id)
    {
        _id = id;
        return (BLDR)this;
    }
}

public class ScheduleIntervalBuilder :
    BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
    private int _scheduleId;
    // ...

    public override ScheduleInterval Build()
    {
        return new ScheduleInterval
        {
                Id = base.Id,
                ScheduleId = _scheduleId
                    // ...
        };
    }

    public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
    {
        _scheduleId = scheduleId;
        return this;
    }

    // ...
}

当然,你可以使用封装建造者的方式。
protected BLDR This
{
    get
    {
        return (BLDR)this;
    }
}

2

这是一个适用于C#的良好实现策略。

其他一些语言(我无法想起我曾在哪个研究语言中看到过)拥有支持协变“self”/“this”的类型系统,或者有其他巧妙的方法来表达这种模式,但是使用C#的类型系统,这是一个良好的(唯一的?)解决方案。


感谢您的输入,我认为“abstract T This { get; }”并不是一个很高的代价。 :) - cfeduke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接