Это правильный способ использования и тестирования класса, который использует фабричный шаблон?
У меня нет большого опыта работы с заводским шаблоном, и я столкнулся со сценарием, в котором я считаю, что это необходимо, но я не уверен, что правильно применил шаблон, и я обеспокоен его влиянием. имел на читабельность моих юнит-тестов.
Я создал фрагмент кода, который приближает (из памяти) суть сценария, над которым я работаю на работе. Я был бы очень признателен, если бы кто-то мог взглянуть на это и посмотреть, кажется ли то, что я сделал, разумным.
Это класс, который мне нужно проверить:
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);
}
Это фабрика, которую я создал. В соответствии с моими текущими требованиями я добавил специальный редукторный метод редуктора для использования в конкретном сценарии, поэтому я пытаюсь представить завод.
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;
}
}
Это тестовое приспособление, которое я использую. Что меня беспокоило, так это то, сколько места в тестах занял заводской шаблон и как он, по-видимому, снижает читабельность теста. Пожалуйста, имейте в виду, что в моем классе реального мира у меня есть несколько зависимостей, которые мне нужно смоделировать, что означает, что тесты здесь на несколько строк короче, чем те, которые необходимы для моего реального теста.
[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());
}
}
Так все ли выглядит правильно? Или есть лучший способ использовать фабричный шаблон?
1 ответ
Это довольно длинный вопрос, который вы задаете, но вот некоторые странные мысли:
- AFAIK, нет "фабричной" модели. Есть шаблон под названием " Абстрактная фабрика", а другой - " Метод фабрики". Прямо сейчас вы, кажется, используете Abstract Factory.
- Нет причин, по которым у SomeCalculator есть
reducerFactory
иreducer
поле. Избавьтесь от одного из них - в вашей текущей реализации вам не нужныreducer
поле. - Сделать введенную зависимость (
reducerFactory
) только для чтения. - Избавьтесь от конструктора по умолчанию.
- Оператор switch в ReducerFactory может быть запахом кода. Возможно, вы могли бы переместить метод создания в класс CalculationMethod. Это существенно изменило бы абстрактную фабрику на фабричный метод.
В любом случае, введение слабой связи всегда связано с накладными расходами, но не думайте, что вы делаете это только для проверки. Тестируемость - это на самом деле только принцип Open/Closed, поэтому вы делаете свой код более гибким, чем просто для включения тестирования.
Да, за это нужно заплатить небольшую цену, но она того стоит.
В большинстве случаев введенная зависимость должна быть доступна только для чтения. Хотя это не является технически необходимым, это хороший дополнительный уровень безопасности, чтобы пометить поле с помощью C# readonly
ключевое слово.
Когда вы решите использовать DI, вы должны использовать его последовательно. Это означает, что перегруженные конструкторы являются еще одним антишаблоном. Это делает конструктор неоднозначным и может также привести к Tight Coupling и Leaky Abstractions.
Это каскады и может показаться недостатком, но на самом деле является преимуществом. Когда вам нужно создать новый экземпляр SomeCalculator в каком-то другом классе, вы должны снова либо внедрить его, либо внедрить абстрактную фабрику, которая может его создать. Преимущество приходит, когда вы извлекаете интерфейс из 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();
}
Вопрос о том, имеет ли смысл этот последний совет, зависит от большого количества информации, которой у меня нет, поэтому я просто говорю, что вы должны это учитывать - в вашем конкретном случае это может не иметь смысла.