Подтвердить что ты не робот

Шаблон проектирования Builder с наследованием: есть ли лучший способ?

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

Ответ 1

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

Ответ 2

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

Ответ 3

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

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