りゅうじの学習blog

学習したことをアウトプットしていきます。

5/20 ドメイン駆動設計Chapter6 ユーザ情報更新処理を作成するまで

ユーザ情報を更新する

  • 項目ごとに別々のユースケースにするか単一のユースケースで複数項目を同時更新できるようにするか
    • 項目ごとに別々のユースケースとは、ユーザ名・メルアド・住所とそれぞれを別々のユースケースとするという意味です
    • 複数項目同時更新とは、すべてひっくるめて、ユーザ情報更新という一つのユースケースとするという意味です

今回は、複数項目を同時更新できるユースケースをサンプルにします。

class UserApplicationService {
  constructor(private readonly userRepository: IUserRepository, private readonly userService: UserService) {}

  async updateUser(userId: string, name: string): Promise<void> {
      const targetId = new UserId(userId);
      const user = await this.userRepository.find(targetId);

      if(user === null) {
        throw new Error('ユーザが存在しません');
      }

      const newUserName = new UserName(name);
      user.changeName(newUserName);
      
      await this.userRepository.save(user);
    }
}
  • updateUserメソッドを定義
    • Userにはユーザ名しかパラメータがないので引数として新しいユーザ名を受け取っています
      • 今後Userにはユーザ名以外の項目も増えるがその対応はどうするか

ユーザ情報としてメールアドレス追加された場合の更新処理

async updateUser(userId: string, name: string, email: string): Promise<void> {
      const targetId = new UserId(userId);
      const user = await this.userRepository.find(targetId);

      if(user === null) {
        throw new Error('ユーザが存在しません');
      }
      
      // メールアドレスだけを更新するため、ユーザ名が指定されない場合は、ユーザ名更新はスキップする
      if(name !== null) {
        const newUserName = new UserName(name);
        user.changeName(newUserName);
      }

      // メールアドレスを変更できるように、emailがnullではない場合のみ、処理を行う
      if(email !== null) {
        const newEmail = new Email(email);
        user.changeEmail(newEmail);
      }

      await this.userRepository.save(user);
    }
  • Emailクラスを実装したと仮定
    • 情報更新には、ユーザ名・メールアドレスのみ更新したい場合がある
      • 引数にデータを引き渡すか引き渡さないかで挙動を制御できるようにする

この方法ではユーザ情報が追加されるたびにアプリケーションサービスのメソッドのシグネチャ(メソッド名、パラメータの型、数、順序、戻り値の型等で構成された、メソッドを識別するための情報という意味があります)が変更されることになる。

この問題を避ける方法としてコマンドオブジェクトを用いる戦略がある。

// 名前とメールアドレスが任意だとコンストラクタをオプショナルにすることで主張してもいい
class UserUpdateCommand {
  public name: string | null;
  public email: string | null;

  constructor(public id: string, name: string | null = null, email: string | null = null) {
    this.name = name;
    this.email = email;
  }
}
  • nullではなくundefinedで行わないとstrictmodeではエラーになりますが、一旦書籍に倣ってアウトプットします
async updateUser(command: UserUpdateCommand): Promise<void> {
      const targetId = new UserId(command.id);
      const user = await this.userRepository.find(targetId);
      
      if(user === null) {
        throw new Error('ユーザが存在しません');
      }
      
      // メールアドレスだけを更新するため、ユーザ名が指定されない場合は、ユーザ名更新はスキップする
      const name = command.name;
      if(name !== null) {
        const newUserName = new UserName(name);
        user.changeName(newUserName);
      }

      // メールアドレスを変更できるように、emailがnullではない場合のみ、処理を行う
      const email = command.email;
      if(email !== null) {
        const newEmail = new Email(email);
        user.changeEmail(newEmail);
      }

      await this.userRepository.save(user);
    }
  • updateUserの引数にコマンドオブジェクト受け取るように変更
  • パラメータが追加されてもシグネチャが変更されることはなくなりました
    • updateUserメソッドが取り扱うデータのフィールド(プロパティ)が追加や変更されても、メソッド自体の引数の数や型は変わらないという利点があります
      • コマンドオブジェクトの内部構造が変わっても、それを使用するサービスクラスのメソッド(この例ではupdateUser)は影響を受けません。つまり、サービスクラスのメソッドのシグネチャ(引数の数や型、返り値の型)は安定しています
  • コマンドオブジェクトを作ることは間接的にアプリケーションサービスの処理を制御することと同義です
// ユーザ名変更だけを行うように
const updateNameCommand = new UserUpdateCommand(id);
updateNameCommand.name = 'haga';

userApplicationService.updateUser(updateNameCommand);

// メールアドレス変更だけを行うように
const updateEmailCommand = new UserUpdateCommand(id);
updateEmailCommand.email = 'test@test.com';

userApplicationService.updateUser(updateEmailCommand);
  • コマンドオブジェクトは処理のファサード(建物の正面という意味。転じて複雑な処理を単純な操作にまとめることを意味する)と呼ばれます。

ここまでTypeScript Playgroundで書いてきましたが、やはり、nullではなくundefindで実装した方がコードの可読性も上がるし、? のオプショナルも使えるしいいと思います。

コマンドオブジェクト

// 名前とメールアドレスが任意だとコンストラクタをオプショナルにすることで主張してもいい
class UserUpdateCommand {
  public name?: string;
  public email?: string;

  constructor(public id: string, name?: string, email?: string) {
    this.name = name;
    this.email = email;
  }
}

アプリケーションサービス

class UserApplicationService {
  constructor(private readonly userRepository: IUserRepository, private readonly userService: UserService) {}

  async updateUser(command: UserUpdateCommand): Promise<void> {
      const targetId = new UserId(command.id);
      const user = await this.userRepository.find(targetId);
      
      if(user === null) {
        throw new Error('ユーザが存在しません');
      }
      
      // メールアドレスだけを更新するため、ユーザ名が指定されない場合は、ユーザ名更新はスキップする
      const name = command.name;
      if(name !== undefined) {
        const newUserName = new UserName(name);
        user.changeName(newUserName);
      }

      // メールアドレスを変更できるように、emailがnullではない場合のみ、処理を行う
      const email = command.email;
      if(email !== undefined) {
        const newEmail = new Email(email);
        user.changeEmail(newEmail);
      }

      await this.userRepository.save(user);
    }
}

次回は退会処理を作成するからです。

参考

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本