Позвольте мне показать вам, какую реализацию шаблона проектирования конструктора я вижу в каждом блоге:
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 принцип единой ответственности?
Мне кажется, что строитель в общем определении отвечает за:
- Сбор аргументов конструктора (или значений свойств в другой реализации шаблона построителя)
- Построение объекта с собранными свойствами