19

我正在创建一系列构建器来清理为我的模拟创建域类的语法,作为改进我们整体单元测试的一部分。我的构建器本质上填充了一个域类(例如 a Schedule),其中一些值是通过调用适当的WithXXX并将它们链接在一起来确定的。

我在构建者中遇到了一些共性,我想将其抽象为基类以增加代码重用。不幸的是,我最终得到的结果如下:

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 类型,所以我不能return thisWithId(int).BaseBuilder

在这里公开带有属性的子类型是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()
);
4

3 回答 3

13

我只能说,如果有办法做到这一点,我也想知道——我在我的Protocol Buffers port中使用这个模式。事实上,我很高兴看到其他人使用了它——这意味着我们至少在某种程度上是正确的!

于 2008-10-28T20:44:32.230 回答
4

我知道这是一个老问题,但我认为您可以使用简单的演员表来避免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;
    }
}
于 2012-02-11T23:27:37.190 回答
2

这是 C# 的一个很好的实现策略。

其他一些语言(想不出我见过的研究语言的名称)具有直接支持协变“self”/“this”的类型系统,或者有其他巧妙的方式来表达这种模式,但使用 C#类型系统,这是一个很好的(唯一的?)解决方案。

于 2008-10-29T03:30:46.920 回答