2014-11-05 102 views
4

目前,代码协定不允许在派生类在成员已经有一个前提条件在基类成员集合先决条件(其实我目前得到一个警告,而不是错误)。我不明白这背后的逻辑。我知道这与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(); 
    } 
} 

你可能认为这个类层次里氏打破的规则,因为无线扬声器可能无法传递给期待Speaker方法时发出蜂鸣声。但是,这不就是我们为什么使用代码合同吗?确保满足要求?

回答

8

代码合同不是要求的会议,但他们通信Speaker.Beep的调用者受限于仅在某些情况下才生效的合同。

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

编辑响应评论:

WirelessSpeaker作者选择如何解释Beep命令。选择一个新的合同,在这个级别可见,但不在基级,在使用Speaker时,强制约束100%的时间应用<。

如果是当发射机不上根本就没有发出蜂鸣声,我们不会谈论代码契约。他们的意图不是在运行时进行通信,而是在设计时,调用的语义(不仅仅是其语法)语义

,一个异常发生在运行时,最终防止“不正确”的呼叫事实上,在很大程度上是这里无关紧要。

+0

我明白了。但是,如果我可能会争论从'扬声器'推导'WirelessSpeaker'。如果我是一个类库开发人员,并且想要承担最少的输入参数,并且有一个方法可以获取'List '并在它们全部调用'Beep'。我真的很在乎这些扬声器是否真的发出嘟嘟声?是不是应该由应用程序开发人员来确保他们正确地构造一个扬声器,以便它可以发出嘟嘟声?我说的是即使说话者不能发出嘟嘟声,这就是为什么我们有异常处理机制来处理特殊情况。沟通(req)已经在合同中。 – 2014-11-05 20:01:43

+0

如果这仍然是不正确的。你能说出一个人应该如何扩展'Speaker'的功能吗?请记住,您无法真正预测将来可能添加的所有功能,并且您希望扬声器的任何子类型都能够用该签名发出提示音。 – 2014-11-05 20:02:51

+0

@FarhadAlizadehNoori:编辑 – 2014-11-05 20:47:18

1

如果您确实想要在这种行为中产生差异,您可能需要在基类中公开虚拟“CanBeep”属性,然后将其实施为WirelessSpeaker以返回TransmitterIsOn。通过这种方式,您仍然可以将您的合同交给扬声器,扬声器的消费者有办法知道他们是否能够满足合同要求。

这就是说,有可能被捆绑可变状态的公共属性是不是合同要求的理想选择。如果发射机在检查房屋和调用方法之间关闭,会发生什么?我认为仔细考虑合同的意义是很重要的。一个很好的问题是:这是一个可以在编译时静态证明的条件,还是可以依赖运行时条件?顺便提一句,通过运行静态合约分析工具,最容易回答这个问题。

+0

我明白了。你的关于'CanBeep'的论点绝对是真的。然而,如果你知道我的意思,但是你不可能总是预测将来可能会有'Speaker'的子类型可能无法发出哔哔声。如果'Speaker'处于无法更改的DLL中,并且您需要扩展它,那么您将如何处理该问题。 – 2014-11-05 20:06:39

+0

那么,契约被约束于静态可证明条件的经验法则呢?当你使用If(x!= null)和Contract.Requires(x!= null)的时候,我实际上正在游荡。我想这里是静态条件进来的地方? – 2014-11-05 20:08:03

+2

实际上,它*就是讲话者的原始课程开发者,试图预测派生类可能想要的功能。这就是为什么正确设计和构建基类很困难的原因。一旦创建了约束条件,所有派生类都必须存在于其中,因此您需要确保它们充分考虑到了系统将要发展的方式。就我个人而言,除非在极少数情况下明确定义基类时,我几乎总是将我的类标记为密封的。 – 2014-11-05 20:25:54

2

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

您需要确保您了解Liskov替代原则(LSP)的总体情况。 LSP不能确保interface可以互换使用。

当一个对象从另一个对象继承时,它继承了它的所有父对象的行为。确实,你可以用覆盖这种行为,但是你必须小心这样做。让我们用你的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; 
    } 

    pubic 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(); 
} 

这是你的代码是如何打破里氏替换原则(LSP)。正如罗伯特·&米卡·马丁敏锐的Agile Principles, Patterns and Practices in C#指出在第142-143

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

基本上通过具有前提TransmitterIsOn == trueWirelessSpeakerBeep方法,创建比母Speaker类存在什么更强前提。对于WirelessSpeaker S,两者IsPluggedTransmitterIsOn必须true为了Beep表现为预期(从Speaker的角度来看时),即使在其本身和Speaker没有的TransmitterIsOn概念。

而且,你违反了又一坚实的原则,接口隔离原则(ISP)

客户端不应该被迫依赖于它们不使用的方法。

在这种情况下,WirelessSpeaker不需要接电源。(我假设我们正在谈论的音频输入连接在这里,而不是电气连接。)因此,WirelessSpeaker不应该有任何属性称为IsPlugged,但是,因为它继承自Speaker,它确实!这表明你的对象模型不符合你打算如何使用你的对象。再一次地,请注意,这些讨论大部分集中在对象的行为上,而不是它们彼此之间的关系。

而且,无论LSP和ISP的违反两条信号,有可能是已经违反了开/闭原则(OCP)的,太:

软件实体(类,模块,功能,等)应该开放延期,但关闭进行修改。

因此,现在应该清楚的是,我们现在不应该只使用Code Contracts来确保在对对象调用方法时满足某些先决条件。不,不是代码契约是用来说明担保(因此这个词合同)有关行为状态的对象,并根据所陈述的前置和后置条件下的方法,以及任何的你也可能定义了不变量。

因此,对于您的音箱类,你的意思是:如果扬声器是否插好,然后扬声器可以发出蜂鸣声。好吧,到目前为止,这么好,这很简单。现在,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时,此抽象类将允许扬声器发出哔哔声。而且这种方法是抽象的,所以任何继承此类的类都必须为提供它们自己的逻辑。通过创建这个抽象基类,当且仅当CanBeep()返回true时,我们已启用任何对SpeakerBase类具有依赖关系的类从扬声器发出嘟嘟声。这也解决了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只能如果发射器已打开,则发出哔哔声。另外,WirelessSpeaker对“被插入”一无所知。这根本不是他们本质的一部分。

此外,以下的依赖性倒置原则(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; 
    } 

    pubic 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(); 
    } 
} 

正如你所看到的,我们实际上并不需要代码契约都告诉我们扬声器是否应该发出哔哔声。不,我们让对象本身的状态决定它是否可以发出嘟嘟声。