关于依赖倒置,控制反转和依赖注入的趣谈
昨天经过一朋友的SPACE,看到有关于控制反转的讨论,一时技痒,写下一段留言,完后由于比较长的时间没接触这几个单词,因此又去查了些资料,重新整理了一下,跟大家一起讨论。
整理之前,首先要说说“依赖”,什么是依赖,依赖就是关联,UML中定义的“关联”是最泛泛的一种关系,表现为两个类图之间有根线就有关联,我个人理解成,在C/C++中,A include了另一个头文件B,JAVA/.Net中A using了另一个package或者unit B,则两者就有了关联,A依赖于B,因为假设没有依赖关系,A为啥要include B?肯定了发生了某种调用(B.Call())或者引用(如B做为某个类变量或者参数变量)。所以偶把这个理解成依赖。
网上讨论的假设B发生变化那A也发生变化成为依赖,偶觉得可能会有一定的误导,因为假设变化的是B内部,接口不变,那A为什么要变?或者B增加一个接口A不调用,A也不用变,但是A依然依赖于B。
这是关于依赖,接下来是关于标题三个名词,在这里不想一个个去解释,因为那可能有种就事论事的感觉,想跟大家讨论一些比较本质的东西。
在面对一个复杂事务的时候,我们的处理办法是什么?毫无疑问是分解,想想那些日理万机的领导,凡事不论巨细都要过问的话那是不可能,因为个人的精力有限,而复杂度是随着规模的增大而呈数量级的变化的,所以领导怎么处理?分解,分成市场总监,财务总监,技术总监,行政总监等一个个角色,每个角色负责一块,到时候跟领导汇报各自的问题就OK了,领导来做决策。
以软件来类比的话,这里的总监就是一个个模块,汇报就是OO中的接口(Interface),领导就是框架,把全部的模块串起来完成一个既定的大目标(如实现某个方案)。那工程师或者财务人员呢?毫无疑问就是一个个具体的CLASS了,是实现具体某个功能的执行体,如一项编程工作或者整理出某个报表。
由此可见,每天在我们周围发生的这些事情,这些公司内部的一些行为都可以映射成整个软件构架。那么从公司的这种组织行为我们可以得到什么样的思考?首先有了分解我们可以降低一些的复杂度,但接下来呢,软件最复杂的在于什么?在于变化。公司也一样,外界要面临重重生存压力,内部可能会有一些公司层面的或者个人层面的问题,甚至人员跳槽也会带来一定的风险,那公司怎么处理? 隔离。把容易变化的跟不容易变化的相对稳定的隔离开来,这样就能做到控制影响到最小。那么隔离通过什么来实现?抽象。为什么?因为抽象是相对稳定的。
这里举个比较好笑的例子:一个即将做领导的儿子问曾经做领导的父亲怎么才能平步青云,父亲说你不能说假话,因为老百姓会不答应,也不能说实话,因为领导会对你有意见,儿子思索良久问:那我该说什么? 父亲意味深长的说:空话
笑话不但好笑,还能反映一定的道理,为啥空话这么有用,因为他是抽象的,而抽象不涉及到一些具体的数据或者事务,因此他是稳定的,是不容易变化的,同时基本上也是正确的。所以我们经常在公开场合听到类似的话“我们要团结同志,努力进取,提高工作效率,降低工作成本。。。”你能说这是错的吗,当然不能,所以既然是非常稳定的对的,所以这种依赖就很可靠啦。
公司也一样,假设领导依赖于工程师的能力,成天问这个问那个,那如果有一天工程师跳槽了怎么办?领导处理的都是一些非常重要宏观的事务,不可能因为某个小小的工程师而产生什么大的影响,因此他不会直接依赖工程师,而是依赖于一个叫技术总监这样的一个抽象。如果工程师离职,那换个工程师继续替代他以前做的事情就行了,而且都实现的是技术总监规定好的接口,这样对领导就没什么影响了,也只有这样变化就被隔离开来了。
再反过来想想软件,其实上面描述的都是一些对软件而言非常有意义的做法,OO中非常重要的一点就是模块之间依赖于抽象的接口,而不是具体的实现,为啥,因为抽象是相对稳定的。一个IO他必然就有READ/WRITE这两个抽象,至于具体是磁盘还是键盘,那是下面的实现不同了,通过这种构架,能保持软件的弹性与可维护性。
由公司的行为还有一点容易受到启发的就是公司的组织构架,公司的层次可以映射成软件的分层,领导是框架层,下面通过一个个接口去管控一个个CLASS。我们设计软件的时候毫无疑问也应该这样。设计好框架设计接口,设计好接口再去调用一个个的API或者CLASS去实现某一个具体的实现比如数据库的读写或者SOCKET数据的收发。每个地方有自己相对对立的职责,各尽其职。如果上来就是一个个API,那相当于一大群工程师既做商务谈判,又去编码,那就杂乱无章啦。这样的结果感觉都是一样:混乱。
最后言归正传,谈谈上面的三个名词,依赖倒置DIP(Dependency Inversion Principle)在马丁大叔的杰作《敏捷开发:原则,实践与模式》中描述的比较清楚,高层模块不应该依赖于底层模块,而应该两者都依赖于一个抽象,底层模块实现抽象。相信这点已经在上面讨论的比较清楚了,就好比尽管领导归根结底要依赖工程师来做事,但他不会直接依赖你,而是依赖于一个总监的抽象。这就是倒置,哪里倒了?这种依赖关系倒了,引入了一个新的中间层,一个抽象。以前传统的过程设计中是从上到下的一条依赖线,现在是平的一条领导到总监,然后是一条从下往上的工程师到总监的关联线。
控制反转(Inversion of Control)其实有点类似,主要是OO中提出了框架的概念,什么是框架,按王氏兄弟的《道法自然》中的描述,主要是一个动态的环境体,可以处理一些比较复杂的事务或者逻辑,跟类库静态的行为不大一样,类库都是一个个等待别人调用的服务。 那么控制体现在什么地方?传统的做法,我们在自己的程序中调用一个个类库去完成一个个功能,类库是我们的执行体,但是逻辑算法等是自己实现的,这就是自己的控制,但框架不一样,逻辑算法都是它来实现,我们只要提供它几个需要的接口或者执行体就可。这就是反转,控制在框架这边了,主要是简化了一定的工作量,对于一些常见的业务场景不需要自己去重复实现罢了。那么控制反转主要应用的是模板方法等模式,个人觉得观察者模式是一个比较典型的控制发转的实例,因为论询observer是subject这边实现的一个逻辑,而observer 只要实现notify这样一个接口即可。所以其实也没什么。
依赖注入(Dependency Injection)其实相比以上两者应该不是一个层次的概念,主要是表现为利用构造函数或者接口来完成具体的工作类的绑定而已,这么想好了,比如要完成一个开发工作,可以让总监自己去指定工程师甲来完成这个工作,但为了更大的弹性,可以让总监提供这么个接口
AssignJob(Engineer engineer) {engineer.do();}
这样总监可以默认让甲去完成这个工作,但如果某种原因例如甲请假,那么就可以通过这个接口去让乙去做这件事情,这样在发生变化的时候就有弹性了。顺便提一下,软件多数通过CONFIG来达到更大的灵活性,由于.NET/JAVA等引入了反射的概念,可以在RUNTIME期间动态的去创建类,这个功能就很强大了,参考如上的业务需求,我们就可以把类名放在CONFIG中,通过反射去加载不同的类从而完成不同的实现。这也是很多框架(如structs)的最基本的做法
以上是一些个人看法,感觉一些OO的名词听起来玄乎,其实也就那么回事,只要掌握好OO的几个基本原则,其实基本上都可以推导出来。呵呵。个人愚见,希望跟大家交流