はじめに
おはようございます。Watanabe Jin(@Sicut_study)です。
今回は私の会社でよく使われるクリーンアーキテクチャのレイヤーの説明をたとえ話を交えながらせつめいしていきます。
コードはTypeScriptですが、他の言語のレポジトリもありますのでご確認いただければと思います。
毎朝5分で読める
朝5分で読めるシリーズは平日7時にエンジニアの方に気づきが与えられるような記事を投稿しています。
よければ@Sicut_studyのフォローを宜しくおねがいします!
聞いて学びたい方はこちら👇
私がよく使っているクリーンアーキテクチャ
クリーンアーキテクチャは厳密にやっていくこともできますが、私がよく利用するのは実装も比較的簡単で、テストもしやすいという理由から以下の構成で実装をしています。
私は以下の4層を実装で利用して使います
- usecase
- gateway
- driver
- domain
ここではそれぞれの層がどのような役割なのかをわかりやすさを重視して説明していきます。このレイヤリングは比較的わかりやすく実装しやすいと考えているためリポジトリを参考に実装に使っていただけるといいかと思っています。
なぜクリーンアーキテクチャがいいのか
個人的に感じるのはとにかくテストがしやすいということです。TDD(テスト駆動開発)をする際にはとても実装しやすいです。
また、DI(依存性中注入)があるために仕様の変更などにも影響が少なく変更しやすいのもメリットを感じます。
未経験エンジニアの文脈で言うと、バックエンドをやっている方であればなかなか差別化というのが難しいのでクリーンアーキテクチャを導入してしっかりと説明できることも大きな価値になると考えています。
ユースケース層
ユースケースは呼び出すだけでやりたいことを隠蔽して使えるようにするための層です。
今回はゲームの例で話していきます。
ゲームをするときにあなたは電源ボタンをつけるだけで簡単にゲームができます。ゲーム機に電源が入り、どのような仕組みでゲームが動いているのかということは理解する必要はありません。
ユースケース層はそのように何をしたいのかということだけで簡単に利用できるようにするためのレイヤーになります。
ユーザーがどんなことをしたいのかを表現する層になります。
export class UserLogUsecase {
constructor(private inputPort: UserLogInputPort) {}
async execute(userId: string): Promise<UserLog> {
return await this.inputPort.getUserLog(userId);
}
}
ユースケース層ではGatewayをDI(依存性注入)して使います。
ゲームカセットを入れて呼び出すというイメージです
ポート
ユースケース層とゲートウェイ層の間にはポートというものが存在します。
これはゲームでいうとゲームカセットの規格を決めているような定義書のことです。
たとえば、DSのソフトはSwitchでは使えないですよね(同じ任天堂ですが)
ゲーム機にはそれ専用のゲームカセットの規格というのがあります。
それを定義したものがポートの役割です。
Switchで例えるとポートは、
この規格を守っていないと使うことはできません。内容は関係なくポートはゲーム機が何を使えるのかという仕様だけを決めているものです。
export interface UserLogInputPort {
getUserLog(userId: string): Promise<UserLog>;
}
たとえばこのコードであればこのあとで説明するGateway(カセット)がgetUserLog
を持っているものしか使えない(カセットとして入れられない)ということを決めています
ゲートウェイ層
Switchのカセットの規約を守っているものであればユースケース層(ゲーム機)で使うことができます。そしてそのカセットには色々な種類が存在します。
たとえば、ポケモンであったりぷよぷよであったり。。。
実際のゲーム内容を表しているのがゲートウェイと思ってくれるとイメージがつくかも知れません
ゲートウェイではドライバー層で取得したデータを私たちの世界(ドメイン)に変換するための層となっています。
たとえば、ゲームの矢印ボタンを押したらキャラクターがその方向に動くのような変換をしているイメージです。
ゲームによっては矢印を押したら次のページに進むという動きをすると思います。
ゲームの世界での操作に変換していくようなイメージを持つと良いかも知れないです。
export class UserLogGateway implements UserLogInputPort {
constructor(readonly scraper: UserLogScraper) {}
async getUserLog(userId: string): Promise<UserLog> {
const userLogJson = await this.scraper.run(userId);
return new UserLog(
userLogJson.id,
userLogJson.name.replace("\n ", "").replace("\n\n ", ""),
userLogJson.commit
);
}
}
ドライバー層
ここはすこしゲームの例から外れてしまいますが説明していきます
ドライバー層は外部に依存するような処理を書いているものです
たとえばDBからデータを取得したり、外部APIを叩いたりすることです。
この層は外部の仕様変更で変わってしまうため、依存が発生しています。
ドライバー層としてレイヤーワケすることで、外部の影響をレイヤー内に閉じ込めています。
このレイヤーを変えるだけで仮に依存先の変更があっても簡単に置き換えることが可能です。
ドメイン層
ビジネスモデルやユーザーの行動を表現するそうです。
先程のゲームの例のようにそのアプリケーション特有のモデルを表現しています。
export class UserLog {
constructor(
readonly id: string,
readonly name: string,
readonly commit: boolean
) {}
}
クリーンアーキテクチャの例
私は色々な言語で説明したレイヤーリングで実装をしていますので、参考にして何か実装をしてみてください。そしてこの内容を照らし合わせて理解していただくのが最速なのかなと感じます。
TypeScript
https://github.com/jinwatanabe/sanpo
Go
https://github.com/jinwatanabe/go-todo-clean-app
Kolin
https://github.com/jinwatanabe/realize
Rust
https://github.com/jinwatanabe/rust-todo-clean-app
F#
https://github.com/jinwatanabe/fs-todo-clean-app
おわりに
今回はクリーンアーキテクチャを知らない人にペアプロしながら説明するときによく使うたとえ話についてまとめてみました。厳密にいうとすこし表現が異なるところもあるかもしれませんが、ぜひなんとなく意味を理解して実装しながら理解を深め身につけていただけたら嬉しいです。
アウトプットに関するノウハウを公開するセミナーを開きますのでお時間ある方はぜひご参加ください!!
お申し込みはこちら
ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。
また明日の記事でお会いしましょう!
おすすめ記事
JISOUのメンバー募集中
プログラミングコーチングJISOUではメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
気になる方はぜひHPからライン登録お願いします👇
About Me
Webエンジニア転身して1年間本気で勉強
転職ドラフトで高額オファーを複数獲得
そこでの経験からアウトプットやマインドセットの発信に強みがある
エンジニアをやりながら、起業に挑戦中
アイデア作りやパブリックスピーキングを得意としてる
■ Twitter
https://twitter.com/Sicut_study
■ stand.fm
https://stand.fm/channels/65db27e353b400abe28ec29a
■ Youtube
https://www.youtube.com/channel/UCBFrGAJhPuuaoLwUIHBILsA
■ プログラミングコーチングJISOU
https://projisou.jp
■ 体験で学ぶ!JISOU
https://experience-learn.connpass.com/
■ 登壇依頼などご依頼はこちらから
himaria.jin.watanabe@gmail.com