Java中的(耦合)控制反转

Home / Article 百晓生 2019-3-7 2535

什么是控制反转?什么是依赖注入?这些类型的问题通常会遇到代码示例。

我们使用控制和依赖注入的反转,并经常将其作为构建应用程序的正确方法。然而,我们无法清楚地阐明原因!

原因是我们还没有清楚地确定控制是什么。一旦我们理解了我们正在反转的内容,控制反转与依赖注入的概念实际上并不是要问的问题。它实际上变成了以下内容:

控制反转=依赖(状态)注入+线程注入+连续(功能)注入

为了解释这一点,我们来做一些代码吧。是的,使用代码来解释控制反转的明显问题正在重复,但请耐心等待,答案一直在你眼前。

一个明确使用控制/依赖注入的反转是存储库模式,以避免绕过连接。而不是以下:

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
  }
}

现在,你看到我们刚刚解决的问题了吗?要查看问题是否已解决,请不要查看实施。相反,看看界面。客户端调用代码:

repository.save(entity, connection);

以下内容:

repository.save(entity);

我们已经删除了客户端代码的耦合,以提供一个  connection on调用方法。通过删除耦合,我们可以替换存储库的不同实现

ublic 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

通过依赖注入消除了客户端为该方法提供参数的耦合。

现在,你看到耦合的其他四个问题了吗?

在这一点上,我警告你,一旦我向你展示耦合问题,你将永远不会再看同样的代码了。这是矩阵中我要问你是否想要服用红色或蓝色药丸的要点。一旦我向你展示这个真正兔子洞有多远问题,就没有回头了 -  实际上没有必要进行重构,而且在建模逻辑和计算机科学的基础知识方面存在问题(好的,大的声明,但请继续阅读 - 我可以'把它放在任何其他方式)。

为了识别四个额外的耦合问题,让我们再看一下抽象方法:

@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异常)

  • 使用不同的线程(池)执行方法而不是客户端调用提供的线程

这涉及“ 重构 ”我的方法的所有客户端代码。当实现具有实际执行功能的艰巨任务时,为什么调用者要求耦合?我们实际上应该反转耦合,以便实现可以指示方法签名(而不是调用者)。

你可能就像Neo在黑客帝国中所做的那样“哼”一下吗?让实现定义他们的方法签名?但是,不是覆盖和实现抽象方法签名定义的整个OO原则吗?这只是混乱,因为如果它的返回类型,名称,异常,参数随着实现的发展而不断变化,我如何调用该方法?

简单。你已经知道了模式。你只是没有看到他们一起使用,他们的总和比他们的部分更强大。

因此,让我们遍历方法的五个耦合点(返回类型,方法名称,参数,异常,调用线程)并将它们分离。

我们已经看到依赖注入删除了客户端的参数耦合,所以一个向下。

接下来,让我们解决方法名称。

方法名称解耦

许多语言(包括Java lambdas)允许或具有该语言的一等公民的功能。通过创建对方法的函数引用,我们不再需要知道方法名称来调用该方法:

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)

现在,异常不再是客户端调用者的问题。注入的方法现在处理将调用者与必须处理异常分离的异常。

接下来,让我们解决调用线程。

方法的调用线程解耦

通过使用异步函数签名并注入Executor,我们可以将调用实现方法的线程与调用者提供的线程分离:

Runnable f1 = () -> {
  @Inject Executor executor;
  executor.execute(() -> {
    object.method();
  });
}

通过注入适当的  Executor,我们可以使用我们需要的任何线程池调用的实现方法。要重用客户端的调用线程,我们只使用同步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();
}

现在可以自由更改实现方法,而不会影响客户端调用代码:

方法没有返回类型(总是存在轻微的限制  void,但是异步代码是必需的)

实现方法名称可能会更改,因为它包含在 ManagedFunction.run() 

不再需要参数ManagedFunction。这些是依赖注入的,允许实现方法选择它需要哪些参数(对象)

例外由注入处理Consumers。实现方法现在可以规定它抛出的异常,只需要Consumers 注入不同的异常  。客户端调用代码不知道实现方法现在可能正在抛出  HTTPException 而不是  SQLException 。此外,  Consumers 实际上可以通过ManagedFunctions  注入异常来实现  。

注入Executor 允许实现方法通过指定Executor to inject 来指示其执行的线程  。这可能导致重用客户端的调用线程或让实现由单独的线程或线程池运行

现在,通过其调用者的方法的所有五个耦合点都是分离的。

我们实际上已经“对耦合进行了反向控制”。换句话说,客户端调用者不再指定实现方法可以命名的内容,用作参数,抛出异常,使用哪个线程等。耦合的控制被反转,以便实现方法可以决定它耦合到什么指定它是必需的注射。

此外,由于调用者没有耦合,因此不需要重构代码。实现发生变化,然后将其耦合(注入)配置到系统的其余部分。客户端调用代码不再需要重构。

因此,实际上,依赖注入只解决了方法耦合问题的1/5。对于仅解决20%问题非常成功的事情,它确实显示了该方法的耦合问题究竟有多少。

实现上述模式将创建比您的系统中更多的代码。这就是为什么开源OfficeFloor是控制框架的“真正”反转,并且已经整合在一起以减轻此代码的负担。这是上述概念中的一个实验,以查看真实系统是否更容易构建和维护,具有“真正的”控制反转。 


本文链接:https://www.it72.com/12502.htm

推荐阅读
最新回复 (0)
返回