Является ли это правильным способом использования и тестирования класса, который использует шаблон factory?
У меня нет большого опыта работы с шаблоном factory, и я столкнулся с сценарием, в котором я считаю, что это необходимо, но я не уверен, что правильно реализовал шаблон, и я обеспокоенный тем, какое влияние он оказал на читаемость моих модульных тестов.
Я создал фрагмент кода, который аппроксимирует (из памяти) суть сценария, над которым я работаю на работе. Я был бы очень признателен, если бы кто-то мог взглянуть на него и посмотреть, не кажется ли мне, что я сделал, разумно.
Это класс, который мне нужно проверить:
public class SomeCalculator : ICalculateSomething
{
private readonly IReducerFactory reducerFactory;
private IReducer reducer;
public SomeCalculator(IReducerFactory reducerFactory)
{
this.reducerFactory = reducerFactory;
}
public SomeCalculator() : this(new ReducerFactory()){}
public decimal Calculate(SomeObject so)
{
reducer = reducerFactory.Create(so.CalculationMethod);
decimal calculatedAmount = so.Amount * so.Amount;
return reducer.Reduce(so, calculatedAmount);
}
}
Вот некоторые из основных понятий интерфейса...
public interface ICalculateSomething
{
decimal Calculate(SomeObject so);
}
public interface IReducerFactory
{
IReducer Create(CalculationMethod cm);
}
public interface IReducer
{
decimal Reduce(SomeObject so, decimal amount);
}
Это factory, который я создал. Мои текущие требования заставили меня добавить конкретный редуктор MethodAReducer для использования в конкретном сценарии, поэтому я пытаюсь ввести factory.
public class ReducerFactory : IReducerFactory
{
public IReducer Create(CalculationMethod cm)
{
switch(cm.Method)
{
case CalculationMethod.MethodA:
return new MethodAReducer();
break;
default:
return DefaultMethodReducer();
break;
}
}
}
Это аппроксимации двух реализаций... Сущность реализации заключается в том, что она только уменьшает количество, если объект находится в определенном состоянии.
public class MethodAReducer : IReducer
{
public decimal Reduce(SomeObject so, decimal amount)
{
if(so.isReductionApplicable())
{
return so.Amount-5;
}
return amount;
}
}
public class DefaultMethodReducer : IReducer
{
public decimal Reduce(SomeObject so, decimal amount)
{
if(so.isReductionApplicable())
{
return so.Amount--;
}
return amount;
}
}
Это тестовое оборудование, которое я использую. Меня беспокоило, сколько места в тестах рассмотрено шаблоном factory и как оно уменьшает читаемость теста. Пожалуйста, имейте в виду, что в моем классе реального мира у меня есть несколько зависимостей, которые мне нужно высмеять, что означает, что тесты здесь на несколько строк короче, чем те, которые необходимы для моего реального теста.
[TestFixture]
public class SomeCalculatorTests
{
private Mock<IReducerFactory> reducerFactory;
private SomeCalculator someCalculator;
[Setup]
public void Setup()
{
reducerFactory = new Mock<IReducerFactory>();
someCalculator = new SomeCalculator(reducerFactory.Object);
}
[Teardown]
public void Teardown(){}
Первый тест
//verify that we can calculate an amount
[Test]
public void Calculate_CalculateTheAmount_ReturnsTheAmount()
{
decimal amount = 10;
decimal expectedAmount = 100;
SomeObject so = new SomeObjectBuilder()
.WithCalculationMethod(new CalculationMethodBuilder())
.WithAmount(amount);
Mock<IReducer> reducer = new Mock<IReducer>();
reducer
.Setup(p => p.Reduce(so, expectedAmount))
.Returns(expectedAmount);
reducerFactory
.Setup(p => p.Create(It.IsAny<CalculationMethod>))
.Returns(reducer);
decimal actualAmount = someCalculator.Calculate(so);
Assert.That(actualAmount, Is.EqualTo(expectedAmount));
}
Второй тест
//Verify that we make the call to reduce the calculated amount
[Test]
public void Calculate_CalculateTheAmount_ReducesTheAmount()
{
decimal amount = 10;
decimal expectedAmount = 100;
SomeObject so = new SomeObjectBuilder()
.WithCalculationMethod(new CalculationMethodBuilder())
.WithAmount(amount);
Mock<IReducer> reducer = new Mock<IReducer>();
reducer
.Setup(p => p.Reduce(so, expectedAmount))
.Returns(expectedAmount);
reducerFactory
.Setup(p => p.Create(It.IsAny<CalculationMethod>))
.Returns(reducer);
decimal actualAmount = someCalculator.Calculate(so);
reducer.Verify(p => p.Reduce(so, expectedAmount), Times.Once());
}
}
Так все выглядит правильно? Или есть лучший способ использовать шаблон factory?
Ответы
Ответ 1
Это довольно длинный вопрос, который вы задаете, но вот несколько странных мыслей:
- AFAIK, нет шаблона 'Factory'. Там шаблон называется Аннотация Factory, а другой называется Factory Метод. Прямо сейчас вы, кажется, используете Abstract Factory.
- Нет причин, чтобы SomeCalculator имел как поле
reducerFactory
, так и reducer
. Избавьтесь от одного из них - в вашей текущей реализации вам не нужно поле reducer
.
- Сделать вложенную зависимость (
reducerFactory
) только для чтения.
- Избавьтесь от конструктора по умолчанию.
- Оператор switch в ReducerFactory может быть запахом кода. Возможно, вы можете перенести метод создания в класс CalculationMethod. Это существенно изменило бы абстрактный метод Factory на метод Factory.
В любом случае всегда есть накладные расходы, связанные с введением свободной связи, но не думайте, что вы делаете это только для проверки. Testability - это действительно только принцип Open/Closed, поэтому вы делаете свой код более гибким во многих отношениях, чем просто для тестирования.
Да, есть небольшая цена за это, но это того стоит.
В большинстве случаев инъекционная зависимость должна быть доступна только для чтения. Хотя это технически не требуется, это хороший дополнительный уровень безопасности, чтобы отметить поле ключевым словом С# readonly
.
Когда вы решите использовать DI, вы должны использовать его последовательно. Это означает, что перегруженные конструкторы являются еще одним анти-шаблоном. Это делает конструктор неоднозначным и может также привести к Tight Coupling и Leaky Abstractions.
Этот каскад и может показаться недостатком, но на самом деле является преимуществом. Когда вам нужно создать новый экземпляр SomeCalculator в каком-то другом классе, вы должны снова либо ввести его, либо вставить абстрактный Factory, который может его создать. Преимущество возникает тогда, когда вы извлекаете интерфейс из SomeCalculator (скажем, ISomeCalculator) и вводите это вместо этого. Теперь вы фактически отделили клиента SomeCalculator от IReducer и IReducerFactory.
Вам не нужен контейнер DI, чтобы сделать все это - вместо этого вы можете подключать экземпляры вручную. Это называется Pure DI.
Когда дело доходит до перемещения логики в ReducerFactory до метода CalculationMethod, я думал о виртуальном методе. Что-то вроде этого:
public virtual IReducer CreateReducer()
{
return new DefaultMethodReducer();
}
Для специальных методов CalculationMethods вы можете переопределить метод CreateReducer и вернуть другой редуктор:
public override IReducer CreateReducer()
{
return new MethodAReducer();
}
Является ли этот последний совет обоснованным, зависит от большого количества информации, которую у меня нет, поэтому я просто говорю, что вы должны это учитывать - это может не иметь смысла в вашем конкретном случае.