Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Learning Domain-Driven Design Round-reading session 6

Learning Domain-Driven Design Round-reading session 6

『Learning Domain-Driven Design』 輪読会 No.6 の資料です。

## イベント
https://showcase-gig.connpass.com/event/250332/

Daisuke Sato

June 16, 2022
Tweet

More Decks by Daisuke Sato

Other Decks in Technology

Transcript

  1. Learning Domain-Driven Design 輪読会 No.6 Chapter 5. Implementing Simple Business

    Logic https://showcase-gig.connpass.com/event/250332/ 2022/04/21 daisuke.sato @dskst9
  2. Chapter 5. Implementing Simple Business Logic 5-1. Transaction Script/トランザクションスクリプト 5-2.

    Active Record/アクティブレコード 5-3. Be Pragmatic/現実的である 5-4. Conclusion/結論 5-5. Exercise/エクササイズ
  3. Organizes business logic by procedures where each procedure handles a

    single request from the presentation. -Martin Fowler ビジネスロジックをプロシージャで整理し、各プロシージャがプレゼンテーションからの1 つの要求を処理する。
  4. JSONファイルのバッチをXMLファイ ルに変換するトランザクションスクリプ トの例。 DB.StartTransactions(); var job = DB.LoadNextJob(); var json

    = LoadFile(job.Source); var xml = ConbertJsonToXml(json); WriteFile(job.Destination, xml.ToString()); DB.MarkJobAsCompleted(job); DB.Commit(); 5-1-1. Implementation
  5. トランザクションの実装に失敗する例: 包括的なトランザクションを使用せず に複数の更新をした場合。 public class LogVisit { public void Execute(Guid

    userId, DateTime visitedOn) { // Usersテーブルのレコードを更新 _db.Execute("UPDATE Users SET lats_visit=@p1 WHERE user_id=@p2", visitedOn, userId); // VisitsLogテーブルにレコードを挿入する _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date) VALUES (@p1, @p2)", userId, visitedOn); } } Lack of transactional behavior
  6. Usersテーブルのレコードが更新され た後、ログレコードの追加に成功する 前に何らかの問題が発生した場合、 システムは矛盾した状態に陥る。 public class LogVisit { … public

    void Execute(Guid userId, DateTime visitedOn) { // Usersテーブルのレコードは更新される _db.Execute(“"UPDATE Users SET lats_visit=@p1 WHERE user_id=@p2", visitedOn, userId); // VisitsLogテーブルには対応するレコードが書き込まれない _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date) VALUES (@p1, @p2)", userId, visitedOn); } } Lack of transactional behavior
  7. RDBは、複数のレコードにまたがるトランザ クションをサポートしているため、この修正 は簡単。 しかし、複数レコードのトランザクションをサ ポートしていないデータベースや、複数の ストレージ機構を扱う場合は複雑になる。 public class LogVisit {

    public void Execute(Guid userId, DateTime visitedOn) { try { _db.startTransactions(); _db.Execute(@"UPDATE Users SET last_visit=@p1 WHERE user_id=@p2", visitedOn, userId); _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date) VALUES (@p1, @p2)", userId, visitedOn); _db.Commit(); } catch { _db.Rollback(); throw; } } } Lack of transactional behavior
  8. 先ほどの例で、テーブルへの訪問をログに 記録する代わりに、メッセージバスに発行 するとどうなるか。 前の例と同様に、処理途中で発生した障害 はシステムの状態を破壊できる。 public class LogVisit { public

    void Execute(Guid userId, DateTime visitedOn) { // Usersテーブルのレコードは更新される _db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2", visitedOn, userId); // メッセージバスへの発行に失敗すると、 // 他のコンポーネントには通知されない _messageBus.Publish(“VISITS_TOPIC”, new { UserId = userId, VisitDate = visitedOn }); } } Distributed transactions
  9. 最終訪問日を追跡する代わりに、訪問回 数を保持するケースにするとどうなるか。 しかし、これはまだ分散トランザクションで あり、潜在的に状態の不整合につながる可 能性がある。 public class LogVisit { //

    メソッドを呼び出すと、対応するカウンターの値が 1増える public void Execute(Guid userId) { // 1つのデータベース内の 1つのテーブルの 1つの値を更新するだけ _db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id=@p1", userId); } } Implicit distributed transactions
  10. データベースを更新するメソッドがvoid 型でも、操作が成功したか失敗したか は伝えられるので、失敗したら呼び出し 元は例外を受け取る。 もしメソッドが成功しても、呼び出し側へ の結果の伝達が失敗したらどうなるの か。 Implicit distributed transactions

    • LogVisit が REST サービスの一 部であり、ネットワークに障害が発 生した場合。 • LogVisit と呼び出し元の両方が 同じプロセスで実行されていて、呼 び出し元が LogVisit アクションの 正常な実行を追跡する前に、プロ セスが失敗した場合。
  11. 別の方法は、楽観的同時実行制御を使用 する。 LogVisit 操作を呼び出す前に、呼び出し 元はカウンターの現在値を読み取り、それ をパラメータとして LogVisit に渡す。 LogVisit は、カウンタの値が呼び出し元に

    よって最初に読み取られたものと等しい場 合にのみ、カウンタの値を更新する。 public class LogVisit { … public void Execute(Guid userId, long expectedVisits) { _db.Execute(@"UPDATE Users SET visits=visits+1 WHERE user_id=@p1 and visits=@p2", userId, visits); } } Implicit distributed transactions
  12. トランザクションスクリプトパターンの主 な利点は、そのシンプルさ。 最小限の抽象化を導入し、実行時のパ フォーマンスとビジネスロジックを理解 するためのオーバーヘッドを最小限に 抑える。 5-1-3. When to Use

    Transaction Script しかし、このシンプルさは、このパターン の欠点。 ビジネスロジックが複雑になればなるほ ど、トランザクション間でビジネスロジッ クが重複しやすくなり、その結果、重複 したコードが同期しなくなり、動作に一貫 性がなくなる。
  13. An object that wraps a row in a database table

    or view, encapsulates the database access, and adds domain logic on that data. -Martin Fowler データベースのテーブルやビューの行をラップし、データベースへのアクセスをカプセル 化し、そのデータにドメインロジックを追加するオブジェクト。
  14. アクティブレコードパター ンはツールである 5-2-2. When to Use Active Record アクティブレコードは、デザインパター ンを指すのであって、アクティブレコー

    ドフレームワークを指すのではない。他 のツールと同様に、問題を解決するこ とができるが、間違った文脈で適用さ れると、良いことよりも悪いことをもたら す可能性がある。 ビジネスロジックが単純であれば、アク ティブレコードを使うことに何の問題も ない。
  15. Exercises 高レベルのトランザクションメカニズムがないと仮定 すると、潜在的なデータの一貫性の問題を見つけるこ とができますか? a. 新しいチケットを受け取ると、割り当てられた エージェントのアクティブチケットのカウンターを 1 以上増やすことができる b.

    エージェントのアクティブチケットのカウンター を1つ増やすことができるが、そのエージェントには 新しいチケットが割り当てられない c. エージェントが新しいチケットを取得しても、そ れについて通知されない d. 上記のような問題はすべて考えられる public void CreateTicket(TicketDate data) { var agent = FindLeastBusyAgent(); agent.ActiveTickets = agent.ActiveTickets + 1; agent.Save(); var ticket = new Ticket(); ticket.Id = Guid.New(); ticket.Data = data; ticket.AssinedAgent = agent; ticket.Save(); _alerts.Send(agent, "You have a new ticket!"); }
  16. Exercises 高レベルのトランザクションメカニズムがないと仮定 すると、潜在的なデータの一貫性の問題を見つけるこ とができますか? a. 新しいチケットを受け取ると、割り当てられた エージェントのアクティブチケットのカウンターを 1 以上増やすことができる b.

    エージェントのアクティブチケットのカウンター を1つ増やすことができるが、そのエージェントには 新しいチケットが割り当てられない c. エージェントが新しいチケットを取得しても、そ れについて通知されない d. 上記のような問題はすべて考えられる public void CreateTicket(TicketDate data) { var agent = FindLeastBusyAgent(); agent.ActiveTickets = agent.ActiveTickets + 1; agent.Save(); var ticket = new Ticket(); ticket.Id = Guid.New(); ticket.Data = data; ticket.AssinedAgent = agent; ticket.Save(); _alerts.Send(agent, "You have a new ticket!"); }
  17. Exercises このコードには、システムの状態を破壊する可能性の あるエッジケースが少なくとも、もう一つ存在しま す。発見することができますか? public void CreateTicket(TicketDate data) { var

    agent = FindLeastBusyAgent(); agent.ActiveTickets = agent.ActiveTickets + 1; agent.Save(); var ticket = new Ticket(); ticket.Id = Guid.New(); ticket.Data = data; ticket.AssinedAgent = agent; ticket.Save(); _alerts.Send(agent, "You have a new ticket!"); }