-
Notifications
You must be signed in to change notification settings - Fork 42
Coding(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语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。
参考资料
模块系统与侵入式
此外,还有也几个类似的概念:反向控制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
为了实现一个功能或逻辑,不同模块代码肯定是要互相调用,不可能一个模块就实现了所有的功能,所以模块之前的依赖肯定是少不了,但这种依赖怎么建立呢?举个参考资料中的例子:
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