はじめに
今回は実際に私自身がクリーンアーキテクチャに則った実装経験をした際の備忘録になります。
下記のような方の参考になれば幸いです。
・クリーンアーキテクチャについて勉強を始めたけどよく分からない人
・小難しい内容ではなくもう少し簡単に内容に入りたい人
・Webアプリケーションで実際に実装してみるときどんな風に考えてたか知りたい人
クリーンアーキテクチャの階層とそれに伴う実際のディレクトリ構成について
クリーンアーキテクチャのレイヤーでは以下の4つが存在しています。
- Enterprise Business Rules
- Application Business Rules
- Interface Adapters
- Frameworks & Drivers
それぞれについての解説は色々な方が詳細に解説されているので確認してみてください。
でこのレイヤー層を私が言い換えてみたのが下記になります。
- 型定義
- データ加工
- データ取得
- UI
実際に各項目をディレクトリ構成に落としんだのがこちら
(今回はNuxtのディレクトリ構成をベースにしています。)
https://nuxt.com/docs/4.x/directory-structure
nuxt4-project/
├── app/
│ ├── assets/
│ ├── components/ //4. UI,それに関連する内容
│ ├── composables/
│ ├── repositories 3. データ取得
│ ├── useCase // 2. データ加工
│ ├── layouts/
│ ├── middleware/
│ ├── pages/ //4. UI,それに関連する内容
│ ├── plugins/
│ ├── stores/
│ ├── types/ //1. 型定義
│ ├── app.vue
│ └── error.vue
それぞれのディレクトリでコーディングの内容
1.型定義
┗とにかく型定義はここに宣言する。
// app/types/product.ts
// APIから返ってくる生データの型
export type ProductResponse = {
product_id: number;
product_name: string;
price_amount: number;
stock_count: number;
}
// UIで利用する型
export type Product = {
id: number;
name: string;
price: string; // 通貨記号付きの文字列に加工
stock: number;
status: 'available' | 'out_of_stock'; // 在庫状況を判定した結果
}
2.データ取得
┗APIをコールして結果を返すだけに限定。(UseCaseとかUI層からもらった引数でクエリパラメータとかパスパラメータを設定するくらいは許容)
// app/composables/repositories/useProductRepository.ts
import type { ProductResponse } from '~/types/product'
export const useProductRepository = () => {
const fetchProducts = async (): Promise<ProductResponse[]> => {
// 実際には $fetch で API を叩く
return await $fetch<ProductResponse[]>('/api/v1/products')
}
return { fetchProducts }
}
3.データ加工
┗データの加工のみに注力
(ここではレスポンスデータを画面へ連携する前に加工している)
// app/composables/useCase/useProductList.ts
import type { Product, ProductResponse } from '~/types/product'
export const useProductList = () => {
const { fetchProducts } = useProductRepository()
const getProductList = async (): Promise<Product[]> => {
const rawData = await fetchProducts()
// データのマッピング (加工)
return rawData.map((item): Product => ({
id: item.product_id,
name: item.product_name,
price: `¥${item.price_amount.toLocaleString()}`,
stock: item.stock_count,
status: item.stock_count > 0 ? 'available' : 'out_of_stock'
}))
}
return { getProductList }
}
4.UI
┗そのまんまでUIの実装とそれに伴う内容をscriptに記載
(テーブルのヘッダー定義とかもUIに関係するのでここに実装)
// app/pages/products/index.vue
<script setup lang="ts">
const { getProductList } = useProductList()
// Vuetify のテーブルヘッダー設定
const headers = [
{ title: '商品ID', key: 'id' },
{ title: '商品名', key: 'name' },
{ title: '価格', key: 'price' },
{ title: '在庫数', key: 'stock' },
{ title: '状態', key: 'status' },
]
// データの取得(VueとかならonMountedのタイミングとかがいいかも)
const { data: products } = await useAsyncData('products', () => getProductList())
</script>
<template>
<v-container>
<h2>商品在庫一覧</h2>
<v-data-table
:headers="headers"
:items="products || []"
class="elevation-1"
>
<template #[`item.status`]="{ value }">
<v-chip :color="value === 'available' ? 'green' : 'red'">
{{ value === 'available' ? '在庫あり' : '品切れ' }}
</v-chip>
</template>
</v-data-table>
</v-container>
</template>
未来へ
現時点での筆者の理解度はこんな感じになります。お恥ずかしながらクリックイベント関数に処理ガン積みしていた過去もあるので非常に良い体験でした。とはいえまだまだ理解度では体感3~4割程度なので学習を経てさらにブラッシュアップした内容でまたお届けできればと思います。