💡
独立したコアレイヤパターンをJavaScript(TypeScript)で実装してみる
@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 でも頑張ればなんとか。。。)
- クリーンアーキテクチャーほどレイヤーが多くないので、比較的気軽に導入できそう。
- 案件規模や開発メンバーのスキルに応じてドメインモデルを入れることもできそう。
Discussion