1. 概要

長期的なシステムを開発する場合、変更可能な環境を期待する必要があります。

一般に、機能要件、フレームワーク、I / Oデバイス、さらにはコード設計でさえ、さまざまな理由ですべて変更される可能性があります。 これを念頭に置いて、 Clean Architectureは、私たちの周りのすべての不確実性を考慮した、保守性の高いコードのガイドラインです

この記事では、次のユーザー登録APIの例を作成しますロバートC。 マーティンのクリーンなアーキテクチャ 。 彼の元のレイヤー(エンティティ、ユースケース、インターフェイスアダプター、フレームワーク/ドライバー)を使用します。

2. クリーンなアーキテクチャの概要

クリーンなアーキテクチャは、次のような多くのコード設計と原則をコンパイルします。 個体安定した抽象化、その他。 しかし、の核となるアイデアはです。システムをビジネス価値に基づいたレベルに分割する。 したがって、最上位レベルにはビジネスルールがあり、下位レベルごとにI/Oデバイスに近づきます。

また、レベルをレイヤーに変換することもできます。 この場合、それは反対です。 内層は最高レベルに等しく、以下同様です。

これを考慮して、 ビジネスに必要なレベルをいくつでも持つことができます。 しかし、常に考慮して依存関係ルール–上位レベルが下位レベルに依存してはなりません

3. ルール

ユーザー登録APIのシステムルールの定義を始めましょう。 まず、ビジネスルール:

  • ユーザーのパスワードは5文字以上である必要があります

次に、適用ルールがあります。 ユースケースやストーリーなど、さまざまな形式にすることができます。 ストーリーテリングフレーズを使用します。

  • システムはユーザー名とパスワードを受け取り、ユーザーが存在しないかどうかを検証し、作成時間とともに新しいユーザーを保存します

データベースやUIなどについての言及がないことに注意してください。 なぜなら私たちのビジネスはこれらの詳細を気にしません 、私たちのコードもすべきではありません。

4. エンティティレイヤー

クリーンなアーキテクチャが示唆するように、ビジネスルールから始めましょう。

interface User {
    boolean passwordIsValid();

    String getName();

    String getPassword();
}

そして、 UserFactory

interface UserFactory {
    User create(String name, String password);
}

ユーザーを作成しました ファクトリメソッド 2つの理由があります。 にストックするには 安定した抽象化の原則 そして、ユーザーの作成を分離します。

次に、両方を実装しましょう。

class CommonUser implements User {

    String name;
    String password;

    @Override
    public boolean passwordIsValid() {
        return password != null && password.length() > 5;
    }

    // Constructor and getters
}
class CommonUserFactory implements UserFactory {
    @Override
    public User create(String name, String password) {
        return new CommonUser(name, password);
    }
}

複雑なビジネスがある場合は、ドメインコードをできるだけ明確に構築する必要があります。 したがって、このレイヤーはデザインパターンを適用するのに最適な場所です。 特に、ドメイン駆動設計を考慮に入れる必要があります。

4.1. ユニットテスト

それでは、CommonUserをテストしてみましょう。

@Test
void given123Password_whenPasswordIsNotValid_thenIsFalse() {
    User user = new CommonUser("Baeldung", "123");

    assertThat(user.passwordIsValid()).isFalse();
}

ご覧のとおり、単体テストは非常に明確です。 結局のところ、モックがないことは、このレイヤーの良いシグナルです。

一般に、ここでモックについて考え始めると、エンティティとユースケースが混在している可能性があります。

5. ユースケースレイヤー

ユースケースは、システムの自動化に関連するルールです。 クリーンアーキテクチャでは、それらをインタラクターと呼びます。

5.1. UserRegisterInteractor

まず、 UserRegisterInteractor を作成して、どこに向かっているのかを確認します。 次に、すべての使用済みパーツを作成して説明します。

class UserRegisterInteractor implements UserInputBoundary {

    final UserRegisterDsGateway userDsGateway;
    final UserPresenter userPresenter;
    final UserFactory userFactory;

    // Constructor

    @Override
    public UserResponseModel create(UserRequestModel requestModel) {
        if (userDsGateway.existsByName(requestModel.getName())) {
            return userPresenter.prepareFailView("User already exists.");
        }
        User user = userFactory.create(requestModel.getName(), requestModel.getPassword());
        if (!user.passwordIsValid()) {
            return userPresenter.prepareFailView("User password must have more than 5 characters.");
        }
        LocalDateTime now = LocalDateTime.now();
        UserDsRequestModel userDsModel = new UserDsRequestModel(user.getName(), user.getPassword(), now);

        userDsGateway.save(userDsModel);

        UserResponseModel accountResponseModel = new UserResponseModel(user.getName(), now.toString());
        return userPresenter.prepareSuccessView(accountResponseModel);
    }
}

ご覧のとおり、すべてのユースケースステップを実行しています。 また、このレイヤーはエンティティのダンスを制御する役割を果たします。 それでも、私たちは UIまたはデータベースがどのように機能するかについては何も想定していません。 しかし、私たちは使用しています UserDsGateway UserPresenter 。 では、どうしてそれらを知ることができないのでしょうか? UserInputBoundary とともに、これらは入力境界と出力境界であるためです。

