加入收藏 | 设为首页 | 会员中心 | 我要投稿 安卓应用网 (https://www.0791zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 程序设计 > 正文

小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用

发布时间:2020-05-25 02:27:52 所属栏目:程序设计 来源:互联网
导读:小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用 提纲 开灯的例子 暗流涌动 Guru眼中的依赖 DIP(依赖倒置原则) 为什么要解耦合? 接口的坏味道 同一张类图的不同解释——真假DIP 了解DIP有什么用?DIP用在什么地方? 下回预告 参考文献 开灯

小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用

提纲

  • 开灯的例子
  • 暗流涌动
  • Guru眼中的依赖
  • DIP(依赖倒置原则)
    • 为什么要解耦合?
    • 接口的坏味道
  • 同一张类图的不同解释——真假DIP
  • 了解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表示使用者(调用者),而不是用户的意思。

为什么要解耦合?

(编辑:安卓应用网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读