Skip to content

Coding(1): 侵入式接口、反向控制、依赖注入

clarkehe edited this page Apr 21, 2016 · 1 revision

1. 侵入式接口?

没找到太权威的定义,也不清楚是谁提出来的。找了些资料,感觉说的还是“依赖”的问题。 先说下依赖问题。从C++开发经验,总结了下有两类依赖:一类是编译时依赖,一类是运行时依赖。

编译时依赖

比如自己定义的一个类,继承或引用了其他的类或接口,就要在h或cpp文件中 include其他的头文件, 不include会编译不过。这样会带来什么问题呢?
A.编译耗时。include越多,编译越慢。其中一个include有修改,那怕加了一个空格, 直接或间接包含他的CPP文件都会重新编译。见过有些项目,有专门打包机,项目有点大, 每次编译都要30分钟以上,最后还是换SSD以提升出包速度。觉得还是有include的问题没处理好。
B.要改代码。你依赖的类的类名、类名的成员名改了,接口名改了。这种情况有,程序员们都不太喜欢。
C.不用改代码,但要重新编译下。MFC的DLL中带了版本号就是这种问题。如果你依赖的类的类名、 类的对外公有接口函数名都没变,但这个类成员变量有添加或减少,而这个类又是另外一个LIB或DLL中, 这时就要重新编译下,不然在运行时,就会有问题。如果类和依赖类在一个模块中,依赖类只会重新编译再链接下就好了。 这种问题的本质是对内存布局的依赖,也是一种运行时依赖。

运行时依赖

主要是模块(如DLL)之间的依赖,一个模块对另一个模块的依赖。模块之间能互相调用,还是需要知道模块导出的类名或函数名。 一个模块导出的类名或函数名有修改,依赖他的模块得用新头文件编译下;一个模块的实现有修改,依赖他的模块也要重新编译下。 所以最理想是模块的接口不变或不依赖接口的名称,不依赖模块的实现。C++通常通过纯虑基类,来达到这种效果。Windows有COM也 是为了解决个问题,希望模块之间能减少依赖,模块复用,模块间新旧版本能兼容。

“侵入式”与“非侵入式”

下面引用两段话,是对侵入式的两种描述。感觉说的就是依赖:A模块提供了功能或接口,让B模块调用或实现,但B模块不能(编译时)依赖A模块。

编译时对“引用”的类和接口定义的依赖,我们称之为“侵入性”的;任何显式的“接口”、“基类”都是侵入性的,不可避免的带来编译期依赖;即使这些依赖很小,但依然有办法而且应该尽可能消除。”

“侵入式设计,就是设计者将框架功能“推”给客户端,而非侵入式设计,则是设计者将客户端的功能“拿”到框架中用。”

总结:

侵入式的问题在于引入依赖。模块之间的依赖会让系统变成复杂,难以管理,特别是在大型系统中。C++、Java这类OOP语言在侵入式上天生就有问题, 你继承一个类或接口就已经是侵入式了。C++可通过纯虚基类或COM,还有模板来减轻或避免; Java可通过反射来解决,但反射效率太低,且类型不安全。 GO语言在设计上就避免接口的侵入式。

JAVA接口的定义与实现

interface IFoo {  
    void Bar();  
}  

class Foo implements IFoo {   
    void Bar(){}  
}  

C++接口的定义与实现

class IFoo{
public:
virtual void Bar() = 0;
};

class Foo : public IFoo {
public void Bar() {}
};

Java与C++这种必须明确声明自己实现了个接口的方式我们称为侵入式接口。Java的interface和C++的纯虚基类,能解决一些依赖的问题,但还是侵入式, 除非申明的接口永不变化。

GO语言接口的定义与实现

type IFoo interface{
Bar()
}

type Foo struct{
}

func (Foo) Bar() {
}

在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。

参考资料
模块系统与侵入式

2. 反向控制IoC?

此外,还有也几个类似的概念:反向控制IoC、依赖注入DI、AOP。先说反向控制IoC。学习Windows编程时,听说过”Don't call us, we'll call you“,说的就是反向控制,从我们调用变成我们被调用。framework与library的区别就是:写代码给framework调用,library的代码被调用。当然这个界限并不是绝对的,使用framework时,我们有调用framework的代码,也有代码给framework调用。

反向控制有什么好处呢?降低framework与我们代码的之间依赖。framework是要扩展、升级的,如果调用framwork越少,framewok才能容易扩展,向后兼容,我们代码受的影响也越小。两个深度耦合的代码,肯定是会互相影响。还看到一个说法说,便于代码复用。意思是说,自己写的代码,没有耦合framework的代码,逻辑比较独立,可以复用。

参考资料:
Inversion of Control

3. 依赖注入DI?

为了实现一个功能或逻辑,不同模块代码肯定是要互相调用,不可能一个模块就实现了所有的功能,所以模块之前的依赖肯定是少不了,但这种依赖怎么建立呢?举个参考资料中的例子:

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example() { 
   myDatabase = new DatabaseThingie(); 
  } 

  public void DoStuff() { 
    ... 
    myDatabase.GetData(); 
    ... 
  } 
}

类Example调用了DatabaseThingie的方法GetData,可以说Example是依赖DatabaseThingie的。但这种依赖是怎么建立的呢?看Example的构造方法,是在构造时显式的实例化DatabaseThingie。这种依赖是在Example的内部建立的,不是外部注入的,不是依赖注入。

public class Example { 
  private DatabaseThingie myDatabase; 

  public Example(DatabaseThingie useThisDatabaseInstead) { 
    myDatabase = useThisDatabaseInstead; 
  }

  public void DoStuff() { 
    ... 
    myDatabase.GetData(); 
    ... 
  } 
}

上面的代码就是依赖注入的。有什么区别呢?看Example的构造方法,DatabaseThingie实例作为参数传入,不是在构造函数内实例化的。这样做,看起来也很简单,做有什么意义呢?

有一点很容易想到:Example依赖于DatabaseThingie这个具体类了,如果要使用一种不同的Database实现,Example就要修改,不便于扩展。如果没有Examle的源码,就无法修改了。最好是基于Database接口编程。

还一个原因是从单元测试角度说的:如果我们要单独测试Example模块,按照非依赖注入的写法,我们必须要有DatabaseThingie模块才能测试,因为Example在构造中是显示的实例化了DatabaseThingie,这样就达不到对Example单独测试的目的。

public class ExampleTest { 
  TestDoStuff() { 
    MockDatabase mockDatabase = new MockDatabase(); 

    // MockDatabase is a subclass of DatabaseThingie, so we can 
    // "inject" it here: 
    Example example = new Example(mockDatabase); 

    example.DoStuff(); 
    mockDatabase.AssertGetDataWasCalled(); 
  } 
}

看依赖注入的写法,DatabaseThingie实例是作为参数传给Example的,我们可以使用MOCK技术,实现一个MOCK的DatabaseThingie的实例传给Example,并不一定非要真实的DatabaseThingie模块。

参考资料
Dependency Injection Demystified
Understanding Dependency Injection

Clone this wiki locally