5.2. 入力境界と出力境界

境界は、コンポーネントがどのように相互作用できるかを定義するコントラクトです。 入力境界は、ユースケースを外部レイヤーに公開します:

interface UserInputBoundary {
    UserResponseModel create(UserRequestModel requestModel);
}

次に、外側のレイヤーを利用するための出力境界があります。 まず、データソースゲートウェイを定義しましょう。

interface UserRegisterDsGateway {
    boolean existsByName(String name);

    void save(UserDsRequestModel requestModel);
}

次に、ビュープレゼンター:

interface UserPresenter {
    UserResponseModel prepareSuccessView(UserResponseModel user);

    UserResponseModel prepareFailView(String error);
}

は、 依存性逆転の原則を使用して、データベースやUIなどの詳細からビジネスを解放しています。

5.3. デカップリングモード

先に進む前に、 境界は、システムの自然な分割を定義する契約です。 ただし、アプリケーションの配信方法も決定する必要があります。

  • モノリシック–パッケージ構造を使用して編成されている可能性があります
  • モジュールを使用する
  • サービス/マイクロサービスを使用する

これを念頭に置いて、できる デカップリングモードでクリーンなアーキテクチャの目標を達成する。 したがって、現在および将来のビジネス要件に応じて、これらの戦略を変更する準備をする必要があります。 デカップリングモードを選択した後、境界に基づいてコード分割を行う必要があります。

5.4. 要求および応答モデル

これまで、インターフェースを使用してレイヤー間で操作を作成してきました。 次に、これらの境界を越えてデータを転送する方法を見てみましょう。

すべての境界がStringまたはModelオブジェクトのみを処理していることに注意してください。

class UserRequestModel {

    String login;
    String password;

    // Getters, setters, and constructors
}

基本的に、の単純なデータ構造のみが境界を越えることができます。 また、すべてモデルフィールドとアクセサーのみがあります 。 さらに、データオブジェクトは内側に属します。 したがって、依存関係ルールを維持できます。

しかし、なぜこれほど多くの類似したオブジェクトがあるのでしょうか。 コードが繰り返される場合、次の2つのタイプが考えられます。

  • 誤ったまたは偶発的な重複–各オブジェクトには異なる変更理由があるため、コードの類似性は偶然です。 削除しようとすると、違反するリスクがあります。 単一責任の原則.
  • 真の複製–同じ理由でコードが変更されます。 したがって、それを削除する必要があります

各モデルには異なる責任があるため、これらすべてのオブジェクトを取得しました。

5.5. UserRegisterInteractorのテスト

それでは、単体テストを作成しましょう。

@Test
void givenBaeldungUserAnd12345Password_whenCreate_thenSaveItAndPrepareSuccessView() {
    given(userDsGateway.existsByIdentifier("identifier"))
        .willReturn(true);

    interactor.create(new UserRequestModel("baeldung", "123"));

    then(userDsGateway).should()
        .save(new UserDsRequestModel("baeldung", "12345", now()));
    then(userPresenter).should()
        .prepareSuccessView(new UserResponseModel("baeldung", now()));
}

ご覧のとおり、ユースケーステストのほとんどは、エンティティと境界のリクエストを制御することに関するものです。 そして、私たちのインターフェースは私たちが簡単に詳細を模倣することを可能にします。

6. インターフェイスアダプタ

この時点で、私たちはすべてのビジネスを終了しました。 それでは、詳細をプラグインしてみましょう。

私たちのビジネスは、そのための最も便利なデータ形式のみを扱う必要があります。また、DBまたはUIとしての外部エージェントも同様に扱う必要があります。 ただし、この形式は通常異なります。 このため、インターフェイスアダプタ層がデータの変換を担当します。

6.1. UserRegisterDsGatewayJPAを使用

まず、 JPA を使用して、userテーブルをマップします。

@Entity
@Table(name = "user")
class UserDataMapper {

    @Id
    String name;

    String password;

    LocalDateTime creationTime;

    //Getters, setters, and constructors
}

ご覧のとおり、 Mapper の目標は、オブジェクトをデータベース形式にマップすることです。

次に、 entityを使用したJpaRepository

@Repository
interface JpaUserRepository extends JpaRepository<UserDataMapper, String> {
}

spring -bootを使用することを考えると、ユーザーを保存するために必要なのはこれだけです。

次に、 UserRegisterDsGateway:を実装します。

class JpaUser implements UserRegisterDsGateway {

    final JpaUserRepository repository;

    // Constructor

    @Override
    public boolean existsByName(String name) {
        return repository.existsById(name);
    }

    @Override
    public void save(UserDsRequestModel requestModel) {
        UserDataMapper accountDataMapper = new UserDataMapper(requestModel.getName(), requestModel.getPassword(), requestModel.getCreationTime());
        repository.save(accountDataMapper);
    }
}

ほとんどの場合、コードはそれ自体を物語っています。 私たちの方法に加えて、 UserRegisterDsGatewayの 名前。 代わりにUserDsGatewayを選択した場合、他のUserのユースケースは インターフェイス分離の原則.

