小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用
小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用提纲
开灯的例子选开灯做例子,是因为这个例子既常见又简单,而且潜在的需求多样。对于最简单的灯,从功能上讲,按下灯上的开关,灯就开了。 用代码实现这样一个有开关功能的灯,也是一件很容易的事情。 public class Light {
public void TurnOn() { Console.WriteLine("Light Turn On"); }
public void TurnOff() { Console.WriteLine("Light Turn Off"); }
} 代码1 一个具有开关功能的灯就完成了。这个灯,功能完备、也满足当下的需求。一切美好。 直到有一天,有个客户说,灯上的开关坏了,能不能换一个?我才意识到这个灯的设计有问题——它的开关是换不了的。一面给用户解释,一面考虑着把灯和开关分开。 咱也是学过设计模式的人,知道要面向接口编程,绝不应该简单地把Light类拆解成Light和Switcher两个类。因为Switcher不应该依赖于具体实现,于是写出了下面的代码。 namespace Me.Lighting {
public interface ILightable {
void ShowLight(); void HideLight(); } public class Light : ILightable {
public void ShowLight() { Console.WriteLine("Light Turn On"); }
public void HideLight() { Console.WriteLine("Light Turn Off"); }
} } namespace Me.Switch {
using Me.Lighting; public class Switcher {
public ILightable Light { get; set; }
public void TurnOn() { Light.ShowLight(); }
public void TurnOff() { Light.HideLight(); }
} } 代码 2 这个设计,不仅分离了灯和开关,甚至可以让这个开关灵活地控制要开关哪个灯。只要在开关前设置一下就可以,多方便。我自信满满地迁入了代码。 事实也证明这样的设计是成功的,产品的灵活设计得到了用户的认可,销量直线上升。 亲,请看下代码,在不使用什么别的设计模式的前提下,您觉得代码2有什么问题?无论是什么角度的都可以(当然,可能您的角度不是本文讨论的重点),最好先回复下留个底,别事后诸葛。 如果您一眼看到了问题,请直接阅读DIP那一节。 暗流涌动公司壮大之后 ,开始考虑向收音机行业进军。而且公司希望,这种灵活的设计可以沿用下去,收音机和灯的开关应该可以通用,对用户而言,都是拨那么一下。 我听到这个信息也是相当兴奋,但是当我开始着手写代码时,发现一些坏味道,开关依赖于ILightable 接口,那么我的收音机不得不写成这个样子才能与现有的开关兼容。 public class Radio : ILightable {
public void ShowLight() { Console.WriteLine("Play radio"); }
public void HideLight() { Console.WriteLine("Stop radio"); }
} 代码3 虽然可以工作,但是这是严重的坏味道。因为如果有一天,灯的接口变化,我却要连收音机的代码一起改。这种情况绝不应该出现。且不用把LSP(Liskov替换原则)搬出来说教,很显然Radio其实并没有完成ILightable所定义的功能——发光。无论从哪个角度讲都是错的。 一个可行的设计是,让开关支持收音机的开启和停止。像下面这样。 namespace Me.Radio {
public interface IRadio {
void Play(); void Stop(); } public class Radio : IRadio {
public void Play() { Console.WriteLine("Play radio"); }
public void Stop() { Console.WriteLine("Stop radio"); }
} } namespace Me.Switch {
using Me.Lighting; using Me.Radio; public class Switcher {
public ILightable Light { get; set; }
public IRadio Radio { get; set; }
public void TurnOn() {
if (Light != null) Light.ShowLight(); else if (Radio != null) Radio.Play(); } public void TurnOff() { Light.HideLight(); }
} } 代码4 我看来看去都觉得这个代码太恶心了,因为Switcher的实现方式违反了OCP(开放—封闭原则),如果这样发展下去,公司的产品越丰富,这坨代码就越难以维护。我的末日也就越近。 于是我的考虑Switcher的设计是不是有问题,我已经用上面向接口编程了,为什么还是有问题呢? Guru眼中的依赖我把代码发给了我的导师,一个设计Guru,他看完之后哭笑着说,你的基本功很扎实,理论知识也很全面,可惜却缺乏一定的经验。面向接口编程没有错,但是更重要的是模型的建立。 简单而言,你的开关的依赖关系错了。问你一个问题你就明白了,开关为什么要依赖ILightable呢?但是好在你有一定的设计基础,知道要提取出一个接口,所以要改成正确的设计也非常容易。你只需要把ILightable这个接口的名字改成ISwitchable,再把接口方法名字改下,并把它与Switcher放一起就行了。 听罢,我恍然大悟。原来接口的名字和位置,也会给使用者带来如此大的困扰。在先进的开发工具的帮助下,瞬间就完成了这个简单的重命名和移动操作。现在的代码像这个样子了。 namespace Me.Lighting {
using Me.Switch; public class Light : ISwitchable {
public void TurnOn() { Console.WriteLine("Light Turn On"); }
public void TurnOff() { Console.WriteLine("Light Turn Off"); }
} } namespace Me.Radio {
using Me.Switch; public class Radio : ISwitchable {
public void TurnOn() { Console.WriteLine("Play radio"); }
public void TurnOff() { Console.WriteLine("Stop radio"); }
} } namespace Me.Switch {
public interface ISwitchable {
void TurnOn(); void TurnOff(); } public class Switcher {
public ISwitchable Switchee { get; set; }
public void TurnOn() { Switchee.TurnOn(); }
public void TurnOff() { Switchee.TurnOff(); }
} } 代码5 注意:这个代码与之前有问题的代码2,只是各种名称上的变化。结构上一点儿没变。 以后有新的产品,也只需要实现ISwitchable接口,就可以支持这个开关了。之前的失败设计,看似与这个设计相差无几,但是其中蕴含的设计思想天差地远,也正是在这种地方,才更能体现出设计师间的差距。这一种设计所体现的,即是DIP(依赖倒置原则),的表现之一,接口应当被其使用者所拥有,而非其实现者。1 DIP(依赖倒置原则)具体问题解决了,还需要把整个问题抽象一下,从本质上了解一下DIP的含义。(我会尽量清楚,可能会有些啰嗦,但这比在回复里争论要舒坦得多。) 假设有如下所示的类图。假设我们要把这种关系解耦合。 图1 注:图1中的User表示使用者(调用者),而不是用户的意思。 为什么要解耦合?(编辑:安卓应用网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
