依赖注入:函数组合之上的语法糖

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

引用 依赖注入揭秘


“依赖注入”是一个 5 美分概念的 25 美元术语。

*詹姆斯海岸,2006 年 3 月 22 日

依赖注入,尽管它在编写可测试、可组合和结构良好的应用程序时很重要,但仅意味着拥有带有构造函数的对象。在这篇文章中,我想向您展示依赖注入基本上只是一种隐藏 函数柯里化 和组合的语法糖。别担心,我们会慢慢解释为什么这两个概念非常相似。

设置器、注解和构造器

Spring bean 或 EJB 是一个 Java 对象。然而,如果你仔细观察,大多数 bean 在创建后实际上是无状态的。在 Spring bean 上调用方法很少会修改该 bean 的状态。大多数时候,bean 只是一组在类似上下文中工作的过程的方便命名空间。我们在调用 invoice() 时不修改 CustomerService 的状态,我们只是委托给另一个对象,该对象最终将调用数据库或 Web 服务。这离面向对象编程(我 在这里 讨论的)已经很远了。所以本质上我们在命名空间的多级层次结构中有过程(稍后我们将进入函数):它们所属的包和类。通常这些过程调用其他过程。你可能会说它们在 bean 的依赖项上调用方法,但我们已经知道 bean 是一个谎言,这些只是过程组。

话虽如此,让我们看看如何配置 bean。在我的职业生涯中,我遇到过 setter(以及大量的 XML 格式的 <property name="..."> )、字段上的 @Autowired 以及最后的构造函数注入。另请参阅: 为什么应优先使用构造函数注入? .所以我们通常拥有的是一个对其依赖项具有不可变引用的对象:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

获取包含银行对帐单的文件,将每一行解析为 Payment 对象并存储它。尽可能无聊。现在让我们重构一下。首先,我希望您意识到面向对象编程是一个谎言。不是因为它只是命名空间中的一堆过程,即所谓的类(我希望你不是这样编写软件的)。但是因为对象是作为带有隐式 this 参数的过程实现的,所以当您看到: this.database.insert(payment) 它实际上被编译成这样的东西: Database.insert(this.database, payment) 。不相信我?


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

好吧,如果你是正常的,这对你来说没有任何证据,所以让我解释一下。 aload_0 (代表 this )后跟 getfield #2 this.database 推入操作数堆栈。 aload_1 推送第一个方法参数 ( Payment ),最后 invokevirtual 调用 过程 Database.insert (这里涉及一些多态性,与此上下文无关)。所以我们实际上调用了双参数过程,其中第一个参数由编译器自动填充并命名为... this 。在被调用方, this 是有效的并指向 Database 实例。

忘记对象

让我们把所有这些都说得更明确一点,忘掉对象:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

太疯狂了!请注意, importFile 过程 现在位于 PaymentProcessor 之外,我实际上将其重命名为 ImportDependencies (请原谅字段 public 修饰符)。 importFile 可以是 static ,因为所有依赖项都在 thiz 容器中显式给出,而不是隐式使用 this 和实例变量 - 并且可以在任何地方实现。实际上,我们只是重构了编译期间幕后已经发生的事情。在这个阶段,您可能想知道为什么我们需要一个额外的容器来存放依赖项而不是直接传递它们。当然,这是毫无意义的:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

实际上有些人更喜欢像上面那样将依赖项显式传递给业务方法,但这不是重点。这只是转型的又一步。

柯里化

对于下一步,我们需要将函数重写为 Scala:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

功能上是等价的,就不多说了。请注意 importFile() 如何属于 object ,因此它有点类似于 Java 中单例上的 static 方法。接下来我们将对 参数进行分组


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

这一切都不同了。现在你可以一直提供所有的依赖关系,或者更好的是,只做一次:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

上面的行实际上可以是容器设置的一部分,我们将所有依赖项绑定在一起。设置后我们可以使用

importFileFun

任何地方,对其他依赖项一无所知。我们只有一个函数 (Path) => Unit ,就像一开始的 paymentProcessor.importFile(path) 一样。

一路向下的功能

我们仍然使用对象作为依赖,但是如果你仔细看,我们既不需要 parser 也不需要 storage 。我们真正需要的是一个可以解析的 函数 parser.toPayment )和一个可以存储的 函数 storage.save )。让我们再次重构:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

当然我们可以用 Java 8 和 lambdas 做同样的事情,但是语法更冗长。我们可以提供任何用于解析和存储的函数,例如在测试中我们可以轻松创建存根。哦,顺便说一句,我们只是从面向对象的 Java 转变为函数组合,根本没有对象。当然还有副作用,例如加载文件和存储,但让我们就这样吧。或者,为了使依赖注入和函数组合之间的相似性更加显着,请查看 Haskell 中的等效程序:


 @Component
class PaymentProcessor {
private final Parser parser;
private final Storage storage;

@Autowired
public PaymentProcessor(Parser parser, Storage storage) {
    this.parser = parser;
    this.storage = storage;
}

void importFile(Path statementFile) throws IOException {
    Files.lines(statementFile)
            .map(parser::toPayment)
            .forEach(storage::save);
}

}

@Component class Parser { Payment toPayment(String line) { //om-nom-nom... } }

@Component class Storage {

private final Database database;

@Autowired
public Storage(Database database) {
    this.database = database;
}

public UUID save(Payment payment) {
    return this.database.insert(payment);
}

}

class Payment { //... }

首先,需要 IO monad 来管理副作用。但是您是否看到 importFile 高阶函数如何采用三个参数,但我们可以只提供两个参数并获得 simpleImport ?这就是我们在 Spring 或 EJB 中所说的依赖注入。但是没有语法糖。