使用接口然后检查实现类型是不是很糟糕?

时间:2015-09-25 12:26:34

标签: design-patterns architecture implementation abstraction

考虑以下情况:

我想设计一个折扣计算器,它可以获得可以应用于订单的折扣。订单有两种类型:在线和店内。根据订单类型和订单总金额,折扣计算器计算折扣。

我编程在C#中演示场景,但问题与语言无关。在下面的代码中,DiscountCalculator类通过检查输入参数的实际类型来计算折扣。

我觉得在IOrder方法中检查GetDiscount参数的实际类型是代码气味;因为我隐藏了接口IOrder背后的实现细节,然后我以某种方式带出了隐藏的内容。

    interface IOrder
    {
        int GetTotalPrice();
    }

    class InStoreOrder : IOrder
    {
        public int GetTotalPrice() { // returns the price of order }
    }

    class OnlineOrder : IOrder
    {
        public int GetTotalPrice() { // returns the price of order }
    }

    class DiscountCalculator
    {
        public int GetDiscount(IOrder order)
        {
            Type orderType = order.GetType();
            if (orderType == typeof(OnlineOrder))
            {
                if (order.GetTotalPrice() < 100)
                    return 2;
                else
                    return 5;
            }
            if (orderType == typeof(InStoreOrder))
            {
                if (order.GetTotalPrice() < 100)
                    return 3;
                else
                    return 6;
            }
            else
                throw new Exception("Unknown order type:" + orderType.Name);
        }
    }

有什么想法吗?

更新

我非常感谢人们就此进行合作。所有的解决方案不仅具有启发性,而且还带来了一种优雅的方式。

顺便说一句,自从所有答案都确定解决方案不好的时候,我在想,Abstract Factory可能是一个不错的选择。为什么?因为我们正在处理一系列相关对象:OrderDiscountCalculator

这样的事情:

Factory f = new FactoryRepo ("Online");
IOrder order = f.CreateItem();
IDiscountCalculator discounter = f.CreateDiscountCalculator();
....

通过这种方式,我认为对于未来的变化,正如@Dhruv Rai Puri指出的那样,Decorator模式可能很容易应用。

任何想法?

4 个答案:

答案 0 :(得分:3)

是的,检查输入参数的实际类型会破坏使用接口的目的。更好的方法是像这样修改IOrder接口

interface IOrder
{
   int GetTotalPrice();
   int GetDiscount();
}

然后允许每个实现计算折扣但是合适。完成此操作后,您可以将DiscountCalculator中的方法简化为

order.GetDiscount();

答案 1 :(得分:2)

在我看来,这似乎是Strategy Pattern的好例子。

这是您重新制作的样本

public interface IOrder
{
    int GetTotalPrice();
}

public interface IDiscountStrategy
{
    int CalculateDiscount(IOrder order);
}

public class InStoreOrder : IOrder
{
    public int GetTotalPrice()
    {
        return 25;
    }
}

public class OnlineOrder : IOrder
{
    public int GetTotalPrice()
    {
        return 25;
    }
}

public class InStoreOrderDiscountStrategy : IDiscountStrategy
{
    public int CalculateDiscount(IOrder order)
    {
        if (order.GetTotalPrice() < 100)
            return 3;
        else
            return 6;
    }
}

public class OnlineOrderDiscountStrategy : IDiscountStrategy
{
    public int CalculateDiscount(IOrder order)
    {
        if (order.GetTotalPrice() < 100)
            return 2;
        else
            return 5;

    }
}

public class DiscountCalculator
{
    readonly IDiscountStrategy _discountStrategy;

    public DiscountCalculator(IDiscountStrategy strategy)
    {
        _discountStrategy = strategy;
    }

    public int GetDiscount(IOrder order)
    {
        int discount = _discountStrategy.CalculateDiscount(order);
        return discount;
    }
}

...这里是OnLineOrder的测试样本

public void OnlineOrder_Discount_Equals_2()
{
    IOrder order = new OnlineOrder();
    IDiscountStrategy strategy = new OnlineOrderDiscountStrategy();

    DiscountCalculator calculator = new DiscountCalculator(strategy);
    int discount = calculator.GetDiscount(order);
    Assert.True(discount == 2);
}     

我们的想法是封装特定于每种订单类型的折扣计算逻辑:在线或在店内。如果商店推出了促销活动&#34;订单类型(例如,推出新产品),模式可以扩展到包括新逻辑,同时保持&#34;打开/关闭&#34;原理

答案 2 :(得分:2)

战略的解决方案已经提出https://stackoverflow.com/a/32798708/1168342,但这个答案有一些优势。

折扣和订单是常见的域名问题。这个轮子已经重新发明了几次。我将引用Craig Larman's "Applying UML and Patterns" book第26章的解决方案:

Pricing Strategy classes (part of Figure 26.9)

在此解决方案中,Sale与您的Order类似,ISalePricingStrategy就像您的DiscountCalculator

ISalePricingStrategy是策略模式的应用程序(名称在界面中),策略始终附加到上下文对象。在这种情况下,它是Sale(或在你的IOrder中)。

映射到您的问题

以下是我如何看待您的问题符合Larman建议使用定价策略的UML:

Pricing Strategy applied

如果我理解正确,您的案例都是AbsoluteDiscountOverThresholdPricingStrategy的复合实例。让我们从您的有条件的OnlineOrders中获取代码:

if (order.GetTotalPrice() < 100)
  return 2;
else
  return 5;

这就像向您的订单添加两个

实例
onlineOrder.addPricingStrategy(new AbsoluteDiscountOverThresholdPricingStrategy(2,0));  // orders under 100
onlineOrder.addPricingStrategy(new AbsoluteDiscountOverThresholdPricingStrategy(5,100));  // orders >= 100

因此,Larman更进了一步,并解释说您可以使用Composite模式组合这些策略。我将它应用于您的问题(提示在上面的add...方法中):

Composite strategies per Larman, figure 26.14

我用粉红色的两个类是可选的。如果您始终为客户提供最佳策略(如我附加到GetTotalPrice的注释的伪代码中),则您不需要它们。 Larman解释说,您可以更进一步说,如果适用多种策略,则计算方式是支持商店或客户。同样,它是一个实例化类并附加它的问题。执行此操作的代码可以从&#34;配置&#34;软件中的菜单命令。

使用它的代码如下所示:

IOrder onlineOrder = new OnlineOrder();  //...
...
CompositePricingStrategy comp = new CompositePricingStrategy();
comp.add(new AbsoluteDiscountOverThresholdPricingStrategy(2,0));  // orders under 100
comp.add(new AbsoluteDiscountOverThresholdPricingStrategy(5,100));  // orders >= 100
onlineOrder.setPricingStrategy(comp);
// repeat as above for instoreOrders ...

使用工厂再次采用更灵活的方式。有关Java / .NET中非常酷的示例,请参阅Larman的书。

优点

由于这个答案类似于另一个答案,我想解释一下这种方法的一些优点,即使它更复杂。如果您在折扣逻辑中使用GetTotal(),则它比GetDiscount()具有一些优势:

  • GetTotal()处理(封装)所有逻辑以计算总数。
  • 如果适用多种策略(例如,在线订单获得5%的折扣,超过200美元的订单可获得10%的折扣),您可能需要对此处理方式进行编码。 Larman给出了一个使用Composite模式的例子,其中GetTotal()无可挑剔地工作,而客户端代码不必做任何特殊的事情。
  • 您可以扩展其他类型的策略并根据需要实例化它们。例如,对于任何超过500美元的订单,您可以进行折扣50.这是在代码中实例化新类的问题(不编码逻辑)。

答案 3 :(得分:1)

是的,在定义界面后检查类型是不太好的,因为它会破坏目的。

但我对上面给出的设计解决方案,即getOrderDiscount方法不太相信。如果您在同一商店Instore中有不同的折扣或在线享受不同的折扣怎么办 - 除了特定于商品的折扣之外,还要说明临时的网站折扣。具有getOrderDiscount()方法的设计不考虑这些场景。

但如果这些情况不可行/适用,那么你可以忽略我的下一篇文章。实际上我曾在一家零售产品软件公司工作,因此我想到了很多可能性。

  • 应该有一个IItemDiscount接口,用于在列出和/或订单结账时“装饰”一个项目。
  • IOrder实例应该有一个applyOrderDiscounts(基本上是对现有getOrderDiscount()的一种扭曲)方法,该方法应该采用可以应用于订单的订单级折扣列表。