@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;
}
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 == true
为WirelessSpeaker
的Beep
方法,创建比母Speaker
类存在什么更强前提。对于WirelessSpeaker
S,两者IsPlugged
和TransmitterIsOn
必须是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
;我怀疑这是你最初想到这个对象层次结构时的意图。
那么,你会如何解决这个问题?那么,你需要提出一个更好地符合所讨论对象行为的模型。我们对这些物体以及他们的行为了解多少?
- 他们都是一种扬声器。
- 因此,潜在地,无线扬声器可能是扬声器的专长。相反,扬声器可能是无线扬声器的推广。
- 在当前的对象模型中(与您发布的一样多),这两个对象之间共享的行为或状态并不多。
- 由于两个对象之间没有太多共同的状态或行为,所以可以在这里指出不应该有继承层次结构。我会和你一起扮演魔鬼的拥护者并维护这个无情的等级制度。
- 它们都发出哔哔声。
- 但是,每种扬声器发出嘟嘟声的条件不同。
- 因此,这些发言者不能直接继承另一个发言者,否则他们会分享可能不适合他们的行为(并且如果现有的“共享行为”绝对不适合所有类型的发言者) 。这解决了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
并呼吁发出嘟嘟声,可以用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
对“被插入”一无所知。这根本不是他们本质的一部分。
此外,以下的依赖性倒置原则(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;
}
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();
}
}
正如你所看到的,我们实际上并不需要代码契约都告诉我们扬声器是否应该发出哔哔声。不,我们让对象本身的状态决定它是否可以发出嘟嘟声。
我明白了。但是,如果我可能会争论从'扬声器'推导'WirelessSpeaker'。如果我是一个类库开发人员,并且想要承担最少的输入参数,并且有一个方法可以获取'List'并在它们全部调用'Beep'。我真的很在乎这些扬声器是否真的发出嘟嘟声?是不是应该由应用程序开发人员来确保他们正确地构造一个扬声器,以便它可以发出嘟嘟声?我说的是即使说话者不能发出嘟嘟声,这就是为什么我们有异常处理机制来处理特殊情况。沟通(req)已经在合同中。 –
2014-11-05 20:01:43
如果这仍然是不正确的。你能说出一个人应该如何扩展'Speaker'的功能吗?请记住,您无法真正预测将来可能添加的所有功能,并且您希望扬声器的任何子类型都能够用该签名发出提示音。 – 2014-11-05 20:02:51
@FarhadAlizadehNoori:编辑 – 2014-11-05 20:47:18