記事におけるNext.jsは、v13以降のApp Router (Server Components)を用いる前提での説明となります。
Pages Routerを用いている場合は、適宜Pages Routerでの書き方として読み替えてください。
この記事は何
最近Next.jsでウェブアプリケーションを作ることが多いのですが、
その時にオニオンアーキテクチャを採用してアプリケーションを作ることが多く、型も固まってきたので数回の記事にわたり紹介をしていきたいと思っています。
前回は
でディレクトリ構成について紹介しました。
今回はmodels
ディレクトリに実装していくDomain Model/Domain Serviceについて説明をしていきます。
Domain Model/Domain Serviceとは
Domain Model/Domain Serviceとは、特定の問題領域を表現するために使用される概念的なクラスを実装していくレイヤーです。
の記事で書いた「モデリング」のプロセスで実装していくレイヤーになります。
このレイヤーはオニオンアーキテクチャでは最下層/最も中心になるレイヤーであり、基本的にどのレイヤーにも依存しないコードの記述を行なっていきます。
Domain Modelでは大きく
- Value Object
- Entity
- Infrastructure Interface
という三種類のクラスの実装を行なっていくことになります。
次項では、それぞれのクラスの役割や実装方法について紹介を行っていきます。
なお、Domain Serviceもレイヤーとしては分かれているものですが、基本的に役割はDomain Modelのレイヤーと変わりません。
Domain Serviceについては以下の記事で説明しています。
https://qiita.com/getty104/items/3578662b455f58221203 で紹介した通り、Domain ModelとDomain Serviceは同じディレクトリに実装を行うため、この記事では二つのレイヤーの実装を一緒に説明していきます。
つまり、models
ディレクトリでは
- Value Object
- Entity
- Domain Service
- Infrastructure Interface
の4つのクラスの実装を行なっていきます。
クラスの説明
前項で紹介した通り、models
ディレクトリには
- Value Object
- Entity
- Domain Service
- Infrastructure Interface
の4つのクラスについて実装を行なっていきます。
これらのクラスの役割と、Next.jsでの実装方法を紹介します。
なお、Next.jsといっていますが、このレイヤーは基本的に外部のライブラリなどに依存しないように実装を行うため、このレイヤーの実装ではNext.js特有のテクニックなどはほとんど使いません。
今回はユーザーが様々な通貨を保持することができるようなデータ構造を考えサンプルの実装を行います。
Value Object
Value Objectは不変性を持ち、等価性によって識別されるオブジェクトです。つまり、それらはビジネス上の概念や値を表現するために使われ、それ自体には識別子(ID)を持たず、その属性の組み合わせによって定義されます。
今回は通貨をValue Objectで表現してみます。
type CurrencyType = "dollar" | "yen";
export class Money {
public readonly amount: number;
public readonly currencyType: CurrencyType;
constructor(args: { amount: number; currencyType: CurrencyType }) {
this.amount = args.amount;
this.currencyType = args.currencyType;
}
public isEqual(money: Money) {
return this.amount === money.amount && this.currencyType === money.currencyType;
}
}
Entity
Entityは独自の識別子(ID)を持ち、アプリケーションのビジネスルールをカプセル化するドメインオブジェクトです。Entityはその識別子によって区別され、その状態(プロパティ)は処理を通して変化させることが可能です。Entityの状態はRepositoryを通じてデータベースなどの外部リソースで永続化されます。(Repositoryについては別の記事で説明していきます)
Value Objectのサンプルで用意したMoney
クラスを用いて、User
クラスを実装していきます。
imoprt { Money } from "./Money";
export class User {
private monies: Money[]
public readonly uuid: string
constructor(args: { monies: Money[], uuid: string }) {
this.monies = args.monies;
this.uuid = args.uuid;
}
public isEqual(user: User) {
this.uuid === user.uuid
}
public getMonies() {
return this.monies
}
public addMoney(money: Money) {
const targetMoney = this.monies.find(m => m.currencyType === money.currencyType)
if(targetMoney) {
this.monies = this.monies.map(m => {
return m.currencyType === money.currencyType ? new Money({ currencyType: m.currencyType, amount: m.amount + money.amount }) : m
})
} else {
this.monies.push(money)
}
}
public subtractMoney(money: Money) {
const targetMoney = this.monies.find(m => m.currencyType === money.currencyType)
if(!targetMoney) throw new Error('指定された通貨タイプのお金が見つかりません');
this.monies = this.monies.map(m => {
if(m.currencyType === money.currencyType) {
if(m.amount < money.amount) throw new Error('所持金が足りません');
return new Money({ currencyType: m.currencyType, amount: m.amount - money.amount })
} else {
return m
}
})
}
}
Domain Service
Domain Serviceは特定のEntityに属さないドメインロジックをカプセル化する役割を持ちます。これらは複数のEntityやValue間で行われる操作やビジネスロジックで、単一のエンティティに閉じ込めるには適さない処理を含むものです。
詳しくはこちらの記事でも紹介しています。
サンプルとして、「ユーザーからユーザーへ特定の通貨を送金する」処理を実装していみます。
import { Money } from "./Money";
import { User } from "./User";
export class UserService {
public sendMoney(args: { sendUser: User; receiveUser: User; money: Money }) {
args.sendUser.subtractMoney(args.money)
args.receiveUser.addMoney(args.money)
}
}
Infrastructure Interface
Infrastructure Interfaceは、Infrastructureレイヤーで実装していくクラスのインターフェースを定義していきます。
この後実装していくApplication Serviceレイヤーなどでは、このInfrastructure Interfaceで実装したインターフェースに依存した実装にしていくことで、Infrastructureレイヤーに依存せずに実装を行うことができるようになります。
Application Serviceについてはまた別の記事で説明を行います。
Userの永続化を行うUserRepositoryのインターフェースIUserRepository
をサンプルとして実装します。
import { User } from "./User"
export interface IUserRepository {
save(user: User): Promise<User>;
findByUuid(uuid: string): Promise<User | null>;
}
ディレクトリの分け方について
ここまでmodels
で実装していくコードについて紹介してきましたが、今回はmodels
以下にもuser
というディレクトリを切り、その中にクラスの実装を行なっています。
どのような基準でディレクトリを分けていくと良いかについて最後紹介していきます。
このディレクトリの分け方は、DDDなどで語られることの多い「集約」という考え方を基準に決めていくと良いと考えています。
今回はMoney
は基本的にUser
を通して扱われるものだと判断したため、user
という同じネームスペース内に入れています。
しかしMoney
をDBなどに永続化などを行い、User
ではなく管理画面など、別の流れでMoney
の操作をしたいという場面もあるかもしれません。
このような場合は「境界づけられたコンテキスト」という考え方を用い、改めて別のネームスペースでmoneyの値が保存されているテーブルの値を扱うようなクラスを記述していくという実装を行なっていくことになります。
この辺りの説明は
を参考にしてみてください。
まとめ
今回はDomain Model/Domain Serviceについての紹介を行いました。
レイヤーの特性上、ほとんどNext.jsに関係する話は出てきませんでしたが、今回紹介した考え方を用いることで、Next.jsなどでもある程度クリーンにビジネスロジックの実装を行なっていくことが可能になります。
次回は「Application Service」について紹介していきます。