【问题标题】:Code Contracts and Inheritance(Precondition on overridden method)代码契约和继承(覆盖方法的前提条件)
【发布时间】:2018-08-25 00:00:36
【问题描述】:

当前代码协定不允许对派生类中的成员设置先决条件,其中成员已经在基类中设置了先决条件(实际上我目前收到的是警告而不是错误)。我不明白这背后的逻辑。我知道这与 Liskov 的替换规则有关,即派生类应该始终能够在预期父类的地方使用。当然,“使用”意味着按预期工作。对于接口,这对我来说似乎没问题,因为实现接口的不同类型不会添加状态,因此可以完全遵守合同。但是,当您从基类继承时,您这样做是为了添加状态和特殊功能,而且覆盖方法通常会有额外的要求。为什么不能像后置条件和对象不变量一样将前置条件 AND 在一起?

看看下面:

class Speaker
{
    public bool IsPlugged { get; set; }
    protected virtual void Beep()
    {
        Contract.Requires(IsPlugged);
        Console.WriteLine("Beep");
    }
}

class WirelessSpeaker : Speaker
{
    public bool TransmitterIsOn { get; set; }
    protected override void Beep()
    {
        Contract.Requires(TransmitterIsOn);
        base.Beep();
    }
}

您可能会争辩说,这个类层次结构违反了 Liskov 的规则,因为无线扬声器在传递给期望 Speaker 的方法时可能无法发出哔哔声。但这不就是我们使用代码合约的原因吗?确保满足要求?

