目前代码契约不允许派生类中成员的前提条件,其中成员已经在基类中设置了前置条件(我实际上当前得到警告而不是错误)。我不明白这背后的逻辑。我理解它与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
的方法时,无线扬声器可能无法发出蜂鸣声。但这不是我们使用代码合同的原因吗?确保满足要求?
答案 0 :(得分:9)
代码合同不是要求的满足,而是它们的通信。 Speaker.Beep
的呼叫者受合同约束,该合同仅在某些情况下生效。
WirelessSpeaker
缩小 Speaker
的功能空间 - Liskov发挥作用的地方。如果我知道它的无线,我只能有效地使用该特定Speaker
。在这种情况下,我应明确接受WirelessSpeaker
,而不是Speaker
,并避免替换问题。
修改以回应评论:
WirelessSpeaker
的作者选择如何解释Beep
命令。选择一个新的合同,在此级别可见但不在基级,可以使用Speaker
时约100%的约束条件。
如果在发射器未开启时根本不发出哔哔声,我们就不会谈论代码合同。他们的目的不是在运行时进行通信,而是在设计时,调用的语义(而不仅仅是语法)。
在运行时发生异常的事实,最终阻止了"错误的"电话,在这里基本上无关紧要。
答案 1 :(得分:3)
@BryanWatts是对的。 OP提出的类别违反了Liskov替代原则。并且您不应该使用异常来控制程序流 - 这也是代码的味道。例外情况是指例外 - 异常情况,不允许您的对象以预期的方式行事,这可能导致您的对象的状态和/或未来行为的破坏。
您需要确保了解 Liskov替换原则(LSP)的总体情况。 LSP不是要确保interface
可以互换使用。
当一个对象继承自另一个对象时,它继承了它的所有父对象的行为。没错,你可以覆盖这种行为,但你必须小心这样做。让我们使用您的Speaker
和WirelessSpeaker
示例,看看它们是如何崩溃的。
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)的方式。罗伯特& Micah Martin敏锐地指出Agile Principles, Patterns and Practices in C# pp。 142-143 强>:
LSP明确指出,在OOD中,IS-A关系属于可以合理假设的行为,并且客户端依赖于...... [W]通过其基础使用对象类接口,用户只知道基类的前提条件和后置条件。因此,派生对象不能指望这样的用户遵守比基类所要求的更强的前提条件。也就是说,用户必须接受基类可以接受的任何内容。此外,派生类必须符合base [class]的所有后置条件。
基本上为TransmitterIsOn == true
的{{1}}方法设置前提条件Beep
,您创建了更强大的前提条件,而不是基础{{1}上存在的前提条件} .class。对于WirelessSpeaker
,Speaker
和WirelessSpeaker
必须为IsPlugged
,以使TransmitterIsOn
按预期行事(从true
查看时Beep
)的视角,即使Speaker
本身没有Speaker
的概念。
另外,您违反了另一个SOLID原则,接口隔离原则(ISP):
客户不应该被迫依赖他们不使用的方法。
在这种情况下,TransmitterIsOn
不需要插入。(我假设我们在这里讨论的是音频输入连接,而不是电气连接。)因此,WirelessSpeaker
不应该有任何名为WirelessSpeaker
的属性,但是,因为它继承自IsPlugged
,它确实存在!这表明您的对象模型与您打算使用对象的方式不一致。再次注意,大多数讨论都围绕着对象的行为,而不是彼此之间的关系。
此外,违反LSP和ISP的行为都表明可能违反了开放/封闭原则(OCP):
软件实体(类,模块,函数等)应该是可以扩展的,但是关闭以进行修改。
因此,在这一点上,现在应该清楚的是,我们不仅仅使用代码约定来确保在调用对象上的方法时满足某些先决条件。不,而代码合同用于声明保证(因此单词合同)关于行为和状态您的对象及其方法基于所述的前后条件,以及您可能已定义的任何不变量。
因此,对于您的演讲者课程,您所说的是:如果插入扬声器,然后扬声器就会发出哔声。好的,到目前为止,这么好;这很简单。现在,Speaker
类呢?
好吧,WirelessSpeaker
继承自WirelessSpeaker
。因此,Speaker
还具有WirelessSpeaker
布尔属性。此外,因为它继承自IsPlugged
,所以为了使Speaker
发出哔哔声, 必须也具有WirelessSpeaker
属性设为IsPlugged
。 "但是等等!"你说,"我已经覆盖true
的实施,以便Beep
的发射机必须开启。"是的,这是真的。但它也必须必须 插入! WirelessSpeaker
不仅继承了Beep方法,还继承了其父类实现的行为! (考虑使用基类引用来代替派生类。)因为父类可以插入",所以也可以WirelessSpeaker
;我怀疑这是你最初想到这个对象层次结构时的意图。
那么,你会如何解决这个问题?那么,您需要提出一个更好地与所讨论对象的行为保持一致的模型。我们对这些物体及其行为有何了解?
好的,所以这些扬声器的共享行为是他们发出的嘟嘟声。因此,让我们将这种行为抽象为抽象基类:
WirelessSpeaker
大!现在我们有一个代表发言者的抽象基类。当且仅当// 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()
时才会从发言者发出蜂鸣声。这也解决了LSP违规问题!在true
可以使用并呼叫蜂鸣声的任何地方,可以替换SpeakerBase
或Speaker
,我们可以确定行为:如果说话者可以发出哔声,它将会
现在剩下的就是从WirelessSpeaker
派生我们的每种演讲者类型:
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
对于被插入"一无所知。它根本不是其本质的一部分。
此外,遵循依赖性倒置原则(DIP):
- 高级模块不应依赖于低级模块。两者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应取决于抽象。
醇>
这意味着发言人的消费者不应直接直接 WirelessSpeaker
或Speaker
,而应取决于WirelessSpeaker
。这样,无论出现什么类型的发言者,如果它继承自SpeakerBase
,我们知道如果条件保证由依赖类中的抽象类型引用的子类型的说话者,我们可以发出嘟嘟声。 。这也意味着SpeakerBase
不再知道如何将说话者置于状态以使其发出哔哔声,因为IBeepSpeakers
可以用来做出这样的决定的说话者类型之间没有共同的行为。因此,必须将该行为作为依赖项传递给IBeepSpeakers
。 (这是一个可选的依赖项;你可以让这个类接受IBeepSpeakers
并调用SpeakerBase
,如果Beep()
对象处于正确的状态,它就可以了发出哔哔声,否则就不会。)
SpeakerBase
正如您所看到的,我们实际上并不需要代码合同来告诉我们说话者是否应该发出哔哔声。不,我们让对象本身的状态决定它是否能发出哔哔声。
答案 2 :(得分:1)
如果你真的想要在这样的行为上出现差异,你可能想要公开一个虚拟的' CanBeep'基类中的属性,然后为WirelessSpeaker实现它以返回TransmitterIsOn。通过这种方式,您仍然可以将合同放在发言人中,而发言人的消费者可以知道他们是否符合合同要求。
尽管如此,可能与可变状态挂钩的公共财产并不是合同要求的最佳选择。如果发送器在检查属性和调用方法之间发生了什么?我认为仔细考虑合同的含义很重要。一个很好的问题是:这是一个条件,我可以在编译时静态证明,还是可以依赖于运行时条件?顺便提一下,通过运行静态契约分析工具可以很容易地回答这个问题。