依赖注入:如何重构一个丑陋的if-else-if块

时间:2014-05-20 14:42:02

标签: .net dependency-injection refactoring ninject castle-windsor

我有一个类可以调用不同的支付服务,如Paypal,Payline和其他服务,具体取决于主参数(Payment对象)的属性值:

public class PaymentExecutor
{
    IPaymentProcessor fooProcessor;
    IPaymentProcessor barProcessor;
    IPaymentProcessor bazProcessor;
    IPaymentProcessor quxProcessor;

    public PaymentExecutor(...)
    {
        fooProcessor = new FooProcessor(...);
        barProcessor = new BarProcessor(...);
        bazProcessor = new BazProcessor(...);
        quxProcessor = new QuxProcessor(...);
    }

    public Payment Execute(Payment payment)
    {
        IPaymentProcessor processor;

        if (payment.Channel == "foo" && payment.Some = thing
            && (payment.Foo == bar || payment.Bar == foo))
        {
            processor = fooProcessor;
            // ...
        }
        else if (azerty == b && c == d && e.UseCase.IsIn("a", "b", "c", "d"))
        {
            processor = barProcessor;
            // ...
        }
        else if (azerty == g && h == i && k.UseCase.IsIn("g", "h"))
        {
            processor = bazProcessor;
            // ...
        }
        else if (azerty == b && c == d && e.UseCase.IsIn("u", "i", "p")
                 && somethingElse == "foobar")
        {
            processor = quxProcessor;
            // ...
        }

        return processor.Process(payment);
    }
}

此代码已经非常简化,并为DI(接口,实际上我们使用继承)做了一点准备。

