@BryanWatts 是对的。OP 提供的类违反了 Liskov 替换原则。而且你不应该使用异常来控制程序流程——这也是一种代码味道。异常意味着,好吧,异常 - 不允许您的对象以预期方式运行的异常情况,这可能导致您的对象状态和/或未来行为的损坏。
您需要确保您了解Liskov 替换原则 (LSP)的全部内容。LSP 并不是要确保interface
s 可以互换使用。
当一个对象从另一个对象继承时,它继承了它的所有父对象的行为。诚然,您可以覆盖该行为,但您必须小心这样做。让我们使用您的 aSpeaker
和 a示例,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)的方式。正如 Robert & Micah Martin在第 142-143 页的 C# 中的敏捷原则、模式和实践中敏锐地指出的那样:
LSP 明确指出,在 OOD 中,IS-A 关系属于可以合理假设的行为,并且客户端依赖于....[当通过其基类接口使用对象时,用户只知道先决条件和基类的后置条件。因此,派生对象不能期望这些用户遵守比基类要求的更强大的先决条件。也就是说,用户必须接受基类可以接受的任何内容。此外,派生类必须符合基 [类] 的所有后置条件。
通过本质上具有 的方法的前提条件TransmitterIsOn == true
,Beep
您WirelessSpeaker
创建了比基类中存在的更强大的前提条件。Speaker
对于WirelessSpeaker
s,两者IsPlugged
和都TransmitterIsOn
必须是true
为了Beep
使行为符合预期(从 a 的角度来看Speaker
),即使 aSpeaker
本身并没有 的概念TransmitterIsOn
。
此外,您违反了另一个 SOLID 原则,即接口隔离原则 (ISP):
不应强迫客户依赖他们不使用的方法。
在这种情况下, aWirelessSpeaker
不需要插入。(我假设我们在这里谈论的是音频输入连接,而不是电气连接。)因此,WirelessSpeaker
应该没有任何名为 的属性IsPlugged
,因为它继承自Speaker
,确实如此!这表明您的对象模型与您打算使用对象的方式不一致。再次注意,大部分讨论都集中在对象的行为上,而不是它们之间的关系。
此外,违反 LSP 和 ISP 都表明可能也违反了开放/封闭原则 (OCP):
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
因此,现在应该清楚的是,我们不使用代码契约只是为了确保在调用对象上的方法时满足某些先决条件。不,相反,代码合同用于根据所述的前置和后置条件以及您可能还定义的任何不变量来声明关于您的对象的行为和状态及其方法的保证(因此使用了这个词contract) 。
因此,对于您的扬声器类,您要说的是:如果扬声器已插入,那么扬声器会发出哔哔声。好的,到目前为止,一切都很好;这很简单。现在,WirelessSpeaker
课堂呢?
嗯,WirelessSpeaker
继承自Speaker
. 因此,WirelessSpeaker
也具有IsPlugged
布尔属性。此外,因为它继承自Speaker
,所以为了WirelessSpeaker
发出哔哔声,它还必须将其IsPlugged
属性设置为true
。“可是等等!” 你说,“我已经覆盖了发射器必须打开的实现。Beep
” WirelessSpeaker
是的,这是真的。但它也必须插入!WirelessSpeaker
不仅继承了Beep方法,还继承了其父类实现的行为!(考虑何时使用基类引用代替派生类。)由于父类可以“插入”,因此也可以WirelessSpeaker
; 我怀疑这是您最初想到此对象层次结构时的意图。
那么,你将如何解决这个问题?好吧,您需要想出一个更好地与相关对象的行为保持一致的模型。我们对这些物体及其行为了解多少?
- 他们都是一种演讲者。
- 因此,无线扬声器可能是扬声器的专业化。相反,扬声器可以是无线扬声器的一般化。
- 在当前的对象模型中(与您发布的一样多),这两个对象之间没有太多共享的行为或状态。
- 由于两个对象之间没有太多共同的状态或行为,人们可能会争辩说这里不应该存在继承层次结构。我将和你一起扮演魔鬼的拥护者并维持继承等级。
- 他们都发出哔哔声。
- 但是,每种扬声器发出蜂鸣声的条件不同。
- 因此,这些说话者不能直接从另一个继承一个,否则,他们会共享可能不适合他们的行为(在这种情况下,现有的“共享行为”肯定不适用于所有类型的说话者)。这解决了 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
从扬声器发出哔声。这也解决了 LSP 违规!任何可以使用 a 并要求发出哔声的地方,都可以用 a或 a代替,我们可以确定其行为:如果扬声器可以发出哔声,它就会发出哔声。CanBeep()
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
只有在发射器打开时才会发出哔声的声音。此外,WirelessSpeaker
s 对被“插入”一无所知。这根本不是他们本质的一部分。
此外,遵循依赖倒置原则(DIP):
- 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
- 抽象不应依赖于细节。细节应该取决于抽象。
这意味着扬声器的消费者不应直接依赖于Speaker
或WirelessSpeaker
,而应依赖于SpeakerBase
。这样,无论出现什么样的说话者,如果它继承自SpeakerBase
,我们知道如果条件允许,我们可以在依赖类中抽象类型引用的说话者子类型的情况下发出声音。这也意味着IBeepSpeakers
不再知道如何将扬声器置于可以发出哔声的状态,因为扬声器类型之间没有共同的行为IBeepSpeakers
可以用来做出这样的决定。因此,该行为必须作为依赖项传递给IBeepSpeakers
. (这是一个可选依赖项;您可以让类接受 aSpeakerBase
并调用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();
}
}
如您所见,我们实际上根本不需要代码合同来告诉我们扬声器是否应该发出哔哔声。不,我们让对象本身的状态决定它是否可以发出哔哔声。