了解有关Java中控件和依赖项注入反转的更多信息。本文来自国内专业IT教育学院【优锐课】。Java学习资料交流qq群:907135806,在接下来的学习如果过程中有任何疑问,欢迎进群探讨。
什么是控制反转?什么是依赖注入?这些类型的问题通常会通过代码示例,模糊的解释以及在StackOverflow上被识别为“低质量答案”的内容来满足。
我们使用控制反转和依赖注入,并经常将其作为构建应用程序的正确方法。但是,我们无法清楚地说明原因!
原因是我们尚未明确确定什么是控制。一旦了解了我们要反转的内容,控制反转与依赖注入的概念实际上就不是要问的问题了。它实际上变为以下内容:
**控制反转=依赖(状态)注入+线程注入+连续(函数)注入**
为了解释这一点,让我们做一些代码。是的,重复使用代码解释控制反转的明显问题,但是请忍受,答案一直就在你的眼前。
反转控制/依赖项注入的一种明显用途是使用存储库模式,以避免绕过连接。代替以下内容:
public class NoDependencyInjectionRepository implements Repository<Entity> {
public void save(Entity entity, Connection connection) throws SQLException {
// Use connection to save entity to database
}
}
依赖注入允许将存储库重新实现为:
public class DependencyInjectionRepository implements Repository<Entity> {
@Inject Connection connection;
public void save(Entity entity) throws SQLException {
// Use injected connection to save entity to database
}
}
现在,你看到我们刚刚解决的问题了吗?
如果你正在考虑“我现在可以更改连接以说出REST调用”,那么这一切都可以灵活更改,那么,你将很近。
要查看问题是否已解决,请不要查看实现。相反,请查看界面。 客户端调用代码来自:
repository.save(entity, connection);
到以下内容:
repository.save(entity);
我们已经删除了客户端代码的耦合,以在调用该方法时提供连接。 通过删除耦合,我们可以替换存储库的其他实现(同样,乏味的旧消息,但请耐心等待):
public class WebServiceRepository implements Repository<Entity> {
@Inject WebClient client;
public void save(Entity entity) {
// Use injected web client to save entity
}
}
客户端可以继续调用该方法,方法相同:
repository.save(entity);
客户端不知道存储库现在正在调用微服务来保存实体,而不是直接与数据库对话。(实际上,客户知道,但是我们很快就会解决。)
因此,将这种方法带入一个抽象的层次:
R method(P1 p1, P2 p2) throws E1, E2
// with dependency injection becomes
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
依赖项注入消除了客户端为该方法提供参数的耦合。
现在,你是否看到耦合的其他四个问题?
在这一点上,我警告你,一旦我向你展示了耦合问题,你将再也不会看到相同的代码。这是矩阵中我要问你要服用红色或蓝色药丸的要点。一旦我向你展示了这个问题的真正意义,那就再也没有回头路了——重构实际上是没有必要的,并且建模逻辑和计算机科学的基础知识也存在问题(好的,大胆的声明,但请继续阅读—我不能用其他任何方式来说)。
因此,你选择了red pill。
让我们为你做准备。
为了确定四个额外的耦合问题,让我们再次看一下抽象方法:
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
// and invoking it
try {
R result = object.method();
} catch (E1 | E2 ex) {
// handle exception
}
客户代码是什么?
• 返回类型
• 方法名称
• 异常处理
• 提供给方法的线程
依赖注入使我可以更改方法所需的对象,而无需更改调用该方法的客户端代码。但是,如果我想通过以下方式更改实现方法:
• 更改返回类型
• 更改名称
• 引发新异常(在上述情况下,交换到微服务存储库时,引发HTTP异常而不是SQL异常)
• 使用与客户端调用提供的线程不同的线程(池)执行方法
这涉及“重构”我的方法的所有客户端代码。当实现难以实际完成功能时,调用方为什么要规定耦合?实际上,我们应该反转耦合,以便实现可以规定方法签名(而不是调用方)。
你可能会像Matrix中Neo那样看待我,这是“呵呵”吗?让实现定义其方法签名吗?但是,不是有关覆盖和实现抽象方法签名定义的整个OO原理吗?那只是混乱,因为如果方法的返回类型,名称,异常,参数随着实现的发展而不断变化,那么该如何调用该方法呢?
简单。你已经知道了模式。你只是没有看到它们一起使用,它们的总和比它们的零件强大得多。
因此,让我们遍历方法的五个耦合点(返回类型,方法名称,参数,异常,调用线程)并将它们解耦。
我们已经看到依赖注入消除了客户端的参数耦合,因此减少了下来。
接下来,让我们讨论方法名称。
方法名称去耦
许多语言(包括Java lambda)都允许或具有作为该语言的一等公民的功能。通过创建对方法的函数引用,我们不再需要知道方法名称来调用该方法:
Runnable f1 = () -> object.method();
// Client call now decoupled from method name
f1.run()
现在,我们甚至可以围绕依赖注入传递方法的不同实现:
@Inject Runnable f1;
void clientCode() {
f1.run(); // to invoke the injected method
}
好的,这是一些额外的代码,没有太多附加值。我们已经将方法的名称与调用者分离了。
接下来,让我们解决方法中的异常。
方法异常解耦
通过使用以上注入函数的技术,我们注入函数来处理异常:
Runnable f1 = () -> {
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
try {
object.method();
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
}
}
// Note: above is abstract pseudo code to identify the concept (and we will get to compiling code shortly)
现在,异常不再是客户端调用者的问题。现在,注入的方法可以处理异常,从而使调用者不必处理异常。
接下来,让我们解决调用线程。
方法的调用线程解耦
通过使用异步函数签名并注入执行程序,我们可以将调用非常规方法的线程与调用者提供的方法分离:
Runnable f1 = () -> {
@Inject Executor executor;
executor.execute(() -> {
object.method();
});
}
通过注入适当的Exectutor,我们可以使所需的任何线程池调用实现方法。要重用客户端的调用线程,我们仅使用同步Exectutor:
Executor synchronous = (runnable) -> runnable.run();
因此,现在我们可以从调用代码的线程中分离一个线程来执行实现方法。
但是没有返回值,我们如何在方法之间传递状态(对象)?让我们将其与依赖注入结合在一起。
控制反转(耦合)
让我们将上述模式与依赖注入结合起来以获得ManagedFunction:
public interface ManagedFunction {
void run();
}
public class ManagedFunctionImpl implements ManagedFunction {
@Inject P1 p1;
@Inject P2 p2;
@Inject ManagedFunction f1; // other method implementations to invoke
@Inject ManagedFunction f2;
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
@Inject Executor executor;
@Override
public void run() {
executor.execute(() -> {
try {
implementation(p1, p2, f1, f2);
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
});
}
private void implementation(
P1 p1, P2 p2,
ManagedFunction f1, ManagedFunction f2
) throws E1, E2 {
// use dependency inject objects p1, p2
// invoke other methods via f1, f2
// allow throwing exceptions E1, E2
}
}
好的,这里有很多事情要做,只是上面的模式结合在一起了。客户端代码现在可以直接从方法实现中解耦出来,因为它可以运行:
@Inject ManagedFunction function;
public void clientCode() {
function.run();
}
现在可以自由更改实现方法,而不会影响客户端调用代码:
• 方法没有返回类型(轻微的限制始终为空,但是对于异步代码而言是必需的)
• 实现方法的名称可能会更改,因为它由ManagedFunction.run()包装
• ManagedFunction不再需要参数。这些都是依赖注入的,允许实现方法选择它需要的参数(对象)
• 异常由注入的使用者处理。现在,该实现方法可以规定抛出什么异常,仅要求注入不同的使用者。客户端调用代码不知道实现方法现在可能会抛出HTTPException而不是SQLException。此外,可以通过注入异常的ManagedFunctions实现消费者。
• 执行程序的注入允许实现方法通过指定要注入的执行程序来指示其执行线程。这可能导致重新使用客户端的调用线程,或者使实现由单独的线程或线程池运行
方法的调用者的所有五个耦合点现在都已解耦。
我们实际上有“联轴器的反向控制”。换句话说,客户端调用方不再规定可以命名的实现方法,用作参数,作为异常抛出,使用哪个线程等。对耦合的控制被颠倒,以便实现方法可以通过以下方式决定耦合的对象:指定它是必需的注射。
此外,由于调用者之间没有耦合,因此无需重构代码。实现更改,然后配置它与系统其余部分的耦合(注入)。客户端调用代码不再需要重构。
因此,实际上,依赖注入仅解决了方法耦合问题的1/5。对于仅解决20%的问题如此成功的事情,它确实表明了该方法真正具有问题耦合的能力。
实现上述模式将创建超出系统价值的代码。这就是为什么开源OfficeFloor是控制框架的“真正”反转,并且被放在一起以减轻此代码的负担。这是上述概念中的一项实验,目的是查看真实的系统是否更容易通过“真正的”控制反转来构建和维护。
总结
因此,下次访问“重构按钮/命令”时,请意识到,这是由于每次编写代码时都盯着我们的方法的耦合而实现的。
真的,为什么我们要有方法签名?这是由于线程堆栈。我们需要将内存加载到线程堆栈上,并且方法签名遵循计算机的行为。但是,在现实世界中,对象之间的行为建模不会提供线程堆栈。对象与很小的接触点松散耦合,而不是该方法施加的五个耦合方面。
此外,在计算中,我们努力实现低耦合和高内聚。可能提出一种情况,与ManagedFunctions相比,方法是:
• 高耦合:方法具有与客户端调用代码耦合的五个方面
• 内聚性低:随着时间的流逝,方法的异常和返回类型的处理开始使方法的职责模糊,连续的更改和快捷方式可能会迅速降低方法实现的内聚性,从而开始处理超出其职责范围的逻辑
由于我们追求低耦合和高内聚性,因此我们最基本的构建块(方法和功能)实际上可能与我们最核心的编程原则背道而驰。
————————————————————————————————
本文来自国内专业IT教育学院【优锐课】
Java学习资料交流qq群:907135806,在接下来的学习如果过程中有任何疑问,欢迎进群探讨。
也可以添加vx:ddmsiqi,有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java学习资料和视频课程!抽丝剥茧 细说架构那些事——【优锐课】