我是DI的新手(我已经使用过Ninject和Castle Windsor,我不知道如何使用DI替换/重构这个丑陋的if-else-if块。

任何帮助都将不胜感激。

2 个答案:

答案 0 :(得分:2)

if else块不是DI要解决的问题。 DI的目的是让对象明确地识别和声明它们所需的依赖关系以便正常运行,这样就可以为它们提供所述依赖关系而不是自己创建它们。

例如,PaymentExecutor依赖于一组IPaymentProcessor个对象,但会创建这些对象。为了支持DI,可以重构PaymentExecutor以接受这些支付处理器作为其构造函数的参数,而不是直接创建它们。像这样:

public PaymentExecutor(IPaymentProcessor fooProcessor, IPaymentProcessor barProcessor, IPaymentProcessor bazProcessor, IPaymentProcessor quxProcessor)
{
    this.fooProcessor = fooProcessor;
    this.barProcessor = barProcessor;
    this.bazProcessor = bazProcessor;
    this.quxProcessor = quxProcessor;
}

即便如此,您仍然可以将if else块重构为更易于维护的内容(DI只是不会进入它)。

目前驻留在if语句中的逻辑可能打破了各个支付处理器的封装。也就是说,PaymentExecutor正在决定特定支付处理器是否能够实际处理付款。 这个决定最好留给处理者自己。

例如,考虑IPaymentProcessor接口是否声明了以下方法:

    bool CanProcessPayment(Payment payment);

然后可以将条件逻辑重构为各个处理器:

class FooProcessor : IPaymentProcessor
{
    // ...

    public bool CanProcessPayment(Payment payment)
    {
        return payment.Channel == "foo" && payment.Some == thing
            && (payment.Foo == bar || payment.Bar == foo);
    }
}

......以及PaymentExecutor

class PaymentExecutor
{
    // ...

    public Payment Execute(Payment payment)
    {
        if (fooProcessor.CanProcessPayment(payment))
        {
            return fooProcessor.Process(payment);
        }
        else if (barProcessor.CanProcessPayment(payment))
        {
            return barProcessor.Process(payment);
        }
        // ...
}

修改:此时,您可以看到PaymentExecutor在技术上甚至不关心它包含哪种类型的支付处理器。 (它只关心找到能够处理付款的处理器。)

如果我们允许它使用任意数量的处理器,而不是强迫PaymentExecutor使用4个任意支付处理器?例如:

class PaymentExecutor
{
    IEnumerable<IPaymentProcessor> processors;

    PaymentExecutor(IEnumerable<IPaymentProcessor> processors)
    {
        this.processors = processors;
    }

    public Payment Execute(Payment payment)
    {
        foreach(var processor in processors)
        {
            if(processor.CanProcessPayment(payment))
            {
                return processor.Process(payment);
            }
        }
        // Handle what should happen if no processors can process the payment...
}

这个版本的PaymentExecutor可能更具表现力,但更重要的是它与DI Containers的效果更好。现在,您不必弄清楚如何告诉DI Container解析4个任意支付处理器,而是告诉它如何解析一组支付处理器。

有关这方面的更多详情,请参阅以下答案:

Castle Windsor: How do I inject all implementations of interface into a ctor?

答案 1 :(得分:1)

我同意接受的答案,但我想提出一种我过去使用的替代方法,以达到良好效果。它需要更多的设置,但从长远来看提供了很好的好处。

注意:下面的代码省略了所有错误检查,请原谅任何拼写错误等。

我们可以说我们可以在解决方案中添加PaymentType枚举:

public enum PaymentType
{
    Foo=0,Bar,Baz,Qux
}

然后,您可以创建一个界面IPaymentTypeDecider

public interface IPaymentypeDecider
{
    PaymentType DeterminePaymentType(Payment payment);
}

并使用您所拥有的逻辑来实现它,以决定使用哪种付款方式:

public class PaymentTypeDecider: IPaymentTypeDecider
{
   public PaymentType DeterminePaymentType(Payment payment)
   {
      PaymentType result = PaymentType.Foo; //e.g default option
      if
      {
         //your complex logic here based on payment
      }
      return result;
   }
}
  

我的理由是保持与实际执行者分开使用的付款类型的逻辑。这有助于保持PaymentExecutor的代码更简单,并允许您测试用于决定与PaymentExecutor隔离使用的付款方式的逻辑。

接下来,您定义了一组空标记接口,每个支付类型对应一个继承自IPaymentType的接口:

public interface IFooPaymentProcessor:IPaymentProcessor{}
public interface IBarPaymentProcessor:IPaymentProcessor{}
public interface IBazPaymentProcessor:IPaymentProcessor{}
public interface IQuxPaymentProcessor:IPaymentProcessor{}

并实施它们:

public class FooPaymentProcessor: IFooPaymentProcessor
{
    //implement it.
}....etc.
  

注意:标记接口的原因是因为这是我学习如何操作的原因;很可能很多IOC框架的自动工厂生成可以为你做很多这个解决方案(虽然我不太确定他们可以做上面的自定义字典工厂)。

接下来,在Composition Root的范围内(示例假定您使用的是Ninject,但这适用于任何IOC框架),您为PaymentProcessor工厂创建了一个接口和实现:

public interface IPaymentProcessorFactory
{
    IPaymentProcessor GetProcessor(PaymentType paymentType)
}

public class PaymentProcessorFactory
{
    readonly IKernel _kernel;
    Dictionary<PaymentType,IPaymentProcessor> _processors;
    public PaymentProcessorFactory(IKernel kernel)
    {
        //save the kernel for later use
       _kernel=kernel;
       //register our payment processors for given PaymentTypes
       _processors = new Dictionary<PaymentType,IPaymentProcessor>()
       {
          {PaymentType.Foo,typeof(IFooPaymentProcessor)},
          {PaymentType.Bar,typeof(IBarPaymentProcessor)},
          {PaymentType.Baz,typeof(IBazPaymentProcessor)},
          {PaymentType.Qux,typeof(IQuxPaymentProcessor)}
       }
    }
    public IPaymentProcessor GetProcessor(PaymentType paymentType)
    {
        //pop the Payment Type from the dictionary, and resolve via kernel:
        return _kernel.Get(_processors[paymentType].Value) as IPaymentProcessor;
    }
}
  

这个工厂级的表现就像一个懒惰的解析器&#34;对于国际奥委会来说,它只会产生IPaymentProcessors,但只有当它被你的代码询问时才会自动产生。这意味着您可以向其传递上下文(在本例中为PaymentType)并在运行时返回正确的实现。

然后连接所有绑定:

_kernel.Bind<IPaymentTypeDecider>().To<PaymentTypeDecider>();
_kernel.Bind<IPaymentProcessorFactory>().To<PaymentProcessorFactory>();
_kernel.Bind<IFooPaymentProcessor>().To<FooPaymentProcessor>();
_kernel.Bind<IBarPaymentProcessor>().To<BarPaymentProcessor>();
_kernel.Bind<IBazPaymentProcessor>().To<BazPaymentProcessor>();
_kernel.Bind<IQuxPaymentProcessor>().To<QuxPaymentProcessor>();

最后,您重构PaymentExecutor课程,以依赖IPaymentTypeDeciderIPaymentProcessorFactory

public class PaymentExecutor
{
   readonly IPaymentTypeDecider _decider;
   readonly IPaymentProcessorFactory _factory;

   public PaymentExecutor(IPaymentTypeDecider decider, IPaymentProcessorFactory factor)
   {
       _decider=decider;
       _factory=factory;
   }

   public Payment Execute(Payment payment)
   {
      var paymentType=_decider.DeterminePaymentType(payment);
      var processor = _factory.GetProcessor(paymentType);
      return processor.Process(payment);
   }   
}

我看到这种方法的主要优点是

  • 依赖关系的优秀解耦;您可以轻松地模拟PaymentExecutor类的依赖关系以进行测试

  • 能够添加新的支付类型而无需修改PaymentExecutor(您只需创建界面,更新决策逻辑,在工厂类/绑定中注册,然后您就可以了好的去)。

  • 单独添加支付处理器作为构造函数参数意味着如果你最终得到20个处理器(谁知道!)它会变得混乱,更不用说你也将创建每个支付处理器类型的实例每次创建PaymentExecutor

希望有所帮助。