Шаблон проектирования Builder с наследованием: есть ли лучший способ? - PullRequest
16 голосов
/ 28 октября 2008

Я создаю серию сборщиков, чтобы очистить синтаксис, который создает классы домена для моих макетов, как часть улучшения наших общих модульных тестов. Мои компоновщики по существу заполняют класс домена (такой как 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 this в методе WithId(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()
);

Ответы [ 3 ]

11 голосов
/ 28 октября 2008

Все, что я могу сказать, это то, что если - это способ сделать это, я тоже хочу знать об этом - я использую точно этот шаблон в моих протокольных буферах порт . На самом деле, я рад видеть, что кто-то еще прибегнул к этому - это значит, что мы, по крайней мере, в некоторой степени правы!

3 голосов
/ 12 февраля 2012

Я знаю, что это старый вопрос, но я думаю, что вы можете использовать простое приведение, чтобы избежать 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 голосов
/ 29 октября 2008

Это хорошая стратегия реализации для C #.

Некоторые другие языки (не могу вспомнить название языка исследования, на котором я видел это) имеют системы типов, которые либо напрямую поддерживают ковариант «я» / «это», либо имеют другие умные способы выражения этого шаблона, но с системой типов C # это хорошее (только?) решение.

...