Slide 1

Slide 1 text

Realworld Domain Model on Rails @joker1007

Slide 2

Slide 2 text

self.inspect @joker1007 Repro inc. CTO ( 要は色々やる人) Ruby/Rails fluentd/embulk RDB Docker/ECS (Fargate ェ……) Bigquery/EMR/Hive/Presto 最近、kafka を触り始めました

Slide 3

Slide 3 text

Rubykaigi 2018 登壇予定 TracePoint を中心にした黒魔術テクニックの話をする予定です。 (tagomoris さんと共同発表) お楽しみに!

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

We are hiring Rails ごりごり書きたい人 データ量の多いサービスに興味がある人 DB パフォーマンスに敏感な人 Bigquery やPresto 等の分散クエリエンジンを触りたい人 コード化されたインフラの快適度を更に上げたい人 色々と仕事あります!お声がけください!

Slide 6

Slide 6 text

本題

Slide 7

Slide 7 text

この話のテーマ 継続してアプリケーションを改善し続けるために、より良い設計 を考えるというのは大事なことだと思う。 その中でも、DDD という考え方は今日かなりの影響力を持ってい るし、有用だと思う。 Rails はWeb の開発効率に特化したフレームワークであり、現実的 にはDDD と相性が悪い点が多々ある。 一方で、Rails でかなり複雑なドメインを表現しなければならない ケースも増えてきている。 Rails において、現実的でかつ複雑なアプリに耐えうるモデルの設 計や表現方法を考えたい。

Slide 8

Slide 8 text

Rails の基本構成要素 Model View Helper Controller Job ( 他にも色々あるけど、省略) それぞれが何であり、どういう捉え方をするべきか改めて復習し ておこう

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Form Rails は基本がWeb なのでWeb からの入力を意識した名前が付けら れているが、実質的にはファクトリやオブジェクトビルダーと言 える。 ユーザからの入力データを元に複数のモデルや構造が複雑なモデ ルを生成する。 入力に対するバリデーションも行う。 永続化のためのインターフェースは持つが、処理自体は内部のモ デルに移譲する。 実は、かなり重要な要素であると思う。詳しくは後述。

Slide 15

Slide 15 text

Decorator / View Object 表示のための加工処理をオブジェクト指向的に行うためのもの。 Helper だとポリモーフィズムが難しい。 どうしても分岐が増えてとっ散らかるので、それを避けるための レイヤー。

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

DDD の概念とのマッピング 直接の対応関係にある訳ではないが、Rails が基本的に持っている 要素の中で近いと考えられるもの。 エンティティ ­> Model (AR or not AR) 値オブジェクト ­> Model (not AR) リポジトリ ­> ActiveRecord アプリケーションサービス ­> Controller ドメインサービス ­> Model (Service Class)

Slide 19

Slide 19 text

設計とは何か ( 私見) 設計の半分以上は概念を発見して名前を付けることだと思う。 基本的にアプリケーションの動作は、何かを入力して何かを出力 するI/O の管理である。 どこからデータが入ってきて、どういう名前の処理を経由してど ういう形式でどこに出力するか、それを決めるのが設計。 (UI とかUX はまた別であると思うけど)

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

ActiveRecord と集約の表現

Slide 28

Slide 28 text

開発初期 開発初期は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 からしか触らないし生成されない

Slide 29

Slide 29 text

Rails アプリの成長に伴う変化 アプリが大きくなると、データベースのレコードは共通だが、コ ンテキストが異なるケースが出てくる。 AR をロジックからインフラレイヤー寄りの役割に寄せて、レコー ドレベルの整合性だけを任せる様になる。 コンテキスト毎に新しい集約クラスをPORO で定義し、整合性は そこでコントロールする。

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

集約の子になるコレクションの表現 Rails において、微妙に扱いが困るものがコレクション。 ActiveRecord が変にクラスレベルでレコードコレクションを扱う ので、そこに色々定義しがちになる。 scope を触るだけで済まなくなってきたら、コレクションクラスを 自分で定義するか e x t e n d i n g 等を使う。 地味に厄介なのが、 h a s _ m a n y に追加したら即永続化処理が動く 点。

