Как организуется взаимодействие между компонентами

Организация взаимодействия компонентов производится путём обращения одного компонента к 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-объекта, переданного точкой запуска.