【问题讨论】:

    标签: c# .net code-contracts solid-principles liskov-substitution-principle


    【解决方案1】:

    代码合同不是关于满足要求,而是关于它们的通信Speaker.Beep 的调用者受仅在某些情况下生效的合同的约束。

    WirelessSpeaker 缩小Speaker 的功能空间——这就是 Liskov 发挥作用的地方。如果我知道它是无线的,我只能有效地使用那个特定的Speaker。在这种情况下,我应该明确接受WirelessSpeaker,而不是Speaker,并避免替换问题。

    编辑以响应 cmets:

    WirelessSpeaker 的作者选择如何解释Beep 命令。选择在此级别可见但在基本级别不可见的新合约会施加限制,在使用Speakers 时应用

    如果只是在发射器未打开时不发出哔哔声,我们就不会谈论代码合同。他们的目的不是在运行时进行通信,而是在设计时进行调用的语义(不仅仅是它的语法)。

    在运行时发生异常,最终阻止“不正确”调用这一事实在很大程度上无关紧要。

    【讨论】:

    • 我明白了。但是,如果我可以争论从Speaker 派生WirelessSpeaker。如果我是一个类库开发人员并且想假设最少的输入参数并且有一个方法可以获取List<Speaker> 并在所有这些参数上调用Beep。我真的在乎这些扬声器是否真的发出哔哔声吗?难道不是由应用程序开发人员来确保他们正确构建扬声器以使其能够发出哔哔声吗?我的意思是即使扬声器不能发出哔哔声,这就是为什么我们有异常处理机制来处理特殊情况。合同中已经存在通信(请求)。
    • 如果这仍然不是真的。你能告诉我们应该如何扩展Speaker 的功能吗?请记住,您无法真正预测将来可能添加的所有功能,并且您希望扬声器的任何子类型都能够发出带有该签名的哔哔声。
    • @FarhadAlizadehNoori “我真的关心这些扬声器是否真的发出哔哔声吗?”如果是这种情况,那么您的合同是不需要 WirelessSpeaker 的;它不应该抛出,如果它没有打开它就不应该发出哔哔声。无论如何,这不是合同问题,如果你真的想要一个异常,我会在不使用合同的情况下抛出 InvalidOperationException。
    【解决方案2】:

    @BryanWatts 是对的。 OP 提供的类违反了 Liskov 替换原则。而且你不应该使用异常来控制程序流程——这也是一种代码味道。例外是指例外——不允许您的对象以预期方式运行的例外情况,这可能导致您的对象状态和/或未来行为的损坏。

    您需要确保了解里氏替换原则 (LSP) 的全部内容。 LSP 并不是要确保interfaces 可以互换使用。

    当一个对象从另一个对象继承时,它继承了它的所有父对象的行为。诚然,您可以覆盖该行为,但您必须小心这样做。让我们以SpeakerWirelessSpeaker 为例,看看它们是如何分崩离析的。

    public class Speaker
    {
        public bool IsPlugged { get; set; }
    
        public virtual void Beep()
        {
            if (!IsPlugged)
            {
                throw
                new InvalidOperationException("Speaker is not plugged in!");
            }
    
            Console.WriteLine("Beep.");
        }
    }
    
    public class WirelessSpeaker : Speaker
    {
        public bool TransmitterIsOn { get; set }
    
        public override void Beep()
        {
            if (!TransmitterIsOn)
            {
                throw
                new InvalidOperationException("Wireless Speaker transmitter is not on!");
            }
    
            Console.WriteLine("Beep.");
        }
    }
    
    public class IBeepSpeakers
    {
        private readonly Speaker _speaker;
    
        public IBeepSpeakers(Speaker speaker)
        {
            Contract.Requires(speaker != null);
            Contract.Ensures(_speaker != null && _speaker == speaker);
            _speaker = speaker;
    
            // Since we know we act on speakers, and since we know
            // a speaker needs to be plugged in to beep it, make sure
            // the speaker is plugged in.
            _speaker.IsPlugged = true;
        }
    
        public void BeepTheSpeaker()
        {
            _speaker.Beep();
        }
    }
    
    public static class MySpeakerConsoleApp
    {
        public static void Main(string[] args)
        {
            BeepWiredSpeaker();
    
            try
            {
                BeepWirelessSpeaker_Version1();
            }
            catch (InvalidOperationException e)
            {
                Console.WriteLine($"ERROR: e.Message");
            }
    
            BeepWirelessSpeaker_Version2();
        }
    
        // We pass in an actual speaker object.
        // This method works as expected.
        public static BeepWiredSpeaker()
        {
            Speaker s = new Speaker();
            IBeepSpeakers wiredSpeakerBeeper = new IBeepSpeakers(s);
            wiredSpeakerBeeper.BeepTheSpeaker();
        }
    
        public static BeepWirelessSpeaker_Version1()
        {
            // This is a valid assignment.
            Speaker s = new WirelessSpeaker();
    
            IBeepSpeakers wirelessSpeakerBeeper = new IBeepSpeakers(s);
    
            // This call will fail!
            // In WirelessSpeaker, we _OVERRODE_ the Beep method to check
            // that TransmitterIsOn is true. But, IBeepSpeakers doesn't
            // know anything _specifically_ about WirelessSpeaker speakers,
            // so it can't set this property!
            // Therefore, an InvalidOperationException will be  thrown.
            wirelessSpeakerBeeper.BeepTheSpeaker();
        }
    
        public static BeepWirelessSpeaker_Version2()
        {
            Speaker s = new WirelessSpeaker();
            // I'm using a cast, to show here that IBeepSpeakers is really
            // operating on a Speaker object. But, this is one way we can
            // make IBeepSpeakers work, even though it thinks it's dealing
            // only with Speaker objects.
            //
            // Since we set TransmitterIsOn to true, the overridden
            // Beep method will now execute correctly.
            //
            // But, it should be clear that IBeepSpeakers cannot act on both
            // Speakers and WirelessSpeakers in _exactly_ the same way and
            // have confidence that an exception will not be thrown.
            ((WirelessSpeaker)s).TransmitterIsOn = true;
    
            IBeepSpeakers wirelessSpeakerBeeper = new IBeepSpeaker(s);
    
            // Beep the speaker. This will work because TransmitterIsOn is true.
            wirelessSpeakerBeeper.BeepTheSpeaker();
    }
    

    这就是您的代码违反 Liskov 替换原则 (LSP) 的原因。正如 Robert & Micah Martin 在 pp 上的 Agile Principles, Patterns and Practices in C# 中敏锐地指出的那样。 142-143

    LSP 清楚地表明,在 OOD 中,IS-A 关系属于可以合理假设的行为,并且客户端依赖于....[当通过其基础使用对象时类接口,用户只知道基类的前置条件和后置条件。因此,派生对象不能期望这些用户遵守比基类要求的更强大的先决条件。也就是说,用户必须接受基类可以接受的任何内容。此外,派生类必须符合基 [class] 的所有后置条件。

    通过本质上为WirelessSpeakerBeep 方法提供前提条件TransmitterIsOn == true,您创建了一个比基础Speaker 类中存在的前提条件更强。对于WirelessSpeakers,IsPluggedTransmitterIsOn 必须true,以便Beep 的行为符合预期(从Speaker 的角度来看),甚至尽管Speaker 本身并没有TransmitterIsOn 的概念。

    此外,您还违反了另一个 SOLID 原则,即接口隔离原则 (ISP)

    不应强迫客户依赖他们不使用的方法。

    在这种情况下,不需要插入WirelessSpeaker。(我假设我们在这里讨论的是音频输入连接,而不是电气连接。)因此,WirelessSpeaker 应该没有任何名为IsPlugged 的属性,但是,因为它继承自Speaker,所以它有!这表明您的对象模型与您打算使用对象的方式不一致。再次注意,大部分讨论都集中在对象的行为上,而不是它们之间的关系。

    此外,违反 LSP 和 ISP 都表明可能也违反了开放/封闭原则 (OCP)

    软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

    因此,现在应该清楚了,我们不使用代码协定只是为了确保在对对象调用方法时满足某些先决条件。不,代码合同用于声明关于行为状态保证(因此使用了合同这个词)您的对象及其方法基于所述的前置条件和后置条件,以及您可能还定义的任何不变量。

    因此,对于您的扬声器类,您的意思是:如果扬声器已插入,那么扬声器会发出哔哔声。好的,到目前为止,一切都很好;这很简单。现在,WirelessSpeaker 类呢?

    好吧,WirelessSpeaker 继承自 Speaker。因此,WirelessSpeaker 也有一个 IsPlugged 布尔属性。此外,因为它继承自Speaker,所以为了让WirelessSpeaker 发出哔哔声,它必须将其IsPlugged 属性设置为true。 “可是等等!”你说,“我已经覆盖了Beep 的实现,所以WirelessSpeaker 的发射器必须打开。”是的,这是真的。但它必须插入! WirelessSpeaker 不仅继承了 Beep 方法,还继承了其父类实现的行为! (考虑使用基类引用代替派生类的情况。)由于父类可以“插入”,WirelessSpeaker 也可以“插入”;我怀疑这是否是您最初想到此对象层次结构时的意图。

    那么,您将如何解决这个问题?好吧,您需要想出一个更好地与相关对象的行为保持一致的模型。我们对这些对象及其行为了解多少?

    1. 他们都是一种演讲者。
      • 因此,无线扬声器可能是扬声器的专业化产品。相反,扬声器可能是无线扬声器的泛化。
      • 在当前的对象模型中(与您发布的一样多),这两个对象之间没有太多共享的行为或状态。
      • 由于两个对象之间没有太多共同的状态或行为,人们可能会争辩说这里不应该存在继承层次结构。我要和你一起扮演魔鬼的拥护者并维护继承等级。
    2. 它们都发出哔哔声。
      • 但是,每种扬声器发出蜂鸣声的条件不同。
      • 因此,这些说话者不能直接从另一个继承,否则,它们会共享可能不适合他们的行为(在这种情况下,现有的“共享行为”肯定不适用于所有类型的说话者)。这解决了 ISP 问题。

    好的,这些演讲者的共同行为之一就是发出哔哔声。因此,让我们将这种行为抽象为一个抽象基类:

    // NOTE: I would prefer to simply call this Speaker, and call
    // Speaker 'WiredSpeaker' instead--but to leave your concrete class
    // names as they were in your original code, I've chosen to call this
    // SpeakerBase.
    public abstract class SpeakerBase
    {
        protected SpeakerBase() { }
    
        public void Beep()
        {
            if (CanBeep())
            {
                Console.WriteLine("Beep.");
            }
        }
    
        public abstract bool CanBeep();
    }
    

    太棒了!现在我们有一个代表说话者的抽象基类。当且仅当CanBeep() 方法返回true 时,此抽象类将允许扬声器发出哔声。而且这个方法是抽象的,所以任何继承这个类的类必须为这个方法提供自己的逻辑。通过创建这个抽象基类,我们允许任何依赖于SpeakerBase 类的类在当且仅当CanBeep() 返回true 时从扬声器发出哔声。这也解决了 LSP 违规!在任何可以使用SpeakerBase 并要求其发出哔声的地方,都可以用SpeakerWirelessSpeaker 代替,我们可以确定其行为:如果扬声器可以发出哔声,它就会发出哔声。

    现在剩下的就是从SpeakerBase 派生我们的每个扬声器类型:

    public class Speaker : SpeakerBase
    {
        public bool IsPlugged { get; set; }
    
        public override bool CanBeep() => IsPlugged;
    }
    
    public class WirelessSpeaker : SpeakerBase
    {
        public bool IsTransmiterOn { get; set; }
    
        public override bool CanBeep() => IsTransmitterOn;
    }
    

    所以,现在我们有一个Speaker,它只有在插入时才会发出哔哔声。我们还有一个WirelessSpeaker,它只有在发射器打开时才会发出哔哔声。此外,WirelessSpeakers 对被“插入”一无所知。这根本不是他们本质的一部分。

    此外,遵循依赖倒置原则(DIP)

    1. 高级模块不应依赖于低级模块。两者都应该依赖于抽象。
    2. 抽象不应依赖于细节。细节应该取决于抽象。

    这意味着扬声器的消费者不应直接依赖SpeakerWirelessSpeaker,而应依赖SpeakerBase。这样,无论出现什么样的扬声器,如果它继承自SpeakerBase,我们知道如果条件允许,我们可以在依赖类中抽象类型引用的扬声器子类型的情况下发出声音。这也意味着IBeepSpeakers 不再知道如何将扬声器置于可以发出哔声的状态,因为在IBeepSpeakers 可以用来做出此类决定的扬声器类型之间没有共同的行为。因此该行为必须作为依赖项传递给IBeepSpeakers。 (这是一个可选的依赖项;你可以让类接受SpeakerBase 并调用Beep(),如果SpeakerBase 对象处于正确状态,它会发出哔声,否则不会。 )

    public class IBeepSpeakers
    {
        private readonly SpeakerBase _speaker;
        private readonly Action<SpeakerBase> _enableBeeping;
    
        public IBeepSpeakers(SpeakerBase speaker, Action<SpeakerBase> enableBeeping)
        {
            Contract.Requires(speaker != null);
            Contract.Requires(enableBeeping != null);
            Contract.Ensures(
                _speaker != null && 
                _speaker == speaker);
            Contract.Ensures(
                _enableBeeping != null && 
                _enableBeeping == enableBeeping);
    
            _speaker = speaker;
            _enableBeeping = enableBeeping;
        }
    
        public void BeepTheSpeaker()
        {
            if (!_speaker.CanBeep())
            {
               _enableBeeping(_speaker);
            }
            _speaker.Beep();
        }
    }
    
    public static class MySpeakerConsoleApp
    {
        public static void Main(string[] args)
        {
            BeepWiredSpeaker();
    
            // No more try...catch needed. This can't possibly fail!
            BeepWirelessSpeaker();
        }
    
        public static BeepWiredSpeaker()
        {
            Speaker s = new Speaker();
            IBeepSpeakers wiredSpeakerBeeper =
                new IBeepSpeakers(s, s => ((Speaker)s).IsPlugged = true);
            wiredSpeakerBeeper.BeepTheSpeaker();
        }
    
        public static BeepWirelessSpeaker()
        {
            WirelessSpeaker w = new WirelessSpeaker();
            IBeepSpeakers wirelessSpeakerBeeper =
                new IBeepSpeakers(w, s => ((WiredSpeaker)s).IsTransmitterOn = true);
            wirelessSpeakerBeeper.BeepTheSpeaker();
        }
    }
    

    如您所见,我们实际上根本不需要代码合同来告诉我们扬声器是否应该发出哔哔声。不,我们让对象本身的状态来决定它是否可以发出哔哔声。

    【讨论】:

      【解决方案3】:

      如果您真的想改变这样的行为,您可能希望在基类中公开一个虚拟的“CanBeep”属性,然后为 WirelessSpeaker 实现它以返回 TransmitterIsOn。这样您仍然可以将合同放在 Speaker 中,并且 Speaker 的消费者可以知道他们是否能够满足合同要求。

      也就是说,可能与可变状态相关联的公共属性并不是合同要求的最佳选择。如果发送器在检查属性和调用方法之间中断会发生什么?我认为仔细考虑合同的含义很重要。一个很好的问题是:这是我可以在编译时静态证明的条件,还是取决于运行时条件?顺便说一句,这个问题最容易通过运行静态合约分析工具来回答。

      【讨论】:

      • 我明白了。您关于CanBeep 的论点绝对正确。但是,如果您知道我的意思,您不能总是预测将来可能会有Speaker 的子类型可能无法发出哔哔声。如果Speaker 位于无法更改的 DLL 中并且您需要能够扩展它,您将如何处理。
      • 那么,合约受静态可证明条件约束是经验法则吗?当你使用 If(x!=null) 和 Contract.Requires(x!=null) 时,我实际上是在徘徊。我想这就是静态条件的来源?
      • 实际上, 由 Speaker 的原始类开发人员来尝试预测派生类可能希望发挥作用的方式。这就是为什么正确设计和构建基类如此困难的原因。创建约束后,所有派生类都必须存在于其中,因此您需要确保对它们进行充分考虑以适应系统发展的方式。就个人而言,我几乎总是将我的类标记为密封,除非在我明确定义基类的极少数情况下。
      • @FarhadAlizadehNoori 伴随着 Dan Bryant 的评论,如果您发现无法根据需要扩展类,也可能表明发生了违反开放/封闭原则的情况。
      猜你喜欢
      • 2011-06-14
      • 1970-01-01
      • 1970-01-01
      • 2019-04-27
      • 2015-09-19
      • 1970-01-01
      • 2013-08-13
      • 1970-01-01
      • 2012-11-20
      相关资源
      最近更新 更多