Slide 33

Slide 33 text

クラスメソッドの弊害 内包しているデータが無いので、引数でしかデータを渡せな い。 クラスレベルからは基本的にpublic メソッドのみしか使えない ( 使わない) マルチスレッドで扱う時に危険 (sidekiq とか) これらの弊害に加えて、あるクラスの責任範囲が過剰に広くな る。 実際には、もう一つ上のレイヤーで処理すべきことである。

Slide 34

Slide 34 text

集約ルートとの関係 集約がコレクションを包含することは、よくある。 一行単位のレコードと、集合全体の情報両方が必要な場合等。 (ex, ページネーション、データ分析の結果を表示するケース) もしくは、集約ルートが肥大化するのを避けるために、コレクシ ョンを挟む等。

Slide 35

Slide 35 text

単純なコレクションクラスの例 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

Slide 36

Slide 36 text

ここまでのまとめ 責任範囲の境界を明確にするための地図を作る 境界の外から触っていいI/F を最小化する 成長と共にAR を複数管理する集約ルートとしてのPORO を作 る 集約ルートが肥大化しない様に、子になるオブジェクトに処理を 移譲することを意識する。

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

よくあるテーブル定義の例 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

Slide 40

Slide 40 text

駄目な理由 NULLABLE カラムの嵐になる ( あったり無かったり) 状態によって整合性を管理しなければならない範囲が変化す る 登録完了時には必須だが、confirm 待ちや招待の承認前は 不要 他オブジェクトとの関連が、必須になったり初期化でき なかったりする 自身の状態とどういう遷移を経てきたかを管理する必要がある それはそのまま、分岐の氾濫や整合性違反に繋がる

Slide 41

Slide 41 text

改善方法 User 、UserRegistration, UserInvitation に分割する

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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 の永続化を行う

Slide 44

Slide 44 text

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 の処理とは異なる

Slide 45

Slide 45 text

どう変わったか Registration, Invitation はそれぞれ自分の中で2 値の状態だけ管 理すれば良い User はNOT NULL カラムonly になり、バリデーションが単純化 する 通知のコールバック等を定義する場所が明確になる User を利用する際に、登録に関するカラムやメソッドが邪魔 にならない

Slide 46

Slide 46 text

正規化? この例はデータベースのテーブル設計が責任や状態管理の分離と 上手く合致するが、常にそう上手くはいかない。 Rails においては、どうやってもテーブル構造とモデル構造の癒着 が避けられない。 現実的には、どちらも意識して設計する必要がある。 モデル設計とテーブル設計は別と書かれている書籍が多いが、 Rails ではAR で表現しやすい形に落としこむのも重要。

Slide 47

Slide 47 text

ちなみに弊社のテーブルは自分で駄目だと言っている例そのまん まです 現実は色々あって厳しい…… 。

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Rails でのイベントの表現 rails_event_store がかなり現実的に見える。 ただ、私はこれをproduction で利用したことがないため、正しく理 解している自信はない。 あくまで参考程度として聞いて欲しい。

Slide 50

Slide 50 text

イベントの発行 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 )

Slide 51

Slide 51 text

イベントの購読 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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

イベント駆動アーキテクチャの利点 イベントをベースにすることで以下の様な利点が得られる 任意の状態のオブジェクトを再現できる オブジェクトの更新理由が明確になり、再取得できる 処理同士がイベントで繋がるため疎結合化が進む 分散処理やマイクロサービスと親和性が高い

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

まとめ

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

そして最も難しいことは 描いた地図を皆で共有すること。 本当に難しい。 共有できる地図を作るためにモデリング技法やユビキタス言語と いうものがある。 ( 岸辺露伴 VS 大柳賢 ( ジャンケン小僧) を思い浮かべてください)

Slide 59

Slide 59 text

正直自分のことは棚上げした でないとこんな話中々できない それでも考えていきたいし より良い開発がしたいと思う

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

No content