使用接口然后检查实现类型是否不好?

Is it bad to use interface and then check for implementation type?

考虑以下场景:

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

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

我觉得检查 GetDiscount 方法中 IOrder 参数的实际类型是代码味道;因为我隐藏了接口后面的实现细节 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);
        }
    }

有什么想法吗?

更新:

我真的很感谢大家在这方面的合作。所有的解决方案不仅具有启发性,而且在 table 上带来了一种优雅的方式。

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

像这样:

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

这样,我认为对于未来的变化,正如@Dhruv Rai Puri 指出的那样,可以轻松应用装饰器模式。

有什么想法吗?

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

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

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

order.GetDiscount();

是的,在定义接口后检查类型不是很好,因为它违背了目的。

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

但如果这些场景不是 possible/applicable 那么你可以忽略我的下一个 para.Actually 我曾在一家零售产品软件组织工作,因此正在考虑很多可能性。

  • 应该有一个 IItemDiscount 接口,它应该用于 "decorate" 商品在上市时 and/or 订单结帐。
  • IOrder 实例应该有一个 applyOrderDiscounts(基本上是现有 getOrderDiscount() 的一个转折)方法,它应该采用可以应用于订单的订单级别折扣列表。

在我看来,这看起来是 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);
}     

想法是封装特定于每种订单类型的折扣计算逻辑:在线或店内。如果商店引入 "promotional" 订单类型(例如,推出了新产品),则可以扩展该模式以包含新逻辑,同时保持 "Open/Closed" 原则。

Strategy的解决方案已经有人提出,但是这个答案有一些优点。

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

在此解决方案中,Sale 类似于您的 OrderISalePricingStrategy 类似于您的 DiscountCalculator

ISalePricingStrategy是Strategy模式的一个应用(名称在接口中),Strategies总是依附于一个context对象。在这种情况下,它是 Sale(或者在你的情况下,IOrder)。

映射到您的问题

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

如果我理解正确的话,你的两个案例都是 AbsoluteDiscountOverThresholdPricingStrategy 的复合实例。让我们从您的在线订单条件中获取代码:

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... 方法中):

我放在粉红色的两个class是可选的。如果您总是将最佳策略提供给客户(如我在 GetTotalPrice 所附注释的伪代码中所示),那么您就不需要它们。 Larman 解释说,您可以更进一步,如果应用了不止一种策略,则计算要么有利于商店,要么有利于客户。同样,这是实例化 class 并附加它的问题。执行此操作的代码可以是来自软件中 "Configuration" 菜单命令的 运行。

使用它的代码类似于:

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 的折扣。这是在代码中实例化新 class 的问题(不编写逻辑代码)。