Как организуется взаимодействие между компонентами
Организация взаимодействия компонентов производится путём обращения одного компонента к API другого через его фасад.
Каждый компонент обладает собственным API, и другие компоненты взаимодействуют с ним через этот API. Поскольку API компонента — это множество операций, доступных через его фасад (см. Что такое интерфейс компонента), то для того, чтобы компонент A мог обращаться к компоненту B, компоненту A достаточно знать, какие операции предоставляет компонент B, и иметь возможность обратиться к его фасаду. Это можно выразить следующей записью:
B exposes API
B.start() → Facade
A depends on API
A calls Facade
Способ предоставления доступа к фасаду и получения знания, какие операции предоставлены, зависит от среды исполнения компонентов. В модульном монолите интерфейс фасада и доступные через него классы дают компоненту A знание об API компонента B. Сам фасад можно передать напрямую как обычный Java-объект. Очевидно, для этого модуль компонента B должен быть доступен в classpath компонента A: например, Maven может добавить нужный набор классов в classpath через <dependency>. Если компоненты работают в разных процессах, передать фасад в компонент A в виде Java-объекта уже не получится. В этом случае A должен содержать прокси фасада или клиент, который передаёт вызовы фасаду по транспортному протоколу. Ярким примером взаимодействия с фасадом через прокси является RMI. В случае HTTP фасадом компонента B является совокупность его HTTP-эндпоинтов, а знание об API передаётся в виде документации. В качестве документации компонент B может предоставить SDK, который содержит описание HTTP API компонента на языке Java. Вендорные реализации DataSource являются аналогами SDK. Рассмотрим примеры.
Компонент «Игрок» раз в секунду вызывает компонент «Кубик», чтобы бросить кубик, и затем сверяет результат броска со своим предсказанием:
Компонент «Кубик»:
public class DiceLauncher {
public static void main(String[] args) throws IOException {
var server = new ServerSocket(5000); // Exposing the facade
var player = server.accept(); // Waiting for a player
var dice = new Dice();
var in = new Scanner(player.getInputStream());
var out = new PrintWriter(player.getOutputStream(), true);
while (in.hasNextLine()) {
if (in.nextLine().equals("roll")) {
out.println(dice.roll());
} else {
out.println("Unrecognized command");
}
}
}
}
public class Dice {
public int roll() {
return (int) (Math.random() * 6 + 1);
}
}
Компонент «Игрок»:
public class PlayerLauncher {
public static void main(String[] args) throws InterruptedException, IOException {
var player = new Player();
var dice = connect(); // Borrow a dice
var in = new Scanner(dice.getInputStream());
var out = new PrintWriter(dice.getOutputStream(), true);
while (true) {
Thread.sleep(1000); // Roll the dice once per second
player.predictRank();
out.println("roll"); // Roll the dice
var response = in.nextLine();
var actualRank = Integer.parseInt(response);
boolean result = player.reveal(actualRank);
System.out.println(result ? ":-D" : ":-(");
}
}
private static Socket connect() throws InterruptedException {
while (true) {
try {
return new Socket("localhost", 5000);
} catch (IOException e) {
Thread.sleep(1000); // Makes component startup independent of other component status.
}
}
}
}
public class Player {
private int prediction;
public void predictRank() {
prediction = (int) (Math.random() * 6 + 1);
}
public boolean reveal(int actual) {
return prediction == actual;
}
}
Для обращения одного компонента к другому первый компонент должен получить как знание об API второго, так и доступ к его фасаду. Компонент «Игрок» знает, что компонент «Кубик» предоставляет операцию Dice.roll, возвращающую целое число, и знает, как вызвать эту операцию через TCP-соединение. Само TCP-соединение является фасадом компонента «Кубик». Концептуально оба компонента можно описать так:
Dice exposes "socket://<host>:<port> : roll → int"
Dice.start() → socket://localhost:5000/
Player depends on "socket://<host>:<port> : roll → int"
Player calls socket://localhost:5000/
Точкой запуска компонента «Кубик» является метод main, в котором открывается ServerSocket и тем самым выставляется фасад компонента. Компонент «Игрок» получает доступ к этому фасаду через метод connect, содержащий примитивную логику retry, благодаря которой «Игрок» может быть запущен независимо от того, запущен ли уже «Кубик».
В модульном монолите «Игрок» получает знание об API компонента «Кубик» через Maven-зависимость:
...
<groupId>org.example</groupId>
<artifactId>player</artifactId>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>dice</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
Фасад компонента «Кубик» можно предоставить напрямую:
public class MainLauncher {
public static void main(String[] args) throws InterruptedException {
Dice facade = DiceLauncher.start();
PlayerLauncher.start(facade);
}
}
public class DiceLauncher {
public static Dice start() {
return new Dice();
}
}
public class PlayerLauncher {
public static void start(Dice dice) throws InterruptedException {
var player = new Player();
while (true) {
Thread.sleep(1000);
player.predictRank();
var actualRank = dice.roll();
player.reveal(actualRank);
}
}
}
Таким образом, знание об API компонента «Кубик» компонент «Игрок» получает через Maven-зависимость, а доступ к фасаду — напрямую, в виде Java-объекта, переданного точкой запуска.