依赖注入
依赖注入是一种解耦的有效方式,它可以将类的创建和使用分离,从而降低类与类之间的耦合度。(高耦合表示代码逻辑紧密,难以从外部进行修改,但并不意味着代码不好)
下面是一段没有使用依赖注入的代码,打印机根据news
和color
进行打印,User
需要使用(依赖)Printer
进行打印报纸。
class Printer:
def __init__(self, color):
self.color = color
def do_print(self, news):
print('print %s in %s' % (news, self.color))
class User:
def __init__(self, news, printer_color):
self.printer = Printer(printer_color)
self.news = news
def print_newspaper(self):
self.printer.do_print(self.news)
user = User('hello world', 'red')
打印机对象是在User
对象内部进行创建的,这说明打印报纸的逻辑被耦合在了User
内部。而依赖注入则将被依赖对象的创建,放在了外部,从而降低了耦合度。
class User:
def __init__(self, news, printer):
self.printer = printer
self.news = news
def print_newspaper(self):
self.printer.do_print(self.news)
printer = Printer('red')
user = User('hello world', printer)
只要是被依赖对象(Printer
)的创建,脱离了使用者(User
),都可以被认为是依赖注入。依赖注入的方式有很多种,上面的例子是构造函数注入,还有属性注入,接口注入等。
# 属性注入
printer = Printer('blue')
# 接口注入
class User:
def __init__(self, news):
self.news = news
self.printer = None
def print_newspaper(self, printer):
printer.do_print(self.news)
def set_printer(self, printer):
self.printer = printer
user = User('hello world')
printer = Printer('blue')
user.set_printer(printer) # 接口注入
依赖注入的好处
使用依赖注入对测试而言非常方便,因为依赖注入可以将被依赖对象的创建放在外部,从而可以在测试中使用mock对象来替换被依赖对象。比如下面的例子,在测试过程中,不需要修改User
内部的逻辑,只需要将打印机替换成DebugPrinter
即可。
class DebugPrinter:
def __init__(self, color):
self.color = color
def do_print(self, news):
print("debug start ...")
print('print %s in %s' % (news, self.color))
print('debug end ...')
printer = DebugPrinter('red')
user = User('hello world', printer)
C语言中进行依赖注入
从上面的描述中可以发现,依赖注入似乎是和类相关的,但是C语言中并没有类的概念,那么C语言中如何进行依赖注入呢?可以通过结构体函数指针的方式进行依赖注入,下面是一个例子。
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int a;
int b;
int c;
} data;
typedef void (*print_callback)(data* nums);
typedef int (*sum_callback)(data* nums);
// 依赖结构体
typedef struct {
print_callback print;
sum_callback sum;
} data_handler;
data_handler
init_handler(print_callback print, sum_callback sum)
{
data_handler handler;
handler.print = print;
handler.sum = sum;
return handler;
}
void
task(data* nums, data_handler* handler)
{
handler->print(nums);
printf("sum: %d\n", handler->sum(nums));
}
通过data_handler
将函数逻辑进行抽象,从而降低了task
函数与print
和sum
函数的耦合度。
void
print(data* nums)
{
printf("a: %d, b: %d, c: %d\n", nums->a, nums->b, nums->c);
}
int
sum(data* nums)
{
return nums->a + nums->b + nums->c;
}
int
debug_sum(data* nums)
{
printf("debug -> a: %d\n", nums->a);
printf("debug -> b: %d\n", nums->b);
printf("debug -> c: %d\n", nums->c);
return nums->a + nums->b + nums->c;
}
int
main()
{
data_handler handler = init_handler(print, sum);
data nums = { 1, 2, 3 };
task(&nums, &handler);
// 不需要改变task函数的逻辑,只需要将handler替换成debug_handler即可
data_handler debug_handler = init_handler(print, debug_sum);
task(&nums, &debug_handler);
return 0;
}
依赖注入的本质是将逻辑进行抽象,从而降低耦合度,使得代码更加灵活,更加容易测试,同时也更容易进行维护。与依赖控制紧密相关的一个概念是控制反转,即将所依赖对象的控制权交给外部。从上面C语言的例子可以很容易发现,task
的行为是由handler
决定的,而handler
是由外部进行创建的,这就是控制反转。