Pergunta

Eu não tenho muita experiência com o padrão de fábrica e eu me deparei com um cenário em que eu acredito que é necessário, mas não tenho certeza o que eu tenho implementado o padrão corretamente e eu estou preocupado com o impacto que teve sobre a legibilidade dos meus testes de unidade.

Eu criei um trecho de código que se aproxima (de memória) a essência do cenário que eu estou trabalhando no trabalho. Eu realmente aprecio isso se alguém poderia dar uma olhada e ver se o que eu fiz parece razoável.

Esta é a classe que eu preciso de teste:

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

Aqui estão alguns dos básicos definições de interface ...

public interface ICalculateSomething
{
    decimal Calculate(SomeObject so);
}

public interface IReducerFactory
{
    IReducer Create(CalculationMethod cm);
}

public interface IReducer
{
    decimal Reduce(SomeObject so, decimal amount);
}

Esta é a fábrica que eu criei. Meus requisitos atuais tiveram me adicionar um redutor MethodAReducer específico para ser usado em um cenário específico que é por isso que estou tentando introduzir uma fábrica.

public class ReducerFactory : IReducerFactory
{
    public IReducer Create(CalculationMethod cm)
    {
        switch(cm.Method)
        {
            case CalculationMethod.MethodA:
                return new MethodAReducer();
                break;
            default:
                return DefaultMethodReducer();
                break;
        }
    }
}

Estas são aproximações das duas implementações ... A essência da implementação é que ele só reduz a quantidade se o objeto está em um estado particular.

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

Este é o dispositivo de teste que estou usando. O que me causa é a quantidade de espaço nos testes o padrão de fábrica assumiu e como ele aparece para reduzir a legibilidade do teste. Tenha em mente que na minha classe mundo real Eu tenho várias dependências que eu preciso para zombar fora o que significa que os testes aqui estão várias linhas mais curtas do que os necessários para o meu teste do mundo real.

[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(){}

Primeiro teste

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

Segundo teste

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

Assim, faz todo esse direito olhar? Ou há uma maneira melhor de usar o padrão de fábrica?

Foi útil?

Solução

É um tempo muito longo pergunta que você está pedindo, mas aqui estão alguns pensamentos dispersos:

  • AFAIK, não há é padrão 'Factory'. Há um padrão chamado Abstract Factory e outro chamado Factory Method . Agora você parece estar usando Abstract Factory.
  • Não há nenhuma razão que SomeCalculator ter tanto um reducerFactory e um campo reducer. Se livrar de um deles - em sua implementação atual, você não precisa o campo reducer
  • .
  • Faça a dependência injetado (reducerFactory) somente leitura.
  • Se livrar do construtor padrão.
  • A instrução switch em ReducerFactory pode ser um cheiro de código. Talvez você possa mover o método de criação para a classe CalculationMethod. Isso seria, essencialmente, mudar o Abstract Factory para um método de fábrica.

Em qualquer caso, há sempre uma sobrecarga associada à introdução de acoplamento, mas não acho que você está fazendo isso apenas para capacidade de teste. Testability é realmente apenas o Aberto Princípio / Closed , então você está fazendo seu código mais flexível em muitos mais caminho do que apenas para permitir o teste.

Sim, há um pequeno preço a ser pago para isso, mas é bem a pena.


Na maioria dos casos, a dependência injetado deve ser somente leitura. Embora não seja tecnicamente necessário, é um bom nível extra de segurança para marcar o campo com a palavra-chave C # readonly.

Quando você decidir usar DI, você deve usá-lo de forma consistente. Isto significa que sobrecarregados construtores são ainda um outro anti-padrão. Isso faz com que o construtor ambígua e pode também leva a acoplamento apertado e Leaky Abstrações .

Este cascatas e pode parecer uma desvantagem, mas é realmente uma vantagem. Quando você precisa criar uma nova instância do SomeCalculator em alguma outra classe, você deve novamente ou injetá-lo ou injetar um Abstract Factory que pode criá-lo. A vantagem vem quando você extraia uma interface de SomeCalculator (digamos, ISomeCalculator) e injetar que em vez. Você já efectivamente dissociado o cliente de SomeCalculator de IReducer e IReducerFactory.

Você não precisa de uma DI Container para fazer tudo isso - você pode conectar-se casos manualmente em vez. Isso é chamado Pure DI .

Quando se trata de mover a lógica em ReducerFactory para CalculationMethod, eu estava pensando sobre um método virtual. Algo parecido com isto:

public virtual IReducer CreateReducer()
{
    return new DefaultMethodReducer();
}

Para CalculationMethods especiais, então você pode substituir o método CreateReducer e retornar um redutor diferente:

public override IReducer CreateReducer()
{
    return new MethodAReducer();
}

Se este último conselho faz sentido depende de um monte de informações que eu não tenho, então eu só estou dizendo que você deve consideram -lo - ele pode não fazer sentido no seu caso específico .

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top