3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita株式会社Advent Calendar 2023

Day 10

Next.jsで実現するオニオンアーキテクチャ (2) - Domain Model/Domain Service

Last updated at Posted at 2023-12-09

記事における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で表現してみます。

models/user/Money.ts
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クラスを実装していきます。

models/user/User.ts
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間で行われる操作やビジネスロジックで、単一のエンティティに閉じ込めるには適さない処理を含むものです。

詳しくはこちらの記事でも紹介しています。

サンプルとして、「ユーザーからユーザーへ特定の通貨を送金する」処理を実装していみます。

models/user/UserService.ts
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をサンプルとして実装します。

models/user/IUserRepository.ts
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」について紹介していきます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?