使用接口然后检查实现类型是否不好?
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
可能是一个不错的选择。为什么?因为我们正在处理一系列相关对象:Order
和 DiscountCalculator
.
像这样:
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
类似于您的 Order
,ISalePricingStrategy
类似于您的 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 的问题(不编写逻辑代码)。
考虑以下场景:
我想设计一个折扣计算器,它可以计算出可应用于订单的折扣。有两种类型的订单:在线和店内。根据订单类型和订单总金额,折扣计算器计算折扣。
我用 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
可能是一个不错的选择。为什么?因为我们正在处理一系列相关对象:Order
和 DiscountCalculator
.
像这样:
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
类似于您的 Order
,ISalePricingStrategy
类似于您的 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 的问题(不编写逻辑代码)。