Это правильный способ использования и тестирования класса, который использует фабричный шаблон?

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

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

Это класс, который мне нужно проверить:

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();
}

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

Другие вопросы по тегам