引用 依赖注入揭秘 :
“依赖注入”是一个 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 中所说的依赖注入。但是没有语法糖。