代码之家  ›  专栏  ›  技术社区  ›  mezoid

这是使用和测试使用工厂模式的类的正确方法吗?

  •  2
  • mezoid  · 技术社区  · 15 年前

    我对工厂模式没有太多的经验,我遇到了一个场景,我认为这是必要的,但我不确定我是否正确地实现了模式,我担心它对我的单元测试的可读性的影响。

    我已经创建了一个代码片段,它(从内存中)接近我正在工作的场景的本质。如果有人能看看它,看看我所做的是否合理,我会非常感激。

    这是我需要测试的课程:

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

    这是我创建的工厂。我目前的要求是在特定的场景中添加一个特定的reducer方法reducer,这就是为什么我要引入一个工厂。

    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 回复  |  直到 8 年前
        1
  •  9
  •   Steven    8 年前

    这是一个很长的问题,你在问,但这里有一些杂念:

    • 阿法克,没有“工厂”模式。有一种模式叫做 抽象工厂 还有一个叫 工厂法 . 现在你似乎在使用抽象工厂。
    • 没有理由有些计算器同时拥有 reducerFactory 和A reducer 字段。去掉其中的一个——在当前的实现中,您不需要 减速器 字段。
    • 使注入的依赖( 还原厂 )只读。
    • 去掉默认的构造函数。
    • ReducerFactory中的switch语句可能有代码味道。也许您可以将创建方法移动到CalculationMethod类。这基本上会将抽象工厂更改为工厂方法。

    在任何情况下,引入松耦合总是有一个开销,但不要认为这样做只是为了测试性。 Testability is really only the Open/Closed Principle 因此,您在许多方面使您的代码更加灵活,而不仅仅是为了启用测试。

    是的,要付一点钱,但很值得。


    在大多数情况下,注入的依赖项应该是只读的。虽然技术上没有必要,但用C标记现场是一个很好的额外安全级别。# readonly 关键字。

    当您决定使用DI时,必须始终如一地使用它。这意味着重载的构造函数是另一种反模式。这使得构造函数不明确,也可能导致 紧耦合 泄漏的抽象 .

    这似乎是一个缺点,但实际上是一个优势。当需要在其他类中创建某个计算器的新实例时,必须再次注入该实例或注入可创建该实例的抽象工厂。当你从计算器(比如isomecalculator)中提取一个接口并注入它时,优势就显现出来了。现在,您已经有效地将某个计算器的客户机与iReducer和iReducerFactory分离开来。

    您不需要一个DI容器来完成所有这些工作—您可以手动连接实例。这叫做 Pure DI .

    当谈到将ReducerFactory中的逻辑转移到CalculationMethod时,我考虑的是一种虚拟方法。像这样:

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

    对于特殊的计算方法,然后可以重写CreateReducer方法并返回不同的Reducer:

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

    最后的建议是否有意义取决于我没有的很多信息,所以我只是说你应该 考虑 它-在你的特定情况下可能没有意义。