【问题标题】:Dealing with non-inherited methods in a subclass处理子类中的非继承方法
【发布时间】:2011-11-13 18:25:59
【问题描述】:

我有上课券:

public abstract class Voucher
    {
        public int Id { get; set; }
        public decimal Value { get; protected set; }
        public const string SuccessMessage = "Applied";
    }

和一个子类 GiftVoucher

public class GiftVoucher : Voucher
    {
    }

和另一个子类 DiscountVoucher

public class DiscountVoucher : Voucher
    {
        public decimal Threshold { get; private set; }
        public string FailureMessage { get { return "Please spend £{0} to use this discount"; } }
    }

您可以看到 DiscountVoucher 有几个特定的​​属性 Threshold 和 FailureMessage,它们分别表示您需要花费的金额才能获得折扣,以及如果用户没有花费该金额则显示的失败消息。

我的问题是这样的。我有一组 Voucher 对象,我不想在我的代码中做这样的事情

if (voucher is DiscountVoucher)
{
   // cast voucher to a DiscountVoucher and then call the specific methods on it
} 

因为这根本不是可维护的。同时我不想把那些特定的方法放在 Voucher 抽象类中,因为它们并不适用于所有类型的 Voucher。有谁知道如何设计这个功能?

【问题讨论】:

  • 您必须对您的收藏执行哪些任务?

标签: c# oop design-patterns


【解决方案1】:

一般情况下:否!

在一般代码流中处理特殊场景而没有任何代码处理特殊情况是行不通的。

但在某些情况下,您可以作弊。您可以在提供默认“无”实现的抽象基类中实现虚拟方法。

可以是返回 null、0 或什么都不做的方法。

在这种情况下

public virtual string FailureMessage { get { return string.Empty; } }

可能是一个合理的实现。

我猜你的实现看起来很像template method pattern。那么对于不适用于某些实现的步骤有无效的实现是完全正常的。

【讨论】:

  • 问题是,这违反了基本的 OO 原则。我们不应该在 Voucher 中包含不适用于所有 Voucher 的内容。
  • 它不违反 OO 原则。从 Voucher 返回一个空的失败消息是完全有意义的,这意味着没有发生错误。
  • 如果你的原则在实践中不起作用:使用其他原则。您需要决定通用凭证应该能够处理什么并将它们放在基类中。这不一定对应于应用于您为基类建模的物理对象的操作。
  • 问题中显示的方法的一个问题是,使用凭证的人需要知道凭证 A 会产生错误,但凭证 B 不会。在一个好的 OO 设计中,应该尽可能少地了解一个对象。尽可能将事物隐藏在对象中,并将对象视为黑匣子。
  • 从消费者的角度来看,所有代金券都可能产生错误。您不需要知道 B 在当前实现中永远不会产生任何错误。如果遇到需要以不同方式处理 B 的情况,则需要重新定义 B 的接口或创建单独的 B 处理策略。
【解决方案2】:

嗯,你在这里得到的是策略模式的一个版本。我认为最终必须决定您是否拥有一种或另一种代金券并没有什么可避免的,但您可以限制变体的数量 - 如果您愿意,可以使用代金券类别 - 使用界面。

例如,您最终可能会得到五个实现称为“StandardVoucher”的接口的凭证和三个称为“DiscountVoucher”的凭证,但您现在只需处理两个而不是处理八个案例。

这些接口可以涵盖一系列显示可用方法的凭证,而无需担心每个凭证实现的细节。

【讨论】:

    【解决方案3】:

    不,你不能,因为迭代更通用的对象然后调用特定的方法需要使用多态性才能在每个子类中拥有专用的功能。没有超类中的方法可以覆盖,你就无法获得你想要的。

    【讨论】:

      【解决方案4】:

      我认为你对你描述的代码持怀疑态度是对的。

      我的第一个想法是,如果DiscountVoucher 的成员不够广泛,无法在Voucher 中作为虚拟或抽象存在,那么以Voucher 作为参数的函数不应该触及它们。

      所以,为了解决这个问题,我想说你可以做以下两件事之一:

      首先,您可以向Voucher 添加虚拟方法或属性,例如

      public abstract class Voucher
      {
          public int Id { get; set; }
          public decimal Value { get; protected set; }
          public const string SuccessMessage = "Applied";
          public decimal Threshold { get { return 0.0; } }
          public string FailureMessage { get { return ""; } }
      }
      

      其次,您可以添加对每个Voucher 执行您期望的方法。您已将它们组合为优惠券,因此请考虑它们的共同点。例如,如果GiftVoucherDiscountVoucher 都在进行自己的计算以确定它们是否适用于当前的ShoppingCart,那么您可以使用称为isValid()Voucher 方法来检测这一点。例如,

      public abstract class Voucher
      {
          public bool isValid(ShoppingCart sc);
          public string FailureMessage { get { return "This voucher does not apply"; } }
          // ...
      }
      
      public class DiscountVoucher : Voucher
      {
          private decimal Threshold;
          public override bool isValid(ShoppingCart sc)
          {
              return (sc.total >= Threshold);
          }
          public override string FailureMessage
          {
              get { return FormatString("Please spend £{0} to use this discount", Threshold); }
          }
      

      【讨论】:

        【解决方案5】:

        只有在某些情况下您必须施放。这里我会实现一个通用的错误检查机制:

        public abstract class Voucher
        {
            public int Id { get; set; }
            public decimal Value { get; protected set; }
            public virtual string SuccessMessage { get { return "Applied"; } }
            public virtual string FailureMessage { get { return String.Empty; } }
            public virtual bool Ok { get { return true; } }
        }
        
        public class GiftVoucher : Voucher { }
        
        public class DiscountVoucher : Voucher
        {
            public decimal Threshold { get; private set; }
            public override string FailureMessage { get { return "Please spend £{0} to use this discount"; } }
            public override bool Ok { get { return Value >= Threshold; } }
        }
        

        然后您可以在不强制转换的情况下测试任何类型的凭证的完整性:

        if (voucher.Ok) {
            Console.WriteLine(voucher.SuccessMessage);
        } else {
            Console.WriteLine(voucher.FailureMessage);
        }
        

        作为一般规则,尽量让对象做自己的事情(在这里测试它们是否正常),而不是从“外部”做。即使是 GiftVoucher 中不会发生错误的事实也不需要被“外部世界”知道。

        【讨论】:

        • 谢谢。这看起来是一个完美的解决方案。
        • 只想说Ok的实现是错误的。价值是指代金券的价值(5 英镑折扣),阈值是指您需要花费多少才能获得 5 英镑折扣(例如,您需要花费 50 英镑)。所以,这就是问题所在。您说得非常正确,对象应该自己进行计算,但这意味着我需要给 Voucher 另一个属性(用户花费的金额)。然后,在 Ok 方法中,我可以将 Threshold 与该方法进行比较,您同意这种方法吗?我问的原因是优惠券看起来有点不自然......
        • ...知道用户花了多少钱。但另一种选择是我们从“外部”决定凭证应用程序是否可以。我们如何处理这种矛盾?
        • @Sachin:更自然的解决方案是将花费的金额作为参数传递给函数。
        • 是的,将 Ok 属性替换为已通过金额或折扣的方法。
        猜你喜欢
        • 1970-01-01
        • 2023-04-05
        • 2016-01-24
        • 2023-04-09
        • 2013-05-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-17
        相关资源
        最近更新 更多