依赖注入

关注微信公众号塔容万物

依赖注入

依赖注入是一种解耦的有效方式,它可以将类的创建和使用分离,从而降低类与类之间的耦合度。(高耦合表示代码逻辑紧密,难以从外部进行修改,但并不意味着代码不好)

下面是一段没有使用依赖注入的代码,打印机根据newscolor进行打印,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函数与printsum函数的耦合度。


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是由外部进行创建的,这就是控制反转。