🥵

ドメインモデル貧血症という言葉にとらわれる前に理解すること

2022/08/03に公開

前提

バックエンドのMVCアーキテクチャを例にする。単純なブログサービスを考える。

  • ユーザーがいて、ユーザーが記事を作成・編集・削除する。
  • ユーザーは自分のアカウント情報を編集できる。

モデルは2つ。

  • ユーザー
  • 記事

フレームワークのアーキテクチャは コントローラー - サービス - レポジトリ の3層とする。それぞれの責務は

  • コントローラーはIOに関することのみ興味を持つ
  • サービスはユースケースの中心。
  • レポジトリはDBに関することのみ興味をもつ。

理解すること

ここでいうモジュールとはそのモデルに関するファイル群のことを指す。例えばユーザーモジュールは

  • UserController
  • UserService
  • UserRepository
    のこと。

ロジックを散らばらせない。ロジックの散らばり具合で3例。

別のモジュールにそのモデルのロジックが書いてある。

例: なぜかブログサービスにユーザーアクティベート実装がある。関係ないのであればユーザーサービスでやる。

bad
class BlogService {
  activateUser(userId){
    const user = await this.userSerivce.findUserById(userId)
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    user.status = 'ACTIVE';
    
    await this.userService.update(user);
  }
}
good
class UserService {
  activateUser(userId){
    const user = await this.userSerivce.findUserById(userId)
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    user.status = 'ACTIVE';
    
    await this.userService.update(user);
  }
}

コントローラー - サービス - レポジトリの3層において、サービス以外にモデルのロジックが書いてある。

例: ユーザーアクティベート処理をコントローラーでやってしまっている。サービスにロジックは閉じ込める。

bad
class UserController {
  async activate(userId){
    const user = await this.userSerivce.findUserById(userId)
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    user.status = 'ACTIVE';
    
    await this.userService.update(user);
  }
}
good
class UserController {
  async activate(userId){
    await this.userSerivce.activate(userId);
  }
}

class UserService {
  async activate(userId){
    const user = await this.userRepository.findUserById(userId)
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    user.status = 'ACTIVE';
    
    await this.userRepository.update(user);
  }
}

サービス内において、if文が出てきたり、直接モデルの値を更新している。

bad
class UserService {
  async activate(userId){
    const user = await this.userRepository.findUserById(userId)
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    user.status = 'ACTIVE';
    
    await this.userRepository.update(user);
  }
}
good
class UserService {
  async activate(userId){
    const user = await this.userRepository.findUserById(userId)
    
    user.activate();
    
    await this.userRepository.update(user);
  }
}

class User {
  // 省略
  
  activate(){
    if(user.status === 'ONBOAEDING'){
      throw new Error('ONBOAEDINGステータスのときのみactivateできます');
    }
    
    this.status = 'ACTIVE';
  }
}

まとめ

ポイントは

  • 再利用性
  • 修正用意性

そのために

  • ロジックをできる限りまとめる

ロジックとは基本的には

  • if文などの条件
  • モデルの値の変更
  • 整合性を保つためのルール(例えばcという値は必ずaとbの足し算担っていないといけないなど)

そうすれば「コアはシンプルに保ち、周辺部は多様にする、ツリー型のワークフロー」になる。
https://zenn.dev/dove/articles/68faf940a311b6

Discussion