Нарушает ли шаблон проектирования Builder принцип единой ответственности?

Позвольте мне показать вам, какую реализацию шаблона проектирования конструктора я вижу в каждом блоге:

interface IProductBuilder
{
    void BuildPart1(Part1 value);
    void BuildPart2(Part2 value);
    void BuildPart3(Part3 value);
}

class ConcreteProduct
{
    public readonly Part1 Part1;
    public readonly Part2 Part2;
    public readonly Part3 Part3;

    public ConcreteProduct(Part1 part1, Part2 part2, Part3 part3)
    {
        Part1 = part1;
        Part2 = part2;
        Part3 = part3;
    }
}

class ConcreteProductBuilder : IProductBuilder
{
    Part1 _part1;
    Part2 _part2;
    Part3 _part3;

    public void BuildPart1(Part1 value)
    {
        _part1 = value;
    }

    public void BuildPart2(Part2 value)
    {
        _part2 = value;
    }

    public void BuildPart3(Part3 value)
    {
        _part3 = value;
    }

    public ConcreteProduct GetResult()
    {
        return new ConcreteProduct(part1, part2, part3);
    }
}

Обычный способ построения модульных тестов выглядит примерно так:

[TestMethod]
void TestBuilder()
{
    var target = new ConcreteBuilder();

    var part1 = new Part1();
    var part2 = new Part2();
    var part3 = new Part3();

    target.BuildPart1(part1);
    target.BuildPart2(part2);
    target.BuildPart3(part3);

    ConcreteProduct product = target.GetResult();

    Assert.IsNotNull(product);
    Assert.AreEqual(product.Part1, part1);
    Assert.AreEqual(product.Part2, part2);
    Assert.AreEqual(product.Part3, part3);
}

Итак, это довольно простой пример.

Я думаю, что строительный шаблон - действительно хорошая вещь. Это дает вам возможность разместить все изменяемые данные в одном месте и оставить все остальные классы неизменными, это здорово для тестируемости.

Но что, если я не хочу показывать кому-то поля продукта (или я просто не могу этого сделать, потому что продукт является частью какой-то библиотеки).

Как мне провести модульное тестирование моего сборщика?

Теперь это будет выглядеть так?

[TestMethod]
void TestBuilder()
{
    var target = new ConcreteProductBuilder();

    var part1 = new Part1();
    var part2 = new Part2();
    var part3 = new Part3();

    target.BuildPart1(part1);
    target.BuildPart2(part2);
    target.BuildPart3(part3);

    ConcreteProduct product = target.GetResult();

    TestConcreteProductBehaviorInUseCase1(product);
    TestConcreteProductBehaviorInUseCase2(product);
    ...
    TestConcreteProductBehaviorInUseCaseN(product);
}

Здесь я вижу как минимум одно простое решение - модифицировать ConcreteProductBuilder.GetResult, чтобы взять фабрику:

public ConcreteProduct GetResult(IConcreteProductFactory factory)
{
    return factory.Create(part1, part2, part3);
}

и реализовать IConcreteProductFactory двумя способами:

public MockConcreteProductFactory
{
    public Part1 Part1;
    public Part2 Part2;
    public Part3 Part3;
    public ConcreteProduct Product;
    public int Calls;

    public ConcreteProduct Create(Part1 part1, Part2 part2, Part3 part3)
    {
        Calls++;

        Part1 = part1;
        Part2 = part2;
        Part3 = part3;

        Product = new ConcreteProduct(part1, part2, part3);
        return Product;
    }
}

public ConcreteProductFactory
{
    public ConcreteProduct Create(Part1 part1, Part2 part2, Part3 part3)
    {
        return new ConcreteProduct(part1, part2, part3);
    }
}

В этом случае проверка будет такой же простой, как и раньше:

[TestMethod]
void TestBuilder()
{
    var target = new ConcreteBuilder();

    var part1 = new Part1();
    var part2 = new Part2();
    var part3 = new Part3();

    target.BuildPart1(part1);
    target.BuildPart2(part2);
    target.BuildPart3(part3);

    var factory = new MockConcreteProductFactory();

    ConcreteProduct product = target.GetResult(factory);

    Assert.AreEqual(1, factory.Calls);
    Assert.AreSame(factory.Product, product);
    Assert.AreEqual(factory.Part1, part1);
    Assert.AreEqual(factory.Part2, part2);
    Assert.AreEqual(factory.Part3, part3);
}

Так что мой вопрос не в том, как решить эту проблему лучше, а в самом шаблоне Builder.

Нарушает ли шаблон проектирования Builder принцип единой ответственности?

Мне кажется, что строитель в общем определении отвечает за:

  1. Сбор аргументов конструктора (или значений свойств в другой реализации шаблона построителя)
  2. Построение объекта с собранными свойствами

person gerichhome    schedule 05.12.2012    source источник


Ответы (2)


Здесь мы имеем дело с двумя концепциями:

Соблюдается ли в шаблоне конструктора принцип единой ответственности? Да.

Вы можете подумать, что у строителя две обязанности:

  • Собирать недвижимость
  • Создание продукта на основе собранных свойств

Фактически, есть только одна обязанность - создать продукт на основе собранных свойств. Видите ли, за сбор свойств отвечает класс Director (см. ссылку на Википедию). Строитель только получает свойства пассивным способом. На самом деле это не его ответственность. После получения свойств строит объект.

А как насчет модульного тестирования?

Что ж, технически, когда вы не хотите раскрывать поле в шаблоне, на это есть причина, и это часть вашего основного дизайна. Так что приспособление тестера агрегата - не задача «Дизайна». Это работа юнит-тестера, отвечающая «дизайну».

Вы можете добиться этого с помощью отражения (хорошо, это обман) или купить конструктор мокапов, который наследует Concrete Builder, который вы хотите протестировать. Этот конструктор макетов будет хранить детали, которые он собирает, и делать их доступными для тестировщика модулей.

person David Dugué    schedule 07.12.2012

Я не думаю, что это нарушает принцип единой ответственности (SRP). Рассмотрим пример, упомянутый в вики для SRP:

Мартин определяет ответственность как причину изменения и заключает, что у класса или модуля должна быть одна и только одна причина для изменения. В качестве примера рассмотрим модуль, который составляет и распечатывает отчет. Такой модуль можно изменить по двум причинам. Во-первых, может измениться содержание отчета. Во-вторых, формат отчета может измениться. Эти две вещи меняются по очень разным причинам; одно основное и одно косметическое.

Но в случае, о котором вы упомянули, если одно изменяется, другое также должно измениться, т.е. если есть дополнительный аргумент, то объект должен быть построен с этим дополнительным аргументом. По сути, это одна обязанность с двумя подзадачами.

person user1168577    schedule 08.12.2012