💡

独立したコアレイヤパターンをJavaScript(TypeScript)で実装してみる

2021/03/13に公開

@shin1x1さんの独立したコアレイヤパターンを JS(TS)でも利用できるといいかなと思い、サンプルコードを作成してみた。

独立したコアレイヤパターンとは?

  • アーキテクチャパターンのひとつ
  • コアレイヤとアプリケーションレイヤの 2 つのレイヤからなる
    • コアレイヤ
      • コアレイヤロジックやビジネスロジックを実装する
      • 外部の IF(WebAPI や DB の orm など)には依存しない
    • アプリケーションレイヤ
      • コアレイヤと外部の IF の連携を実装する
      • 外部の IF(WebAPI や DB の orm など)に依存する
  • スーパーヒーローがいなくても使えるパターン

ゴール

サンプルとして、記事に対するファボ(お気に入り)を設定・解除する機能のユースケースを作成する。

サンプルリポジトリ

ohnaka0410/Vue-Ts-Independent-Core-Layer-Pattern

実装

ベース

サクッと TS 環境を構築したかったので、@vue/cliを利用した。

コアレイヤ

コアレイヤは、ユースケースとユースケースが利用するサービスのポート(インタフェース)を実装する。ユースケースは、想定される処理を順に記述していく。データベースアクセスなど技術詳細を利用したい場合、必要な API をインタフェースとして実装して、それに依存しておく。ユースケース内では、このインタフェースを利用して値の取得や保存などの処理を行う。

本家サイトより引用

モデル

src/modules/Sample/Article/Core/Model/Article.ts
/**
 * 記事クラス
 */
export default class Article {
  /** 記事ID */
  private id: number;

  /**
   * コンストラクタ
   */
  public constructor(id:number) {
    this.id = id;
  }

  /**
   * ID取得処理
   */
  public getId() {
    return this.id;
  }
}
src/modules/Sample/Article/Core/Model/ArticleFav.ts
/**
 * 記事お気に入りクラス
 */
export default class ArticleFav {
  /** 記事ID */
  private articleId: number;
  /** ユーザID */
  private userId: number;
  /** 記事お気に入りID */
  private id: number | undefined;

  /**
   * コンストラクタ
   */
  public constructor(articleId:number, userId:number, id?:number) {
    this.articleId = articleId;
    this.userId = userId;
    this.id = id;
  }

  /**
   * 記事ID取得処理
   */
  public getArticleId(): number {
    return this.articleId;
  }

  /**
   * ユーザID取得処理
   */
  public getUserId(): number {
    return this.userId;
  }

  /**
   * 記事お気に入りID取得処理
   */
  public getId(): number | undefined {
    return this.id;
  }
}

エクセプション

src/modules/Sample/Article/Core/Exception/NotFoundException.ts
/**
 * 404エラーエクセプションクラス
 */
export default class NotFoundException extends Error {}

ポート

src/modules/Sample/Article/Core/Port/ToggleFavPort.d.ts
import Article from '../Model/Article';
import ArticleFav from '../Model/ArticleFav';

/**
 * お気に入り切り替えIF
 */
export default interface ToggleFavPort {
  /**
   * 記事取得処理
   */
  findArticle(articleId: number): Promise<Article | undefined>;
  /**
   * 記事お気に入り取得処理
   */
  findArticleFav(
    articleId: number,
    userId: number
  ): Promise<ArticleFav | undefined>;
  /**
   * 記事お気に入り保存処理
   */
  saveArticleFav(articleFav: ArticleFav): Promise<void>;
  /**
   * 記事お気に入り削除処理
   */
  deleteArticleFav(articleFav: ArticleFav): Promise<void>;
}

ユースケース

src/modules/Sample/Article/Core/UseCase/ToggleFav.ts
import ToggleFavPort from '../Port/ToggleFavPort';
import NotFoundException from '../Exception/NotFoundException';
import Article from '../Model/Article';
import ArticleFav from '../Model/ArticleFav';

/**
 * お気に入り切り替えユースケースクラス
 */
