引用依赖注入揭开神秘面纱 :
“依赖注入”是5美分概念的25美元术语。
*詹姆斯·肖尔(James Shore),2006年3月22日
依赖注入,在编写可测试,可组合和结构良好的应用程序时非常重要,它仅意味着具有带有构造函数的对象。 在本文中,我想向您展示依赖注入基本上是一种语法糖,它隐藏了函数的循环和组合。 不用担心,我们将非常缓慢地尝试解释为什么这两个概念非常相似。
设置器,注释和构造函数
Spring bean或EJB是Java对象。 但是,如果仔细观察,大多数bean在创建后实际上是无状态的。 在Spring bean上调用方法很少会修改该bean的状态。 大多数时候,bean只是在类似上下文中运行的一系列过程的便捷命名空间。 调用invoice()
,我们不会修改CustomerService
的状态,而只是委托给另一个对象,该对象最终将调用数据库或Web服务。 这已经远离面向对象的编程(我在这里讨论了 )。 因此,从本质上讲,我们在名称空间的多层层次结构中拥有程序(我们将在以后介绍功能):它们所属的包和类。 通常,这些过程调用其他过程。 您可能会说他们在bean的依赖项上调用方法,但是我们已经了解到bean是一个谎言,它们只是过程组。
话虽如此,让我们看看如何配置Bean。 在我的职业生涯中,我有几集关于setter(以及成吨的XML的<property name="...">
),@ @Autowired
字段以及最后的构造函数注入的情节。 另请参见: 为什么应优先考虑构造函数的注入? 。 因此,我们通常拥有的对象具有对其引用的不可变引用:
Hello Rajeev, @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 {
try(Stream<string> lines = Files.lines(statementFile)) {
lines
.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 {
//...
}</string>
提取一个带有银行对帐单的文件,将每一行解析为Payment
对象,并将其存储。 尽可能无聊。 现在让我们重构一下。 首先,我希望您意识到面向对象编程是一个谎言。 不是因为这只是名称空间(所谓的类)中的一堆过程(我希望您不是用这种方式编写软件)。 但是由于对象是使用隐式this
参数实现的过程,因此在您看到: this.database.insert(payment)
它实际上已编译为以下内容: Database.insert(this.database, payment)
。 不相信我吗?
$ javap -c Storage.class
...
public java.util.UUID save(com.nurkiewicz.di.Payment);
Code:
0: aload_0
1: getfield #2 // Field database:Lcom/nurkiewicz/di/Database;
4: aload_1
5: invokevirtual #3 // Method com/nurkiewicz/di/Database.insert:(Lcom/nurkiewicz/di/Payment;)Ljava/util/UUID;
8: areturn
好吧,如果您很正常,这并不能证明您,所以让我解释一下。 aload_0
getfield #2
之后的aload_0
(代表this
)将this.database
推入操作数堆栈。 aload_1
推第一个方法参数( Payment
),最后invokevirtual
调用进程 Database.insert
(有一些多态性这里涉及到,不相干在这种情况下)。 因此,我们实际上调用了两参数过程,其中第一个参数由编译器自动填充并命名为… this
。 在被调用方, this
是有效的,并且指向Database
实例。
忘记对象
让我们将所有这些变得更加明确,而忽略对象:
class ImportDependencies {
public final Parser parser;
public final Storage storage;
//...
}
static void importFile(ImportDependencies thiz, Path statementFile) throws IOException {
Files.lines(statementFile)
.map(thiz.parser::toPayment)
.forEach(thiz.storage::save);
}
太疯狂了! 注意importFile
过程现在在外面PaymentProcessor
,我居然改名为ImportDependencies
(原谅public
修饰符字段)。 importFile
可以是static
因为所有依赖项都在thiz
容器中显式给出,而不是使用this
和instance变量隐式提供-并可在任何地方实现。 实际上,我们只是重构为编译过程中幕后已经发生的事情。 在此阶段,您可能想知道为什么我们需要一个额外的容器来存储依赖关系,而不仅仅是直接传递它们。 当然,这毫无意义:
static void importFile(Parser parser, Storage storage, Path statementFile) throws IOException {
Files.lines(statementFile)
.map(parser::toPayment)
.forEach(storage::save);
}
实际上,有些人喜欢将依赖关系显式传递给上述业务方法,但这不是重点。 这只是转型的又一步。
咖喱
下一步,我们需要将函数重写为Scala:
object PaymentProcessor {
def importFile(parser: Parser, storage: Storage, statementFile: Path) {
val source = scala.io.Source.fromFile(statementFile.toFile)
try {
source.getLines()
.map(parser.toPayment)
.foreach(storage.save)
} finally {
source.close()
}
}
}
它在功能上等效,因此无需多说。 只需注意importFile()
如何属于object
,因此它有点类似于Java中单例的static
方法。 接下来,我们将参数分组 :
def importFile(parser: Parser, storage: Storage)(statementFile: Path) { //...
这一切都与众不同。 现在,您可以一直提供所有依赖项,也可以提供更好的依赖项,只需执行一次即可:
val importFileFun: (Path) => Unit = importFile(parser, storage)
//...
importFileFun(Paths.get("/some/path"))
上面的代码行实际上可以是容器设置的一部分,我们将所有依赖项绑定在一起。 设置完成后,我们可以在任何地方使用importFileFun
,而importFileFun
担心其他依赖项。 我们所拥有的只是一个函数(Path) => Unit
,就像一开始的paymentProcessor.importFile(path)
一样。
一直发挥作用
我们仍然将对象用作依赖项,但是如果您仔细看,我们既不需要parser
也不需要storage
。 我们真正需要的是一个可以解析的函数 ( parser.toPayment
)和一个可以存储的函数 ( storage.save
)。 让我们再次重构:
def importFile(parserFun: String => Payment, storageFun: Payment => Unit)(statementFile: Path) {
val source = scala.io.Source.fromFile(statementFile.toFile)
try {
source.getLines()
.map(parserFun)
.foreach(storageFun)
} finally {
source.close()
}
}
当然,对于Java 8和lambda,我们可以做同样的事情,但是语法更加冗长。 我们可以提供任何用于解析和存储的功能,例如,在测试中,我们可以轻松创建存根。 哦,顺便说一句,顺便说一句,我们刚刚从面向对象的Java转变为功能组合,完全没有对象。 当然,仍然存在一些副作用,例如加载文件和存储,但是让我们这样吧。 或者,要使依赖注入和函数组成之间的相似性更加显着,请查看Haskell中的等效程序:
let parseFun :: String -> Payment
let storageFun :: Payment -> IO ()
let importFile :: (String -> Payment) -> (Payment -> IO ()) -> FilePath -> IO ()
let simpleImport = importFile parseFun storageFun
// :t simpleImport
// simpleImport :: FilePath -> IO ()
首先,需要IO
monad来管理副作用。 但是,您是否看到importFile
高阶函数如何接受三个参数,但是我们只能提供两个参数并获得simpleImport
? 这就是我们在Spring或EJB中所谓的依赖注入。 但是没有语法糖。