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

Realworld Domain Model on Rails

Realworld Domain Model on Rails

railsdm 2018の発表資料

A5e5ee2fb9e4ce3c728ed9e3ef6e916f?s=128

Tomohiro Hashidate

March 25, 2018
Tweet

Transcript

  1. Realworld Domain Model on Rails @joker1007

  2. self.inspect @joker1007 Repro inc. CTO ( 要は色々やる人) Ruby/Rails fluentd/embulk RDB

    Docker/ECS (Fargate ェ……) Bigquery/EMR/Hive/Presto 最近、kafka を触り始めました
  3. Rubykaigi 2018 登壇予定 TracePoint を中心にした黒魔術テクニックの話をする予定です。 (tagomoris さんと共同発表) お楽しみに!

  4. Repro.inc について モバイルアプリケーションの行動トラッキング 分析結果の提供と、それと連動したマーケティングの提供 Ruby biz Grand prix 2017 特別賞

  5. We are hiring Rails ごりごり書きたい人 データ量の多いサービスに興味がある人 DB パフォーマンスに敏感な人 Bigquery やPresto

    等の分散クエリエンジンを触りたい人 コード化されたインフラの快適度を更に上げたい人 色々と仕事あります!お声がけください!
  6. 本題

  7. この話のテーマ 継続してアプリケーションを改善し続けるために、より良い設計 を考えるというのは大事なことだと思う。 その中でも、DDD という考え方は今日かなりの影響力を持ってい るし、有用だと思う。 Rails はWeb の開発効率に特化したフレームワークであり、現実的 にはDDD

    と相性が悪い点が多々ある。 一方で、Rails でかなり複雑なドメインを表現しなければならない ケースも増えてきている。 Rails において、現実的でかつ複雑なアプリに耐えうるモデルの設 計や表現方法を考えたい。
  8. Rails の基本構成要素 Model View Helper Controller Job ( 他にも色々あるけど、省略) それぞれが何であり、どういう捉え方をするべきか改めて復習し

    ておこう
  9. Model アプリケーションロジックの本体。 業務に関する概念を定義し振舞いを表現する。 Rails において全ての中心。

  10. View アプリケーションの出力を表現する。 出力先はHTTP である。 HTML に限らず、JSON やCSV 等の出力を表現するものはView であ る。

  11. Controller アプリケーションの入力に対するインターフェース。 Rails では入力元はHTTP である。 URL が各アクションのエントリポイントであり、フォームパラメ ータやJSON 等を入力値とする。 業務ロジックを表現するものではない。

  12. Job 非同期処理実行のためのインターフェース。 メッセージキューを入力として、処理の実行を開始する。 入力元は異なるがController と同種の役割を持つ。 Rails においては、キュー投入のインターフェースやメッセージ規 約も兼ねる。

  13. オプショナルな構成要素 これまで良く利用されてきた追加の構成レイヤー。 Form Decorator / View Object Service

  14. Form Rails は基本がWeb なのでWeb からの入力を意識した名前が付けら れているが、実質的にはファクトリやオブジェクトビルダーと言 える。 ユーザからの入力データを元に複数のモデルや構造が複雑なモデ ルを生成する。 入力に対するバリデーションも行う。

    永続化のためのインターフェースは持つが、処理自体は内部のモ デルに移譲する。 実は、かなり重要な要素であると思う。詳しくは後述。
  15. Decorator / View Object 表示のための加工処理をオブジェクト指向的に行うためのもの。 Helper だとポリモーフィズムが難しい。 どうしても分岐が増えてとっ散らかるので、それを避けるための レイヤー。

  16. Service 業務上重要な振舞いそのものに名前を付けてクラスとして表現し たもの。 複数のモデルオブジェクトに跨る処理を管理する。 詳しくは 似非サービスクラスの殺し方 を参照

  17. まず大事なことは、それぞれの責任範囲 を守ること そして、ちゃんと命名規約を守ること でないとフレームワークというものの利 点を自ら潰すことになる

  18. DDD の概念とのマッピング 直接の対応関係にある訳ではないが、Rails が基本的に持っている 要素の中で近いと考えられるもの。 エンティティ ­> Model (AR or

    not AR) 値オブジェクト ­> Model (not AR) リポジトリ ­> ActiveRecord アプリケーションサービス ­> Controller ドメインサービス ­> Model (Service Class)
  19. 設計とは何か ( 私見) 設計の半分以上は概念を発見して名前を付けることだと思う。 基本的にアプリケーションの動作は、何かを入力して何かを出力 するI/O の管理である。 どこからデータが入ってきて、どういう名前の処理を経由してど ういう形式でどこに出力するか、それを決めるのが設計。 (UI

    とかUX はまた別であると思うけど)
  20. コンテキストマッピング 設計に必要なことは地図を作ること。 責任範囲の境界にしっかり線を引き、それぞれの範囲に名前を付 けられる様にする。 各コンテキストは独立しており、通常は他のコンテキストを触ら ずその中で処理が完結する様にする。 コンテキスト間で連携が発生する場合は、処理の流れを一方向に 限定し、依存関係をコントロールする。

  21. 地図のブレイクダウン コンテキスト内部でも、原則として処理は一方向に進むことを維 持する。 複雑さのコントロールとして重要であり、読み易さにも大きく影 響する。

  22. 責任の境界と集約ルート あるコンテキストの処理を実行する場合、境界の外から触ってい いエントリポイントを明確にしておく必要がある。 そして、それ以外の場所から決して内側のオブジェクトを触って はいけない。 集約はデータの整合性を守る単位である。

  23. 辛さの根源 人間は複数のことを同時に考えるのに向いていな い。 一連の処理そのものの複雑さや長さそのものはそこまで重要では ない。 オブジェクト同士の関係性の地図が無いことが辛さを生む。

  24. 辛さの例 境界が不明瞭でどこから処理が呼ばれるか分からない 特にデータの流入元が複数あって把握が困難なケース 依存関係が明確でないため、弄った時に一緒に壊れるクラス が多数ある 一連の処理フローの中で各オブジェクトを行ったり来たりす る 一連の処理フローにおいて分岐の場所が散乱している

  25. 一度に考えれば良い範囲を限定する そのための責任境界と集約ルート

  26. Rails における集約の表現 Rails で難しいのは、どのクラスが集約のルートに位置するかを明 示すること。 Ruby は可視性の制約が緩いので、触ってはいけない場所を強制し づらい。 現実的には以下の様になると思う。 ネームスペースを区切り、ネームスペースのルートを集約の

    ルートとする コメントやドキュメントで明示する
  27. ActiveRecord と集約の表現

  28. 開発初期 開発初期はActiveRecord をそのまま利用するケースが多いと思 う。 AR のままでも集約境界とルートがコントロールできるなら問題な い。 c l a

    s s O r d e r < A c t i v e R e c o r d : : B a s e h a s _ m a n y : o r d e r _ d e t a i l s e n d c l a s s O r d e r D e t a i l < A c t i v e R e c o r d : : B a s e b e l o n g s _ t o : o r d e r e n d # O r d e r D e t a i l はO r d e r からしか触らないし生成されない
  29. Rails アプリの成長に伴う変化 アプリが大きくなると、データベースのレコードは共通だが、コ ンテキストが異なるケースが出てくる。 AR をロジックからインフラレイヤー寄りの役割に寄せて、レコー ドレベルの整合性だけを任せる様になる。 コンテキスト毎に新しい集約クラスをPORO で定義し、整合性は そこでコントロールする。

  30. 再びForm オブジェクトの役割について 複数のモデルに跨り、ケース毎の整合性を保ってAR のレコードを 生成・更新するのがForm 。 Rails においては、ビューに渡して表示を行うためのインターフェ ースも兼用する。 実質的には、集約のルートに近いと言える。

    ただ、そうなるとForm という名前がフィットしないかも。
  31. Form オブジェクトのvalidation について validation をAR と二重でやるかについては好みがある。 私はAR に移譲できるものはAR に任せて、errors を収集する方が良

    いと思う。 G i f t F o r m = S t r u c t . n e w ( : b u d g e t , : d e t a i l s , k e y w o r d _ i n i t : t r u e i n c l u d e A c t i v e M o d e l : : V a l i d a t i o n s d e f v a l i d ? v a l i d i t y = @ g i f t _ d e t a i l s . a l l ? ( & : v a l i d ? ) @ g i f t _ d e t a i l s . e a c h _ w i t h _ i n d e x d o | g i f t _ d e t a i l , i d x | g i f t _ d e t a i l . e r r o r s . e a c h d o | a t t r , m e s s a g e | s e l f . e r r o r s . a d d ( " g i f t _ d e t a i l [ # { i d x } ] " , m e s s a g e : m e s s a g e n d e n d e n d e n d
  32. 集約の子になるコレクションの表現 Rails において、微妙に扱いが困るものがコレクション。 ActiveRecord が変にクラスレベルでレコードコレクションを扱う ので、そこに色々定義しがちになる。 scope を触るだけで済まなくなってきたら、コレクションクラスを 自分で定義するか e

    x t e n d i n g 等を使う。 地味に厄介なのが、 h a s _ m a n y に追加したら即永続化処理が動く 点。
  33. クラスメソッドの弊害 内包しているデータが無いので、引数でしかデータを渡せな い。 クラスレベルからは基本的にpublic メソッドのみしか使えない ( 使わない) マルチスレッドで扱う時に危険 (sidekiq とか)

    これらの弊害に加えて、あるクラスの責任範囲が過剰に広くな る。 実際には、もう一つ上のレイヤーで処理すべきことである。
  34. 集約ルートとの関係 集約がコレクションを包含することは、よくある。 一行単位のレコードと、集合全体の情報両方が必要な場合等。 (ex, ページネーション、データ分析の結果を表示するケース) もしくは、集約ルートが肥大化するのを避けるために、コレクシ ョンを挟む等。

  35. 単純なコレクションクラスの例 c l a s s R e t e

    n t i o n C o l l e c t i o n i n c l u d e E n u m e r a b l e d e f i n i t i a l i z e ( b a s e _ c o n v e r s i o n , c o n v e r s i o n s ) @ b a s e _ c o n v e r s i o n = b a s e _ c o n v e r s i o n @ r e c o r d s = c o n v e r s i o n s . e a c h _ w i t h _ o b j e c t ( { } ) d o | r , h | h [ r . i d ] = r e n d e n d d e f e a c h ( & b l o c k ) ; @ r e c o r d s . e a c h ( & b l o c k ) ; e n d d e f e a c h _ w i t h _ r e t e n t i o n _ r a t e ( & b l o c k ) e a c h d o | _ i d , r | y i e l d r , r . v a l u e . f d i v ( @ b a s e _ c o n v e r s i o n . v a l u e ) e n d e n d e n d
  36. ここまでのまとめ 責任範囲の境界を明確にするための地図を作る 境界の外から触っていいI/F を最小化する 成長と共にAR を複数管理する集約ルートとしてのPORO を作 る 集約ルートが肥大化しない様に、子になるオブジェクトに処理を 移譲することを意識する。

  37. 単一のレコードレベルでの複雑さ 個別のエンティティレベルの複雑さの根源は、状態管理の複雑さ だと思う。 オブジェクトが不変であれば、インスタンスがあるか無いかしか 知らなくていい。

  38. 状態管理の悪い例 devise が駄目な例なので、それを元に話をする。 (devise 自体が駄目という話ではなく、サンプルとか使われ方が駄 目という話)

  39. よくあるテーブル定義の例 c r e a t e _ t a

    b l e : u s e r s d o | t | t . s t r i n g : e m a i l , n u l l : f a l s e t . s t r i n g : n a m e t . s t r i n g : e n c r y p t e d _ p a s s w o r d t . s t r i n g : c o n f i r m a t i o n _ t o k e n t . d a t e t i m e : c o n f i r m e d _ a t t . d a t e t i m e : c o n f i r m a t i o n _ s e n t _ a t t . s t r i n g : i n v i t a t i o n _ t o k e n t . d a t e t i m e : i n v i t a t i o n _ c r e a t e d _ a t t . d a t e t i m e : i n v i t a t i o n _ s e n t _ a t t . d a t e t i m e : i n v i t a t i o n _ a c c e p t e d _ a t e n d
  40. 駄目な理由 NULLABLE カラムの嵐になる ( あったり無かったり) 状態によって整合性を管理しなければならない範囲が変化す る 登録完了時には必須だが、confirm 待ちや招待の承認前は 不要

    他オブジェクトとの関連が、必須になったり初期化でき なかったりする 自身の状態とどういう遷移を経てきたかを管理する必要がある それはそのまま、分岐の氾濫や整合性違反に繋がる
  41. 改善方法 User 、UserRegistration, UserInvitation に分割する

  42. User の例 c r e a t e _ t

    a b l e : u s e r s d o | t | t . s t r i n g : e m a i l , n u l l : f a l s e t . s t r i n g : n a m e , n u l l : f a l s e t . s t r i n g : e n c r y p t e d _ p a s s w o r d , n u l l : f a l s e e n d c l a s s U s e r < A c t i v e R e c o r d : : B a s e v a l i d a t e s : e m a i l , p r e s e n c e : t r u e v a l i d a t e s : n a m e , p r e s e n c e : t r u e v a l i d a t e s : e n c r y p t e d _ p a s s w o r d , p r e s e n c e : t r u e e n d
  43. UserRegistration の例 c r e a t e _ t

    a b l e : u s e r s d o | t | t . s t r i n g : e m a i l , n u l l : f a l s e t . s t r i n g : c o n f i r m a t i o n _ t o k e n , n u l l : f a l s e t . d a t e t i m e : c o n f i r m e d _ a t t . d a t e t i m e : c o n f i r m a t i o n _ s e n t _ a t , n u l l : f a l s e e n d c l a s s U s e r R e g i s t r a t i o n < A c t i v e R e c o r d : : B a s e v a l i d a t e s : e m a i l , p r e s e n c e : t r u e v a l i d a t e s : c o n f i r m a t i o n _ t o k e n , p r e s e n c e : t r u e e n d Form オブジェクトがUserRegistration の整合性を確認 c o n f i r m e d _ a t の記録とUser の永続化を行う
  44. UserInvitation の例 c r e a t e _ t

    a b l e : u s e r s d o | t | t . s t r i n g : e m a i l , n u l l : f a l s e t . s t r i n g : i n v i t a t i o n _ t o k e n , n u l l : f a l s e t . d a t e t i m e : i n v i t a t i o n _ c r e a t e d _ a t , n u l l : f a l s e t . d a t e t i m e : i n v i t a t i o n _ s e n t _ a t , n u l l : f a l s e t . d a t e t i m e : i n v i t a t i o n _ a c c e p t e d _ a t e n d UserRegistration と同様Form オブジェクトが整合性を確認 i n v i t a t i o n _ a c c e p t e d _ a t の記録とUser の永続化を行う この時Form オブジェクトはUserRegistration の処理とは異なる
  45. どう変わったか Registration, Invitation はそれぞれ自分の中で2 値の状態だけ管 理すれば良い User はNOT NULL カラムonly

    になり、バリデーションが単純化 する 通知のコールバック等を定義する場所が明確になる User を利用する際に、登録に関するカラムやメソッドが邪魔 にならない
  46. 正規化? この例はデータベースのテーブル設計が責任や状態管理の分離と 上手く合致するが、常にそう上手くはいかない。 Rails においては、どうやってもテーブル構造とモデル構造の癒着 が避けられない。 現実的には、どちらも意識して設計する必要がある。 モデル設計とテーブル設計は別と書かれている書籍が多いが、 Rails ではAR

    で表現しやすい形に落としこむのも重要。
  47. ちなみに弊社のテーブルは自分で駄目だと言っている例そのまん まです 現実は色々あって厳しい…… 。

  48. 不変を更に推し進める レコードの更新コストは結構高い。 また、不変であれば分散処理との親和性が高くなりスケーラビリ ティの向上が見込める。 レコード自体は不変に保ち、イベントを記録することで現在の状 態を再現する。 こうすることで追加のみでデータの更新を表現できる。 イミュータブルデータモデルと呼ばれる表現方法。 これをDDD に意識的に組込んだものが、ドメインイベントとイベ

    ントソーシングだと思う。 ( この辺り、成り立ちについての知識が無いため、私見です)
  49. Rails でのイベントの表現 rails_event_store がかなり現実的に見える。 ただ、私はこれをproduction で利用したことがないため、正しく理 解している自信はない。 あくまで参考程度として聞いて欲しい。

  50. イベントの発行 c l a s s O r d e

    r P l a c e d < R a i l s E v e n t S t o r e : : E v e n t e n d s t r e a m _ n a m e = " o r d e r _ 1 " e v e n t = O r d e r P l a c e d . n e w ( d a t a : { o r d e r _ i d : 1 , o r d e r _ d a t a : " s a m p l e " , f e s t i v a l _ i d : " b 2 d 5 0 6 f d ­ 4 0 9 d ­ 4 e c 7 ­ b 0 2 f ­ c 6 d 2 2 9 5 c 7 e d d " } ) # p u b l i s h i n g a n e v e n t f o r a s p e c i f i c s t r e a m e v e n t _ s t o r e . p u b l i s h _ e v e n t ( e v e n t , s t r e a m _ n a m e : s t r e a m _ n a m e )
  51. イベントの購読 c l a s s I n v o

    i c e R e a d M o d e l d e f c a l l ( e v e n t ) # P r o c e s s a n e v e n t h e r e . e n d e n d s u b s c r i b e r = I n v o i c e R e a d M o d e l . n e w e v e n t _ s t o r e . s u b s c r i b e ( s u b s c r i b e r , t o : [ I n v o i c e C r e a t e d , I n v o i c e
  52. AR のテーブル構造 c r e a t e _ t

    a b l e ( : e v e n t _ s t o r e _ e v e n t s _ i n _ s t r e a m s , f o r c e : f a l s e ) t . s t r i n g : s t r e a m , n u l l : f a l s e t . i n t e g e r : p o s i t i o n , n u l l : t r u e t . r e f e r e n c e s : e v e n t , n u l l : f a l s e , t y p e : : s t r i n g t . d a t e t i m e : c r e a t e d _ a t , n u l l : f a l s e e n d a d d _ i n d e x : e v e n t _ s t o r e _ e v e n t s _ i n _ s t r e a m s , [ : s t r e a m , : p o s i t i o n a d d _ i n d e x : e v e n t _ s t o r e _ e v e n t s _ i n _ s t r e a m s , [ : c r e a t e d _ a t ] a d d _ i n d e x : e v e n t _ s t o r e _ e v e n t s _ i n _ s t r e a m s , [ : s t r e a m , : e v e n t _ i d c r e a t e _ t a b l e ( : e v e n t _ s t o r e _ e v e n t s , i d : f a l s e , f o r c e : f a l s e ) t . s t r i n g : i d , l i m i t : 3 6 , p r i m a r y _ k e y : t r u e , n u l l : f a l s e t . s t r i n g : e v e n t _ t y p e , n u l l : f a l s e t . t e x t : m e t a d a t a t . t e x t : d a t a , n u l l : f a l s e t . d a t e t i m e : c r e a t e d _ a t , n u l l : f a l s e e n d
  53. イベント駆動アーキテクチャの利点 イベントをベースにすることで以下の様な利点が得られる 任意の状態のオブジェクトを再現できる オブジェクトの更新理由が明確になり、再取得できる 処理同士がイベントで繋がるため疎結合化が進む 分散処理やマイクロサービスと親和性が高い

  54. イベント駆動アーキテクチャの不利な点 一方で、以下の点が懸念材料となる 読み込み時のコスト 特にイベントが蓄積された後にどうするか トランザクションのコントロールが難しい ユーザーへの戻り値のコントロールが難しい 今迄とは異なる概念でデータの流れを捉える必要がある メッセージングミドルウェアの運用管理 純粋に技術的な難易度が上がるのでリスクも多い。

  55. まとめ

  56. 設計とは 概念に名前を付けること 地図を描くこと 境界を決め、それを守ること

  57. 読み易く維持しやすいコードを書くため 評価軸を明確に 責任範囲を小さく 状態を少なく 依存を少なく オブジェクトの視点から見える世界を想像する 多くの選択肢を知る

  58. そして最も難しいことは 描いた地図を皆で共有すること。 本当に難しい。 共有できる地図を作るためにモデリング技法やユビキタス言語と いうものがある。 ( 岸辺露伴 VS 大柳賢 (

    ジャンケン小僧) を思い浮かべてください)
  59. 正直自分のことは棚上げした でないとこんな話中々できない それでも考えていきたいし より良い開発がしたいと思う

  60. 弊社で一緒に考えてくれる人を募集中

  61. ご静聴ありがとうございました

  62. None