export default class ToggleFav {
  /**
   * お気に入り切り替え実装ポート
   */
  private port: ToggleFavPort;

  /**
   * コンストラクタ
   */
  public constructor(port: ToggleFavPort) {
    this.port = port;
  }

  /**
   * ユースケース実行処理
   */
  public async run(
    articleId: number,
    on: boolean,
    userId: number
  ): Promise<void> {
    if (!articleId) {
      throw new NotFoundException('articleId not found');
    }

    const article: Article | undefined = await this.port.findArticle(articleId);
    if (!article) {
      throw new NotFoundException('Article not found');
    }

    const articleFav: ArticleFav | undefined = await this.port.findArticleFav(
      article.getId(),
      userId
    );

    if (on) {
      await this.port.saveArticleFav(
        articleFav || new ArticleFav(article.getId(), userId)
      );
    } else {
      if (articleFav) {
        await this.port.deleteArticleFav(articleFav);
      }
    }
  }
}

アプリケーションレイヤ

サービスレイヤは、UI や データベースなどアプリケーション外部との連携を実装する。

このレイヤでは、2 つの責務を担う。1 つ目は、UI からのアクションを契機にユースケースを実行することである。コアレイヤから提供されるユースケースクラスを実行することになる。2 つ目は、実行するユースケースが依存しているポートに対するアダプタを実装することだ。インタフェースを満たせば、その実装の詳細は自由である。

本家サイトより引用

アダプタ

実際はaxiosなどを使って API を叩いたり、FireBase の FireStorage への保存処理を行う想定です。

src/modules/Sample/Article/Application/Adapter/ToggleFavAdapter.ts
import ToggleFavPort from '../../Core/Port/ToggleFavPort';
import Article from '../../Core/Model/Article';
import ArticleFav from '../../Core/Model/ArticleFav';

/**
 * お気に入り切り替えアダプタクラス
 */
export default class ToggleFavAdapter implements ToggleFavPort {
  /**
   * 記事取得処理
   */
  public async findArticle(articleId: number): Promise<Article | undefined> {
    console.log("findArticle");
    console.log(articleId);
    // call api
    // return dummy data
    return new Article(articleId);
  }
  /**
   * 記事お気に入り取得処理
   */
  public async findArticleFav(
    articleId: number,
    userId: number
  ): Promise<ArticleFav | undefined> {
    console.log("findArticleFav");
    console.log(articleId);
    console.log(userId);
    // call api
    // return dummy data
    const dummyArticleFavId =1;
    return new ArticleFav(articleId, userId, dummyArticleFavId);
  }
  /**
   * 記事お気に入り保存処理
   */
  public async saveArticleFav(articleFav: ArticleFav): Promise<void> {
    console.log("saveArticleFav");
    // call api
  }
  /**
   * 記事お気に入り削除処理
   */
  public async deleteArticleFav(articleFav: ArticleFav): Promise<void> {
    console.log("deleteArticleFav");
    // call api
  }
}

呼び出し部分

※DI コンテナの実装は省略。

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
    <button @click="toggleFav">ToggleFav</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "./components/HelloWorld.vue";

import ToggleFavUseCase from "./modules/Sample/Article/Core/UseCase/ToggleFav";
import ToggleFavAdapter from "./modules/Sample/Article/Application/Adapter/ToggleFavAdapter";
const useCase = new ToggleFavUseCase(new ToggleFavAdapter());

@Component({
  components: {
    HelloWorld
  },
  methods: {
    toggleFav: () => {
      const articleId: number = 11;
      const on: boolean = true;
      // const on: boolean = false;
      const userId: number = 22;
      useCase.run(articleId, on, userId);
    }
  }
})
export default class App extends Vue {}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

まとめ

  • TypeScript であれば型が利用できるので、利用できそう。(VanillaJS でも頑張ればなんとか。。。)
  • クリーンアーキテクチャーほどレイヤーが多くないので、比較的気軽に導入できそう。
  • 案件規模や開発メンバーのスキルに応じてドメインモデルを入れることもできそう。
GitHubで編集を提案
株式会社ナンバーフォー

Discussion