@BryanWatts 是对的。 OP 提供的类违反了 Liskov 替换原则。而且你不应该使用异常来控制程序流程——这也是一种代码味道。例外是指例外——不允许您的对象以预期方式运行的例外情况,这可能导致您的对象状态和/或未来行为的损坏。
您需要确保了解里氏替换原则 (LSP) 的全部内容。 LSP 并不是要确保interfaces 可以互换使用。
当一个对象从另一个对象继承时,它继承了它的所有父对象的行为。诚然,您可以覆盖该行为,但您必须小心这样做。让我们以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) 的原因。正如 Robert & Micah Martin 在 pp 上的 Agile Principles, Patterns and Practices in C# 中敏锐地指出的那样。 142-143:
LSP 清楚地表明,在 OOD 中,IS-A 关系属于可以合理假设的行为,并且客户端依赖于....[当通过其基础使用对象时类接口,用户只知道基类的前置条件和后置条件。因此,派生对象不能期望这些用户遵守比基类要求的更强大的先决条件。也就是说,用户必须接受基类可以接受的任何内容。此外,派生类必须符合基 [class] 的所有后置条件。
通过本质上为WirelessSpeaker 的Beep 方法提供前提条件TransmitterIsOn == true,您创建了一个比基础Speaker 类中存在的前提条件更强。对于WirelessSpeakers,IsPlugged 和TransmitterIsOn 必须为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 也可以“插入”;我怀疑这是否是您最初想到此对象层次结构时的意图。
那么,您将如何解决这个问题?好吧,您需要想出一个更好地与相关对象的行为保持一致的模型。我们对这些对象及其行为了解多少?
- 他们都是一种演讲者。
- 因此,无线扬声器可能是扬声器的专业化产品。相反,扬声器可能是无线扬声器的泛化。
- 在当前的对象模型中(与您发布的一样多),这两个对象之间没有太多共享的行为或状态。
- 由于两个对象之间没有太多共同的状态或行为,人们可能会争辩说这里不应该存在继承层次结构。我要和你一起扮演魔鬼的拥护者并维护继承等级。
- 它们都发出哔哔声。
- 但是,每种扬声器发出蜂鸣声的条件不同。
- 因此,这些说话者不能直接从另一个继承,否则,它们会共享可能不适合他们的行为(在这种情况下,现有的“共享行为”肯定不适用于所有类型的说话者)。这解决了 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 并要求其发出哔声的地方,都可以用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,它只有在发射器打开时才会发出哔哔声。此外,WirelessSpeakers 对被“插入”一无所知。这根本不是他们本质的一部分。
此外,遵循依赖倒置原则(DIP):
- 高级模块不应依赖于低级模块。两者都应该依赖于抽象。
- 抽象不应依赖于细节。细节应该取决于抽象。
这意味着扬声器的消费者不应直接依赖Speaker 或WirelessSpeaker,而应依赖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();
}
}
如您所见,我们实际上根本不需要代码合同来告诉我们扬声器是否应该发出哔哔声。不,我们让对象本身的状态来决定它是否可以发出哔哔声。