Save 37% off PRO during our Black Friday Sale! »

結果整合性ができない開発者のドメインイベント活用例 / rakus-meetup-tokyo-5-how-to-use-domainevent

結果整合性ができない開発者のドメインイベント活用例 / rakus-meetup-tokyo-5-how-to-use-domainevent

Rakus Meetup Tokyo #5 「SaaSを支える開発原則」の登壇資料です。
https://rakus.connpass.com/event/178046/

0c7a284c3c3cdbe89fe6b12156e955e7?s=128

goldminerks

June 24, 2020
Tweet

Transcript

  1. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ©2020 RAKUS Co., Ltd. 結果整合性ができない開発者の

    ドメインイベント活用例 開発統括部 第三開発部 峰村 崇
  2. #RAKUSMeetup ©2020 RAKUS Co., Ltd. アジェンダ •ドメインイベントを使うことのうれしみ •ドメインイベント活用例 •集約とドメインイベントの関係 •チーム内にドメインイベントを定着させるため

    の取り組み
  3. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使うことのうれしみ ドメイン駆動設計(DDD)では「コードが仕様を語 る」ことが重要。 コードに語らせるためにはコードの語彙力を増す必 要がある。

    ドメインイベントを使えるようになることでコード の語彙力が増す。
  4. #RAKUSMeetup ©2020 RAKUS Co., Ltd. DDD でよく見かけるモデル概念 •エンティティ •値オブジェクト •リポジトリ

    •サービス(ドメインサービス) •ドメインイベント
  5. #RAKUSMeetup ©2020 RAKUS Co., Ltd. DDD でよく見かけるモデル概念 • エンティティ、値オブジェクト ⇒クラス図で表せるのでオブジェクト指向言語と相性がよい

    ・リポジトリ ⇒DAO と大体同じだよね(ちょっと乱暴) ・サービス ⇒プロセスを書けばいいんだよね(ちょっと乱暴) ・ドメインイベント ⇒何それ?どういうときに使うの?
  6. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントとは? •アプリケーションで起きた出来事を表現する •「◦◦が起きた時に△△になる」を実現する

  7. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント活用例 <前提> ログインアカウントのIDとパスワードで認証(ロ グイン)するとアクセストークンが発行される アクセストークンには有効期限がある

  8. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント活用例 <モデリングするケース>アクセストークンを有 効期限が切れる前に削除するケース ・ログインIDを変更する ・ログインパスワードを変更する

  9. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使わないコード 1/2 public class LoginAccountService

    { /** * ログインIDを変更する. */ public void changeLoginId(String loginId, String newLoginId) { LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changeLoginId(newLoginId); loginAccountRepository.save(loginAccount); // 発行済みのアクセストークンを削除する. accessTokenRepository.deleteAll(newLoginId); } /** * ログインパスワードを変更する. */ public void changePassword(String loginId, String newPassword) { LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changePassword(newPassword); loginAccountRepository.save(loginAccount); // 発行済みのアクセストークンを削除する. accessTokenRepository.deleteAll(loginId); } }
  10. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使わないコード 2/2 public class LoginAccount

    { public String loginId; public String password; public boolean invalid; public void changeLoginId(String newLoginId) { this.loginId = newLoginId; } public void changePassword(String newPassword) { this.password = newPassword; } }
  11. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使わないコード 前ページのコードの問題点 ・アクセストークンを削除する理由が書かれていない ・各ケースの処理の後ろにアクセストークンの削除処理を直列につなげている だけで何も抽象化できていない

    ・アクセストークンの削除プロセスがドメイン層に書かれていない 新たに「ログインアカウントを無効化する」処理を追加することになったとき アクセストークンを削除すべきかどうかが判断できない
  12. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント活用例 <再掲>アクセストークンを有効期限が切れる前に削除するケース ・ログインIDを変更する ・ログインパスワードを変更する 「何故この2つのケースでアクセストークンを削除しなければならないのか?」

    を考えることが抽象化(=モデリング)である。 <問いの答え> ログインアカウントの認証資格が変更されたので古い資格で発行されたアクセストークン は削除しなければならない 「ログインアカウントを無効化する」のケースでは無効化も認証資格の変更であるので 「アクセストークンを削除する」と判断できる
  13. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント活用例 <再掲>DDD で重要なこと コードが仕様を語る 前述のコードは「ログインアカウントの認証資格が

    変更されたら古い資格で発行されたアクセストーク ンを削除する」を語っていない
  14. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント活用例 「ログインアカウントの認証資格が変更されたら 古い資格で発行されたアクセストークンを削除す る」は 「◦◦が起きた時に△△になる」の形式

    になっているのでドメインイベントを使うのが適 切
  15. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使ったコード 1/3 public class LoginAccountService

    { /** * ログインIDを変更する. */ public void changeLoginId(String loginId, String newLoginId) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changeLoginId(newLoginId); loginAccountRepository.save(loginAccount); } /** * ログインパスワードを変更する. */ public void changePassword(String loginId, String newPassword) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changePassword(newPassword); loginAccountRepository.save(loginAccount); } /** * ログインアカウントを無効化する. */ public void invalidate(String loginId) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.invalidate(); loginAccountRepository.save(loginAccount); } } オブザーバパターンを使って いる。 サブスクライバーを登録する ことで処理を待ち受けする。
  16. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使ったコード 2/3 public class LoginAccount

    { public String loginId; public String password; public boolean invalid; public void changeLoginId(String newLoginId) { this.loginId = newLoginId; DomainEventPublisher.publish(new CredentialChanged(loginId)); } public void changePassword(String newPassword) { this.password = newPassword; DomainEventPublisher.publish(new CredentialChanged(loginId)); } public void invalidate() { this.invalid = true; DomainEventPublisher.publish(new CredentialChanged(loginId)); } } ドメインイベントを発行する ことで本来の処理の後にアク セストークンを削除するプロ セスであることを表している
  17. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使ったコード 3/3 public class CredentialChangedSubscriber

    implements DomainEventSubscriber<CredentialChanged> { /** * 認証資格の変更により無効になったトークンを削除する. */ @Override public void handleEvent(CredentialChanged domainEvent) { accessTokenRepository.deleteAll(domainEvent.getLoginId()); } @Override public Class<CredentialChanged> subscribedToEventType() { return CredentialChanged.class; } }
  18. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベントを使ったコード ドメインイベントを使わないコードの問題点を解決している ・アクセストークンを削除する理由は認証資格が変更された結果であることが明示されて いる ・各ケースの処理を認証資格の変更処理に抽象化できている

    ・アクセストークンの削除プロセスがドメインモデルである「LoginAccount」に書かれ ている ※「DomainEventPublisher」を使ったドメインイベントの発行・購読については 「実践ドメイン駆動設計」(ヴァーン・ヴァーノン著)に出てくる 「軽量オブザーバパターン」を参考にしました。
  19. #RAKUSMeetup ©2020 RAKUS Co., Ltd. チーム内にドメインイベントを定着させるための 取り組み ・どういう場合にドメインイベントを使うのか慣れないと難し い ・DDD

    では複数の「集約」にまたがって不変条件を担保しなけ ればならない更新処理でドメインイベントを使うように言われ ているが難易度が高い ドメインイベントの導入の難易度を下げる必要がある。
  20. #RAKUSMeetup ©2020 RAKUS Co., Ltd. 集約について 集約とは: 「関連したオブジェクトの集合で、データを変更する目的で1つの単 位として扱われるもの」であり、 「一貫性に関するルールの集合が、集約の境界内に適用される」

    それゆえ: 複数の集約にまたがってトランザクションをかけてはいけない 「 updating one aggregate per transaction 」のルールが存在する
  21. #RAKUSMeetup ©2020 RAKUS Co., Ltd. (再掲)ドメインイベントを使わないコード public class LoginAccountService {

    /** * ログインIDを変更する. */ public void changeLoginId(String loginId, String newLoginId) { LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changeLoginId(newLoginId); loginAccountRepository.save(loginAccount); // 発行済みのアクセストークンを削除する. accessTokenRepository.deleteAll(newLoginId); } /** * ログインパスワードを変更する. */ public void changePassword(String loginId, String newPassword) { LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changePassword(newPassword); loginAccountRepository.save(loginAccount); // 発行済みのアクセストークンを削除する. accessTokenRepository.deleteAll(loginId); } } ドメインイベントを使わない コードでは2つの集約(ログ インアカウントとアクセス トークン)を一つのトランザ クションで更新している
  22. #RAKUSMeetup ©2020 RAKUS Co., Ltd. 集約とドメインイベントの関係 「 updating one aggregate

    per transaction 」のルールを遵守しようとすると複数の集約にまたがって不変条件を担保しなければならない更新処理は次 のような流れになる 1. ある集約を更新する 2. ドメインイベントを発行する 3. ドメインイベントを購読(subscribe)して別の集約を更新する 1と3は非同期で実行し結果整合性を担保する 詳しくは「結果整合性」「イベント駆動アーキテクチャ」などの単語で調べてみてください。 その結果: 結果整合性を担保するために処理のべき等性やリトライなど種々の仕組みが必要になり非常に実装コストが高い 補足: 複数の集約をまたがない場合はドメインイベントの出番はないと考えてよい 「◦◦が起きた時に△△になる」がドメインイベントだが一つの集約しか出てこない場合は「◦◦が起きる」でしかない
  23. #RAKUSMeetup ©2020 RAKUS Co., Ltd. ドメインイベント導入の障害 「 updating one aggregate

    per transaction 」のルールを遵守しようとすると実 装コストが高くなる その結果: 1. 集約を巨大にして複数の集約にまたがらないようにする 2. 「 updating one aggregate per transaction 」のルールを守らない 上記のどちらかで思考停止してしまう。 複数の集約にまたがる不変条件は本質的に複雑な処理であり、思考停止してモ デリングの深化を止めるべきではない。
  24. #RAKUSMeetup ©2020 RAKUS Co., Ltd. チーム内にドメインイベントを定着させるための 取り組み 「 updating one

    aggregate per transaction 」のルー ルを守るのが難しいので守ることのできるルールを 導入する それが「updating one aggregate per application service」(一つのアプリケーションサービスで更新 してよい集約は一つだけ)のルール
  25. #RAKUSMeetup ©2020 RAKUS Co., Ltd. チーム内にドメインイベントを定着させるための 取り組み 「 updating one

    aggregate per application service 」のルール下での複数の集約にまたがって不変条件を担保しなければな らない更新処理の流れ 1. アプリケーションサービスAである集約を更新する 2. ドメインイベントを発行する 3. ドメインイベントを購読(subscribe)してアプリケーションサービスBを呼び出す 4. アプリケーションサービスBで別の集約を更新する 1と4は非同期でなく同期処理でも可とする この方式のメリット: ・複数の集約にまたがる処理ではドメインイベントを使わなければならないので ドメインイベントを使用する方向にバイアスが掛かる ・同期処理ではあるがアプリケーションサービスが別なので責務は分離されている
  26. #RAKUSMeetup ©2020 RAKUS Co., Ltd. (再掲)ドメインイベントを使ったコード 1/3 public class LoginAccountService

    { /** * ログインIDを変更する. */ public void changeLoginId(String loginId, String newLoginId) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changeLoginId(newLoginId); loginAccountRepository.save(loginAccount); } /** * ログインパスワードを変更する. */ public void changePassword(String loginId, String newPassword) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.changePassword(newPassword); loginAccountRepository.save(loginAccount); } /** * ログインアカウントを無効化する. */ public void invalidate(String loginId) { // サブスクライバーを登録する. DomainEventPublisher.subscribe(new CredentialChangedSubscriber()); LoginAccount loginAccount = loginAccountRepository.get(loginId); loginAccount.invalidate(); loginAccountRepository.save(loginAccount); } } このアプリケーションサービ スでは一つの集約(ログイン アカウント)のみ更新してい る
  27. #RAKUSMeetup ©2020 RAKUS Co., Ltd. (再掲)ドメインイベントを使ったコード 3/3 public class CredentialChangedSubscriber

    implements DomainEventSubscriber<CredentialChanged> { /** * 認証資格の変更により無効になったトークンを削除する. */ @Override public void handleEvent(CredentialChanged domainEvent) { accessTokenRepository.deleteAll(domainEvent.getLoginId()); } @Override public Class<CredentialChanged> subscribedToEventType() { return CredentialChanged.class; } } ここでは一つの集約(アクセス トークン)のみ更新(削除)し ている ※このコードは省略して書いて ありサブスクライバーで直接リ ポジトリを操作していますが、 実際はアプリケーションサービ スを呼び出すだけでトークンの 削除は呼び出し先のアプリケー ションサービスが行います
  28. #RAKUSMeetup ©2020 RAKUS Co., Ltd. まとめ ・ドメインイベントを使うことでコードの表現力が増し た例を見ていただいた ・DDD の原則通りにやろうとするとドメインイベントの

    導入に困難を伴うことを見ていただいた ・DDD の原則をいい感じに破ることでドメインイベント の導入の難易度を下げられることを見ていただいた