6.2. ユーザー登録API

それでは、HTTPアダプターを作成しましょう。

@RestController
class UserRegisterController {

    final UserInputBoundary userInput;

    // Constructor

    @PostMapping("/user")
    UserResponseModel create(@RequestBody UserRequestModel requestModel) {
        return userInput.create(requestModel);
    }
}

ご覧のとおり、ここでのの唯一の目標は、要求を受信し、応答をクライアントに送信することです。

6.3. 応答の準備

返信する前に、返信をフォーマットする必要があります。

class UserResponseFormatter implements UserPresenter {

    @Override
    public UserResponseModel prepareSuccessView(UserResponseModel response) {
        LocalDateTime responseTime = LocalDateTime.parse(response.getCreationTime());
        response.setCreationTime(responseTime.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
        return response;
    }

    @Override
    public UserResponseModel prepareFailView(String error) {
        throw new ResponseStatusException(HttpStatus.CONFLICT, error);
    }
}

私たちの UserRegisterInteractor プレゼンターを作成することを余儀なくされました。 それでも、表示規則はアダプター内でのみ関係します。 その上、 w 何かをテストするのが難しい場合でも、それをテスト可能なオブジェクトと控えめなオブジェクトに分割する必要があります。 そう、 UserResponseFormatter プレゼンテーションルールを簡単に確認できます。

@Test
void givenDateAnd3HourTime_whenPrepareSuccessView_thenReturnOnly3HourTime() {
    UserResponseModel modelResponse = new UserResponseModel("baeldung", "2020-12-20T03:00:00.000");
    UserResponseModel formattedResponse = userResponseFormatter.prepareSuccessView(modelResponse);

    assertThat(formattedResponse.getCreationTime()).isEqualTo("03:00:00");
}

ご覧のとおり、ビューに送信する前にすべてのロジックをテストしました。 したがって、控えめなオブジェクトのみがテストの難しい部分にあります。

7. ドライバーとフレームワーク

実際、私たちは通常ここでコーディングしません。 これは、このレイヤーが外部エージェントへの最低レベルの接続を表すためです。 たとえば、データベースまたはWebフレームワークに接続するためのH2ドライバー。 この場合、Webおよび依存性注入フレームワークとしてspring-bootを使用します。  したがって、その起動ポイントが必要です。

@SpringBootApplication
public class CleanArchitectureApplication {
    public static void main(String[] args) {
      SpringApplication.run(CleanArchitectureApplication.class);
    }
}

これまで、を使用していませんでした春の注釈私たちのビジネスで。 スプリング専用アダプターを除く 、 私たちのように UserRegisterController。 これはです。必要があるため スプリングブーツを他の詳細と同じように扱う.

8. ひどいメインクラス

いよいよ最後のピース!

これまでのところ、安定した抽象化の原則に従いました。 また、内部層を外部エージェントから保護しました。 制御の反転 。 最後に、すべてのオブジェクトの作成をその使用から分離しました。 この時点で、それは私たち次第です残りの依存関係を作成し、それらをプロジェクトに注入します

@Bean
BeanFactoryPostProcessor beanFactoryPostProcessor(ApplicationContext beanRegistry) {
    return beanFactory -> {
        genericApplicationContext(
          (BeanDefinitionRegistry) ((AnnotationConfigServletWebServerApplicationContext) beanRegistry)
            .getBeanFactory());
    };
}

void genericApplicationContext(BeanDefinitionRegistry beanRegistry) {
    ClassPathBeanDefinitionScanner beanDefinitionScanner = new ClassPathBeanDefinitionScanner(beanRegistry);
    beanDefinitionScanner.addIncludeFilter(removeModelAndEntitiesFilter());
    beanDefinitionScanner.scan("com.baeldung.pattern.cleanarchitecture");
}

static TypeFilter removeModelAndEntitiesFilter() {
    return (MetadataReader mr, MetadataReaderFactory mrf) -> !mr.getClassMetadata()
      .getClassName()
      .endsWith("Model");
}

この例では、spring-bootを使用しています 依存性注入 すべてのインスタンスを作成します。 使用していないので @成分、ルートパッケージをスキャンし、モデルオブジェクトのみを無視しています.

この戦略はより複雑に見えるかもしれませんが、それは私たちのビジネスをDIフレームワークから切り離します。 一方、メインクラスはすべてのシステムを支配しました。 そのため、クリーンアーキテクチャは、他のすべてを含む特別なレイヤーでそれを考慮します。

9. 結論

この記事では、ボブおじさんがどのように クリーンなアーキテクチャは、多くのデザインパターンと原則に基づいて構築されています。 また、SpringBootを使用してそれを適用するユースケースを作成しました。

それでも、いくつかの原則は脇に置いておきました。 しかし、それらはすべて同じ方向に進んでいます。 その作成者を引用することで要約できます。 行われない決定の数を最大化する必要があります。」、そして私たちはそれをしました 境界を使用して詳細からビジネスコードを保護する.

いつものように、完全なコードはGitHubから入手できます。