かとじゅんの技術日誌 技術の話をするところ 2023-12-22T00:00:00+09:00 j5ik2o Hatena::Blog hatenablog://blog/12704346814673891348 Rustでブロッキングキューを実装する hatenablog://entry/6801883189066435925 2023-12-22T00:00:00+09:00 2023-12-22T10:33:46+09:00 Rustでブロッキングキューを実装した話。これはRustのカレンダー | Advent Calendar 2023 - Qiitaの22日目の記事です。 ブロッキングキューはご存じだろうか。(えっ…スレッドはブロックしたくない…と思った人は最後まで読むとよいかも) Javaにはあります。 docs.oracle.com 要素の取得時にキューが空でなくなるまで待機したり、要素の格納時にキュー内に空きが生じるまで待機する操作を追加でサポートしたりするQueueです。 これはRustの標準にはない。今回はブロッキングキューを実装してみる。 「そういえば、ブロッキングキューが欲しい!」と思ったときに、… <p>Rustでブロッキングキューを実装した話。これは<a href="https://qiita.com/advent-calendar/2023/rust">Rust&#x306E;&#x30AB;&#x30EC;&#x30F3;&#x30C0;&#x30FC; | Advent Calendar 2023 - Qiita</a>の22日目の記事です。</p> <p>ブロッキングキューはご存じだろうか。(えっ…スレッドはブロックしたくない…と思った人は最後まで読むとよいかも)</p> <p>Javaにはあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.oracle.com%2Fjavase%2Fjp%2F21%2Fdocs%2Fapi%2Fjava.base%2Fjava%2Futil%2Fconcurrent%2FBlockingQueue.html" title="BlockingQueue (Java SE 21 &amp; JDK 21)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.oracle.com/javase/jp/21/docs/api/java.base/java/util/concurrent/BlockingQueue.html">docs.oracle.com</a></cite></p> <blockquote><p>要素の取得時にキューが空でなくなるまで待機したり、要素の格納時にキュー内に空きが生じるまで待機する操作を追加でサポートしたりするQueueです。</p></blockquote> <p>これはRustの標準にはない。今回はブロッキングキューを実装してみる。</p> <p>「そういえば、ブロッキングキューが欲しい!」と思ったときに、ぜひこのブログ記事を思い出してほしい。</p> <h2 id="まず作るな既存のコードを調査しろ">まず作るな・既存のコードを調査しろ</h2> <p>標準にはないが、クレートとして実装がある。</p> <ul> <li><a href="https://github.com/JimFawcett/RustBlockingQueue">https://github.com/JimFawcett/RustBlockingQueue</a></li> <li><a href="https://github.com/TeaEntityLab/fpRust">https://github.com/TeaEntityLab/fpRust</a></li> <li><a href="https://github.com/julianbuettner/rust-blockinqueue">https://github.com/julianbuettner/rust-blockinqueue</a></li> </ul> <p><code>JimFawcett/RustBlockingQueue</code>と<code>julianbuettner/rust-blockinqueue</code>をみるとMutextとCondvarを使うと実装できるということが分かる。<code>TeaEntityLab/fpRust</code>ではmpscを使っている模様。</p> <p><code>Mutex</code>と<code>Condvar</code>があれば実装できることがわかる。詳しいことは以下参照</p> <ul> <li><a href="https://doc.rust-lang.org/std/sync/struct.Condvar.html">Condvar in std::sync - Rust</a></li> <li><a href="https://qiita.com/syncbunny/items/b871c96111206ee7b93e">Rust&#x3067;&#x306E;&#x4ED6;&#x30B9;&#x30EC;&#x30C3;&#x30C9;&#x306E;&#x5F85;&#x3061;&#x5408;&#x308F;&#x305B;(&#x6761;&#x4EF6;&#x5909;&#x6570;, wait, notify) #Rust - Qiita</a></li> </ul> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4814400519?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/41XN-QMtSAL._SL500_.jpg" class="hatena-asin-detail-image" alt="詳解 Rustアトミック操作とロック ―並行処理実装のための低レイヤプログラミング" title="詳解 Rustアトミック操作とロック ―並行処理実装のための低レイヤプログラミング"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4814400519?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">詳解 Rustアトミック操作とロック ―並行処理実装のための低レイヤプログラミング</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="https://d.hatena.ne.jp/keyword/Mara%20Bos" class="keyword">Mara Bos</a></li><li>オーム社</li></ul><a href="https://www.amazon.co.jp/dp/4814400519?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>この本にも<code>Mutex</code>,<code>Condvar</code>の使い方の紹介がある。</p> <h2 id="BlockingQueueのインターフェイス">BlockingQueueのインターフェイス</h2> <p>JavaのAPIを参考にインターフェイスは以下のようにした。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue.rs#L306">queue-rs/src/queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">BlockingQueueBehavior</span><span class="synStatement">&lt;</span>E: Element<span class="synStatement">&gt;</span>: QueueBehavior<span class="synStatement">&lt;</span>E<span class="synStatement">&gt;</span> <span class="synStatement">+</span> <span class="synType">Send</span> { <span class="synSpecial">/// Inserts the specified element into this queue. If necessary, waits until space is available.&lt;br/&gt;</span> <span class="synSpecial">/// 指定された要素をこのキューに挿入します。必要に応じて、空きが生じるまで待機します。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Arguments / 引数</span> <span class="synSpecial">/// - `element` - The element to be inserted. / 挿入する要素。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `Ok(())` - If the element is inserted successfully. / 要素が正常に挿入された場合。</span> <span class="synSpecial">/// - `Err(QueueError::OfferError(element))` - If the element cannot be inserted. / 要素を挿入できなかった場合。</span> <span class="synSpecial">/// - `Err(QueueError::InterruptedError)` - If the operation is interrupted. / 操作が中断された場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">put</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, element: E) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>()<span class="synStatement">&gt;</span>; <span class="synSpecial">/// Inserts the specified element into this queue. If necessary, waits until space is available. You can specify the waiting time.&lt;br/&gt;</span> <span class="synSpecial">/// 指定された要素をこのキューに挿入します。必要に応じて、空きが生じるまで待機します。待機時間を指定できます。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Arguments / 引数</span> <span class="synSpecial">/// - `element` - The element to be inserted. / 挿入する要素。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `Ok(())` - If the element is inserted successfully. / 要素が正常に挿入された場合。</span> <span class="synSpecial">/// - `Err(QueueError::OfferError(element))` - If the element cannot be inserted. / 要素を挿入できなかった場合。</span> <span class="synSpecial">/// - `Err(QueueError::InterruptedError)` - If the operation is interrupted. / 操作が中断された場合。</span> <span class="synSpecial">/// - `Err(QueueError::TimeoutError)` - If the operation times out. / 操作がタイムアウトした場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">put_timeout</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, element: E, timeout: Duration) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>()<span class="synStatement">&gt;</span>; <span class="synSpecial">/// Retrieve the head of this queue and delete it. If necessary, wait until an element becomes available.&lt;br/&gt;</span> <span class="synSpecial">/// このキューの先頭を取得して削除します。必要に応じて、要素が利用可能になるまで待機します。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `Ok(Some(element))` - If the element is retrieved successfully. / 要素が正常に取得された場合。</span> <span class="synSpecial">/// - `Ok(None)` - If the queue is empty. / キューが空の場合。</span> <span class="synSpecial">/// - `Err(QueueError::InterruptedError)` - If the operation is interrupted. / 操作が中断された場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">take</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span><span class="synType">Option</span><span class="synStatement">&lt;</span>E<span class="synStatement">&gt;&gt;</span>; <span class="synSpecial">/// Retrieve the head of this queue and delete it. If necessary, wait until an element becomes available. You can specify the waiting time.&lt;br/&gt;</span> <span class="synSpecial">/// このキューの先頭を取得して削除します。必要に応じて、要素が利用可能になるまで待機します。待機時間を指定できます。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Arguments / 引数</span> <span class="synSpecial">/// - `timeout` - The maximum time to wait. / 待機する最大時間。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `Ok(Some(element))` - If the element is retrieved successfully. / 要素が正常に取得された場合。</span> <span class="synSpecial">/// - `Ok(None)` - If the queue is empty. / キューが空の場合。</span> <span class="synSpecial">/// - `Err(QueueError::InterruptedError)` - If the operation is interrupted. / 操作が中断された場合。</span> <span class="synSpecial">/// - `Err(QueueError::TimeoutError)` - If the operation times out. / 操作がタイムアウトした場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">take_timeout</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, timeout: Duration) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span><span class="synType">Option</span><span class="synStatement">&lt;</span>E<span class="synStatement">&gt;&gt;</span>; <span class="synSpecial">/// Returns the number of elements that can be inserted into this queue without blocking.&lt;br/&gt;</span> <span class="synSpecial">/// ブロックせずにこのキューに挿入できる要素数を返します。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `QueueSize::Limitless` - If the queue has no capacity limit. / キューに容量制限がない場合。</span> <span class="synSpecial">/// - `QueueSize::Limited(num)` - If the queue has a capacity limit. / キューに容量制限がある場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">remaining_capacity</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> QueueSize; <span class="synSpecial">/// Interrupts the operation of this queue.&lt;br/&gt;</span> <span class="synSpecial">/// このキューの操作を中断します。</span> <span class="synStatement">fn</span> <span class="synIdentifier">interrupt</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>); <span class="synSpecial">/// Returns whether the operation of this queue has been interrupted.&lt;br/&gt;</span> <span class="synSpecial">/// このキューの操作が中断されたかどうかを返します。</span> <span class="synSpecial">///</span> <span class="synSpecial">/// # Return Value / 戻り値</span> <span class="synSpecial">/// - `true` - If the operation is interrupted. / 操作が中断された場合。</span> <span class="synSpecial">/// - `false` - If the operation is not interrupted. / 操作が中断されていない場合。</span> <span class="synStatement">fn</span> <span class="synIdentifier">is_interrupted</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">bool</span>; } </pre> <h2 id="データ構造">データ構造</h2> <p>今回のブロッキングキューとは別に前提となるtraitが<a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue.rs#L182">QueueBehavior</a>です。ブロッキングしないキューの責務を<code>QueueBehavior</code> traitとして定義しています。型パラメータQはこのトレイトを実装していなければなりません。Q以外にConvarの2個がタプルでまとめてArcにラップされています。Cloneが実装されているので、<code>clone</code>してもArcなので同じデータを参照する。<code>p</code>は要素の型を保持するためのもの。<code>is_interrupted</code>は操作を中断させるためのフラグとなる。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, </span><span class="synType">Clone</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">BlockingQueue</span><span class="synStatement">&lt;</span>E, Q: QueueBehavior<span class="synStatement">&lt;</span>E<span class="synStatement">&gt;&gt;</span> { underlying: Arc<span class="synStatement">&lt;</span>(Mutex<span class="synStatement">&lt;</span>Q<span class="synStatement">&gt;</span>, Condvar, Condvar)<span class="synStatement">&gt;</span>, p: PhantomData<span class="synStatement">&lt;</span>E<span class="synStatement">&gt;</span>, is_interrupted: Arc<span class="synStatement">&lt;</span>AtomicBool<span class="synStatement">&gt;</span>, } </pre> <h2 id="putメソッド">putメソッド</h2> <p><code>put</code>は「指定された要素をこのキューに挿入します。必要に応じて、空きが生じるまで待機します。」という機能。空きがない場合はメソッド内でスレッドをブロックするので制御が戻ってこなくなる。</p> <h3 id="putのテスト">putのテスト</h3> <p><code>put</code>のテストを書いてみた。キャパシティ一杯になるとブロックする。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synPreProc">#[test]</span> <span class="synPreProc">#[serial]</span> <span class="synStatement">fn</span> <span class="synIdentifier">test_blocking_put</span>() { <span class="synIdentifier">init_logger</span>(); <span class="synStatement">let</span> <span class="synType">mut</span> q <span class="synStatement">=</span> <span class="synIdentifier">create_blocking_queue</span>(<span class="synPreProc">QueueType</span><span class="synSpecial">::</span>VecDeque, QUEUE_SIZE); <span class="synStatement">let</span> <span class="synType">mut</span> q_cloned <span class="synStatement">=</span> q.<span class="synIdentifier">clone</span>(); <span class="synStatement">let</span> please_interrupt <span class="synStatement">=</span> <span class="synPreProc">Arc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synPreProc">CountDownLatch</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synConstant">1</span>)); <span class="synStatement">let</span> please_interrupt_cloned <span class="synStatement">=</span> please_interrupt.<span class="synIdentifier">clone</span>(); <span class="synStatement">let</span> handler1 <span class="synStatement">=</span> <span class="synPreProc">thread</span><span class="synSpecial">::</span><span class="synIdentifier">spawn</span>(<span class="synType">move</span> <span class="synStatement">||</span> { <span class="synComment">// キューにデータをputする</span> <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synConstant">0</span>..QUEUE_SIZE.<span class="synIdentifier">to_usize</span>() { <span class="synStatement">let</span> _ <span class="synStatement">=</span> q_cloned.<span class="synIdentifier">put</span>(i <span class="synStatement">as</span> <span class="synType">i32</span>).<span class="synIdentifier">unwrap</span>(); } <span class="synPreProc">assert_eq!</span>(q_cloned.<span class="synIdentifier">len</span>(), QUEUE_SIZE); <span class="synComment">// 割り込みを要求する</span> q_cloned.<span class="synIdentifier">interrupt</span>(); <span class="synComment">// QUEUE_SIZEを超えたputなのでブロックするはずだが</span> <span class="synComment">// すでにinterruptされているので割り込みエラーが発生する。</span> <span class="synStatement">match</span> q_cloned.<span class="synIdentifier">put</span>(<span class="synConstant">99</span>) { <span class="synConstant">Ok</span>(_) <span class="synStatement">=&gt;</span> { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;put: finish: 99, should not be here&quot;</span>); } <span class="synConstant">Err</span>(err) <span class="synStatement">=&gt;</span> { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;put: finish: 99, error = {:?}&quot;</span>, err); <span class="synStatement">let</span> queue_error <span class="synStatement">=</span> err.<span class="synIdentifier">downcast_ref</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>QueueError<span class="synStatement">&lt;</span><span class="synType">i32</span><span class="synStatement">&gt;&gt;</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">assert_eq!</span>(queue_error, <span class="synType">&amp;</span><span class="synPreProc">QueueError</span><span class="synSpecial">::</span>InterruptedError); } } <span class="synComment">// 割り込みが検出されると割り込み状態がリセットされることを確認する</span> <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>q_cloned.<span class="synIdentifier">is_interrupted</span>()); please_interrupt_cloned.<span class="synIdentifier">count_down</span>(); <span class="synComment">// 先にputするのでブロックが発生するが</span> <span class="synComment">// 非同期にinterruptされるので割り込みエラー</span> <span class="synStatement">match</span> q_cloned.<span class="synIdentifier">put</span>(<span class="synConstant">99</span>) { <span class="synConstant">Ok</span>(_) <span class="synStatement">=&gt;</span> { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;put: finish: 99, should not be here&quot;</span>); } <span class="synConstant">Err</span>(err) <span class="synStatement">=&gt;</span> { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;put: finish: 99, error = {:?}&quot;</span>, err); <span class="synStatement">let</span> queue_error <span class="synStatement">=</span> err.<span class="synIdentifier">downcast_ref</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>QueueError<span class="synStatement">&lt;</span><span class="synType">i32</span><span class="synStatement">&gt;&gt;</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">assert_eq!</span>(queue_error, <span class="synType">&amp;</span><span class="synPreProc">QueueError</span><span class="synSpecial">::</span>InterruptedError); } } <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>q_cloned.<span class="synIdentifier">is_interrupted</span>()); }); <span class="synComment">// カウントが0になるまでブロックする</span> <span class="synComment">// つまり最初のputのテストが終わるまで待機する</span> please_interrupt.<span class="synIdentifier">wait</span>(); <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>handler1.<span class="synIdentifier">is_finished</span>()); <span class="synComment">// 非同期に割り込みを行う</span> q.<span class="synIdentifier">interrupt</span>(); handler1.<span class="synIdentifier">join</span>().<span class="synIdentifier">unwrap</span>(); } </pre> <h2 id="putの実装">putの実装</h2> <p>仕様は「指定された要素をこのキューに挿入します。必要に応じて、空きが生じるまで待機します。」。 実装は以下。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/blocking_queue.rs#L122">queue-rs/src/queue/blocking_queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <p>空きがある場合は2)はスルーされて5)だけ実行される。空きがないつまりフルの場合は、2)の内部に入り、4)が<code>not_full</code>のwaitを呼び出すとスレッドをブロックする。 <code>take</code>メソッド内で要素を取得するときに<code>not_full.notify_once</code>が実行されると、<code>not_full</code>の<code>wait</code>が解除される(待機しているスレッドが複数ある場合そのうち一つだけが解除される)。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span>E: Element <span class="synStatement">+</span> <span class="synSpecial">'static</span>, Q: QueueBehavior<span class="synStatement">&lt;</span>E<span class="synStatement">&gt;&gt;</span> BlockingQueueBehavior<span class="synStatement">&lt;</span>E<span class="synStatement">&gt;</span> <span class="synStatement">for</span> BlockingQueue<span class="synStatement">&lt;</span>E, Q<span class="synStatement">&gt;</span> { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">put</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, element: E) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>()<span class="synStatement">&gt;</span> { <span class="synComment">// 1) underlyingから必要な要素を取得する</span> <span class="synComment">// queue_vec_mutexは内部キュー、not_fullのConvar, not_emptyのConvar</span> <span class="synStatement">let</span> (queue_vec_mutex, not_full, not_empty) <span class="synStatement">=</span> <span class="synType">&amp;*</span><span class="synConstant">self</span>.underlying; <span class="synStatement">let</span> <span class="synType">mut</span> queue_vec_mutex_guard <span class="synStatement">=</span> queue_vec_mutex.<span class="synIdentifier">lock</span>().<span class="synIdentifier">unwrap</span>(); <span class="synComment">// 2) 内部キューがフルのときにはループする</span> <span class="synStatement">while</span> queue_vec_mutex_guard.<span class="synIdentifier">is_full</span>() { <span class="synComment">// 3) 割り込みのチェック</span> <span class="synStatement">if</span> <span class="synConstant">self</span>.<span class="synIdentifier">check_and_update_interrupted</span>() { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;put: return by interrupted&quot;</span>); <span class="synStatement">return</span> <span class="synConstant">Err</span>(<span class="synIdentifier">QueueError</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>E<span class="synStatement">&gt;</span><span class="synSpecial">::</span>InterruptedError.<span class="synIdentifier">into</span>()); } <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;put: blocking start...&quot;</span>); <span class="synComment">// 4) 空きが生じるまで待機する</span> queue_vec_mutex_guard <span class="synStatement">=</span> not_full.<span class="synIdentifier">wait</span>(queue_vec_mutex_guard).<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;put: blocking end...&quot;</span>); } <span class="synComment">// 5) 内部のキューにofferを使って挿入する</span> <span class="synStatement">let</span> result <span class="synStatement">=</span> queue_vec_mutex_guard.<span class="synIdentifier">offer</span>(element); <span class="synComment">// 6) not_emptyを使って通知する</span> not_empty.<span class="synIdentifier">notify_one</span>(); result } <span class="synComment">// ...</span> } </pre> <h2 id="takeメソッド">takeメソッド</h2> <p><code>take</code>は「このキューの先頭を取得して削除します。必要に応じて、要素が利用可能になるまで待機します。」という機能。要素がない場合はメソッド内でスレッドをブロックするので制御が戻ってこなくなる。</p> <h3 id="takeのテスト">takeのテスト</h3> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synPreProc">#[test]</span> <span class="synPreProc">#[serial]</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">test_blocking_take</span>() { <span class="synIdentifier">init_logger</span>(); <span class="synStatement">let</span> <span class="synType">mut</span> q <span class="synStatement">=</span> <span class="synIdentifier">populated_queue</span>(<span class="synPreProc">QueueType</span><span class="synSpecial">::</span>VecDeque, QUEUE_SIZE); <span class="synStatement">let</span> <span class="synType">mut</span> q_cloned <span class="synStatement">=</span> q.<span class="synIdentifier">clone</span>(); <span class="synStatement">let</span> please_interrupt <span class="synStatement">=</span> <span class="synPreProc">Arc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synPreProc">CountDownLatch</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synConstant">1</span>)); <span class="synStatement">let</span> please_interrupt_cloned <span class="synStatement">=</span> please_interrupt.<span class="synIdentifier">clone</span>(); <span class="synStatement">let</span> handler1 <span class="synStatement">=</span> <span class="synPreProc">thread</span><span class="synSpecial">::</span><span class="synIdentifier">spawn</span>(<span class="synType">move</span> <span class="synStatement">||</span> { <span class="synComment">// キューにデータをputする</span> <span class="synStatement">for</span> _ <span class="synStatement">in</span> <span class="synConstant">0</span>..QUEUE_SIZE.<span class="synIdentifier">to_usize</span>() { <span class="synStatement">let</span> _ <span class="synStatement">=</span> q_cloned.<span class="synIdentifier">take</span>().<span class="synIdentifier">unwrap</span>(); } <span class="synComment">// 割り込みを要求する</span> q_cloned.<span class="synIdentifier">interrupt</span>(); <span class="synComment">// QUEUE_SIZEを超えたtakeなのでブロックするはずだが</span> <span class="synComment">// すでにinterruptされているので割り込みエラーが発生する。</span> <span class="synStatement">match</span> q_cloned.<span class="synIdentifier">take</span>() { <span class="synConstant">Ok</span>(_) <span class="synStatement">=&gt;</span> { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;take: finish: should not be here&quot;</span>); } <span class="synConstant">Err</span>(err) <span class="synStatement">=&gt;</span> { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;take: finish: error = {:?}&quot;</span>, err); <span class="synStatement">let</span> queue_error <span class="synStatement">=</span> err.<span class="synIdentifier">downcast_ref</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>QueueError<span class="synStatement">&lt;</span><span class="synType">i32</span><span class="synStatement">&gt;&gt;</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">assert_eq!</span>(queue_error, <span class="synType">&amp;</span><span class="synPreProc">QueueError</span><span class="synSpecial">::</span>InterruptedError); } } <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>q_cloned.<span class="synIdentifier">is_interrupted</span>()); please_interrupt_cloned.<span class="synIdentifier">count_down</span>(); <span class="synComment">// 先にtakeするのでブロックが発生するが</span> <span class="synComment">// 非同期にinterruptされるので割り込みエラー</span> <span class="synStatement">match</span> q_cloned.<span class="synIdentifier">take</span>() { <span class="synConstant">Ok</span>(_) <span class="synStatement">=&gt;</span> { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;take: finish: should not be here&quot;</span>); } <span class="synConstant">Err</span>(err) <span class="synStatement">=&gt;</span> { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;take: finish: error = {:?}&quot;</span>, err); <span class="synStatement">let</span> queue_error <span class="synStatement">=</span> err.<span class="synIdentifier">downcast_ref</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>QueueError<span class="synStatement">&lt;</span><span class="synType">i32</span><span class="synStatement">&gt;&gt;</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">assert_eq!</span>(queue_error, <span class="synType">&amp;</span><span class="synPreProc">QueueError</span><span class="synSpecial">::</span>InterruptedError); } } <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>q_cloned.<span class="synIdentifier">is_interrupted</span>()); }); <span class="synComment">// カウントが0になるまでブロックする</span> <span class="synComment">// つまり最初のtakeのテストが終わるまで待機する</span> please_interrupt.<span class="synIdentifier">wait</span>(); <span class="synPreProc">assert!</span>(<span class="synStatement">!</span>handler1.<span class="synIdentifier">is_finished</span>()); <span class="synComment">// 非同期に割り込みを行う</span> q.<span class="synIdentifier">interrupt</span>(); handler1.<span class="synIdentifier">join</span>().<span class="synIdentifier">unwrap</span>(); } </pre> <h2 id="takeの実装">takeの実装</h2> <p>仕様は「このキューの先頭を取得して削除します。必要に応じて、要素が利用可能になるまで待機します」。 実装は以下。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/blocking_queue.rs#L161">queue-rs/src/queue/blocking_queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <p>要素が一つでもある場合は2)はスルーされて5)だけ実行される。要素がないつまり空の場合は、2)の内部に入り、4)が<code>not_empty</code>のwaitを呼び出すとスレッドをブロックする。<code>put</code>メソッド内で要素を取得するときに<code>not_empty.notify_once</code>が実行されると、<code>not_empty</code>の<code>wait</code>が解除される(待機しているスレッドが複数ある場合そのうち一つだけが解除される)。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">take</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span><span class="synType">Option</span><span class="synStatement">&lt;</span>E<span class="synStatement">&gt;&gt;</span> { <span class="synComment">// 1) underlyingから必要な要素を取得する</span> <span class="synStatement">let</span> (queue_vec_mutex, not_full, not_empty) <span class="synStatement">=</span> <span class="synType">&amp;*</span><span class="synConstant">self</span>.underlying; <span class="synStatement">let</span> <span class="synType">mut</span> queue_vec_mutex_guard <span class="synStatement">=</span> queue_vec_mutex.<span class="synIdentifier">lock</span>().<span class="synIdentifier">unwrap</span>(); <span class="synComment">// 2) 内部キューが空のときにはループする</span> <span class="synStatement">while</span> queue_vec_mutex_guard.<span class="synIdentifier">is_empty</span>() { <span class="synComment">// 3) 割り込みのチェック</span> <span class="synStatement">if</span> <span class="synConstant">self</span>.<span class="synIdentifier">check_and_update_interrupted</span>() { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;take: return by interrupted&quot;</span>); <span class="synStatement">return</span> <span class="synConstant">Err</span>(<span class="synIdentifier">QueueError</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>E<span class="synStatement">&gt;</span><span class="synSpecial">::</span>InterruptedError.<span class="synIdentifier">into</span>()); } <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;take: blocking start...&quot;</span>); <span class="synComment">// 4) 要素が利用可能になるまで待機する</span> queue_vec_mutex_guard <span class="synStatement">=</span> not_empty.<span class="synIdentifier">wait</span>(queue_vec_mutex_guard).<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;take: blocking end...&quot;</span>); } <span class="synComment">// 5) 内部キューのpollを使って要素を取得する</span> <span class="synStatement">let</span> result <span class="synStatement">=</span> queue_vec_mutex_guard.<span class="synIdentifier">poll</span>(); <span class="synComment">// 6) not_fullを使って通知</span> not_full.<span class="synIdentifier">notify_one</span>(); result } </pre> <h2 id="putとtakeの相互作用">putとtakeの相互作用</h2> <p><code>put</code>メソッドと<code>take</code>メソッドは以下の点で相互に作用する。</p> <ul> <li>ブロッキングと通知: <code>put</code>メソッドはキューが満杯の場合にブロックし、<code>take</code>メソッドはキューが空の場合にブロックする。これにより、プロデューサー(要素を追加するスレッド)とコンシューマー(要素を取り出すスレッド)は、キューの状態に応じて適切に待機または動作する</li> <li>スレッド間の協調: 一方のメソッドがブロックされているスレッドを解放するために、もう一方のメソッドは条件変数を使って通知を行う。例えば、<code>put</code>メソッドは新しい要素を追加した後、<code>take</code>メソッドで待機中のスレッドにキューに新しい要素が利用可能であることを通知する</li> </ul> <h2 id="whileをなぜ使うのか">whileをなぜ使うのか</h2> <p>一見不要そうな<code>while</code>文は、実はブロッキングキューのような同期処理においては非常に重要だ。これは、条件変数(Condvar)を使用する際の一般的なパターンとなる。<code>while</code>文を使用する理由は以下の通り:</p> <ul> <li>スプリアス・ウェイクアップ(Spurious wakeup)の防止 <ul> <li>スレッドは何らかの理由で誤って(スプリアス・ウェイクアップと呼ばれる)目覚めることがある。<code>while</code>文を使用することで、スレッドが再び目覚めたときに、待機条件が依然として満たされているかを再確認する</li> </ul> </li> <li>競合状態の回避 <ul> <li>複数のスレッドが同時にキューにアクセスし、条件変数によって待機が解除される場合、一つのスレッドが条件を満たして操作を行うと、他のスレッドにとってはその条件がもはや真でなくなる可能性がある。<code>while</code>文を用いることで、各スレッドは操作を行う前に条件を再チェックする</li> </ul> </li> <li>明確なロジック <ul> <li>条件のチェックをループ内で行うことで、コードのロジックがより明確になり、他の開発者がコードを理解しやすくなる</li> </ul> </li> </ul> <p>たとえば、<code>put</code>メソッドの実装において、<code>while</code>文はキューがフルである限り、スレッドをブロックし続ける。キューに空きができたときだけ、ループを抜けて要素の挿入を試みる。同様に、<code>take</code>メソッドではキューが空である限りスレッドをブロックし、要素が利用可能になったときのみループを抜けて要素の取得を行う。したがって、<code>while</code>文はこれらの場合において安全性と正確性を高めるために非常に重要となる。</p> <h2 id="中断機構">中断機構</h2> <p>ブロッキングキューでは、特定の状況で操作を中断する機能が重要だ。中断機構は<code>put</code>および<code>take</code>メソッドと密接に連携して動作する。ここでは、その中断機構の実装について詳しく見ていく。</p> <h3 id="check_and_update_interrupted">check_and_update_interrupted</h3> <p><code>put</code>および<code>take</code>メソッドで呼び出される<code>check_and_update_interrupted</code>メソッドは、中断要求があったかどうかを判定し、その状態を更新します。もし<code>is_interrupted</code>フラグが<code>true</code>であれば、メソッドは<code>true</code>を返し、同時に(アトミック操作により)そのフラグを<code>false</code>に更新する。これにより、中断要求が一度だけ認識され、処理されるようになる。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/blocking_queue.rs#L30">queue-rs/src/queue/blocking_queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">check_and_update_interrupted</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> { <span class="synStatement">match</span> <span class="synConstant">self</span> .is_interrupted .<span class="synIdentifier">compare_exchange</span>(<span class="synConstant">true</span>, <span class="synConstant">false</span>, <span class="synPreProc">Ordering</span><span class="synSpecial">::</span>Relaxed, <span class="synPreProc">Ordering</span><span class="synSpecial">::</span>Relaxed) { <span class="synConstant">Ok</span>(_) <span class="synStatement">=&gt;</span> <span class="synConstant">true</span>, <span class="synConstant">Err</span>(_) <span class="synStatement">=&gt;</span> <span class="synConstant">false</span>, } } </pre> <h3 id="interrupt">interrupt</h3> <p><code>interrupt</code>メソッドでは、<code>is_interrupted</code>フラグを<code>true</code>に設定する。これにより、<code>put</code>や<code>take</code>メソッドが中断される。しかし、スレッドがブロックされている場合、このフラグの設定だけでは中断は完了しません。そのため、<code>not_empty</code>と<code>not_full</code>の<code>Condvar</code>を用いて<code>notify_all</code>メソッドを呼び出し、すべてのスレッドのブロックを解除し、中断を実現する。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/blocking_queue.rs#L212">queue-rs/src/queue/blocking_queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">interrupt</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>) { <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;interrupt: start...&quot;</span>); <span class="synConstant">self</span>.is_interrupted.<span class="synIdentifier">store</span>(<span class="synConstant">true</span>, <span class="synPreProc">Ordering</span><span class="synSpecial">::</span>Relaxed); <span class="synStatement">let</span> (_, not_full, not_empty) <span class="synStatement">=</span> <span class="synType">&amp;*</span><span class="synConstant">self</span>.underlying; not_empty.<span class="synIdentifier">notify_all</span>(); <span class="synComment">// すべてのスレッドのブロックを解除</span> not_full.<span class="synIdentifier">notify_all</span>(); <span class="synComment">// すべてのスレッドのブロックを解除</span> <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;interrupt: end...&quot;</span>); } </pre> <h3 id="is_interrupted">is_interrupted</h3> <p><code>is_interrupted</code>メソッドは、外部からの中断要求があったかどうかを確認するものだ。これにより、他のメソッドが中断状態を認識し、適切に反応できるようになる。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/blocking_queue.rs#L221">queue-rs/src/queue/blocking_queue.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">is_interrupted</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> { <span class="synConstant">self</span>.is_interrupted.<span class="synIdentifier">load</span>(<span class="synPreProc">Ordering</span><span class="synSpecial">::</span>Relaxed) } </pre> <h2 id="tokio版の実装">tokio版の実装</h2> <p><code>std</code>ライブラリの<code>Mutex</code>や<code>Condvar</code>などは、リソースが利用可能になるまでスレッドを物理的にブロックする。この方法は同期処理で一般的に使用され、スレッドがリソースを待っている間は他の作業を行うことができない。これはCPUリソースを消費せず、単純な同期処理には適しているが、スケーラビリティや非同期処理の観点では効率的ではない。<strong>tokioを使った非同期処理の中でこの実装を利用するのは推奨されないので、要注意。</strong></p> <p>一方で、tokioの<code>Mutex</code>や<code>Condvar</code>は非同期の待ち受けを実現する。これはスレッドを物理的にブロックするのではなく、リソースが利用可能になるまでの操作を中断し、その間に他のタスクを実行する。この方法は非同期プログラミングで一般的で、特にI/Oバウンドの処理やスケーラブルなシステムに適している。<code>await</code>キーワードを使用することで、非同期タスクの実行を「一時停止」し、リソースが利用可能になると再開する。つまるところ、<code>std</code>の待ち受けはスレッドをブロックする同期的なアプローチであり、tokioの待ち受けはスレッドをブロックせずに非同期処理を行うアプローチとなる。適切な選択は、アプリケーションの要件と性能の要求によって異なる。</p> <p>ということで、tokio版の実装も作った。この実装はアクターシステムを実装するときに使えそう。</p> <p>あと、tokio版の実装はブロッキングしないのに、<code>BlockingQueue</code>という名前になっている…。<code>AsyncBlockingQueue</code>だろうか?それとも<code>NonBlockingQueue</code>だろうか…。命名が難しい。</p> <p><a href="https://github.com/j5ik2o/queue-rs/blob/f4de43386432b0d9873af07e7cbba41e6133a75f/src/queue/tokio.rs">queue-rs/src/queue/tokio.rs at f4de43386432b0d9873af07e7cbba41e6133a75f &middot; j5ik2o/queue-rs &middot; GitHub</a></p> <h2 id="まとめ">まとめ</h2> <p>Rustでブロッキングキューを実装する過程を詳細に掘り下げた。Javaの<code>BlockingQueue</code>を参考にしながらも、Rust独自の機能である<code>Mutex</code>、<code>Condvar</code>、およびアトミック操作を駆使して、効率的かつスレッドセーフなキューを作成できた。この実装は、Rustの同期プリミティブを活用し並行処理をモデリングする一つの方法を示した。また、Javaの<code>BlockingQueue</code>のコンセプトをRustに適用する試みとして、異なるプログラミング言語間のアプローチを探求する良い機会となった。</p> j5ik2o Scala 3でデータ指向プログラミングは可能か hatenablog://entry/6801883189066374249 2023-12-14T00:00:00+09:00 2023-12-14T08:58:08+09:00 Scala 3におけるデータ指向プログラミング(以下DOP)について深掘りする。久々にScalaの話題を取り上げるが、これはScala Advent Calendar 2023の14日目の内容でもある。 早速だけど、DOPの基本原則は意外とシンプルだ。 コード(動作)をデータから切り離す データを汎用的なデータ構造で表現する データをイミュータブル(不変)として扱う データスキーマをデータ表現から切り離す イミュータブルなデータは採用することは多いと思うが、これをそのまま実践している人はどのくらいいるだろうか。Scalaではクラス中心の関数型プログラミングが主流だと思うし、私もそうしている。 … <p>Scala 3におけるデータ指向プログラミング(以下DOP)について深掘りする。久々にScalaの話題を取り上げるが、これは<a href="https://qiita.com/advent-calendar/2023/scala">Scala Advent Calendar 2023</a>の14日目の内容でもある。</p> <p>早速だけど、DOPの基本原則は意外とシンプルだ。</p> <ol> <li>コード(動作)をデータから切り離す</li> <li>データを汎用的なデータ構造で表現する</li> <li>データをイミュータブル(不変)として扱う</li> <li>データスキーマをデータ表現から切り離す</li> </ol> <p>イミュータブルなデータは採用することは多いと思うが、これをそのまま実践している人はどのくらいいるだろうか。Scalaではクラス中心の関数型プログラミングが主流だと思うし、私もそうしている。</p> <p>DOPの詳細は下記の本(以下DOP本)を参照してほしい。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B0BWR57K64?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51MKZOlzOOL._SL500_.jpg" class="hatena-asin-detail-image" alt="データ指向プログラミング" title="データ指向プログラミング"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B0BWR57K64?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">データ指向プログラミング</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="https://d.hatena.ne.jp/keyword/Yehonathan%20Sharvit" class="keyword">Yehonathan Sharvit</a></li><li>翔泳社</li></ul><a href="https://www.amazon.co.jp/dp/B0BWR57K64?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>ちなみに留意すべき点がある。DOP本とJavaのProject Amberにおけるデータ指向プログラミングは、同じ用語を使っているが方向性が異なる。DOP本では上記の原則に重点を置いている一方で、Project Amberではデータの解釈を型で表現しモデリングのしやすさに焦点を当てている。これは多義的な用語となっているため注意が必要である。この違いは、データ指向アプローチを実践する際の重要な考慮事項である。詳細は <a href="https://slides.com/kawasima/truth-of-data-oriented-programming">川島さんのスライド</a>を参照すると良い。今回の記事もDOP本のほうで書いているので注意。</p> <p>さて、今回はScala 3でDOPを試みる。題材は「ショッピングカート」。中心的な集約はカート(Cart型)と注文(Order型)。</p> <p>興味のある方は、OOPスタイルとDOPスタイルの両方で記述された以下のソースコードを参照してほしい。(ちなみにドメイン部分を中心にコードを書いている)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fj5ik2o%2Foop-dop-other" title="GitHub - j5ik2o/oop-dop-other" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/j5ik2o/oop-dop-other">github.com</a></cite></p> <p>DOPを試すために、まずはご多分に漏れず「お金」オブジェクトを実装してみた。(Scala 3で実践する都合上、DOP本のまま推奨の方法で行っていない部分もありますが、そこはご容赦ください)</p> <p><a href="https://github.com/j5ik2o/oop-dop-other/blob/main/src/main/scala/example/j5ik2o/dop/domain/Money.scala">oop-dop-other/src/main/scala/example/j5ik2o/dop/domain/Money.scala at main &middot; j5ik2o/oop-dop-other &middot; GitHub</a></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// 原則2, 3</span> opaque <span class="synType">type</span> Money = Map[<span class="synConstant">String</span>, Any] <span class="synType">object</span> Money { <span class="synType">final</span> <span class="synType">val</span> DefaultCurrency: Currency = Currency.getInstance(Locale.getDefault) <span class="synType">final</span> <span class="synType">val</span> JPY: Currency = Currency.getInstance(<span class="synConstant">&quot;JPY&quot;</span>) <span class="synIdentifier"> def</span> zero(currency: Currency = DefaultCurrency): Money = Money(<span class="synConstant">0</span>, currency) <span class="synIdentifier"> def</span> apply(amount: BigDecimal, currency: Currency = DefaultCurrency): Money = Map(<span class="synConstant">&quot;amount&quot;</span> -&gt; amount, <span class="synConstant">&quot;currency&quot;</span> -&gt; currency) <span class="synIdentifier"> def</span> apply(amount: BigDecimal, currency: <span class="synConstant">String</span>): Money = Money(amount, Currency.getInstance(currency)) <span class="synIdentifier"> def</span> unapply(self: Money): Option[(BigDecimal, Currency)] = Some((self.amount, self.currency)) <span class="synComment">// 原則1, 3</span> extension (self: Money) { <span class="synComment">// 原則4</span> <span class="synIdentifier"> def</span> amount: BigDecimal = self(<span class="synConstant">&quot;amount&quot;</span>).asInstanceOf[BigDecimal] <span class="synIdentifier"> def</span> currency: Currency = self(<span class="synConstant">&quot;currency&quot;</span>).asInstanceOf[Currency] <span class="synComment">// 二項演算子を定義。 例) val m3: Money = m1 + m2</span> infix<span class="synIdentifier"> def</span> +(other: Money): Money = { require(currency == other.currency) Money(amount + other.amount, currency) } infix<span class="synIdentifier"> def</span> -(other: Money): Money = self + -other infix<span class="synIdentifier"> def</span> *(multiplier: Int): Money = Money(amount * multiplier, currency) infix<span class="synIdentifier"> def</span> *(multiplier: Double): Money = Money(amount * multiplier, currency) infix<span class="synIdentifier"> def</span> /(divisor: Int): Money = Money(amount / divisor, currency) infix<span class="synIdentifier"> def</span> /(divisor: Double): Money = Money(amount / divisor, currency) <span class="synComment">// 単項演算子を定義。 例) val m2: Money = -m1</span> <span class="synIdentifier"> def</span> unary_- : Money = Money(-amount, currency) <span class="synIdentifier"> def</span> negated: Money = -self <span class="synIdentifier"> def</span> plus(other: Money): Money = self + other <span class="synIdentifier"> def</span> minus(other: Money): Money = self - other <span class="synIdentifier"> def</span> times(multiplier: Int): Money = self * multiplier <span class="synIdentifier"> def</span> times(multiplier: Double): Money = self * multiplier <span class="synIdentifier"> def</span> divide(divisor: Int): Money = self / divisor <span class="synIdentifier"> def</span> divide(divisor: Double): Money = self / divisor } <span class="synComment">// Moneyどうしの比較</span> given Ordering[Money] = (x: Money, y: Money) =&gt; { require(x.currency == y.currency) x.amount.compare(y.amount) } <span class="synComment">// IntからMoneyへの暗黙的型変換。 例) val m1: Money = 100</span> given Conversion[Int, Money] = (amount: Int) =&gt; Money(amount, DefaultCurrency) } </pre> <p>定義側は全く見慣れない構造だが、利用側は意外と慣れ親しんだスタイルで使える。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> money1 = Money(<span class="synConstant">100</span>) <span class="synType">val</span> money2 = Money(<span class="synConstant">200</span>) <span class="synType">val</span> result1 = money1 + money2 <span class="synComment">// Money(300)</span> <span class="synType">val</span> result2 = money1 - money2 <span class="synComment">// Money(-100)</span> <span class="synType">val</span> result3 = money1 * <span class="synConstant">2</span> <span class="synComment">// Money(200)</span> <span class="synType">val</span> result4 = money1 / <span class="synConstant">2</span> <span class="synComment">// Money(50)</span> </pre> <h2 id="原則1-コード動作をデータから切り離す">原則1 「コード(動作)をデータから切り離す」</h2> <p>原則1では、データ構造とメソッドを分離する。つまり、関数やメソッドを独立させるということ。</p> <h3 id="データの定義">データの定義</h3> <p>データ型としての<code>Money</code>は<code>Map[String, Any]</code>として実装されている。クラスベースだと属性と振る舞いを含めて型というイメージだが、DOPでは型はデータのみ。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>opaque <span class="synType">type</span> Money = Map[<span class="synConstant">String</span>, Any] </pre> <p>もちろん、これはScala 3の固有の機能なのでDOP本に言及はなく、今回の独自の工夫をしている部分になる。<code>opaque type</code>を使うことで、<code>Map</code>型でありながら<code>Money</code>として型安全を保てる。間違って他の型や<code>Map[String, Any]</code>型の引数に渡すとコンパイルエラーになる。DOPするならこの機能は是非使いたいところ。</p> <h3 id="コードの定義">コードの定義</h3> <p><code>Money</code>に関連するメソッド群は、拡張メソッドとして定義した。これも今回の独自の工夫と言ってよい。<code>this</code>に相当する部分はメソッドの引数として渡される。実装上は<code>self</code>としている。GoやRustのメソッドに似たような記述になる。<code>Money.plus(self, other)</code>のように<code>self</code>を引数の取ってもよいが、左から右に流れるように読むことが難しいので拡張メソッドにした。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>extension (self: Money) { <span class="synComment">// ...</span> infix<span class="synIdentifier"> def</span> +(other: Money): Money = { require(currency == other.currency) Money(amount + other.amount, currency) <span class="synComment">// copyメソッドは使えない…</span> } <span class="synComment">// ...</span> } </pre> <p><code>amount</code>や<code>currency</code>もメソッドとして定義されているため、ここではあまり差分はない。</p> <p><code>+</code>と<code>plus</code>の加算メソッドの使い方として、以下はすべて同じ意味になる。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> r1 = money1 + money2 <span class="synType">val</span> r2 = money1.+(money2) <span class="synType">val</span> r3 = Money.+(money1)(money2) </pre> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> r4 = money1.plus(money2) <span class="synType">val</span> r5 = Money.plus(money1)(money2) </pre> <h2 id="原則2データを汎用的なデータ構造で表す">原則2「データを汎用的なデータ構造で表す」</h2> <p>コンストラクタは<code>apply</code>メソッドで表現。ただの<code>Map[String, Any]</code>生成だ。これは柔軟性を得るためのトレードオフ。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synIdentifier"> def</span> apply(amount: BigDecimal, currency: Currency = DefaultCurrency): Money = Map(<span class="synConstant">&quot;amount&quot;</span> -&gt; amount, <span class="synConstant">&quot;currency&quot;</span> -&gt; currency) <span class="synIdentifier"> def</span> apply(amount: BigDecimal, currency: <span class="synConstant">String</span>): Money = Money(amount, Currency.getInstance(currency)) </pre> <p>もちろん、値型が<code>Any</code>なので型安全性が失われる。これは具体的な値型を使うクラスの場合と違って値を取り扱う場合に注意が必要になる。 DOP的な柔軟性が下がるが型安全性が気になる場合は、構造体的な<code>case class</code>でもいいのではないか。</p> <h2 id="原則3-データをイミュータブル不変として扱う">原則3 「データをイミュータブル(不変)として扱う」</h2> <p>Scalaではミュータブルなデータ構造を避けるだけで、この原則は容易に実現できる。<code>Money</code>のメソッド群も不変。</p> <h2 id="原則4-データスキーマをデータ表現から切り離す">原則4 「データスキーマをデータ表現から切り離す」</h2> <p>原則4 「データスキーマをデータ表現から切り離す」は、データとデータスキーマを分離する考え方。</p> <p>一つには、フィールドアクセス用の専用メソッドがある意味データスキーマになっている。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>extension (self: Money) { <span class="synComment">// ...</span> <span class="synIdentifier"> def</span> amount: BigDecimal = self(<span class="synConstant">&quot;amount&quot;</span>).asInstanceOf[BigDecimal] <span class="synIdentifier"> def</span> currency: Currency = self(<span class="synConstant">&quot;currency&quot;</span>).asInstanceOf[Currency] <span class="synComment">// ...</span> } </pre> <p>ところで、サブタイプの表現はDOPではどう扱うか。DOPでは継承を利用しないため、サブタイプごとのデータ型だけが存在することになる。</p> <p>しかし、Scalaの場合、このアプローチに固執する必要はないだろう。<code>sealed trait</code>や型クラスを利用すれば良いのではないか。今回の取り組みはあくまで実験的なものである。</p> <p><code>Cart</code>に含める商品にはダウンロード型のコンテンツ(<code>DownloadableItem</code>)や車(<code>CarItem</code>)やそれ以外の商品(<code>GenericItem</code>)がある。それらの共通項を括りだしたのが<code>Item</code>型になる。</p> <p><a href="https://github.com/j5ik2o/oop-dop-other/blob/main/src/main/scala/example/j5ik2o/dop/domain/Item.scala">oop-dop-other/src/main/scala/example/j5ik2o/dop/domain/Item.scala at main &middot; j5ik2o/oop-dop-other &middot; GitHub</a></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> example.j5ik2o.dop.domain <span class="synPreProc">import</span> example.j5ik2o.common.domain.ItemType <span class="synPreProc">import</span> java.net.URL opaque <span class="synType">type</span> Item = Map[<span class="synConstant">String</span>, Any] opaque <span class="synType">type</span> ItemId = <span class="synConstant">String</span> <span class="synType">object</span> ItemId { <span class="synIdentifier"> def</span> apply(value: <span class="synConstant">String</span>): ItemId = value <span class="synIdentifier"> def</span> unapply(self: ItemId): Option[<span class="synConstant">String</span>] = Some(self) given Conversion[<span class="synConstant">String</span>, ItemId] = ItemId(_) extension (self: ItemId) { <span class="synIdentifier"> def</span> value: <span class="synConstant">String</span> = self } } <span class="synType">object</span> Item { <span class="synIdentifier"> def</span> apply(id: ItemId, name: ItemName, price: Money, itemType: ItemType): Item = Map(<span class="synConstant">&quot;id&quot;</span> -&gt; id, <span class="synConstant">&quot;name&quot;</span> -&gt; name, <span class="synConstant">&quot;price&quot;</span> -&gt; price, <span class="synConstant">&quot;type&quot;</span> -&gt; itemType) extension (self: Item) { <span class="synIdentifier"> def</span> id: ItemId = self(<span class="synConstant">&quot;id&quot;</span>).asInstanceOf[ItemId] <span class="synIdentifier"> def</span> name: ItemName = self(<span class="synConstant">&quot;name&quot;</span>).asInstanceOf[ItemName] <span class="synIdentifier"> def</span> price: Money = self(<span class="synConstant">&quot;price&quot;</span>).asInstanceOf[Money] <span class="synIdentifier"> def</span> itemType: ItemType = self(<span class="synConstant">&quot;type&quot;</span>).asInstanceOf[ItemType] } } opaque <span class="synType">type</span> GenericItem = Map[<span class="synConstant">String</span>, Any] <span class="synType">object</span> GenericItem { <span class="synIdentifier"> def</span> apply(id: ItemId, name: ItemName, price: Money): GenericItem = Item.apply(id, name, price, ItemType.Generic) <span class="synIdentifier"> def</span> unapply(self: GenericItem): Option[(ItemId, ItemName, Money)] = Some((Item.id(self), Item.name(self), Item.price(self))) extension (self: GenericItem) { <span class="synIdentifier"> def</span> id: ItemId = Item.id(self) <span class="synIdentifier"> def</span> name: ItemName = Item.name(self) <span class="synIdentifier"> def</span> price: Money = Item.price(self) } given Conversion[GenericItem, Item] = _.asInstanceOf[Item] } opaque <span class="synType">type</span> DownloadableItem = Map[<span class="synConstant">String</span>, Any] <span class="synType">object</span> DownloadableItem { <span class="synIdentifier"> def</span> apply(id: ItemId, name: ItemName, url: URL, price: Money): DownloadableItem = Item.apply(id, name, price, ItemType.Download) + (<span class="synConstant">&quot;url&quot;</span> -&gt; url) <span class="synIdentifier"> def</span> unapply(self: DownloadableItem): Option[(ItemId, ItemName, URL, Money)] = Some( ( Item.id(self), Item.name(self), self.url, Item.price(self) ) ) given Conversion[DownloadableItem, Item] = _.asInstanceOf[Item] extension (self: DownloadableItem) { <span class="synIdentifier"> def</span> id: ItemId = Item.id(self) <span class="synIdentifier"> def</span> name: ItemName = Item.name(self) <span class="synIdentifier"> def</span> url: URL = self(<span class="synConstant">&quot;url&quot;</span>).asInstanceOf[URL] <span class="synIdentifier"> def</span> price: Money = Item.price(self) } } opaque <span class="synType">type</span> CarItem = Map[<span class="synConstant">String</span>, Any] <span class="synType">object</span> CarItem { <span class="synIdentifier"> def</span> apply(id: ItemId, name: ItemName, price: Money): CarItem = Item.apply(id, name, price, ItemType.Car) <span class="synIdentifier"> def</span> unapply(self: CarItem): Option[(ItemId, ItemName, Money)] = Some((Item.id(self), Item.name(self), Item.price(self))) given Conversion[CarItem, Item] = _.asInstanceOf[Item] } </pre> <p>共通処理を行いたい場合、各型を共通の<code>Item</code>型に変換する。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> genericItem: GenericItem = GenericItem(<span class="synConstant">&quot;1&quot;</span>, <span class="synConstant">&quot;name&quot;</span>, <span class="synConstant">100</span>) assert(genericItem.id.value == <span class="synConstant">&quot;1&quot;</span>) <span class="synType">val</span> item: Item = genericItem assert(item.id.value == <span class="synConstant">&quot;1&quot;</span>) </pre> <p>暗黙的型変換により、<code>GenericItem</code>を<code>Item</code>に変換できる。本質的には<code>Map</code>なので、単なるキャストに過ぎない。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>given Conversion[GenericItem, Item] = _.asInstanceOf[Item] </pre> <p>抽象型に相当する<code>Item</code>のメソッドは、<code>GenericItem</code>に対しても呼び出せる。ただ、これは実装がやや面倒な側面がある。ちなみに、<code>Item</code>のメソッドに<code>GenericItem</code>の<code>self</code>を渡しているが暗黙的型変換で<code>Item</code>に変換されているので、問題なくコンパイル可能となっている。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>extension (self: GenericItem) { <span class="synIdentifier"> def</span> id: ItemId = Item.id(self) <span class="synIdentifier"> def</span> name: ItemName = Item.name(self) <span class="synIdentifier"> def</span> price: Money = Item.price(self) } </pre> <p>他にも<code>url</code>を持つ<code>DownloadableItem</code>のデータを必要に応じて<code>Item</code>型で利用する部分があるが、検証すべきデータを自由に選択できるので確かに柔軟性が高いかもしれない。</p> <h1 id="Cart型-Order型">Cart型, Order型</h1> <p>次は<code>Cart</code>を実装。<code>Money</code>と同じスタイルで可能。</p> <p><a href="https://github.com/j5ik2o/oop-dop-other/blob/main/src/main/scala/example/j5ik2o/dop/domain/Cart.scala">oop-dop-other/src/main/scala/example/j5ik2o/dop/domain/Cart.scala at main &middot; j5ik2o/oop-dop-other &middot; GitHub</a></p> <p><code>CartId</code>も<code>opaque type</code>で文字列型。<code>apply</code>メソッドで検証を行う。</p> <p><code>copy</code>メソッドが使えないが、<code>Map</code>の<code>+</code>で<code>copy</code>メソッド相当のことができる。</p> <p><code>Order</code>型も問題なく実装できた。<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p> <p><a href="https://github.com/j5ik2o/oop-dop-other/blob/main/src/main/scala/example/j5ik2o/dop/domain/Order.scala">oop-dop-other/src/main/scala/example/j5ik2o/dop/domain/Order.scala at main &middot; j5ik2o/oop-dop-other &middot; GitHub</a></p> <p>ざっと実装コードを眺めるとやはりGo, Rustのような雰囲気がでてくる。面白い。</p> <h1 id="まとめ">まとめ</h1> <p>Scala 3の機能を活用し、DOPの概念とその実装方法について深く掘り下げた結果、いくつかの重要な洞察を得た。</p> <p>DOPの原則には現代のプログラミングにとって有益な考え方が多く含まれている。また、Scala 3でのDOP実践においては<code>opaque type</code>などの新機能がかなり役に立った。もはや必須機能!これにより、型安全性を維持しながらも、より柔軟で表現力に富んだデータモデリングが実現可能となる。このアプローチはOOPの一部の制約を超越し、データとその操作の明確な分離を可能にする。OOP上に組み込むとクラスを使わないので、ある意味新しいプログラミングスタイルに見える。</p> <p>しかし、今回紹介したコードスタイルはOOPの標準的なスタイルと異なるため、人によっては慣れるまでに時間がかかるかもしれない。さらにDOPがその言語の設計思想にあっているかよく考える必要がある。驚きが最小にならないことも…。Scalaの場合でも無理にこのようなスタイルを採用する必要性はないと思われる。もちろん、DOPを部分的導入する考え方もあるが、やはりClojureのようなDOPに向いた言語の使用も検討する価値があると感じられる…。</p> <p>いずれにしても、Scala 3の強力な表現力と型安全性を上手く活用することで、DOPスタイルでもScalaが有用であることがわかった。</p> <h1 id="付録">付録</h1> <p>DOP本のソースコードはこちら。本書掲載のコード以外に<code>challenges</code>以下に他の言語での実装例もある。異世界を探検したい人は覗いてみるといいかも。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fviebel%2Fdata-oriented-programming" title="GitHub - viebel/data-oriented-programming" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/viebel/data-oriented-programming">github.com</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> Order#adjustPriceがクソコード臭がしてなんともいえないが、別の機会にDOPのリファクタリングについて書く際の題材にする予定。あしからず…。<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> j5ik2o RustでProperty-based testing ライブラリを実装してみた hatenablog://entry/4207112889944378984 2022-12-12T16:58:55+09:00 2022-12-12T16:58:55+09:00 この記事はRust Advent Calendar 2022 - Qiitaの12日目の記事です。 以前、Rustの実装力をあげるために、Property-based testingライブラリを作ったのですが、それについて説明する記事です。 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/rust">Rust Advent Calendar 2022 - Qiita</a>の12日目の記事です。</p> <p>以前、Rustの実装力をあげるために、Property-based testingライブラリを作ったのですが、それについて説明する記事です。</p> <h2 id="Property-based-testingとは">Property-based testingとは</h2> <p>Property-based testingとは何か。詳しくは以下を参照してください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fjessitron.com%2F2013%2F04%2F25%2Fproperty-based-testing-what-is-it%2F" title="Property-based testing: what is it?" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://jessitron.com/2013/04/25/property-based-testing-what-is-it/">jessitron.com</a></cite></p> <blockquote><p>Property-based tests make statements about the output of your code based on the input, and these statements are verified for many different possible inputs.</p></blockquote> <p>statementsはDSLのことかなと理解していますが、がくぞさんの説明のほうが簡潔なので以下の資料も参考にしてみてください。</p> <blockquote><p>テストデータをランダムに半自動生成して、その生成された全ての値について、満たすべき性質を満たしているかテストする</p></blockquote> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgakuzzzz.github.io%2Fslides%2Fproperty_based_testing_for_domain%2F%231" title="Property Based Testing でドメインロジックをテストする" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://gakuzzzz.github.io/slides/property_based_testing_for_domain/#1">gakuzzzz.github.io</a></cite></p> <h2 id="実装はしたくない使いたいという人はこっち">実装はしたくない使いたいという人はこっち</h2> <p>なんでもかんでも作ればいいってわけではないので、crates.ioから検索すると以下が代表的なクレートです。</p> <ul> <li><a href="https://crates.io/crates/quickcheck">https://crates.io/crates/quickcheck</a></li> <li><a href="https://crates.io/crates/proptest">https://crates.io/crates/proptest</a></li> </ul> <p>実用ならこっちのほうがいいかも。</p> <p>今回は実装力を上げるために車輪を再実装します。</p> <h2 id="実装したクレートについて">実装したクレートについて</h2> <p>これです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fj5ik2o%2Fprop-check-rs" title="GitHub - j5ik2o/prop-check-rs: A Rust crate for property-based testing." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/j5ik2o/prop-check-rs">github.com</a></cite></p> <p>コード量はそんなにないので、さらっと読めるかも。</p> <h3 id="なにがうれしいのか">なにがうれしいのか</h3> <p>URIパーサを実装したときに、このクレートを使ってテストを書きました。以下のようなものです。 <code>absolute_uri</code>関数や<code>uri</code>関数がURIパーサーです。その関数をテストしています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synPreProc">#[test]</span> <span class="synStatement">fn</span> <span class="synIdentifier">test_uri</span>() <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>()<span class="synStatement">&gt;</span> { <span class="synStatement">let</span> <span class="synType">mut</span> counter <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synStatement">let</span> uri_gen <span class="synStatement">=</span> <span class="synIdentifier">uri_gen</span>(); <span class="synComment">// テストデータを生成するジェネレータ</span> <span class="synComment">// Property-based testingを行うPropを生成する</span> <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synPreProc">prop</span><span class="synSpecial">::</span><span class="synIdentifier">for_all_gen</span>(uri_gen, <span class="synType">move</span> <span class="synStatement">|</span>s<span class="synStatement">|</span> { counter <span class="synStatement">+=</span> <span class="synConstant">1</span>; <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;{:&gt;03}, uri:string = {}&quot;</span>, counter, s); <span class="synStatement">let</span> input <span class="synStatement">=</span> s.<span class="synIdentifier">as_bytes</span>(); <span class="synStatement">let</span> uri <span class="synStatement">=</span> (<span class="synIdentifier">uri</span>() <span class="synStatement">-</span> <span class="synIdentifier">end</span>()).<span class="synIdentifier">parse</span>(input).<span class="synIdentifier">success</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">log</span><span class="synSpecial">::</span><span class="synPreProc">debug!</span>(<span class="synConstant">&quot;{:&gt;03}, uri:object = {:?}&quot;</span>, counter, uri); <span class="synPreProc">assert_eq!</span>(uri.<span class="synIdentifier">to_string</span>(), s); <span class="synConstant">true</span> }); <span class="synComment">// Property-based testingの実行</span> <span class="synPreProc">prop</span><span class="synSpecial">::</span><span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()) } </pre> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/9b9a465e8ab098e8fae18c0121f27fcf6b53cf0a/uri/src/parsers/uri_parsers.rs#L73">https://github.com/j5ik2o/oni-comb-rs/blob/9b9a465e8ab098e8fae18c0121f27fcf6b53cf0a/uri/src/parsers/uri_parsers.rs#L73</a></p> <p>テストでは<code>uri_gen</code>関数が利用されます。これはURIの仕様に沿った文字列をランダムに生成するジェネレータ(Gen)です。<code>uri_gen</code>関数は<code>scheme_gen</code>関数や<code>query_gen</code>関数や<code>fragment_gen</code>関数によって実装されます。Genどうしを合成したGenが作れます。</p> <p>他のケースとして、IPv6アドレスのパーサーのテストとかもあります。これはまぁまぁ面倒で、こういうのは手でテストデータを作りたくないですね。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/43484e77f885ea4f48bbb893de9afc3c13e44959/uri/src/parsers/ip_v6_address_parsers.rs#L201">https://github.com/j5ik2o/oni-comb-rs/blob/43484e77f885ea4f48bbb893de9afc3c13e44959/uri/src/parsers/ip_v6_address_parsers.rs#L201</a></p> <p>このような手法を使わずに自前でテストデータを作るのは骨が折れます。原始的なGenを使って高度なGenを組み上げるスタイルで、Property based testingのライブラリを使うと割と楽ができます。</p> <h2 id="Genの利用方法">Genの利用方法</h2> <p>もうちょっと実装よりのGenの利用方法について。</p> <p>なんらかの方法でGenを生成(後述します)し<code>for_all_gen</code>関数に第一引数に渡して、第二引数の関数でGenによって生成された値を使います(この関数の戻り値がfalseを返した場合はProperty based testingが失敗します)。第二引数で与えた関数はテスト実行時に指定されたテスト回数分 呼び出されます。すべてのテストにおいて戻り値がtrueでないとテストは成功と見なされません。</p> <p>そして、<code>for_all_gen</code>関数はPropを生成するので、それを<code>test_with_prop</code>関数を渡すとテストを実行します。テストの実行にはProp以外にテストサイズ、テスト数、乱数生成器を指定する必要があります。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> ... <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synComment">// テストコード</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <h3 id="参考にした実装">参考にした実装</h3> <p>この記事では詳しく説明しませんが、FP in Scalaのfirst-edtionのコードを参考にしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffpinscala%2Ffpinscala%2Ftree%2Ffirst-edition" title="GitHub - fpinscala/fpinscala at first-edition" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/fpinscala/fpinscala/tree/first-edition">github.com</a></cite></p> <p>興味がある方は、日本語の書籍もあるので読んでみてください。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4844337769?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51yVEKnEKlL._SL500_.jpg" class="hatena-asin-detail-image" alt="Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)" title="Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4844337769?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Paul%20Chiusano" class="keyword">Paul Chiusano</a>,<a href="http://d.hatena.ne.jp/keyword/R%8F%AB%E2nar%20Bjarnason" class="keyword">Rúnar Bjarnason</a></li><li>インプレス</li></ul><a href="https://www.amazon.co.jp/dp/4844337769?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <h3 id="実装コード">実装コード</h3> <p>主な実装を説明します。</p> <h2 id="for_all_gen関数とtest_with_prop関数">for_all_gen関数とtest_with_prop関数</h2> <p><code>for_all_gen</code>関数はGenを基にPropを生成する関数です。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">for_all_gen</span><span class="synStatement">&lt;</span>A, F<span class="synStatement">&gt;</span>(g: Gen<span class="synStatement">&lt;</span>A<span class="synStatement">&gt;</span>, <span class="synType">mut</span> test: F) <span class="synStatement">-&gt;</span> Prop <span class="synStatement">where</span> F: <span class="synType">FnMut</span>(A) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span>, A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'static</span>, { Prop { run_f: <span class="synPreProc">Rc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synPreProc">RefCell</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>_, n, rng<span class="synStatement">|</span> { <span class="synStatement">let</span> success_counter <span class="synStatement">=</span> <span class="synPreProc">itertools</span><span class="synSpecial">::</span><span class="synIdentifier">iterate</span>(<span class="synConstant">1</span>, <span class="synStatement">|</span><span class="synType">&amp;</span>i<span class="synStatement">|</span> i <span class="synStatement">+</span> <span class="synConstant">1</span>).<span class="synIdentifier">into_iter</span>(); <span class="synIdentifier">random_stream</span>(g.<span class="synIdentifier">clone</span>(), rng) .<span class="synIdentifier">zip</span>(success_counter) .<span class="synIdentifier">take</span>(n <span class="synStatement">as</span> <span class="synType">usize</span>) .<span class="synIdentifier">map</span>(<span class="synStatement">|</span>(test_value, success_count)<span class="synStatement">|</span> { <span class="synStatement">if</span> <span class="synIdentifier">test</span>(test_value.<span class="synIdentifier">clone</span>()) { <span class="synPreProc">PropResult</span><span class="synSpecial">::</span>Passed { test_cases: n } } <span class="synStatement">else</span> { <span class="synPreProc">PropResult</span><span class="synSpecial">::</span>Falsified { failure: <span class="synPreProc">format!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, test_value), successes: success_count, } } }) .<span class="synIdentifier">find</span>(<span class="synType">move</span> <span class="synStatement">|</span>e<span class="synStatement">|</span> e.<span class="synIdentifier">is_falsified</span>()) .<span class="synIdentifier">unwrap_or</span>(<span class="synPreProc">PropResult</span><span class="synSpecial">::</span>Passed { test_cases: n }) })), } } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/prop.rs#L149">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/prop.rs#L149</a></p> <p>PropはFnMutを内包する構造体です。メソッドはそのFnMutを実行するrunがあります。他にもPropとPropを合成するandメソッドがあったりします。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Prop</span> { run_f: Rc<span class="synStatement">&lt;</span>RefCell<span class="synStatement">&lt;</span>dyn <span class="synType">FnMut</span>(MaxSize, TestCases, RNG) <span class="synStatement">-&gt;</span> PropResult<span class="synStatement">&gt;&gt;</span>, } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/prop.rs#L190-L192">prop-check-rs/prop.rs at 4776da568ca35ca319e66c866d9670ce7c9fe40f &middot; j5ik2o/prop-check-rs &middot; GitHub</a></p> <p><code>for_all_gen</code>関数では、Genを実行できる無限ストリームを生成し、そのGenから生成された値を引数として渡し、指定されたテスト関数に指定回数分呼び出すPropを生成します。</p> <p><code>test_with_prop</code>はテスト条件を指定して、Propを実行するだけです。</p> <h2 id="GenとGenを生成するファクトリGens">GenとGenを生成するファクトリGens</h2> <p>Genを実行する仕組みがわかったところで、Gen自体をどうやって生成するかについて簡単に説明します。</p> <p>その前にGenの定義から。 GenはState型を内包する型です。<a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/rng.rs">RNG</a>は純粋な乱数生成器です。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Gen</span><span class="synStatement">&lt;</span>A<span class="synStatement">&gt;</span> { sample: State<span class="synStatement">&lt;</span>RNG, A<span class="synStatement">&gt;</span>, } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L354">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L354</a></p> <p>State型はいわゆるStateモナドのような実装になっていて、Stateどうしを合成して新たなStateを生成することができます。そして、それを使うGenもGenどうしを合成可能です。</p> <h3 id="Genspure">Gens#pure</h3> <p>GensはGenのファクトリメソッドが定義されています。</p> <p>最も単純なメソッドは<code>pure</code>関数でしょう。何度呼び出してもpureの引数に与えた値しか返さないGenを生成できます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synError">pure</span>(<span class="synConstant">1</span>); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { input <span class="synStatement">==</span> <span class="synConstant">1</span> <span class="synComment">// inputは常に1</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p>やっていることは単純にrunされたときに実行される関数内で固定値を返すようにします。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">pure</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>(value: B) <span class="synStatement">-&gt;</span> Gen<span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> B: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span>, { <span class="synIdentifier">Gen</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synPreProc">State</span><span class="synSpecial">::</span><span class="synIdentifier">value</span>(value)) } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L27">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L27</a></p> <h3 id="Gensone_char">Gens#one_char</h3> <p><code>one_char</code>関数は任意のcharを返します。他にもone_u8, one_u32, ...など型に応じたメソッドがあります。ジェネリック版のoneメソッドもあります。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">one_char</span>(); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synPreProc">println!</span>(<span class="synConstant">&quot;input = {}&quot;</span>, input.<span class="synIdentifier">to_string</span>()); <span class="synComment">// 任意の文字</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/rng.rs">prop-check-rs/rng.rs at 4776da568ca35ca319e66c866d9670ce7c9fe40f &middot; j5ik2o/prop-check-rs &middot; GitHub</a></p> <p>やっていることは単純にrunされたときに実行される関数内でRNGから任意の値を取っているだけです。</p> <h3 id="Genslist_of_n">Gens#list_of_n</h3> <p>このファクトリメソッドは任意の数値の配列を生成できるGenが欲しい場合に使えます。以下のようにすると、任意のu8を5個保持するVecを返すGenを生成できます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">list_of_n</span>(<span class="synConstant">5</span>, <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">one_u8</span>()); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synPreProc">println!</span>(<span class="synConstant">&quot;input = {:?}&quot;</span>, input); <span class="synComment">// 任意の文字の配列</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p>複数の、Gen#sampleを返す関数を保持するVecを素に作ったStateをGenに組み込むことで、複数の値を生成して返せるようになります。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">list_of_n</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>(n: <span class="synType">usize</span>, gen: Gen<span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> Gen<span class="synStatement">&lt;</span><span class="synType">Vec</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;&gt;</span> <span class="synStatement">where</span> B: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span>, { <span class="synStatement">let</span> <span class="synType">mut</span> v: <span class="synType">Vec</span><span class="synStatement">&lt;</span>State<span class="synStatement">&lt;</span>RNG, B<span class="synStatement">&gt;&gt;</span> <span class="synStatement">=</span> <span class="synType">Vec</span><span class="synSpecial">::</span><span class="synIdentifier">with_capacity</span>(n); v.<span class="synIdentifier">resize_with</span>(n, <span class="synType">move</span> <span class="synStatement">||</span> gen.<span class="synIdentifier">clone</span>().sample); Gen { sample: <span class="synPreProc">State</span><span class="synSpecial">::</span><span class="synIdentifier">sequence</span>(v), } } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L93">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L93</a></p> <h3 id="Genschoose">Gens#choose</h3> <p>特定の値の範囲から一つの値を選択する場合は、<code>choose</code>が使えます。以下のようにすると、1から10までの値を一つ選択するGenを生成できます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose</span>(<span class="synConstant">1</span>, <span class="synConstant">10</span>); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synPreProc">println!</span>(<span class="synConstant">&quot;input = {}&quot;</span>, input); <span class="synComment">// 1から10までの任意の数値</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p>map関数の中で、生成された乱数を素に、与えられた最小・最大の範囲から対応する数値を選ぶようにしています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">choose_u32</span>(min: <span class="synType">u32</span>, max: <span class="synType">u32</span>) <span class="synStatement">-&gt;</span> Gen<span class="synStatement">&lt;</span><span class="synType">u32</span><span class="synStatement">&gt;</span> { Gen { sample: <span class="synIdentifier">State</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>RNG, <span class="synType">u32</span><span class="synStatement">&gt;</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>rng: RNG<span class="synStatement">|</span> rng.<span class="synIdentifier">next_u32</span>()), } .<span class="synIdentifier">map</span>(<span class="synType">move</span> <span class="synStatement">|</span>n<span class="synStatement">|</span> min <span class="synStatement">+</span> n <span class="synStatement">%</span> (max <span class="synStatement">-</span> min <span class="synStatement">+</span> <span class="synConstant">1</span>)) } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L259">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L259</a></p> <h3 id="Gensone_of">Gens#one_of</h3> <p><code>choose</code>を使うと引数で与えた複数のGenから一つ選ぶ<code>one_of</code>も実装できます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">one_of</span>(<span class="synPreProc">vec!</span>[<span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose</span>(<span class="synConstant">1</span>, <span class="synConstant">10</span>), <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose</span>(<span class="synConstant">90</span>, <span class="synConstant">100</span>)]); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synPreProc">println!</span>(<span class="synConstant">&quot;input = {}&quot;</span>, input); <span class="synComment">// 1から10までもしくは90から100までの任意の数値</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p>これもシンプル。コレクションのインデックスをchooseで選ぶ。そのインデックスを使ってVecからGenを取得して返すだけ。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">one_of</span><span class="synStatement">&lt;</span>T: Choose <span class="synStatement">+</span> <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span><span class="synStatement">&gt;</span>(values: <span class="synStatement">impl</span> <span class="synType">IntoIterator</span><span class="synStatement">&lt;</span>Item <span class="synStatement">=</span> Gen<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;&gt;</span>) <span class="synStatement">-&gt;</span> Gen<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> { <span class="synStatement">let</span> <span class="synType">mut</span> vec <span class="synStatement">=</span> <span class="synPreProc">vec!</span>[]; vec.<span class="synIdentifier">extend</span>(values.<span class="synIdentifier">into_iter</span>()); <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">choose</span>(<span class="synConstant">0usize</span>, vec.<span class="synIdentifier">len</span>() <span class="synStatement">-</span> <span class="synConstant">1</span>).<span class="synIdentifier">flat_map</span>(<span class="synType">move</span> <span class="synStatement">|</span>idx<span class="synStatement">|</span> vec[idx <span class="synStatement">as</span> <span class="synType">usize</span>].<span class="synIdentifier">clone</span>()) } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L205">prop-check-rs/gen.rs at 4776da568ca35ca319e66c866d9670ce7c9fe40f &middot; j5ik2o/prop-check-rs &middot; GitHub</a></p> <h3 id="Gensfrequency">Gens#frequency</h3> <p>出現比率を指定できる<code>frequency</code>もあります。比率を表す数値とGenのタプルの配列かVecを引数に指定できます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> gens <span class="synStatement">=</span> [ (<span class="synConstant">3</span>, <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose_u32</span>(<span class="synConstant">1</span>, <span class="synConstant">10</span>)), (<span class="synConstant">2</span>, <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose_u32</span>(<span class="synConstant">50</span>, <span class="synConstant">100</span>)), (<span class="synConstant">5</span>, <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">choose_u32</span>(<span class="synConstant">200</span>, <span class="synConstant">300</span>)), ]; <span class="synStatement">let</span> gen <span class="synStatement">=</span> <span class="synPreProc">Gens</span><span class="synSpecial">::</span><span class="synIdentifier">frequency</span>(gens); <span class="synStatement">let</span> prop <span class="synStatement">=</span> <span class="synIdentifier">for_all_gen</span>(gen, <span class="synType">move</span> <span class="synStatement">|</span>input<span class="synStatement">|</span> { <span class="synPreProc">println!</span>(<span class="synConstant">&quot;input = {}&quot;</span>, input); <span class="synComment">// 1〜10 or 50〜100 or 200から300の任意の数値が指定された出現比率で得られる</span> <span class="synConstant">true</span> }); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">test_with_prop</span>(prop, <span class="synConstant">5</span>, TEST_COUNT, <span class="synPreProc">RNG</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()); </pre> <p>GenをBTreeMapに格納して、生成された乱数でキーを指定し、対応するGenを引き当てているだけです。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">frequency</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>(values: <span class="synStatement">impl</span> <span class="synType">IntoIterator</span><span class="synStatement">&lt;</span>Item <span class="synStatement">=</span> (<span class="synType">u32</span>, Gen<span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>)<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> Gen<span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> B: Debug <span class="synStatement">+</span> <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span>, { <span class="synStatement">let</span> filtered <span class="synStatement">=</span> values.<span class="synIdentifier">into_iter</span>().<span class="synIdentifier">filter</span>(<span class="synStatement">|</span>kv<span class="synStatement">|</span> kv.<span class="synConstant">0</span> <span class="synStatement">&gt;</span> <span class="synConstant">0</span>).<span class="synIdentifier">collect</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span><span class="synType">Vec</span><span class="synStatement">&lt;</span>_<span class="synStatement">&gt;&gt;</span>(); <span class="synStatement">let</span> (tree, total) <span class="synStatement">=</span> filtered .<span class="synIdentifier">into_iter</span>() .<span class="synIdentifier">fold</span>((<span class="synPreProc">BTreeMap</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(), <span class="synConstant">0</span>), <span class="synStatement">|</span>(<span class="synType">mut</span> tree, total), (weight, value)<span class="synStatement">|</span> { <span class="synStatement">let</span> t <span class="synStatement">=</span> total <span class="synStatement">+</span> weight; tree.<span class="synIdentifier">insert</span>(t, value.<span class="synIdentifier">clone</span>()); (tree, t) }); <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">choose_u32</span>(<span class="synConstant">1</span>, total).<span class="synIdentifier">flat_map</span>(<span class="synType">move</span> <span class="synStatement">|</span>n<span class="synStatement">|</span> tree.<span class="synIdentifier">range</span>(n..).<span class="synIdentifier">into_iter</span>().<span class="synIdentifier">next</span>().<span class="synIdentifier">unwrap</span>().<span class="synConstant">1</span>.<span class="synIdentifier">clone</span>()) } </pre> <p><a href="https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L77">https://github.com/j5ik2o/prop-check-rs/blob/4776da568ca35ca319e66c866d9670ce7c9fe40f/src/gen.rs#L77</a></p> <h2 id="まとめ">まとめ</h2> <p>あまり良い方法だと思いませんが、構造体に参照を持ち込むとどうしてもロジックが複雑になってしまうので、dyn FnをRcでラップしClone可能にしました。ほかに代替のよい方法があればよいのですが…。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">State</span><span class="synStatement">&lt;</span>S, A<span class="synStatement">&gt;</span> { <span class="synStatement">pub</span><span class="synSpecial">(</span><span class="synStatement">crate</span><span class="synSpecial">)</span> run_f: Rc<span class="synStatement">&lt;</span>dyn <span class="synType">Fn</span>(S) <span class="synStatement">-&gt;</span> (A, S)<span class="synStatement">&gt;</span>, } <span class="synStatement">impl&lt;</span>S, A<span class="synStatement">&gt;</span> <span class="synType">Clone</span> <span class="synStatement">for</span> State<span class="synStatement">&lt;</span>S, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> S: <span class="synSpecial">'static</span>, A: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'static</span>, { <span class="synStatement">fn</span> <span class="synIdentifier">clone</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synType">Self</span> { run_f: <span class="synConstant">self</span>.run_f.<span class="synIdentifier">clone</span>(), } } } </pre> <p>ということでご参考になれば幸いです。</p> j5ik2o メモ:値オブジェクトの定義と差異について hatenablog://entry/13574176438094426201 2022-05-22T20:45:35+09:00 2023-12-14T00:04:31+09:00 「値オブジェクト」の定義について不勉強だったので「DDDの値オブジェクト」の定義とDDD以外の「値オブジェクト」との違いについて、改めて関連書籍を読み直し整理してみました。 すごい長いし細かいので他人に読ませるような記事ではなく、自分のために書いたメモです。 もし読むなら興味がある人だけで。 <p>「値オブジェクト」の定義について不勉強だったので「DDDの値オブジェクト」の定義とDDD以外の「値オブジェクト」との違いについて、改めて関連書籍を読み直し整理してみました。</p> <p>すごい長いし細かいので他人に読ませるような記事ではなく、自分のために書いたメモです。</p> <p>もし読むなら興味がある人だけで。</p> <hr /> <p>自分向けのメモですが、一応 この記事の前提や意図を書いておきます。</p> <ul> <li>「DDDの値オブジェクト」以外を否定する記事ではありません。</li> <li>原理主義のように書籍の理想どおり実践するべきだと主張するつもりはありません <ul> <li>「理想に従えばよい」「理想に従うの無意味だ」と決め付けの二項対立的な思考ではなく、理想と現実の絡み合ったグレーゾーンを見極めつつ、現場で手を打つのが優れた実践者ではないでしょうか</li> </ul> </li> <li>下記に紹介する、それぞれの値オブジェクトの優劣について細かく議論し、論破する・されることを目的としていません。</li> <li>言い訳と聞こえるかもしれませんが、読書に完璧はありません。そしてその感想にはたぶんにバイアスがかかります <ul> <li>読書というのは同じ書籍を十人が読めば十人とも同じ解釈になるとは限らないですし、読書の解釈は「ここの部分の解釈は、これで合っていますか?」と著者に聞くことも難しいのです</li> <li>だからといって、いい加減に読んでもよいとは考えていません。13年以上現場でDDDを実践し<a href="#f-49048371" id="fn-49048371" name="fn-49048371" title="経験年数が長ければよいのかというとそうでもないですが">*1</a>、DDD本和訳レビューや他のDDD本のレビューも関わり、著者のEvansからも直接講義を受け、他の有識者を含む日本のDDDコミュニティの方々と対話してきた経験からも考えをまとめています</li> <li>いずれにしても、そもそも完璧な読書など存在しないという前提で、この記事を読んでいただけると嬉しいです</li> </ul> </li> <li>僕の見解が唯一正しいなどと考えていませんし、DDDだけが正しい設計方法だと考えていません。ぜひ他の方の意見も参考にしてみてください。当然、私の解釈が間違っている可能性もあります。そのときは何か根拠付きで指摘していただけると有意義だと思います</li> </ul> <hr /> <p>5/23追記</p> <p>"ドメインモデル(ドメインオブジェクト)"という表記に問題をあると…。これだけで問題になるのですね。</p> <p>その記述の上の方に"ドメインモデルの考え方を反映した実装がドメインオブジェクト"と書いてますが…。</p> <p>こういう重箱の隅をつつかれても不毛ですね…</p> <hr /> <p>前置きが長くなりましたが、値オブジェクトはよく話題になるものは、大きくわけて3つあると思うので、それをみていきましょう。</p> <h2 id="Wikipediaの値オブジェクト">Wikipediaの値オブジェクト</h2> <p>さて、Wikipediaによると、Value Object(値オブジェクト)は以下のように説明されている。この定義を「Wikipediaの値オブジェクト」と呼ぶことにします。</p> <blockquote><p>コンピュータサイエンスでは、Value Object(値オブジェクト)は、同等性がアイデンティティに基づいていない単純なエンティティを表す小さなオブジェクトである。つまり、2つの値オブジェクトは、同じ値を持つ場合は等しく、必ずしもそれらが同一のオブジェクトである必要はない。</p></blockquote> <p><a href="https://ja.wikipedia.org/wiki/Value_object">Value object - Wikipedia</a></p> <h2 id="PofEAAの値オブジェクト">PofEAAの値オブジェクト</h2> <p><strong>Martin Fowler 著</strong>『<strong>エンタープライズアプリケーションアーキテクチャパターン</strong>』(PofEAA)によると</p> <p><a href="https://www.amazon.co.jp/dp/B01B5MX2O2">https://www.amazon.co.jp/dp/B01B5MX2O2</a></p> <blockquote><p>IDに基づいた等価性を確保していない、MoneyやDateRangeなどのシンプルな小型オブジェクト。</p> <p>さまざまな種類のオブジェクトシステムを使うとき、参照オブジェクトと値オブジェクトの相違が役立つことに気づいた。これら2つのオブジェクトではバリューオブジェクトの方が小型であり、純粋にオブジェクト指向ではない多くの言語にあるプリミティブな型に類似している。</p></blockquote> <p>マーチン・ファウラー. エンタープライズアプリケーションアーキテクチャパターン (Japanese Edition) (Kindle の位置No.12687-12690). Kindle 版.</p> <p><strong>「IDに基づいた等価性を確保していない小型のオブジェクト」という定義になる。</strong></p> <h3 id="PofEAAの値オブジェクトにDTOは含まれるか">「PofEAAの値オブジェクト」にDTOは含まれるか</h3> <p>論争になりやすいのは<a href="https://ja.wikipedia.org/wiki/Data_Transfer_Object">DTO</a>の概念が含まれるかという点。</p> <p><a href="https://ja.wikipedia.org/wiki/Data_Transfer_Object">Data Transfer Object - Wikipedia</a></p> <p>DTOとは自身のデータへの格納と取り出し機能しか持たないオブジェクトのことです。JavaではSetter,Getterなどで構成されるオブジェクトで、主にデータ転送の目的で利用されます。例えばJSON形式のリクエストやレスポンスはDTOとして設計することが多いのではないでしょうか。</p> <p><strong>このWikipediaによると「PofEAAの値オブジェクト」はDTOとは異なるらしい。</strong> 理由は書いてないのでよくわかりません。</p> <p><a href="https://martinfowler.com/bliki/ValueObject.html">bliki: ValueObject</a></p> <p>しかし、こちらの別の記事によると、過去に、特定のコミュニティ(J2EE?)ではDTOも「値オブジェクト」として用いられていたらしい。</p> <p><a href="https://codezine.jp/article/detail/10184">実践DDD本 第6章「値オブジェクト」~振る舞いを持つ不変オブジェクト~</a></p> <p>こちらのFowlerのブログでも、過去にJ2EEの文献がDTOをVOに分類していたが、今はみなくなった、と言及があります。</p> <blockquote><p>One source of terminological confusion is that around the turn of the century some J2EE literature used "value object" for Data Transfer Object. That usage has mostly disappeared by now, but you might run into it.</p> <p>用語の混乱の原因の1つは、世紀の変わり目の頃、いくつかのJ2EE文献がData Transfer Objectに対して "value object "を使っていたことです。このような使い方は今ではほとんどなくなりましたが、遭遇することがあるかもしれません。</p></blockquote> <p>というわけで、<strong>今は「PofEAAの値オブジェクト」にDTOの概念が含まれないと判断するのが妥当でしょう。</strong><s> (WikipediaのほうはDTOの概念は含まれるだろうか…判断が付かない…)</s></p> <p>追記: 5/23</p> <p>WikipediaのDTOの解説によるとDTOは含まれないそうだ。</p> <p><a href="https://twitter.com/justinto_nation/status/1528561673061117953">https://twitter.com/justinto_nation/status/1528561673061117953</a></p> <h3 id="IDに基づいた等価性を確保していない以外の特徴はあるのか">「IDに基づいた等価性を確保していない」以外の特徴はあるのか</h3> <p>上記の定義以外はどう解釈すべきか…。「18.7 マネー」あたりです。パターンではなく実装例の解説なので判断が難しい。</p> <p>個人的にはこれらの特徴も定義に含めたいが、人によって意見が分かれるところかもしれません。</p> <p>18.7 マネー</p> <blockquote><p>コンピュータはMoney(貨幣)を処理しているが、困ったことに主流のプログラミング言語はどれもMoneyを最重要データ型として扱っていない。通貨を扱う環境にとって、型がないことが問題の原因であることは明らかである。</p></blockquote> <p>マーチン・ファウラー. エンタープライズアプリケーションアーキテクチャパターン (Japanese Edition) (Kindle の位置No.12754-12756). Kindle 版.</p> <p>「PofEAAの値オブジェクト」は対象の問題を解決する型(ドメインモデル)だ、という見方もできそうです。</p> <blockquote><p>Moneyの場合、Moneyオブジェクトを数字のように簡単に使えるような、算術演算が必要である。しかし、Moneyの算術演算と数字のMoney演算との間には、重要な相違点がいくつかある。最も明確なのは、異なる貨幣単位の金銭を合算しようとする場合、加算や減算では常に貨幣単位を認識する必要があることである。シンプルでよく使われる対処方法は、異なる貨幣単位の合算をエラーとして処理することである。</p></blockquote> <p>マーチン・ファウラー. エンタープライズアプリケーションアーキテクチャパターン (Japanese Edition) (Kindle の位置No.12772-12776). Kindle 版.</p> <p>異なる貨幣単位を無視して合算できてしまうと、Money型として不変条件(invariant)が維持できない。そうなると値としての存在意義や利用価値を失うので、値オブジェクトのメソッドによって保護するような記述と読める。</p> <p>これらはそう読めるだけで「PofEAAの値オブジェクト」としてそうするべきなのかわからない。</p> <h3 id="PofEAAの値オブジェクトにDDDの値オブジェクトは含まれるか">「PofEAAの値オブジェクト」に「DDDの値オブジェクト」は含まれるか</h3> <p>先ほど紹介したブログによると、Vaughn VernonがDDDの観点から値オブジェクトを解説しているとのこと。</p> <blockquote><p>Vaughn Vernon's description is probably the <a href="https://www.amazon.com/gp/product/0321834577/ref=as_li_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=0321834577&amp;linkCode=as2&amp;tag=martinfowlerc-20">best in-depth discussion of value objects</a>  from a DDD perspective. He covers how to decide between values and entities, implementation tips, and the techniques for persisting value objects.</p> <p>Vaughn Vernonの記述は、おそらくDDDの観点から見た値オブジェクトの最も深い議論である。彼は、値とエンティティの間の決定方法、実装のヒント、および値オブジェクトを永続化するためのテクニックをカバーしています。</p></blockquote> <p>「Vaughn Vernonの記述」とは『実践ドメイン駆動設計』(Vaughn Vernon)で書かれた値オブジェクトの説明のことでしょう。つまり「DDDの値オブジェクト」のことです。「DDD観点からみた値オブジェクト」としていますが、「PofEAAの値オブジェクト」に「DDDの値オブジェクト」が含まれるかはこの文章からははっきりしない。</p> <h2 id="DDDの値オブジェクト">DDDの値オブジェクト</h2> <p>下のほうで例示している、複数のDDD関連書籍を改めて読み直しました。</p> <p>【「DDDの値オブジェクト」はドメインオブジェクトである】は、様々なDDDの関連書籍にも明確に書かれているので、これは間違いでしょう。DDDに詳しい人にとっては当たり前のことですが。</p> <p>WikipediaやPofEAAの立場からみると、ドメイン・非ドメインどちらでも使える値オブジェクトが、なぜドメイン限定になるんだ。なぜ値オブジェクトがドメインモデル(ドメインオブジェクト)だといいきるのだ、という印象を持たれるかもしれません。</p> <p>まぁそれが差異です。混ぜて考えると、ゴチャゴチャになります。</p> <p>(この辺りの話はDDDの汎用サブドメインのトピックも関係しますが、また今度にします…)</p> <h3 id="DDDの値オブジェクトの特徴">「DDDの値オブジェクト」の特徴</h3> <p>以下は、DDD関連書籍に記述されている事実から抽出した「DDDの値オブジェクト」の主な特徴です。凝集度を高めるや自己文書化など、他にも挙げだしたらキリがないのでこんなところだと思います。</p> <ol> <li>ドメイン内の何かしらの計測・定量化・説明のいずれかを扱う</li> <li>値が等しいかどうかを、他と比較できる(値が等しいかどうかを、他と比較できる)</li> <li>状態を不変(immutable)に保つこと(が望ましい)</li> <li>計測値や説明が変わったときに、全体を完全に置き換えられる</li> <li>関連する属性を不可欠な単位として組み合わせ、概念的な統一体を形成する</li> <li>協力関係にあるその他の概念に「副作用のない関数(SIDE-EFFECT-FREE-FUNCTIONS)」を提供する <ul> <li>ここでいう関数とは、副作用がない振る舞いやメソッドのこと</li> <li>パターン名としては”副作用のない”は強調されているが、不変(immutable)であれば振る舞いに副作用がないのは自然なこと</li> <li>メソッド名はユビキタス言語で表現されるドメインの概念と対応付く</li> <li>DTOのようなインターフェイスは意図していない</li> </ul> </li> <li>ドメイン上の制約に基づき不変条件(invariant)を表明する(ASSERTIONS)</li> </ol> <p>これらを定義どおり実践するのか。それは時と場合によるとしか言えません。書いてあることは難しい印象がありますが、実際には最近のプログラミング言語に慣れているなら、それほど苦労せず実践できると思います。</p> <h1 id="特徴の比較">特徴の比較</h1> <p>いずれの値オブジェクトも以下が共通する特徴。最初からわかりきってたと思いますが…。</p> <p>2) 値が等しいかどうかを、他と比較できる (IDではなく値による等価性を確保)<br/> 3) 状態を不変(immutable)に保つこと(が望ましい)</p> <p>「Wikipedia及びPofEAAの値オブジェクト」はドメイン・非ドメインに関係なく使えそうです。</p> <p>「DDDの値オブジェクト」はDDD特有の事情が含まれています。</p> <p>明確に差異がある。</p> <h2 id="差異の解釈">差異の解釈</h2> <p>差異を明確にする立場であれば、同じところは2)3)だけで他の項目は違うと言えばいい。確かに用語の定義としてはそうかもしれません。でも、そう簡単な話ではないような気がします。</p> <p>そもそも、他の項目はDDD固有の考え方なのか、という疑問あるかもしれません。たとえば「必要に応じて振る舞いも作るし不変条件も表明するだろ、わざわざDDD文脈を付けるのは意味不明」という意見もあるかもしれません。WikipediaやPofEAAに明確な定義がなくても、オブジェクトならこういうやりかたをするよね、みたいな常識はあるかもしれません。</p> <p>少し話しは逸れますが、そのソフトウェアの存在意義に関わる関心のことをドメインと呼ぶ<a href="#f-91b909e8" id="fn-91b909e8" name="fn-91b909e8" title="詳しいことは下のほうに書いてあるし、具体的にはEvans本を読んでください">*2</a>ので、その関心とそれ以外を分けて考える、ある意味 概念に境界線を引くために使う方法論がDDDだと理解しています<a href="#f-de36833b" id="fn-de36833b" name="fn-de36833b" title="優劣の区分ではありませんので間違えないように">*3</a>。</p> <p>元々ドメインという用語はEvans独自の用語ではありません<a href="#f-8e33a96e" id="fn-8e33a96e" name="fn-8e33a96e" title="歳がバレそうですが、僕が生まれる前からあります">*4</a>し、DDDのようなある種の形式的な手法<a href="#f-40836317" id="fn-40836317" name="fn-40836317" title="人によっては宗教では?と言われます。そういう人はスクラムも宗教という人が多い。まぁ否定はできませんが、それほど大袈裟なことではないと思っています">*5</a>に頼らずとも、暗黙的にドメインを見出しそれを解決するモデルを作れる人は少ないですがいます<a href="#f-4a04f58c" id="fn-4a04f58c" name="fn-4a04f58c" title="優秀な方に多いと思います">*6</a>。そういう人にはDDDの手法が回りくどく感じるかもしれません。そうであればもはやDDDのよう手法に頼る必要もないでしょう。本質的には、目的の結果が得られる、適切な方法論ならいずれでもよいでしょう。</p> <p>一方で、そのように誰もが問題を解決できるのであれば、DDDはこれほどに注目されないと思います。複雑な問題では概念に境界線を引くのは思った以上に難しいものです。簡単に線引きできるなら、人類はモジュールやマイクロサービスの分割点で悩んだりしません。そういうときに、DDDがツールとして選択肢の一つになります。もちろん、DDDは銀の弾丸ではないので結果がうまく行く保証はありません。それでも我々はよりよい結果のために工夫をしながらツールも使うのではないでしょうか。</p> <p>というわけで、この両方の視点で考えると「DDDの観点がないから劣っている」とか「DDDは形式的だから過剰」だとか、一概に言えません。そういう意味では、実用上、例に挙げた値オブジェクトに大きな差異があるとも言い切れないのです。</p> <p>個人的には、チームで機能させられるのはどちらかというとDDDの方だと思いますが、個人の裁量が大きい仕事ではそこまで形式的な方法に頼る必然性もなかったりするので、時と場合によって思考停止せずにどの選択肢がよいか考えたほうがよさそうです。</p> <h2 id="まとめ">まとめ</h2> <p>と、ここで終わるのはよくないですね。</p> <p>少なくとも「目的が違うものを混ぜて語るな。文脈を分けろ」という指摘があるとしたら、ごもっともです。思いのほか「値オブジェクト」は多義的だったので、用語の使い方としては文脈をわきまえるしかなさそうです。</p> <p>今回はこういった文脈を前置きせずに議論してしまったところがあるので、そこは配慮が足りなかったと思っています。なので、自戒を込めて今後は<strong>文脈を共有できない場では「DDDの値オブジェクト」などとプレフィクスを付けて、他と異なる概念として語るとよさそうです。</strong></p> <p>近年、DDD関連図書が増えたこともあり「DDDの値オブジェクト」の概念はよく耳にしていて半ば常識になりつつあると思います<strong>。</strong>ですが、視野が狭くならないように「値オブジェクト」としては解釈が複数あることを必要に応じて啓蒙していくのがよさそうです。</p> <p>長々と最後まで読んでいただき、ありがとうございました。</p> <hr /> <h1 id="追加資料">追加資料</h1> <p>DDDの値オブジェクトの定義はどうなっているか、Evans本及びDDD関連書籍から関係しそうな箇所を以下に列挙。細かすぎるので自分用のメモぐらいのつもりで書いています。読まなくてもよいと思います。</p> <h2 id="エリックエヴァンスのドメイン駆動設計">『エリック・エヴァンスのドメイン駆動設計』</h2> <p>著 Eric Evans(<a href="https://twitter.com/ericevans0">@ericevans0</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B00GRKD6XU">https://www.amazon.co.jp/dp/B00GRKD6XU</a></p> <p>私は和訳版のレビューに参加しました。</p> <h3 id="補足-大前提であるドメインと境界づけられたコンテキストについて">補足: 大前提であるドメインと境界づけられたコンテキストについて</h3> <p>第1部 ドメインモデルを機能させる</p> <blockquote><p>すべてのソフトウェアプログラムは、それを使用するユーザの何らかの活動や関心と関係がある。ユーザがプログラムを適用するこの対象領域が、ソフトウェアのドメインである。ドメインの中には、物理的な世界を含んでいるものもある。例えば、航空会社の予約プログラムのドメインは、実際の航空機に搭乗する現実の人々を含んでいる。一方、実体を含まないドメインもある。会計プログラムのドメインは金銭と財務である。ソフトウェアのドメインは、コンピュータとほとんど関係ないのが普通だが、例外もある。ソースコード管理システムのドメインは、ソフトウェア開発そのものである。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.2). Kindle 版.</p> <h3 id="補足">補足</h3> <p>ドメインとは何か。ソフトウェアプログラムを使用するユーザ(利用者や利害関係者)の何らかの活動や関心に関係あり、ユーザがプログラムを適用する対象領域がドメインである。コンピュータとほとんど関係ないが、例外もあるとしている。つまり、ソフトウェアと関係するものもある。</p> <p>ソフトウェアを利用するユーザの活動や関心がドメイン。問題領域もしくは問題ドメインと呼ばれることがある。広義には開発者にとってのドメイン(言語・処理系やSQLやHTTPやWeb F/Wなど)もある、と読める。</p> <p>たとえば、バージョン管理という問題ドメインに対する、GitのBlob, Tree, Commit, Tagなどは実際は解決ドメイン(後述)のモデルにあたるだろうが、日常的には問題ドメインと解決ドメインを明確に区別しないで語ることが多い。詳しくは以下のツイートを参照</p> <p><a href="https://twitter.com/j5ik2o/status/1527291467198504960">かとじゅん on Twitter: "https://t.co/wMRnW9hQXB杉本さんのこの図とてもわかりやすい。ドメインモデルはソリューションのモデル。その実装はドメインオブジェクト。BC毎に個別に紐付くもの。なので、Gitのコミットモデル、Subversionのコミットモデルになる。 pic.twitter.com/KFBs6NLB3c / Twitter"</a></p> <p>第4部 戦略的設計</p> <p>第14章 モデルの整合性を維持する</p> <p>境界づけられたコンテキスト(BOUNDED CONTEXT)</p> <blockquote><p>モデルが適用されるコンテキストを明示的に定義すること。明示的な境界は、チーム編成、そのアプリケーションに特有の部分が持つ用途、コードベースやデータベーススキーマなどの物理的な表現などの観点から設定すること。その境界内では、モデルを厳密に一貫性のあるものに保つこと。ただし、境界の外部の問題によって注意を逸らされたり、混乱させられたりするのを避けること。境界づけられたコンテキストは、特定のモデルが適用できる範囲を制限する。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.344). Kindle 版.</p> <h3 id="補足-1">補足</h3> <p>境界づけられたコンテキストとは何か。ドメイン、ユーザの活動や関心に対応してソフトウェアで解決する領域のことである。解決ドメインと言われることもある。 通常、ソースコード上でドメインモデルやドメインオブジェクトといっているものはこちらに分類される、ソリューションとしてのモデル。</p> <hr /> <p>第2部 モデル駆動設計の構成要素</p> <blockquote><p>優れたドメインモデルを開発することは芸術である。しかし、モデルの個々の要素を実際に設計し、実装することは、比較的体系立てて行える。ドメインの設計を、ソフトウェアシステムにおけるその他の大量の関心事から分離することで、設計とモデルとのつながりが非常に明確になる。特定の区別に従ってモデル要素を定義することで、モデル要素の意味が鮮明になる。個々の要素に対して実績のあるパターンに従うことで、実際に実装できるモデルを作れるようになる。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (pp.62-63). Kindle 版.</p> <blockquote><p>ドメインモデルに関係するコード全部を1つの層に集中させ、ユーザインタフェース、アプリケーション、インフラストラクチャのコードから分離すること。表示や格納、アプリケーションタスク管理などの責務から解放されることで、ドメインオブジェクトはドメインモデルを表現するという責務に専念できる。これによって、モデルは十分豊かで明確になるように進化し、本質的なビジネスの知識をとらえて、それを機能させることができるようになる。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.68). Kindle 版.</p> <p>第5章 ソフトウェアで表現されたモデル</p> <blockquote><p>その要素とは、エンティティ、値オブジェクト、サービスである。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.79). Kindle 版.</p> <p>値オブジェクト(VALUE OBJECTS)</p> <blockquote><p>あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。</p> <p>値オブジェクトを構成する属性は、概念的な統一体を形成すべきである</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.97). Kindle 版.</p> <p>第7章 言語を使用する:応用例</p> <blockquote><p>配送仕様は値オブジェクトである。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.168). Kindle 版.</p> <p>第10章 しなやかな設計</p> <p>副作用のない関数(SIDE-EFFECT-FREE-FUNCTIONS)</p> <blockquote><p>プログラムロジックのうち、できる限り多くの部分を関数に置くこと。関数とは、目に見える副作用なしに結果を戻す操作である。コマンド(目に見える状態を変更することになるメソッド)は厳密に分離して、ドメインについての情報を戻さない、非常に単純な操作にすること。ある概念が、値オブジェクトの担う責務に合致する場合には、複雑なロジックをその値オブジェクトに移すことによって、副作用をさらに制御すること。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.256). Kindle 版.</p> <blockquote><p>副作用のない関数、中でも不変の値オブジェクトに置かれたものは、操作を安全に組み合わせることができる。関数が意図の明白なインタフェースによって示されていれば、開発者は実装の詳細を理解していなくても、その関数を使うことができる。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.256). Kindle 版.</p> <p>表明(ASSERTIONS)</p> <blockquote><p>操作の事後条件と、クラスおよび集約の不変条件を宣言すること。プログラミング言語で表明を直接コーディングできない場合は、その代わり、自動化されたユニットテストを書くこと。プロジェクトの開発プロセスのスタイルに合う場合には、表明をドキュメンテーションや図の中に記述すること。</p></blockquote> <p>Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (p.262). Kindle 版.</p> <h3 id="感想">感想</h3> <ul> <li>値オブジェクトに関する記述はいろんな章に分散している…。そもそも読み方が難しい。</li> <li>「第2部 モデル駆動設計の構成要素」の図に、モデル駆動設計の値オブジェクトが含まれていることがわかる</li> <li><strong>レイヤー化アーキテクチャの部分では、ドメインモデルとドメインオブジェクトを分けて説明している。ドメインモデルの考え方を反映した実装がドメインオブジェクト。</strong></li> <li><strong>ドメインモデルの構成要素は、エンティティ、値オブジェクト、サービスである</strong></li> <li>値オブジェクトには<strong>「関係した機能を与えること」</strong>としている。つまり振る舞い(メソッド)を持たせる、と読むのが自然</li> <li>「値オブジェクトは概念的な統一体である」の、概念とはドメインモデルのことを指すと文脈から読める</li> <li>「第10章 しなやかな設計」では値オブジェクトが振る舞いを持つ、と示されている <ul> <li>DTOのような、単なるデータ保持役ではない、と読める</li> <li>図10.4は塗料(値オブジェクト)の例があり、振る舞いを提供している</li> </ul> </li> <li>集約およびクラス(=ドメインオブジェクト)は不変条件(invariant)を宣言すること、と読める</li> </ul> <hr /> <h2 id="Domain-Driven-Design-ReferenceDefinitions-and-Pattern-Summaries">『Domain-Driven Design Reference』Definitions and Pattern Summaries</h2> <p>著 Eric Evans</p> <p><a href="https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf">https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf</a></p> <p>DDD本のサマリとなる本。短くてわかりやすい。しかもダウンロードできる。</p> <blockquote><p>Value Objects</p> <p>Therefore:</p> <p>When you care only about the attributes and logic of an element of the model, classify it as a value object. Make it express the meaning of the attributes it conveys and give it related functionality. Treat the value object as immutable. Make all operations Side-effect-free Functions that don't depend on any mutable state. Don't give a value object any identity and avoid the design complexities necessary to maintain entities.</p> <p>モデルの要素の属性とロジックにしか興味がない場合は、値オブジェクトとして分類する。それが伝える属性の意味を表現させ、関連する機能を持たせる。値オブジェクトはイミュータブルとする。すべての操作は、変更可能な状態に依存しないSide-Effect-Free Functionsにする。値オブジェクトにアイデンティティを与えず、エンティティの維持に必要な複雑な設計を回避する。</p></blockquote> <p>-</p> <blockquote><p>Side-Effect-Free Functions</p> <p>Therefore:</p> <p>Place as much of the logic of the program as possible into functions, operations that return results with no observable side effects. Strictly segregate commands (methods which result in modifications to observable state) into very simple operations that do not return domain information.Further control side effects by moving complex logic into value objects when a concept fitting the responsibility presents itself.All operations of a value object should be side-effect-free functions.</p> <p>プログラムのロジックをできるだけ関数に置き、観測可能な副作用を伴わない結果を返す操作とする。コマンド(観測可能な状態を変更するメソッド)は、ドメイン情報を返さない非常に単純な操作に厳密に分離する。 さらに、複雑なロジックを値オブジェクトに移行することで、副作用を抑制します。 値オブジェクトのすべての操作は、副作用のない関数でなければなりません。</p></blockquote> <hr /> <h2 id="実践ドメイン駆動設計">『実践ドメイン駆動設計』</h2> <p>著 Vaughn Vernon(<a href="https://twitter.com/vaughnvernon">@vaughnvernon</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B00UX9VJGW">https://www.amazon.co.jp/dp/B00UX9VJGW</a></p> <p>数多くEvans本を参照している</p> <p>6.1 値の特徴</p> <blockquote><p>ある概念が値であるかどうかを判断するときには、その概念が以下の特性を持っているかどうかを見極める必要がある。</p> <p>そのドメイン内の何かを計測したり定量化したり、あるいは説明したりする。</p> <p>状態を不変に保つことができる。</p> <p>関連する属性を不可欠な単位として組み合わせることで、概念的な統一体を形成する。</p> <p>計測値や説明が変わったときには、全体を完全に置き換えられる。</p> <p>値が等しいかどうかを、他と比較できる。</p> <p>協力関係にあるその他の概念に、副作用のない振る舞い[Evans]を提供する。</p></blockquote> <p>ヴォーン・ヴァーノン. 実践ドメイン駆動設計 (Japanese Edition) (p.211). Kindle 版.</p> <p>副作用のない振る舞い</p> <blockquote><p>オブジェクトの振る舞いは、副作用のない関数[Evans]として設計できる。</p></blockquote> <p>ヴォーン・ヴァーノン. 実践ドメイン駆動設計 (Japanese Edition) (p.218). Kindle 版.</p> <h3 id="感想-1">感想</h3> <ul> <li>値オブジェクトは説明と等価判定と不変(immutable)以外に以下の責務がある <ul> <li>概念的な統一体を形成する</li> <li>振る舞いも提供する</li> </ul> </li> </ul> <hr /> <h2 id="セキュアバイデザイン">『セキュア・バイ・デザイン』</h2> <p>著 Dan Bergh Johnsson(<a href="https://twitter.com/danbjson">@danbjson</a>), Daniel Deogun(<a href="https://twitter.com/danieldeogun">@danieldeogun</a>), Daniel Sawan</p> <p><a href="https://www.amazon.co.jp/dp/B09F697K2V">https://www.amazon.co.jp/dp/B09F697K2V</a></p> <p>3.2 モデルをコード上で表現するための構成要素</p> <blockquote><p>ドメイン駆動設計において、モデルをコード上で表現する 構成要素の中で、本書が特に注目しているものは、エンティティ、値オブジェクト(value object)、集約(aggregate)になります(図3.9)。</p></blockquote> <p>3.2.2 値オブジェクト(value obect)</p> <p>不変条件(invariant)の定義とその確認</p> <blockquote><p>しかしながら、これは適切な年齢の範囲ではないため、適切な制約もしくは不変条件(invariant)を年齢の値オブジェクトに持たせて、その値オブジェクトが表現することを明確にしなくてはなりません。</p> <p>図3.15 値オブジェクトは不変条件を維持しなければならない。</p> <p>この種の不変条件は値オブジェクトの中に定義すべきであり、他のドメイン・オブジェクトやユーティリティ・メソッドなどに定義すべきではありません。</p> <p>また、ここで言う不変条件の確認は一般的に妥当性確認(validation)と呼ばれる種類の確認ではないことに注意してください。</p></blockquote> <p>5.1 ドメイン・プリミティブ(domain primitive)と不変条件(invariant)</p> <p>5.1.1 ドメインモデルにおける最小の構成要素としてのドメイン・プリミティブ</p> <blockquote><p>値オブジェクトはドメインモデルにおいて重要な概念を表すものです。</p> <p>ドメイン・プリミティブはドメイン駆動設計の値オブジェクトと同じようなものです。</p></blockquote> <h3 id="感想-2">感想</h3> <ul> <li>ドメインモデル(ドメインオブジェクト)の構成要素に、値オブジェクトが含まれる、と示されている</li> <li>また値オブジェクトは不変条件(invariant)を維持しなければならない、とも示されている</li> </ul> <hr /> <h2 id="Patterns-Principles-and-Practices-of-Domain-Driven-Design">『Patterns, Principles, and Practices of Domain-Driven Design』</h2> <p>著 Scott Millett, Nick Tune</p> <p>Part III: Tactical Paterns: Creating Effective Domain Models</p> <p>Chapter 14: Introducing the Domain Model Building Blocks</p> <blockquote><p>Each building block pattern is designed to have a single responsibility; it could be to represent a concept in the domain like an entity or a value object, or it could be to ensure that the concepts of the domain are kept uncluttered from lifecycle concerns like the factory or repository objects. In a way, you can view the building blocks as a ubiquitous language (UL) for developers to use as a framework for constructing rich and useful domain models.</p></blockquote> <p>Millett, Scott; Tune, Nick. Patterns, Principles, and Practices of Domain-Driven Design (p.310). Wiley. Kindle 版.</p> <blockquote><p>各ビルディングブロックのパターンは、単一の責任を持つように設計されています。それは、エンティティや値オブジェクトのようなドメインの概念を表すことであったり、ファクトリーやリポジトリオブジェクトのようなライフサイクルの懸念からドメインの概念をすっきりさせることであったりします。ある意味で、ビルディングブロックは、開発者が豊かで有用なドメインモデルを構築するためのフレームワークとして使用するユビキタス言語(UL)と捉えることができます。</p></blockquote> <h3 id="感想-3">感想</h3> <ul> <li>値オブジェクトはエンティティと同様にドメインの概念を表す</li> </ul> <hr /> <h2 id="Learning-Domain-Driven-Design">『Learning Domain-Driven Design』</h2> <p>著 Vladik Khononov(<a href="https://twitter.com/vladikk">@vladikk</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B09J2CMJZY">https://www.amazon.co.jp/dp/B09J2CMJZY</a></p> <p>数多くEvans本を参照している</p> <p>Chapter 6. Tackling Complex Business Logic</p> <blockquote><p>Domain Model</p> <p>The domain model pattern is intended to cope with cases of complex business logic. Here, instead of CRUD interfaces, we deal with complicated state transitions, business rules, and invariants: rules that have to be protected at all times.</p></blockquote> <p>Khononov, Vlad. Learning Domain-Driven Design . O'Reilly Media. Kindle 版.</p> <blockquote><p>ドメインモデルパターンは、複雑なビジネスロジックのケースに対処するためのものである。ここでは、CRUDインターフェースの代わりに、複雑な状態遷移、ビジネスルール、不変条件、つまり常に保護されなければならないルールを扱います。</p> <p>Implementation</p> <p>A domain model is an object model of the domain that incorporates both behavior and data. DDD’s tactical patterns—aggregates, value objects, domain events, and domain services—are the building blocks of such an object model.</p></blockquote> <p>Khononov, Vlad. Learning Domain-Driven Design . O'Reilly Media. Kindle 版.</p> <blockquote><p>ドメインモデルとは、振る舞いとデータの両方を取り込んだドメインのオブジェクトモデルである。DDDの戦術的パターンである集約、値オブジェクト、ドメインイベント、ドメインサービスは、このようなオブジェクトモデルのビルディングブロックである。</p></blockquote> <h3 id="感想-4">感想</h3> <ul> <li>値オブジェクトは振る舞いとデータを含むドメインモデル(ドメインオブジェクト)である、と示されている</li> <li>また、ドメインモデルには状態遷移・ビジネスルール・不変条件(invariant)への関心も含まれるようだ</li> </ul> <hr /> <h2 id="現場で役立つシステム設計の原則-変更を楽で安全にするオブジェクト指向の実践技法-">『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 』</h2> <p>著 増田 亨(<a href="https://twitter.com/masuda220">@masuda220</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B073GSDBGT">https://www.amazon.co.jp/dp/B073GSDBGT</a></p> <p>数多くEvans本を参照している</p> <blockquote><p>「送料」クラスのように、業務で使われる用語に合わせて、その用語の関心事に対応するクラスをドメインオブジェクトと呼びます。アプリケーションの対象領域(ドメイン)の関心事を記述したオブジェクトという意味です。</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.597-599). Kindle 版.</p> <blockquote><p>・業務の関心事に対応したクラス(ドメインオブジェクト)を作る</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.609-610). Kindle 版.</p> <blockquote><p>「値」を扱うための専用のクラスを作る</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.681). Kindle 版.</p> <blockquote><p>正しい数量を扱うための独自クラス(Quantityクラス)を定義する</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.650-651). Kindle 版.</p> <blockquote><p>このように計算結果も含めて、数量が1以上で100以下であるように制限したQuantityクラスを用意することで、数量計算が安全で確実になります。</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.662-664). Kindle 版.</p> <blockquote><p>値の種類ごとに専用の型を用意するとコードが安定し、コードの意図が明確になります。このように、値を扱うための専用クラスを作るやり方を値オブジェクト(ValueObject)と呼びます。業務アプリケーションでよく使う値オブジェクトを表に示します。どの値オブジェクトも、int型/String型/LocalDate型など基本データ型のインスタンス変数を1つか2つ持つだけの小さなクラスです。</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.686-690). Kindle 版.</p> <blockquote><p>このように、業務の用語をそのままクラス名やメソッド名として使うと、プログラムが業務の説明書になってきます。業務ルールに変更があったときにも、クラス名と業務の用語が一致していれば、プログラム上で変更が必要な箇所を直観的に特定できます。その業務に関係するデータとロジックを特定のクラスに集めて整理しておけば、変更の影響範囲をそのクラスに閉じ込めやすくなります。それに対して値オブジェクトを使わない場合はどうでしょうか。コードはint型やString型だらけになります。そういうソースコードは、コンピュータに対するデータ操作命令としては有効です。しかし、動かすことはできても、そのコードが業務的に何をやっているのか、プログラムを読んだだけでは理解ができません。こういうコードは業務ルールの追加や変更がやりにくくなります。また、int型やString型が扱える値の範囲は、業務で必要な値の範囲とはかけ離れています。そういう値を扱えてしまうプログラムは、思わぬバグを生みがちです。</p></blockquote> <p>増田 亨. 現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法 (Japanese Edition) (Kindle の位置No.693-702). Kindle 版.</p> <h3 id="感想-5">感想</h3> <ul> <li>業務とは対象ドメインのことを意味する(はず)</li> <li>例示された「送料クラス」はドメインオブジェクトである。送料クラスは値オブジェクトに該当するとは書かれていないが、値オブジェクトとして解釈するのが自然。</li> <li>「値」を扱うための専用のクラスを値オブジェクトとしている。例として数量クラスが登場する。</li> <li>説明や等価判定や不変(immutable)以外の目的があることを説明している <ul> <li>値の種類ごとに型を作成し、コードが安定(挙動が安定するという意味?)し、コードの意図が明確になる、とのこと</li> <li>『セキュア・バイ・デザイン』のドメイン・プリミティブと同じように、その値が取り扱う値の範囲を保護する目的もあると説明されている(不変条件(invariant)の維持)</li> </ul> </li> </ul> <h2 id="ドメイン駆動設計入門-ボトムアップでわかるドメイン駆動設計の基本">『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』</h2> <p>著 成瀬 允宣(<a href="https://twitter.com/nrslib">@nrslib</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B082WXZVPC">https://www.amazon.co.jp/dp/B082WXZVPC</a></p> <p>数多くEvans本を参照している</p> <p>私はレビュアーとして参加</p> <p>1.4.1 知識を表現するパターン</p> <blockquote><p>値オブジェクト(第2章)はドメイン固有の概念(金銭や製造番号など)を値として表現するパターンです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.39). Kindle 版.</p> <p>1.4.3 知識を表現する、より発展的なパターン</p> <blockquote><p>値オブジェクトやエンティティといったドメインオブジェクトを束ねて複雑なドメインの概念を表現します。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.41). Kindle 版.</p> <p>Chapter2 システム固有の値を表現する「値オブジェクト」</p> <blockquote><p>値オブジェクトはドメインオブジェクトの基本です</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.44). Kindle 版.</p> <p>2.4 ふるまいをもった値オブジェクト</p> <blockquote><p>値オブジェクトで重要なことは独自のふるまいを定義できることです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.71). Kindle 版.</p> <blockquote><p>値オブジェクトはデータを保持するコンテナではなく、ふるまいをもつことができるオブジェクトです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (pp.71-72). Kindle 版.</p> <p>2.5.1 表現力を増す</p> <blockquote><p>値オブジェクトはその定義により自分がどういったものであるかを主張する自己文書化を推し進めます。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.79). Kindle 版.</p> <p>2.5.2 不正な値を存在させない</p> <blockquote><p>もはやシステムにとって異常な値はこのチェックにより許容されません。結果としてシステムは、ルールにしたがっていない不正な値の存在に怯える必要がなくなったのです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.81). Kindle 版.</p> <p>2.5.3 誤った代入を防ぐ</p> <blockquote><p>値オブジェクトを作ることで型の恩恵に与ることができれば、予測不能なエラーが潜む箇所を減らすことができます。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.85). Kindle 版.</p> <p>2.5.4 ロジックの散在を防ぐ</p> <blockquote><p>ルールをまとめることは変更箇所をまとめることと同義です。ソフトウェアが変更を受けいれることができる柔軟さを確保するためには、このまとめる作業こそが重要なのです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.88). Kindle 版.</p> <p>2.6 まとめ</p> <blockquote><p>値オブジェクトはドメインの知識をコードへ落とし込むドメイン駆動設計における基本のパターンです。ドメインの概念をオブジェクトとして定義しようとするときに、まずは値オブジェクトにあてはめてみることを検討してみてください。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.89). Kindle 版.</p> <p>3.1 エンティティとは</p> <blockquote><p>第2章『システム固有の値を表現する「値オブジェクト」』で解説した値オブジェクトもドメインモデルを実装したドメインオブジェクトです。</p></blockquote> <p>成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.92). Kindle 版.</p> <h3 id="感想-6">感想</h3> <ul> <li>この書籍でも値オブジェクトもドメインオブジェクトである、と示されている</li> <li>また、等価判定以外に目的がある、と示されている <ul> <li>振る舞いを持つ</li> <li>自己文書化</li> <li>不変条件の維持</li> <li>凝集度を高める</li> </ul> </li> </ul> <hr /> <h2 id="良いコード悪いコードで学ぶ設計入門保守しやすい成長し続けるコードの書き方">『良いコード/悪いコードで学ぶ設計入門保守しやすい 成長し続けるコードの書き方』</h2> <p>著 仙塲 大也(<a href="https://twitter.com/MinoDriven">@MinoDriven</a>)</p> <p><a href="https://www.amazon.co.jp/dp/B09Y1MWK9N">https://www.amazon.co.jp/dp/B09Y1MWK9N</a></p> <p>Evans本を参照している</p> <p>私はレビュアーとして参加</p> <blockquote><p>値オブジェクト(ValueObject)とは、値をクラス(型)として表現する設計パターンです。アプリケーションでは金額、日付、注文数、電話番号など、さまざまな値を扱います。こうした値をクラスとして表現することで、各値それぞれのロジックを高凝集にする効果があります。たとえば金額を単なるint型のローカル変数や引数で制御していると、金額計算ロジックがあちこちに書かれて低凝集に陥ります。また、同じint型の「注文数」や「割引ポイント」が、金額用のint型変数に不注意で代入されてしまう可能性もあります。こうした事態を防ぐために、値の概念そのものをクラスとして定義します。</p></blockquote> <p>仙塲 大也. 良いコード/悪いコードで学ぶ設計入門保守しやすい 成長し続けるコードの書き方 (Japanese Edition) (p.77). Kindle 版.</p> <h3 id="感想-7">感想</h3> <ul> <li>値オブジェクトがドメインオブジェクトであるか明記されていないが、値オブジェクトに明らかに不変(immutable)と等価判定以外の目的がある、と示されている <ul> <li>凝集度を高めること</li> <li>不変条件(invariant)を維持すること</li> <li>値の概念を表現する</li> </ul> </li> </ul> <hr /> <h2 id="ドメイン駆動設計-モデリング実装ガイド">『ドメイン駆動’設計 モデリング・実装ガイド』</h2> <p>著 松岡 幸一郎(<a href="https://twitter.com/little_hand_s">@little_hand_s</a>)</p> <p><a href="https://booth.pm/ja/items/1835632">https://booth.pm/ja/items/1835632</a></p> <p>Evans本を参照している</p> <p>第6章</p> <p>ドメイン層の実装</p> <blockquote><p>ドメインモデルを表現するもの (ドメインオブジェクト)</p> <p>– エンティティ</p> <p>– 値オブジェクト</p> <p>– ドメインイベント</p></blockquote> <h3 id="感想-8">感想</h3> <ul> <li>この書籍でも値オブジェクトもドメインオブジェクトである、と示されている</li> </ul> <hr /> <h2 id="ドメイン駆動設計-サンプルコードFAQ">『ドメイン駆動設計 サンプルコード&amp;FAQ』</h2> <p>著 松岡 幸一郎(<a href="https://twitter.com/little_hand_s">@little_hand_s</a>)</p> <p><a href="https://little-hands.booth.pm/items/3363104">https://little-hands.booth.pm/items/3363104</a></p> <p>Evans本を参照している</p> <p>用語の定義</p> <p>表 1 重要な単語の定義</p> <blockquote><p>値オブジェクト</p> <p>ドメインモデルを表現するオブジェクトで、同一判定を保持する値で行うもの。必ず不変になる。</p></blockquote> <h3 id="感想-9">感想</h3> <ul> <li>この書籍でも値オブジェクトもドメインオブジェクトである、と示されている</li> </ul> <div class="footnote"> <p class="footnote"><a href="#fn-49048371" id="f-49048371" name="f-49048371" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">経験年数が長ければよいのかというとそうでもないですが</span></p> <p class="footnote"><a href="#fn-91b909e8" id="f-91b909e8" name="f-91b909e8" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">詳しいことは下のほうに書いてあるし、具体的にはEvans本を読んでください</span></p> <p class="footnote"><a href="#fn-de36833b" id="f-de36833b" name="f-de36833b" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">優劣の区分ではありませんので間違えないように</span></p> <p class="footnote"><a href="#fn-8e33a96e" id="f-8e33a96e" name="f-8e33a96e" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">歳がバレそうですが、僕が生まれる前からあります</span></p> <p class="footnote"><a href="#fn-40836317" id="f-40836317" name="f-40836317" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">人によっては宗教では?と言われます。そういう人はスクラムも宗教という人が多い。まぁ否定はできませんが、それほど大袈裟なことではないと思っています</span></p> <p class="footnote"><a href="#fn-4a04f58c" id="f-4a04f58c" name="f-4a04f58c" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">優秀な方に多いと思います</span></p> </div> j5ik2o Re: Re: ドメイン固有型(値オブジェクト含む)を再考する hatenablog://entry/13574176438093827687 2022-05-19T16:32:19+09:00 2022-05-19T16:32:19+09:00 kumagi.hatenablog.com こちらの記事への反応です。 違う。値オブジェクトとはID以外で等価判定をするオブジェクトの事であって、RubyのHash、Pythonのdict、C++のstd::unordered_setすらも値によって等価判定を行うのでこれらは値オブジェクトであるがドメイン固有型ではない RubyのHash、Pythonのdict、C++のstd::unordered_setは、アプリケーションのドメインからみるとインフラストラクチャに見えるかもしれません。 しかし、RubyのHash、Pythonのdict、C++のstd::unordered_set であっ… <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkumagi.hatenablog.com%2Fentry%2Fre-rethink-domain-object" title="Re: ドメイン固有型(値オブジェクト含む)を再考する - Software Transactional Memo" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kumagi.hatenablog.com/entry/re-rethink-domain-object">kumagi.hatenablog.com</a></cite></p> <p>こちらの記事への反応です。</p> <blockquote><p>違う。値オブジェクトとはID以外で等価判定をするオブジェクトの事であって、RubyのHash、Pythonのdict、C++のstd::unordered_setすらも値によって等価判定を行うのでこれらは値オブジェクトであるがドメイン固有型ではない</p></blockquote> <p>RubyのHash、Pythonのdict、C++のstd::unordered_setは、アプリケーションのドメインからみるとインフラストラクチャに見えるかもしれません。</p> <p>しかし、RubyのHash、Pythonのdict、C++のstd::unordered_set であっても、解決する問題領域(ドメイン)があります。なので、<strong>そのドメインにとっては、ドメイン固有型(=ドメインオブジェクト)である、と解釈しています</strong>。(ドメイン固有型の”固有”が紛らわしかったかもしれませんが…)</p> <blockquote><p>型クラスを既存の値型にバリデーションを足すために使う人を見たことが無いけれど(やりたいことは素直なのだから素直にif文やクラスを作るほうがまだわかる)Scalaだとよくある文化なのかしら?</p></blockquote> <p>単にバリデーションを追加するためだけにこういうことをするわけではないです。ドメインモデルに実装を対応づけるために定義するという意味でした。</p> <p>コダックさんのツイートには返信済みでしたが、こういう解釈をしています。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">DateやPointが扱う問題ドメインがあるはずなので、矛盾はしないと考えています。</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1526709938873331712?ref_src=twsrc%5Etfw">2022年5月17日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>追記: 5/19 17:13</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Evansさん作 Time and Money Code Library(もうメンテされてないけど…)。彼は値オブジェクトのライブラリと言っている。こういう共通性のあるライブラリでも、ドメインはあると思うんだけどね。<a href="https://t.co/HSm57PT0LZ">https://t.co/HSm57PT0LZ</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1527197620464398336?ref_src=twsrc%5Etfw">2022年5月19日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> j5ik2o ドメイン固有型(値オブジェクト含む)を再考する hatenablog://entry/13574176438093046599 2022-05-17T13:55:31+09:00 2022-05-17T14:08:40+09:00 Value Objectが盛り上がっているらしい。 Value Objectについて整理しよう - Software Transactional Memo Value Objectの説明に異論がないものの、主題はValue Object Obsessionのほうですよね。 こちらも聞いてみた。 fukabori.fm よい機会なので、よくわかっているつもりの、値オブジェクトというかドメイン固有型について再考してみよう。 <p>Value Objectが盛り上がっているらしい。</p> <p><a href="https://kumagi.hatenablog.com/entry/value-object">Value Objectについて整理しよう - Software Transactional Memo</a></p> <p>Value Objectの説明に異論がないものの、主題はValue Object Obsessionのほうですよね。</p> <p>こちらも聞いてみた。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffukabori.fm%2Fepisode%2F73" title="73. Value Object w/ kumagi | fukabori.fm" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://fukabori.fm/episode/73">fukabori.fm</a></cite></p> <p>よい機会なので、よくわかっているつもりの、値オブジェクトというかドメイン固有型について再考してみよう。</p> <h1>それは値か属性か</h1> <blockquote><p>それはエンティティの全メンバーやデータベースの全列のために「顧客郵便番号」「送付先郵便番号」「事業所郵便番号」「契約日」などのクラス(メンバではなくクラス!)を定義して、immutableな振る舞いを強制する事を以てValue Objectであると言い張り、ドメイン知識の断片をそれぞれのクラスに書き散らして「高凝集になった」「型システムが守ってくれる」と喜ぶ奇行に走る。</p></blockquote> <p>これは妥当な指摘だと思います。Twitterの一部で話題になったのですが、この論点は「値(attribute)」と「属性(property)」を混同するなということかと。「その値オブジェクトは値じゃなくて属性のほうじゃないの?」ってやつ。(過去に属性を値オブジェクト化したことがあったかもしれません。やらかしてたらすみません)</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Value Object は文字通り &quot;値&quot; なのであって &quot;属性&quot; ではないと思ってるので、引数の取り違えを防ぐみたいな目的に使うのをそう呼ぶのはやめましょう派に属しています。</p>&mdash; やきにく (@a_suenami) <a href="https://twitter.com/a_suenami/status/1524237176032346112?ref_src=twsrc%5Etfw">2022年5月11日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>たとえば「住所(PostalAddress)」は値オブジェクトになりうるとしても「出荷先住所(ShippingPostalAddress)」はいずれかのオブジェクトの属性であってこれ自体が値オブジェクトではない。</p> <p>コードにしてみるとものすごい違和感がある…。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synComment">// 出荷</span> <span class="synType">public</span> <span class="synType">class</span> Shipping { <span class="synComment">// 出荷先住所</span> <span class="synType">private</span> ShippingPostalAddress postalAddress; <span class="synComment">// あるとしても、こっちでは? PostalAddress postalAddress;</span> <span class="synComment">// 以下 略</span> } </pre> <p>仮に、出荷先住所(ShippingPostalAddress)を是とするならば、住所(PostalAddress)からサブタイプを作るのか。それとも継承ではなく委譲を使って特化した型を作るのか。いずれにしても、出荷先住所(ShippingPostalAddress)は、一般的な住所(PostalAddress)より特化した責務を担うのかどうか。なかなか想像しにくい…。通常なら住所(PostalAddress)で十分ではないかと。</p> <p>しかし、こういう要求が絶対にないとはいいにくい…。特化した要求があるとしても、出荷(Shipping)クラスの中で閉じるのであれば、住所(PostalAddress)で十分かもしれない。他でも再利用されるならば、独自の型が必要になるかもしれない。状況により判断されるのでどちらがいいとも悪いとも言えない。</p> <p>値か属性かを考えること自体は全く妥当なことだと思う。しかし、値か属性かを明確に判断する基準はつくれるのか…。</p> <p>属性というのは文脈に紐付く値といえる。そもそも文脈に紐付かない値というのはあり得るのか。『実践ドメイン駆動設計』の「アカウント」モデルの話を思い出そう。「アカウント」モデルのコンテキストは、銀行口座コンテキストなのか、文学コンテキストなのか。「アカウント」モデルは境界づけられたコンテキストの属性的な一面を担っているのではないか。単に値と見ているものでも、暗黙的な文脈があるかもしれない。どのように判断すべきか。</p> <p>値か属性か、はっきりする場合はよい。しかし、たなかさんがいうように簡単に線引きできる問題ではないかもしれない。グレーゾーンの中にいたら、僕らはどうするべきか。実験の失敗から学ぶしかないのではないか。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">英語単語のニュアンスからしても、&quot;what it is&quot;=property、&quot;what it does&quot;=attributeと言いたいのだが、、とはいえ、実運用上、両者の差異は極めてわずかだとも思う。明確な線引きはできないというか、多くの場合両義的だろう、とか。 <a href="https://t.co/CiJwrO0Xba">https://t.co/CiJwrO0Xba</a></p>&mdash; たなかこういち (@Tanaka9230) <a href="https://twitter.com/Tanaka9230/status/1524391406772830208?ref_src=twsrc%5Etfw">2022年5月11日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <h1>振る舞いはPofEAAの値オブジェクトの本質ではないのか</h1> <blockquote><p>もちろん郵便番号クラスをValue Objectとして実装するのは順当な話であるが、それをValue Object足らしめているのは「比較のために内部の郵便番号の数値を使うオブジェクトとした事」であって「プリミティブ型を包んで振る舞いを足した事」ではない。</p></blockquote> <p>こちらについて。不変条件(invariant)を維持するための振る舞いがあったとしても、<strong>値オブジェクト由来の概念</strong>ではなく、<strong>ドメイン固有型(ドメインオブジェクト)</strong>が持つ制約でしょうと。つまり、振る舞いの方ではなく、<strong>不変と等価判定</strong>にこそ存在意義があると読めました。うーん、僕は少し違う解釈をしています。</p> <p>そして、<strong>なんでもかんでも値をラップして、別名割当て問題を回避するために不変にする。さらに等価判定も実装する…。余計な複雑さを取り込んでいるよね…</strong>と。まぁこれはわかります。さらに、前述したように属性を誤ってクラス化したりしないように…。</p> <h1>ドメイン固有型はモデリングツールでもある</h1> <p>これは僕の解釈です。<strong>値オブジェクトはドメイン固有型の一種です。なので、不変と等価判定だけではなく、なにかしらのドメイン固有の不変条件(invariant)を維持する責任があると考えます</strong>(もちろん型として切り出すわけですからその投資に見合うだけの見返りがないといけません)。</p> <p>PofEAAの値オブジェクトであるMoney型も不変条件(invariant)を維持する操作を備えていると解釈できます。</p> <blockquote><p>Moneyの場合、Moneyオブジェクトを数字のように簡単に使えるような、算術演算が必要である。しかし、Moneyの算術演算と数字のMoney演算との間には、重要な相違点がいくつかある。最も明確なのは、異なる貨幣単位の金銭を合算しようとする場合、加算や減算では常に貨幣単位を認識する必要があることである。シンプルでよく使われる対処方法は、異なる貨幣単位の合算をエラーとして処理することである。</p></blockquote> <p><strong>つまるところ、値オブジェクトであっても扱う値に健全性がなければ、それは存在価値や利用価値がなくなってしまうので、値オブジェクトである前にドメイン固有型(ドメインオブジェクト)として責任を果たすことは当然だと考えています</strong>。詳しくは述べませんが、こう考える理由は『エリックエヴァンスのドメイン駆動設計』(以下DDD) の <strong>第10章 しなやかな設計</strong> にあります。</p> <blockquote><p>こうした表明が記述するのは、すべて状態であって手続きではないため、容易に分析できる。クラスの不変条件によって、クラスの意味が特徴づけられ、オブジェクトをより予測しやすくすることで、クライアント開発者の仕事が単純化される。事後条件による保証を信用するのであれば、メソッドがどう動くかを気にする必要はない。委譲による影響は、表明の中に組み込まれているはずである。</p></blockquote> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4798121967?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51f7WXHJYCL._SL500_.jpg" class="hatena-asin-detail-image" alt="エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)" title="エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4798121967?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%EA%A5%C3%A5%AF%A1%A6%A5%A8%A5%F4%A5%A1%A5%F3%A5%B9" class="keyword">エリック・エヴァンス</a></li><li>翔泳社</li></ul><a href="https://www.amazon.co.jp/dp/4798121967?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>長くなるので書きませんが理由は他にもあります。「<strong>解決する問題にあった設計を作り出すために、中心となる概念を捉えるモデルが必要。そのモデルをツールとしてソフトウェアに組み込む</strong> 」みたいな考え方があります。DDDの「第3部 より深い洞察へ向かうリファクタリング」あたりに書かれています。『エンタープライズアプリケーションアーキテクチャパターン』(以下 PofEAA), 『UMLモデリングのエッセンス』, 『リファクタリング』ではこのような視点が少なく、DDDとして特徴的な部分だと思います。つまるところ、ドメイン固有型はモデリングツールの側面があるんだと思います。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B01B5MX2O2?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/41Tz5dLB7dL._SL500_.jpg" class="hatena-asin-detail-image" alt="エンタープライズアプリケーションアーキテクチャパターン" title="エンタープライズアプリケーションアーキテクチャパターン"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B01B5MX2O2?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">エンタープライズアプリケーションアーキテクチャパターン</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%C1%A5%F3%A1%A6%A5%D5%A5%A1%A5%A6%A5%E9%A1%BC" class="keyword">マーチン・ファウラー</a></li><li>翔泳社</li></ul><a href="https://www.amazon.co.jp/dp/B01B5MX2O2?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4798107956?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51P6D4Q7T8L._SL500_.jpg" class="hatena-asin-detail-image" alt="UML モデリングのエッセンス 第3版 (Object Oriented SELECTION)" title="UML モデリングのエッセンス 第3版 (Object Oriented SELECTION)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4798107956?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">UML モデリングのエッセンス 第3版 (Object Oriented SELECTION)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%C1%A5%F3%A1%A6%A5%D5%A5%A1%A5%A6%A5%E9%A1%BC" class="keyword">マーチン・ファウラー</a></li><li>翔泳社</li></ul><a href="https://www.amazon.co.jp/dp/4798107956?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B0831M1RK5?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/41XPB3t7-qL._SL500_.jpg" class="hatena-asin-detail-image" alt="リファクタリング 既存のコードを安全に改善する(第2版)" title="リファクタリング 既存のコードを安全に改善する(第2版)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B0831M1RK5?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">リファクタリング 既存のコードを安全に改善する(第2版)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A3%CD%A3%E1%A3%F2%A3%F4%A3%E9%A3%EE%A3%C6%A3%EF%A3%F7%A3%EC%A3%E5%A3%F2" class="keyword">MartinFowler</a></li><li>オーム社</li></ul><a href="https://www.amazon.co.jp/dp/B0831M1RK5?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>なので、<strong>値をなんでもかんでもラップしてクラス化<a href="#f-bc64443d" name="fn-bc64443d" title="Javaのような言語だとクラスということになりますが、Haskell, Scala, Rustなら型クラスも選択肢になるでしょう">*1</a>するという杓子定規ではなく、まずこういった目的に合致するかどうかは、当たり前ですが確認する必要があります。一方で前述したようなデメリットも生じますし、値がプリミティブ型でよい場合もあります。これはトレードオフするしかありません。</strong></p> <p>ドメイン固有型の手段が「とにかくクラス化する」の一辺倒だから問題が生じやすいという意見もあるので、type aliasやopaque type aliasなど実装手段は最適なものをその状況に合わせて考えたいですね。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Javaの場合、YAGNIに従って後から必要になってからPrimitive Obsessionを避けよう、とすると変更がめちゃめちゃ大変だしIDEで一発変換みたいな事もできないので、最初から全部のクラス作れみたいな方向に行きがちっぽい。KotlinやScalaだとtype aliasが使えるから意思決定の遅延がしやすいのよね</p>&mdash; がくぞ (@gakuzzzz) <a href="https://twitter.com/gakuzzzz/status/1525806472596099073?ref_src=twsrc%5Etfw">2022年5月15日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <h1>ドメイン固有型とセキュリティ</h1> <p>「とにかく値オブジェクト化しよう!」は「オブジェクト指向エクササイズの影響ではないか」という話がありました。その説の可能性もありますが、別の説も書いておきます。</p> <p><a href="https://www.altus5.co.jp/blog/programming/2019/09/24/object-oriented-programming-exercise/">&#x300C;&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x6307;&#x5411;&#x30A8;&#x30AF;&#x30B5;&#x30B5;&#x30A4;&#x30BA;&#x300D;&#x3067;&#x30AF;&#x30BB;&#x306E;&#x5F37;&#x3044;&#x30B3;&#x30FC;&#x30C9;&#x3092;&#x77EF;&#x6B63;&#x3057;&#x3088;&#x3046; | ALTUS-FIVE</a></p> <p>プリミティブ型を避けてドメイン固有型を好む考え方は、『セキュア・バイ・デザイン』の影響もあるのではないかと思います。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/512NUC5YzPL._SL500_.jpg" class="hatena-asin-detail-image" alt="セキュア・バイ・デザイン" title="セキュア・バイ・デザイン"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">セキュア・バイ・デザイン</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Dan%20Bergh%20Johnsson" class="keyword">Dan Bergh Johnsson</a>,<a href="http://d.hatena.ne.jp/keyword/Daniel%20Deogun" class="keyword">Daniel Deogun</a>,<a href="http://d.hatena.ne.jp/keyword/Daniel%20Sawano" class="keyword">Daniel Sawano</a></li><li>マイナビ出版</li></ul><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=osi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>安全なアプリケーションを設計するという観点から、ドメイン固有型=ドメイン・プリミティブが注目されています。</p> <blockquote><p>ドメイン・プリミティブとは、その存在だけで、その値が有効であることを保証する厳格な定義がなされた値オブジェクトのことである。</p></blockquote> <p>XSSやインジェクションなど、既知の攻撃を対策すればセキュリティの対策は十分か?答えはノーです。</p> <p>とあるオンライン書店の話。あるユーザーから$39の本 -1冊の注文を受け付けてしまった。注文金額が$-39となった。明らかに不具合。本来であれば、その書籍を返品しなければなりませんが、そうはならなかった。技術的な問題ではなく、システムがドメインのルールから逸れてしまった問題です。既知の攻撃対策では防げません。</p> <p>本書では、多くの開発者が汎用的なデータ型を選択するが、セキュリティの観点からそれは大きな間違いであると指摘しています。例えば、電話番号を文字列型として扱うのは便利と考えるかもしれないが、電話番号以外のいかなる種類の値であっても受け入れることができてしまう。これでは不正な入力や操作を防ぎようがない、と。</p> <blockquote><p>ドメイン・モデルに含まれる概念はどれも基本データ型やStringのような汎用的な型を使って表現されるべきではありません。ドメイン・モデル内の各概念はドメイン・プリミティブとしてモデリングされるべきであり、そうすることで、そのオブジェクトが様々なところに渡されても、そのオブジェクトの意味が伝わり、不変条件を維持できるようになります。</p></blockquote> <p>具体的な例として、<code>UserAccount</code>を表示する入力画面での、XSS対策を考えます。</p> <p><code>UserAccount</code>に以下のような対策コードを埋め込むことがありますが、問題があります。XSS以外の問題に対応できない可能性があります。</p> <p>浅いドメイン理解では、ユーザー名を文字列型として捉えてしまうかもしれません。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> UserAccount { <span class="synType">public</span> UserAccount(<span class="synType">long</span> id, String userName) { validateForXSS(userName); <span class="synComment">// XSSを防ぐ</span> <span class="synComment">// ...</span> } } </pre> <p>『セキュア・バイ・デザインでは』、前述のコードではなく以下のよう変更することを推奨しています。</p> <p>ユーザー名の制約に 4から40文字、半角英数とアンダーバーおよびハイフンのみのルールがあると、深いドメイン理解を得られた場合、以下のような表明<a href="#f-b266ca25" name="fn-b266ca25" title="無害なasseretとするか、例外をスローするかは設計判断によるものとします">*2</a>を追加可能です。ドメイン知識を反映して不変条件を維持すれば、結果的にXSS対策も対策可能になるわけです。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> UserAccount { <span class="synComment">// ...</span> <span class="synType">public</span> UserAccount(<span class="synType">long</span> id, String userName) { <span class="synType">this</span>.id = assertLength(userName, <span class="synConstant">4</span>, <span class="synConstant">40</span>); <span class="synComment">// 長さが4以上40以内か</span> <span class="synType">this</span>.userName = assertPatterns(userName, <span class="synConstant">&quot;^[a-zA-Z0-9_-]+$&quot;</span>); <span class="synComment">// 文字種が適切か</span> } } </pre> <p><code>userName</code>の利用箇所が<code>UserAccount</code>だけならよいですが、他にもあった場合このような表明をあちこちに格納するのは馬鹿らしいので、<code>UserName</code>としてのドメイン・プリミティブを新設することになります。こうすれば、<code>UserAccount</code>は<code>UserName</code>の型を受け取るだけで、正しいユーザー名を受け入れることができます。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> UserName { <span class="synComment">// ...</span> <span class="synType">public</span> UserName(String value) { assertLength(value, <span class="synConstant">4</span>, <span class="synConstant">40</span>); <span class="synComment">// 長さが4以上40以内か</span> assertPatterns(value, <span class="synConstant">&quot;^[a-zA-Z0-9_-]+$&quot;</span>); <span class="synComment">// 文字種が適切か</span> <span class="synType">this</span>.value = value; } } <span class="synType">public</span> <span class="synType">class</span> UserAccount { <span class="synComment">// ...</span> <span class="synType">public</span> UserAccount(<span class="synType">long</span> id, UserName userName) { <span class="synComment">// UserAccountはUserNameを表明すればよいだけになる</span> <span class="synType">this</span>.id = id; <span class="synType">this</span>.userName = userName; } } </pre> <p>他の実装例としては、リスト5.1の1〜200個の制約を持つ数量クラス(Quantity)がありますが、内部属性を一つしか持ちません。compound(合成物)ではありません。数量クラスのどんな操作をやっても、0個や201個以上は許容されません。単にintだけを扱ってしまうと、脆弱性の問題に繋がる可能性があるからドメイン固有型を中心に設計しようという考え方です。これはプリミティブ型を辞める理由になると思います。</p> <p><strong>もちろん、こういった考え方を採用するかは前述したトレードオフを検討すべきです。</strong> どこまでドメインモデルを厳格にするべきか。プレゼンテーション層でバリデーションが終われば、ドメインロジックでわざわざこんなことしなくてもよい?いやいやバリデーション通過後に矛盾が起きる場合はどうするのかなど、考えることがいろいろあります。あと、クラス化が唯一の選択肢なのかも。使える言語が限られますが、型クラスを使えば既存の値型に振る舞いを追加することも可能です。</p> <p><strong>とはいえ、セキュリティ対策はドメイン固有型を採用する大きなモチベーションになっているのではないでしょうか。</strong>私も実際にドメインプリミティブを採用した結果、脆弱性診断でゼロ件を経験したこともあります。「プリミティブ型よりドメイン固有の型を」のエピソードのように、多くの人が探査機を燃やすようなことは避けたいと考えるのではないでしょうか。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.wikisource.org%2Fwiki%2F%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25B0%25E3%2583%25A9%25E3%2583%259E%25E3%2581%258C%25E7%259F%25A5%25E3%2582%258B%25E3%2581%25B9%25E3%2581%258D97%25E3%2581%25AE%25E3%2581%2593%25E3%2581%25A8%2F%25E3%2583%2597%25E3%2583%25AA%25E3%2583%259F%25E3%2583%2586%25E3%2582%25A3%25E3%2583%2596%25E5%259E%258B%25E3%2582%2588%25E3%2582%258A%25E3%2583%2589%25E3%2583%25A1%25E3%2582%25A4%25E3%2583%25B3%25E5%259B%25BA%25E6%259C%2589%25E3%2581%25AE%25E5%259E%258B%25E3%2582%2592" title="プログラマが知るべき97のこと/プリミティブ型よりドメイン固有の型を - Wikisource" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ja.wikisource.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%81%8C%E7%9F%A5%E3%82%8B%E3%81%B9%E3%81%8D97%E3%81%AE%E3%81%93%E3%81%A8/%E3%83%97%E3%83%AA%E3%83%9F%E3%83%86%E3%82%A3%E3%83%96%E5%9E%8B%E3%82%88%E3%82%8A%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E5%9B%BA%E6%9C%89%E3%81%AE%E5%9E%8B%E3%82%92">ja.wikisource.org</a></cite></p> <p>まぁセキュリティ対策のために他のすべてが犠牲になってよいのか。そういう極端なことは現実的ではないので、全体性を考慮に入れるのは当然ですが…。</p> <h1>まとめ</h1> <p>まとめらしい、まとめにならないな…。</p> <p>まぁドメイン固有型もプリミティブ型も「用法・用量を守ろう」としかいいようがないですね…。個人の開発ならどうでもいいと思いますが、開発者間の合意が必要なケースでは、問題のコンテキストに合致していないのに方法論だけ都合よく抜き取って対策したつもりになるのは避けたいですね。Howの解決策だけでなく、なぜそれをやるのかWhyも整理したほうがよいですね。例えばADRを書くなど。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadr.github.io%2F" title="Architectural Decision Records" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://adr.github.io/">adr.github.io</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Ffuubit%2Fitems%2Fdbb22435202acbe48849" title="アーキテクチャの「なぜ?」を記録する!ADRってなんぞや? - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/fuubit/items/dbb22435202acbe48849">qiita.com</a></cite></p> <p>まぁ、すぐどっちの方法論のよいのか悪いのかと考える癖がありますね。僕もあります。方法論にAとBがあるとして、A=プラス・B=マイナスという固定的な見方はしないほうがいい。こういうのを二項対立の思考といいますが、割と無意識にやっています。</p> <p>ここには思考停止の罠があります。Bのマイナスだって状況が変わればマイナスとも言い切れないわけですから。たとえば、プリミティブ型がプラス、ドメイン固有型がマイナスとした場合、モデリングをそこまで重視しない・セキュリティ対策もそこまで大げさにしなくてもよいシンプルな要件なら成り立ちます。しかし、逆の状況ならプラス・マイナスが逆転するかもしれません。</p> <p>さらいうと要件が中間レベルにある場合は、AとBのグレーゾーンを考慮しなくてはならなくなります。たとえば、ある部分ではプリミティブ型を重視、違う部分ではドメイン固有型を重視するなど。</p> <p>二項のうちどちらかに決めつけると不安を払拭できるけど、変化に対する適応力が乏しくなってしまう。二項対立から離れて、グレーゾーンを思考する能力が設計には必要だと思います。</p> <p>ということで、みなさん頑張っていきましょう。</p> <hr /> <h1>(余談) そもそもPofEAAとDDDの値オブジェクトが同じかどうか</h1> <p>そもそもPofEAAとDDDの値オブジェクトを同一視できるのかについては、一部で論争がありました。僕もこれについてはよくわかりません。2009年なので13年も前のことです。まだ和訳本がでていない時期…。</p> <p><a href="https://aufheben.hatenadiary.com/entry/20090501/1241169897">Value と Entity - 感想 - Aufheben - GLAD!! の日記</a></p> <p><a href="https://yyamano.hatenablog.com/entries/2009/05/29#p1">Value ObjectとDTO - yyamanoの日記</a></p> <p><a href="https://blog.j5ik2o.me/entry/20110209/1297258190">DDDのバリューオブジェクトは不変性が本質ではない - かとじゅんの技術日誌</a></p> <p>このときの僕の見解</p> <blockquote><p>個人的な感想は、やはりDDDとPofEAAで共に出てくる概念に関連性を求めてしまうと混乱する可能性があるなと思いました。似てるところが多いとは言え、完全に同じかというとそれはわからないですね。そこはFowlerとEvansを呼んで概念の関連性を確認しないと、どうにもはっきりしない領域です。</p> <p>ともあれ、PofEAAのことはさておき、DDDのバリューオブジェクトは、可変も不変もあるので、複製と共有もありと解釈した。大変参考になるエントリでした。気づきを得ることができました。先人に敬意を払いたいですね。</p></blockquote> <p>つまりこういうこと</p> <ol> <li>DDDの値オブジェクトとPofEAAの値オブジェクトは似ているようで、解釈が異なる部分がある</li> <li>DDDの値オブジェクトは不変は必須ではない。もちろん言語や環境に問題がなければ不変であることが望ましい。</li> </ol> <p>1)のような混乱が生じるときは、無理に同一視しなくてもよいと考えています。</p> <h1>(おまけ) Rustではどうするのか</h1> <p>ハイコンテキストな話題なので興味がある人向けです。</p> <p>JavaのStringは不変です。たとえば <a href="https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/String.html#concat%28java.lang.String%29">String#concat</a> は状態を破壊せずに、追加後の新しい<code>String</code>インスタンスを返します。仮に、あまりに変更が頻繁なら可変オブジェクトである <a href="https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/StringBuilder.html">StringBuilder</a> を使うことになります。共有するなら不変、変更が頻繁なら局所で可変を使うのが基本的な考え方。</p> <p>Rustの場合も不変がデフォルトですが、<code>String#concat</code>のような新しいインスタンスを返す設計にはほとんどしません。<code>String</code>の<a href="https://doc.rust-lang.org/src/alloc/string.rs.html#849">push_str</a>は以下のように<code>&amp;mut self</code>を要求するので、不変参照時では呼びだすことができず、可変参照があるときにしか呼べません。可変を安全に扱えるようになっています。つまりメソッドが可変か不変かを決めるので、わざわざ<code>StringBuilder</code>のような型を作る必要がありません。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> orginal <span class="synStatement">=</span> <span class="synType">String</span><span class="synSpecial">::</span><span class="synIdentifier">from</span>(<span class="synConstant">&quot;abc&quot;</span>); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;orginal = {}&quot;</span>, orginal); <span class="synComment">// orginal.push_str(&quot;def&quot;); コンパイルエラー</span> <span class="synStatement">let</span> <span class="synType">mut</span> modify <span class="synStatement">=</span> orginal.<span class="synIdentifier">clone</span>(); modify.<span class="synIdentifier">push_str</span>(<span class="synConstant">&quot;def&quot;</span>); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;modify = {}&quot;</span>, modify); </pre> <p>Javaでは可変・不変の型を分ける。Rustではメソッドごとに可変(&amp;mut self)・不変(&amp;self)を分けるだけです。</p> <p>さらに、Rustであっても、守るべき不変条件(invariant)があれば型として定義することがあるでしょう。たとえば、通貨単位が同一でなければ<code>Money#add</code>できないなど。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fj5ik2o%2Fbaseunits-rs%2Fblob%2Fmain%2Fsrc%2Fmoney%2Fmoney.rs%23L189-L198" title="baseunits-rs/money.rs at main · j5ik2o/baseunits-rs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/j5ik2o/baseunits-rs/blob/main/src/money/money.rs#L189-L198">github.com</a></cite></p> <p>単一の値に制約を課すだけなら、newtypeする以外に、値型のためのtraitの実装を追加する方法もあると思います。</p> <div class="footnote"> <p class="footnote"><a href="#fn-bc64443d" name="f-bc64443d" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">Javaのような言語だとクラスということになりますが、Haskell, Scala, Rustなら型クラスも選択肢になるでしょう</span></p> <p class="footnote"><a href="#fn-b266ca25" name="f-b266ca25" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">無害なasseretとするか、例外をスローするかは設計判断によるものとします</span></p> </div> j5ik2o Rustを使ってスケーラブルなプログラムを書く方法 hatenablog://entry/13574176438039908744 2021-12-24T10:49:56+09:00 2021-12-24T10:49:56+09:00 この記事はRust Advent Calendar 2021の12/24日の記事です。 仕事ではScalaを使っていますが、趣味のプログラミングではRustで書いたものが増えました。Rustは楽しいですね。 今回は、Rustでオブジェクト指向プログラミングに関数型デザインを導入することで、スケーラブルなプログラムを書く方法(スケーラブル・プログラミング)について書きます。 <p>この記事は<a href="https://qiita.com/advent-calendar/2021/rust0">Rust Advent Calendar 2021</a>の12/24日の記事です。</p> <p>仕事ではScalaを使っていますが、趣味のプログラミングではRustで書いたものが増えました。Rustは楽しいですね。</p> <p>今回は、Rustでオブジェクト指向プログラミングに関数型デザインを導入することで、スケーラブルなプログラムを書く方法(スケーラブル・プログラミング)について書きます。</p> <p>「スケーラブル・プログラミング」といえばScalaです。Scalaの「スケーラブル」という言葉には「小さいプログラムも大規模なプログラムも同じ概念で記述できるべきである」という、柔軟性や拡張性を重視した設計の意図が込められています。それを実現するために必要なものは、オブジェクト指向と関数型を組み合わせたマルチパラダイムな設計です。</p> <p>Scalaはマルチパラダイム言語の先駆者(今も先頭を走り続けています)ですが、他の言語にも広がっています。マルチパラダイム言語の成果は、JavaにScalaが持つ言語機能が取り込まれたり、Rustなどのマルチパラダイム言語の台頭、という形で現れていると思います<a href="#f-c17b088c" name="fn-c17b088c" title="というか、オブジェクト指向言語や関数型言語の境界線が溶け込んで曖昧になってきているように思えます">*1</a>。そういう意味では、スケーラブル・プログラミングは他の言語にも適用可能な時代になっていると考えています。</p> <p>そして、話を元に戻す…。今回の記事では、その「スケーラブル」という概念を、関数型プログラミングのテクニックを使ってRustに取り入れてみたら、どうなるかを簡単に(!?)説明してみたいと思います。簡単にといいつつ、大作になってしまいました…。<a href="#f-b0f8c59c" name="fn-b0f8c59c" title="丁寧に書くと大作になる。ボリュームが多いと読者は意図をくみとるのに失敗する確率が高くなる、ので難しい…。">*2</a></p> <h2>想定読者</h2> <p>少なくともRustを「完全に理解した<a href="#f-fd00c03e" name="fn-fd00c03e" title="このネットスラングを知らない方はこちらをご覧ください→https://togetter.com/li/1783989">*3</a>」方でないと、おすすめできない記事になっています。</p> <p>ちなみに、Scalaというキーワードは少しでてきますが、Scala自体の説明はありません。</p> <p>Scalaを理解しないとRustが使えないということではありません。そのあたりは誤解がないように…。</p> <p>モナドなどの圏論の話もありませんので、安心してください。</p> <p>という予防線を張っておき、どんどん関数型な世界に入っていく。</p> <h2>Rustは関数型言語かオブジェクト指向言語か</h2> <p>いきなり蛇足…。「Rustは関数型言語なの?オブジェクト指向言語なの?」という疑問を持つ方は少なくないと思います。</p> <h3>Rustは関数型言語か</h3> <p>Rustは、関数型言語ではなく命令型言語だと言う方が多いです。関数型言語の定義はこんなふうに書いてある。</p> <blockquote><p>関数型プログラミング言語(英: functional programming language)とは、関数型プログラミングを推奨しているプログラミング言語である[3]。略して関数型言語(英: functional language)ともいう[4]。</p></blockquote> <p><a href="https://ja.wikipedia.org/wiki/%E9%96%A2%E6%95%B0%E5%9E%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0">&#x95A2;&#x6570;&#x578B;&#x30D7;&#x30ED;&#x30B0;&#x30E9;&#x30DF;&#x30F3;&#x30B0; - Wikipedia</a></p> <p>この定義から考えるとRustは「関数型言語である」とは言えないでしょう。とはいえ、関数型プログラミングに関する機能は取り込まれています。関数型言語かどうかはさておき、関数型プログラミングはできます。</p> <p><a href="https://doc.rust-jp.rs/book-ja/ch13-00-functional-features.html">&#x95A2;&#x6570;&#x578B;&#x8A00;&#x8A9E;&#x306E;&#x6A5F;&#x80FD;&#xFF1A;&#x30A4;&#x30C6;&#x30EC;&#x30FC;&#x30BF;&#x3068;&#x30AF;&#x30ED;&#x30FC;&#x30B8;&#x30E3; - The Rust Programming Language &#x65E5;&#x672C;&#x8A9E;&#x7248;</a></p> <h3>Rustはオブジェクト指向言語か</h3> <p>これと似たような話で、Rustはオブジェクト指向言語か?という論争もあります。Rustはクラス・継承・例外・nullがないので、Javaのような既存のオブジェクト指向言語とは一線を画しますが、オブジェクト指向プログラミングはできます(個人の考え)。どちらかというと、C言語でのオブジェクト指向プログラミングに使い方が似ています。</p> <ul> <li><a href="https://doc.rust-jp.rs/book-ja/ch17-01-what-is-oo.html">&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x6307;&#x5411;&#x8A00;&#x8A9E;&#x306E;&#x7279;&#x5FB4; - The Rust Programming Language &#x65E5;&#x672C;&#x8A9E;&#x7248;</a></li> <li><a href="https://opaupafz2.hatenablog.com/entry/2021/06/12/104719">Rust&#x304C;&#x30AA;&#x30D6;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x6307;&#x5411;&#x578B;&#x8A00;&#x8A9E;&#x3067;&#x306F;&#x306A;&#x3044;&#x306E;&#x3068;&#x305D;&#x306E;&#x7406;&#x7531; - &#x306A;&#x3093;&#x304B;&#x8003;&#x3048;&#x3066;&#x308B;&#x3053;&#x3068;&#x3068;&#x304B;</a></li> </ul> <h3>Rustもマルチパラダイム言語</h3> <p>Rustは、手続き型プログラミング、オブジェクト指向プログラミング、関数型プログラミングをサポートする、マルチパラダイム言語です。</p> <blockquote><p>Rustはマルチパラダイムプログラミング言語であり、手続き型プログラミング、オブジェクト指向プログラミング、関数型プログラミングなどの実装手法をサポートしている。基本的な制御構文はC言語に似ているが、その多くが式(expression)であるという点においてはML言語に似ている。コンパイル基盤にMIRとLLVMを用いており[10]、実行時速度性能はC言語と同等程度である[11]。強力な型システムとリソース管理の仕組みにより、メモリ安全性が保証されている。</p></blockquote> <p><a href="https://ja.wikipedia.org/wiki/Rust_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)">Rust (&#x30D7;&#x30ED;&#x30B0;&#x30E9;&#x30DF;&#x30F3;&#x30B0;&#x8A00;&#x8A9E;) - Wikipedia</a></p> <p>「関数型言語か」「オブジェクト指向言語か」という既存のステレオタイプに当てはめようとしても無理があると思います。「あれか・これか」ではなく「あれも・これも」の視点で使い方を考えると生産的ではないでしょうか。</p> <h2>Scala関数型デザイン&プログラミング</h2> <p>本題に戻ります。</p> <p>この記事では、以下の書籍(以下 FP in Scala本)中で紹介されるテクニックを、Rustへ輸入する具体的な方法を書いています。</p> <p>「Rustなんもわからん」もしくは「Rustチョットデキル」の方には考えてみてほしいテーマです。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4844337769/j5ik2o.me-22/" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51yVEKnEKlL._SL500_.jpg" class="hatena-asin-detail-image" alt="Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)" title="Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4844337769/j5ik2o.me-22/" target="_blank" rel="noopener">Scala関数型デザイン&amp;プログラミング ―Scalazコントリビューターによる関数型徹底ガイド (impress top gear)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Paul%20Chiusano" class="keyword">Paul Chiusano</a>,<a href="http://d.hatena.ne.jp/keyword/R%8F%AB%E2nar%20Bjarnason" class="keyword">Rúnar Bjarnason</a></li><li>インプレス</li></ul><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4844337769/j5ik2o.me-22/" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>当然、コード例はScalaで説明されているわけですが、それをRustに脳内トランスパイルして学び直しました。</p> <h3>この書籍を参考にして作ったもの</h3> <p>この本の中盤の「Part II 関数型デザインとコンビネータライブラリ」という部分では、「プロパティベースのテスト」と「パーサコンビネータ」の解説があります。それに沿って作ったRustの実装は以下です。ほとんど同じ機能かそれ以上の機能を提供しています。</p> <ul> <li><a href="https://github.com/j5ik2o/prop-check-rs">GitHub - j5ik2o/prop-check-rs: A Rust crate for property-based testing.</a></li> <li><a href="https://github.com/j5ik2o/oni-comb-rs">GitHub - j5ik2o/oni-comb-rs: A Rust crate for LL(k) parser combinators.</a></li> </ul> <p>今回「パーサコンビネータ」を通して関数型デザインの考え方を紹介したいので「プロパティベースのテスト」は割愛します。</p> <h2>パーサコンビネータ</h2> <p>パーサーとは<a href="https://ja.wikipedia.org/wiki/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90%E5%99%A8">構文解析器</a>のことです。</p> <p>パーサーは自前で実装せずにライブラリを使うことが一般的だと思います。そのパーサライブラリには、パーサージェネレータとパーサコンビネータの大まかに二つの種類があります。</p> <h3>パーサージェネレータ</h3> <p>全く実用性はありませんが、過去にJavaで四則演算だけができる言語を作りましたが、JavaCCというパーサージェネレータを使いました。パーサージェネレータはその名の通り、特殊な記法で記述されたルールに基づき、パーサーを生成してくれるツールです。</p> <p><a href="https://blog.j5ik2o.me/entry/20091107/1257598591">&#x6570;&#x5024;&#x304C;BigDecimal&#x578B;&#x306A;&#x8A08;&#x7B97;&#x5F0F;&#x8A00;&#x8A9E;&#x3092;&#x4F5C;&#x3063;&#x3066;&#x307F;&#x305F; - &#x304B;&#x3068;&#x3058;&#x3085;&#x3093;&#x306E;&#x6280;&#x8853;&#x65E5;&#x8A8C;</a></p> <h3>パーサーコンビネータ</h3> <p>今回は、パーサーをジェネレートする必要のない、パーサコンビネータを紹介します。FP in Scala本によると以下のように説明されています。</p> <blockquote><p>パーサーコンビネータライブラリのパーサーは、どこにでもあるファーストクラスの値にすぎません。解析ロジックを再利用するのはいとも簡単なことであり、プログラミング言語以外に外部ツールの類いはいっさい必要ありません。</p></blockquote> <p>今回実装したパーサコンビネータでは、空白やタブや改行の連続を解析して破棄するパーサは以下のように簡単に書けます。<code>elm_of(" \t\r\n")</code>は空白やタブや改行の連続を解析するパーサーを返す関数です。<code>of_many0()</code>はそれが0個以上連続することを意味し、<code>discard()</code>は解析に成功すると解析結果を破棄することを意味します。<code>of_many0</code>, <code>discard</code>はパーサーと組み合わせて利用するので、コンビネータ<a href="#f-a1c0a2fb" name="fn-a1c0a2fb" title="高階関数のことです">*4</a>と呼ばれます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">space</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">char</span>, ()<span class="synStatement">&gt;</span> { <span class="synIdentifier">elm_of</span>(<span class="synConstant">&quot; </span><span class="synSpecial">\t\r\n</span><span class="synConstant">&quot;</span>).<span class="synIdentifier">of_many0</span>().<span class="synIdentifier">discard</span>() } </pre> <p>読み込んだ文字や文字列がパターンに合致するかなどの手続きのHowを記述するのではなく、パーサーコンビネータでは、その構文が何か(What)をそのままコードとして表現するスタイルになります<a href="#f-1834878f" name="fn-1834878f" title="さらにパーサーはパーサーを組み合わせることで実装可能なので下位層のWhatは上位層のHowになっていきます。下位層には、手続き型のコードはつきものですが、こういった全体の構造からみると一部になります。">*5</a>。</p> <h2>なぜパーサコンビネータライブラリを自作したのか</h2> <p>Starが多いRust製のパーサーライブラリは以下です。</p> <ul> <li><a href="https://github.com/Geal/nom">https://github.com/Geal/nom</a></li> <li><a href="https://github.com/Marwes/combine">https://github.com/Marwes/combine</a></li> <li><a href="https://github.com/zesterer/chumsky">https://github.com/zesterer/chumsky</a></li> <li><a href="https://github.com/J-F-Liu/pom">https://github.com/J-F-Liu/pom</a></li> </ul> <p>どれもそれなりに使えます。</p> <h3>nomのインターフェイスは扱いづらい</h3> <p><a href="https://github.com/Geal/nom"><code>nom</code></a>を使ってURIのパーサーを実装しましたが、関数の呼びだしが g→h→f のような解析順であっても、<code>f(h(g()))</code>のような入れ子になります。直感的にコードを記述できないです。</p> <p><a href="https://github.com/j5ik2o/uri-rs/blob/main/src/parser/parsers/ipv6_address_parsers.rs">uri-rs/ipv6_address_parsers.rs at main &middot; j5ik2o/uri-rs &middot; GitHub</a></p> <p>構文ルールに合わせて、左から順番にコードが読めないのはつらい…。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span><span class="synSpecial">(</span><span class="synStatement">crate</span><span class="synSpecial">)</span> <span class="synStatement">fn</span> <span class="synIdentifier">ls32</span>(i: Elms) <span class="synStatement">-&gt;</span> UResult<span class="synStatement">&lt;</span>Elms, <span class="synType">String</span><span class="synStatement">&gt;</span> { <span class="synIdentifier">context</span>( <span class="synConstant">&quot;ls32&quot;</span>, <span class="synIdentifier">alt</span>(( <span class="synIdentifier">map</span>( <span class="synIdentifier">tuple</span>((h16, <span class="synIdentifier">preceded</span>(<span class="synPreProc">complete</span><span class="synSpecial">::</span><span class="synType">char</span>(<span class="synConstant">':'</span>), h16))), <span class="synStatement">|</span>(c1, c2)<span class="synStatement">|</span> [c1, c2].<span class="synIdentifier">join</span>(<span class="synConstant">&quot;:&quot;</span>), ), <span class="synPreProc">ipv4_address_parsers</span><span class="synSpecial">::</span>ipv4_address, )), )(i) } </pre> <h3>pomは書きやすく読み易い</h3> <p>一方で<a href="https://github.com/J-F-Liu/pom"><code>pom</code></a>は構文ルールをそのままメソッドチェーンで記述できるので、構文ルールどおりに書きやすいし読みやすい<a href="#f-d1617f78" name="fn-d1617f78" title="実はScala Parser Combinatorsも同じようなインターフェイスになっています。">*6</a>。<code>pom</code>では、<code>g() + h() + f()</code> のように直感的に記述できます。</p> <p>たとえば、CROND文字列の分を解析するパーサーは以下のように定義できます。</p> <p>これは入力として<code>u8</code>なのでバイト列を解析するパーサーの例です。<code>pom</code>の方が<code>nom</code>より読みやすいと思います。<code>one_of</code>は指定した要素のうちいずかにマッチするという意味です。<code>sym</code>は単一の要素にマッチするという意味です。</p> <p><code>one_of(b"12345")</code>という数値の羅列ではなく、<code>one_of('1', '5')</code>のようにもう少しすっきり書けないのか…というのはありますが、<code>pom</code>より読みいやすいと思います。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">min_digit</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">u8</span>, Expr<span class="synStatement">&gt;</span> { (<span class="synIdentifier">one_of</span>(<span class="synConstant">b&quot;12345&quot;</span>) <span class="synStatement">+</span> <span class="synIdentifier">one_of</span>(<span class="synConstant">b&quot;0123456789&quot;</span>)).<span class="synIdentifier">map</span>(<span class="synStatement">|</span>(e1, e2)<span class="synStatement">|</span> <span class="synIdentifier">ValueExpr</span>((e1 <span class="synStatement">-</span> <span class="synConstant">48</span>) <span class="synStatement">*</span> <span class="synConstant">10</span> <span class="synStatement">+</span> e2 <span class="synStatement">-</span> <span class="synConstant">48</span>)) <span class="synStatement">|</span> (<span class="synIdentifier">sym</span>(<span class="synConstant">b'0'</span>) <span class="synStatement">*</span> <span class="synIdentifier">one_of</span>(<span class="synConstant">b&quot;0123456789&quot;</span>)).<span class="synIdentifier">map</span>(<span class="synStatement">|</span>e<span class="synStatement">|</span> <span class="synIdentifier">ValueExpr</span>(e <span class="synStatement">-</span> <span class="synConstant">48</span>)) <span class="synStatement">|</span> (<span class="synIdentifier">one_of</span>(<span class="synConstant">b&quot;0123456789&quot;</span>)).<span class="synIdentifier">map</span>(<span class="synStatement">|</span>e<span class="synStatement">|</span> <span class="synIdentifier">ValueExpr</span>(e <span class="synStatement">-</span> <span class="synConstant">48</span>)) } </pre> <h3>そして車輪の再発明へ</h3> <p>ツールを作るだけなら<code>pom</code>を使えばよかったのですが、以下の理由で自作(車輪の再発明)することにしました。</p> <ul> <li>関数型デザインを学ぶため</li> <li>pomにない機能を実装するため</li> </ul> <h2>作ったものは鬼昆布(oni-comb-rs)</h2> <p>作ったものは鬼昆布(oni-comb-rs)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fj5ik2o%2Foni-comb-rs" title="GitHub - j5ik2o/oni-comb-rs: A Rust crate for LL(k) parser combinators." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/j5ik2o/oni-comb-rs">github.com</a></cite></p> <blockquote><p>鬼昆布は非常に濃いダシがとれる昆布です。生産量が少なく、市場に滅多に出回らない非常に珍しい昆布です。</p></blockquote> <p>濃いダシが出るように設計しました(謎</p> <h3>鬼昆布(oni-comb-rs)の使い方</h3> <p>例えば、<code>1+2</code>という文字列<a href="#f-9f321737" name="fn-9f321737" title="空白の読み飛ばしをしないと実用的ではないですがここでは割愛">*7</a>を解析したい場合は、以下のように記述できます。</p> <p><code>elm_digit</code>は0から9までの要素にマッチすると成功するパーサーで、<code>elm</code>は引数の要素にマッチすると成功するパーサーです。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// 入力値</span> <span class="synStatement">let</span> input <span class="synStatement">=</span> <span class="synConstant">&quot;1+2&quot;</span>.<span class="synIdentifier">chars</span>().<span class="synIdentifier">collect</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span><span class="synType">Vec</span><span class="synStatement">&lt;</span><span class="synType">char</span><span class="synStatement">&gt;&gt;</span>(); <span class="synComment">// パーサーの構築</span> <span class="synStatement">let</span> parser: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, ((<span class="synType">char</span>, <span class="synType">char</span>), <span class="synType">char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">elm_digit</span>() <span class="synStatement">+</span> <span class="synIdentifier">elm</span>(<span class="synConstant">'+'</span>) <span class="synStatement">+</span> <span class="synIdentifier">elm_digit</span>(); <span class="synComment">// 解析の実行</span> <span class="synStatement">let</span> parse_result: ParseResult<span class="synStatement">&lt;</span><span class="synType">char</span>, ((<span class="synType">char</span>, <span class="synType">char</span>), <span class="synType">char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> parser.<span class="synIdentifier">parse</span>(<span class="synType">&amp;</span>input); <span class="synComment">// 解析結果の取得</span> <span class="synStatement">let</span> result: ((<span class="synType">char</span>, <span class="synType">char</span>), <span class="synType">char</span>) <span class="synStatement">=</span> parse_result.<span class="synIdentifier">success</span>().<span class="synIdentifier">unwrap</span>(); <span class="synComment">// ((1, +), 2)</span> </pre> <p>パーサーどうしを合成することによって、より複雑な構文を解析するパーサーを構築できます。</p> <p><code>char</code>のタプルを返されてもあまり実用的でないので、以下のように<code>AddExpr</code>などの構造体に変換させることが多いです。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> parser: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, AddExpr<span class="synStatement">&gt;</span> <span class="synStatement">=</span> (<span class="synIdentifier">elm_digit</span>() <span class="synStatement">-</span> <span class="synIdentifier">elm</span>(<span class="synConstant">'+'</span>) <span class="synStatement">+</span> <span class="synIdentifier">elm_digit</span>()).<span class="synIdentifier">map</span>(<span class="synStatement">|</span>(a, b)<span class="synStatement">|</span> <span class="synIdentifier">AddExpr</span>(a, b)); </pre> <h2>鬼昆布(oni-comb-rs)を使って何を作ったか</h2> <p>このライブラリを使っていろいろつくりましたが、いくつか紹介します。</p> <h3>URIパーサー</h3> <p>わかりやすい例としてはURIパーサです。今どき、URIパーサーなんて作らないとは思いますが…。気に入ったクレートがなかったので<a href="https://datatracker.ietf.org/doc/html/rfc3986">RFC</a>を参考に実装しました。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/tree/main/uri">oni-comb-rs/uri at main &middot; j5ik2o/oni-comb-rs &middot; GitHub</a></p> <p>例えば、URI仕様の一部であるIPv6アドレスの<code>ls32</code>は以下のように書けます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">ls32</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">char</span>, LS32<span class="synStatement">&gt;</span> { <span class="synStatement">let</span> p1 <span class="synStatement">=</span> (<span class="synIdentifier">h16</span>() <span class="synStatement">-</span> <span class="synIdentifier">elm</span>(<span class="synConstant">':'</span>) <span class="synStatement">+</span> <span class="synIdentifier">h16</span>()).<span class="synIdentifier">map</span>(<span class="synStatement">|</span>(a, b)<span class="synStatement">|</span> <span class="synPreProc">LS32</span><span class="synSpecial">::</span><span class="synIdentifier">Ls32</span>(a, b)); <span class="synStatement">let</span> p2 <span class="synStatement">=</span> <span class="synIdentifier">ip_v4_address</span>().<span class="synIdentifier">map</span>(<span class="synStatement">|</span>a<span class="synStatement">|</span> <span class="synPreProc">LS32</span><span class="synSpecial">::</span><span class="synIdentifier">Ipv4Address</span>(a)); p1.<span class="synIdentifier">attempt</span>() <span class="synStatement">|</span> p2 } </pre> <p><code>h16</code>もパーサーです。Aの次にBというパターンの場合は<code>a + b</code>と記述できます(連言といいます)。<code>a - b + c</code>のように<code>-</code>を使うと<code>b</code>の結果だけ捨てられます。<code>map</code>は解析に成功した結果が関数へ渡され、解析結果を別の型・値に変換できます。<code>p1.attempt() | p2</code>の<code>attempt()</code>は<code>p1</code>が解析に失敗しても解析を中断しないという意味になります。<code>|</code>は選言といって、AまたはBというパターンの場合で使えます。<code>Parser</code>型は<code>Parser&lt;'a, 入力要素型, 解析結果型&gt;</code>です。</p> <h3>JSONパーサー</h3> <p>JSONパーサーは以下に90行足らずで実装できます<a href="#f-c95ea837" name="fn-c95ea837" title="ちなみにこの程度ならpomでも同様に実装できます">*8</a>。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/main/parser/examples/json_char.rs">oni-comb-rs/json_char.rs at main &middot; j5ik2o/oni-comb-rs &middot; GitHub</a></p> <p>配列を解析する処理は以下のように簡潔に記述できます。</p> <p><code>value</code>はJSONに含まれる値(文字列, 数値, 二値, nullなど)を解析するパーサーです。<code>lazy</code>はパーサを返す関数を引数に取り遅延評価します。これはパーサーの初期化処理が循環し、スタックがあふれしてしまうことを回避するための工夫です。<code>of_many0_sep</code>メソッドは<code>self</code>をデリミタありで0回以上の出現を解析するコンビネータです。この例では、<code>value</code> <code>,</code>の繰り返しが0回以上という意味になります。解析結果は<code>Vec</code>型になります。<code>surround</code>は引数の順序どおりにパーサーを評価しますが、解析に成功すると第一引数と第三引数のパーサーの結果を捨てます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">array</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">char</span>, <span class="synType">Vec</span><span class="synStatement">&lt;</span>JsonValue<span class="synStatement">&gt;&gt;</span> { <span class="synStatement">let</span> elems <span class="synStatement">=</span> <span class="synIdentifier">lazy</span>(value).<span class="synIdentifier">of_many0_sep</span>(<span class="synIdentifier">space</span>() <span class="synStatement">*</span> <span class="synIdentifier">elm_ref</span>(<span class="synConstant">','</span>) <span class="synStatement">-</span> <span class="synIdentifier">space</span>()); <span class="synIdentifier">surround</span>(<span class="synIdentifier">elm_ref</span>(<span class="synConstant">'['</span>) <span class="synStatement">-</span> <span class="synIdentifier">space</span>(), elems, <span class="synIdentifier">space</span>() <span class="synStatement">*</span> <span class="synIdentifier">elm_ref</span>(<span class="synConstant">']'</span>)) } </pre> <h3>Toys言語</h3> <p>そして、@kmizuさんによる、WEB+DB PRESS vol.125の特集記事のToys言語に興味を持ちました。ちょっとしたミニ・プログラミング言語を作ってみようという企画です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgihyo.jp%2Fmagazine%2Fwdpress%2Farchive%2F2021%2Fvol125" title="WEB+DB PRESS Vol.125" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://gihyo.jp/magazine/wdpress/archive/2021/vol125">gihyo.jp</a></cite></p> <p>具体的な実装コードはこちら。</p> <p><a href="https://github.com/kmizu/toys">GitHub - kmizu/toys: A toy programming language to learn how to design and implement programming languages</a></p> <h4>Rust版の実装</h4> <p>特集記事の実装はJavaで実装されていましたが、Rustで実装しようと考えました。<code>oni-comb-rs</code>を使って。</p> <p>詳しく説明しませんが、Rust版の実装コードはこちら。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/tree/main/toys">oni-comb-rs/toys at main &middot; j5ik2o/oni-comb-rs &middot; GitHub</a></p> <p>パーサー自体の定義は320行ぐらいでした。実用性は全然ないですが、FizzBuzzを書くことができます。パーサーを実装するよりインタプリタの実装のほうが面白かったです。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">main</span>() { <span class="synStatement">let</span> source <span class="synStatement">=</span> <span class="synConstant">r#&quot;</span> <span class="synConstant"> fn fizz_buzz(i) {</span> <span class="synConstant"> if ((i % 3 == 0) &amp;&amp; (i % 5 == 0)) {</span> <span class="synConstant"> println(&quot;FizzBuzz&quot;);</span> <span class="synConstant"> } else if (i % 3 == 0) {</span> <span class="synConstant"> println(&quot;Fizz&quot;);</span> <span class="synConstant"> } else if (i % 5 == 0) {</span> <span class="synConstant"> println(&quot;Buzz&quot;);</span> <span class="synConstant"> } else {</span> <span class="synConstant"> println(i);</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> fn main() {</span> <span class="synConstant"> println(&quot;----&quot;);</span> <span class="synConstant"> for (i in 1 to 100) {</span> <span class="synConstant"> fizz_buzz(i);</span> <span class="synConstant"> }</span> <span class="synConstant"> println(&quot;----&quot;);</span> <span class="synConstant"> }</span> <span class="synConstant"> &quot;#</span>; <span class="synStatement">let</span> input <span class="synStatement">=</span> source.<span class="synIdentifier">chars</span>().<span class="synIdentifier">collect</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span><span class="synType">Vec</span><span class="synStatement">&lt;</span>_<span class="synStatement">&gt;&gt;</span>(); <span class="synStatement">let</span> result <span class="synStatement">=</span> <span class="synIdentifier">program</span>().<span class="synIdentifier">parse</span>(<span class="synType">&amp;</span>input).<span class="synIdentifier">to_result</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, result); <span class="synPreProc">Interpreter</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>().<span class="synIdentifier">call_main</span>(result); } </pre> <h3>他のRust版の実装</h3> <p>他にもnomやcombineを使った実装があります。ご参考までに。</p> <ul> <li><a href="https://github.com/pione30/toysrust">https://github.com/pione30/toysrust</a> <ul> <li>nomによる実装</li> </ul> </li> <li><a href="https://github.com/yuk1ty/toy-lang">https://github.com/yuk1ty/toy-lang</a> <ul> <li>combineによる実装</li> </ul> </li> </ul> <h2>どうやって設計したか</h2> <p>やっとここからが本題。</p> <h3>代数的設計</h3> <p>今回の実装は、代数的設計というものを重視しています。</p> <p>FP in Scala本では、代数は以下のように説明されています。</p> <blockquote><p>代数とは、一つ以上のデータ型を操作する関数の集まりと、そうした関数の間の関係を指定する一連の法則のことです。</p></blockquote> <p>つまり、代数とは関数とその関数を合成する法則のことです。この考え方をうまく使うと、スケーラブルなプログラミングができます。</p> <p>わかるようなわからないような…コードをみたほうが早いのですね。実装コードの抜粋を説明します。</p> <h3>要素(文字)を解析するパーサー</h3> <p>最初に設計した関数は要素(文字)を解析するパーサーを返す<a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/element_parsers.rs#L30"><code>elm_pred_ref</code></a>関数です<a href="#f-451e29b9" name="fn-451e29b9" title="文字をわざわざ要素と抽象的な書き方をしたのは、このパーサーが文字以外も解析できるような設計になっているからです。ある規則に沿ったバイト列上の要素であっても解析できます">*9</a>。パーサーを生成するファクトリです。このtraitは要素を解析するパーサーを返す関数を実装します。数値文字列や小文字を解析するパーサーを返す関数も提供されます。<code>elm_pred_ref</code>関数は<code>f</code>が<code>true</code>を返すとき、解析に成功し入力の文字への参照を返すパーサーを返します。</p> <p>ちなみに、関数名の<code>ref</code>というサフィックスは解析結果への参照を返すパーサと返すという意味で、<code>ref</code>がついてない関数を利用すると実体の値を返すので複製(Clone)が生じます。<code>ref</code>の付いた関数を使うことで、できるだけCloneを遅延させたり最適化させたりできます。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/element_parsers.rs#L30"><code>element_parsers.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">ElementParsers</span>: Parsers { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">elm_pred_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, F<span class="synStatement">&gt;</span>(f: F) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> F: <span class="synType">Fn</span>(<span class="synType">&amp;</span>I) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synComment">// ...</span> <span class="synComment">// Self::Pは`Parsers`トレイトで定義されているパーサー型のエイリアス</span> } </pre> <p>このメソッドは内部の<code>ParsersImpl</code>に実装されており、外部には同名の<a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/lib.rs#L389"><code>elm_pred_pref</code></a>関数で公開されます。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/lib.rs#L389"><code>lib.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">elm_pred_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, F<span class="synStatement">&gt;</span>(f: F) <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> F: <span class="synType">Fn</span>(<span class="synType">&amp;</span>I) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synPreProc">ParsersImpl</span><span class="synSpecial">::</span><span class="synIdentifier">elm_pred_ref</span>(f) } </pre> <p>使い方は以下のようになります。特に複雑さはないと思います。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> text: <span class="synType">&amp;str</span> <span class="synStatement">=</span> <span class="synConstant">&quot;x&quot;</span>; <span class="synStatement">let</span> input: <span class="synType">Vec</span><span class="synStatement">&lt;</span><span class="synType">char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> text.<span class="synIdentifier">chars</span>().<span class="synIdentifier">collect</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span><span class="synType">Vec</span><span class="synStatement">&lt;</span>_<span class="synStatement">&gt;&gt;</span>(); <span class="synStatement">let</span> parser: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">elm_pred_ref</span>(<span class="synStatement">|</span>c<span class="synStatement">|</span> <span class="synType">*</span>c <span class="synStatement">==</span> <span class="synConstant">'x'</span>); <span class="synStatement">let</span> result: ParseResult<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> parser.<span class="synIdentifier">parse</span>(<span class="synType">&amp;</span>input); <span class="synPreProc">assert!</span>(result.<span class="synIdentifier">is_success</span>()); <span class="synPreProc">assert_eq!</span>(result.<span class="synIdentifier">success</span>().<span class="synIdentifier">unwrap</span>(), <span class="synType">&amp;</span>input[<span class="synConstant">0</span>]); </pre> <p>文字を解析するだけなら関数より文字自体を指定したほうが楽なので、以下のメソッドも定義します。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/element_parsers.rs#L18"><code>element_parsers.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">ElementParsers</span>: Parsers { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">elm_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I<span class="synStatement">&gt;</span>(element: I) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">elm_pred_ref</span>(<span class="synType">move</span> <span class="synStatement">|</span>actual<span class="synStatement">|</span> <span class="synType">*</span>actual <span class="synStatement">==</span> element) } <span class="synComment">// ...</span> } </pre> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/lib.rs#L336"><code>lib.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">elm_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I<span class="synStatement">&gt;</span>(element: I) <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synPreProc">ParsersImpl</span><span class="synSpecial">::</span><span class="synIdentifier">elm_ref</span>(element) } </pre> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// ...</span> <span class="synStatement">let</span> parser: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">elm_ref</span>(<span class="synConstant">'x'</span>); <span class="synComment">// ...</span> </pre> <p>ちなみに文字と仮定して説明しましたが、要素は<code>I</code>型なので文字(<code>char</code>)以外にバイト(<code>u8</code>)でも扱えます。</p> <h3><code>or</code>パーサー</h3> <p>代数的設計の真骨頂は、このあたりから垣間見えます。</p> <p><code>parser1</code>または<code>parser2</code>の選言を行うパーサーを返す<a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/operator_parsers.rs#L20"><code>or</code></a>関数は以下のようにしました。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/operator_parsers.rs#L20"><code>operator_parsers.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">OperatorParsers</span>: Parsers { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">or</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>(parser1: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, parser2: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synComment">// ...</span> } </pre> <p>これをそのまま使うと以下のようになります。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">or</span>(p1, p2); <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">or</span>(<span class="synIdentifier">or</span>(p4, p5), p3); <span class="synComment">// ...</span> </pre> <p>読み書きしにくい理由は前述したとおりです。これを以下のように変形します。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> p1.<span class="synIdentifier">or</span>(p2); <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> p4.<span class="synIdentifier">or</span>(p5).<span class="synIdentifier">or</span>(p3); <span class="synComment">// ...</span> </pre> <p>絶対こっちのがほうが読みやすいです。</p> <p>さらに演算子を再定義して読みやすくします<a href="#f-4d3a0c49" name="fn-4d3a0c49" title="演算子の再定義がScalaほど柔軟ではないので限界がありますが…">*10</a>。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> p1 <span class="synStatement">|</span> p2; <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, <span class="synType">&amp;char</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> p4 <span class="synStatement">|</span> p5 <span class="synStatement">|</span> p3; <span class="synComment">// ...</span> </pre> <p>第一引数を<code>self</code>に置き換えたトレイトも提供して内部で<code>OperatorParsers#or</code>を呼ぶようにします。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parser/operator_parser.rs#L10"><code>operator_parser.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">OperatorParser</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>: ParserRunner<span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span> { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">or</span>(<span class="synConstant">self</span>, other: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, <span class="synType">Self</span><span class="synSpecial">::</span>Output<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, <span class="synType">Self</span><span class="synSpecial">::</span>Output<span class="synStatement">&gt;</span> <span class="synStatement">where</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output: Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synComment">// ...</span> } </pre> <p>さらにRustの演算子再定義の仕組みを使って<code>OperatorParser#or</code>を呼びようにします。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parser_impl/bitor_impl.rs#L6"><code>bitor_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> BitOr <span class="synStatement">for</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synStatement">type</span> <span class="synIdentifier">Output</span> <span class="synStatement">=</span> <span class="synType">Self</span>; <span class="synStatement">fn</span> <span class="synIdentifier">bitor</span>(<span class="synConstant">self</span>, rhs: Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output { <span class="synConstant">self</span>.<span class="synIdentifier">or</span>(rhs) } } </pre> <h3><code>and_then</code>パーサー</h3> <p><code>parser1</code>の次に<code>parser2</code>という解析を行うパーサーも以下のように定義されます。<code>and_then</code>関数は<code>parser1</code>と<code>parser2</code>の解析結果をタプルで返すパーサーが返します。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parsers/operator_parsers.rs#L24"><code>operator_parsers.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">OperatorParsers</span>: Parsers { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">and_then</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span>(parser1: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, parser2: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, (A, B)<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synComment">// ...</span> } </pre> <p>こちらも<code>or</code>のときと同じです。こんなコードを書かされたら混乱するしかありません…。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, (<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">and_then</span>(p1, p2); <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, ((<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>), <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">and_then</span>(<span class="synIdentifier">and_then</span>(p4, p5), p3); <span class="synComment">// ...</span> </pre> <p>同様に変形します。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, (<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> p1.<span class="synIdentifier">and_then</span>(p2); <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, ((<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>), <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> p4.<span class="synIdentifier">and_then</span>(p5).<span class="synIdentifier">and_then</span>(p3); <span class="synComment">// ...</span> </pre> <p>演算子も再定義します。まぁ<code>+</code>がいいのかどうかはさておき。<code>pom</code>は<code>+</code>だったのでそれに倣いました。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">let</span> p3: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, (<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> p1 <span class="synStatement">+</span> p2; <span class="synStatement">let</span> p6: Parser<span class="synStatement">&lt;</span><span class="synType">char</span>, ((<span class="synType">&amp;char</span>, <span class="synType">&amp;char</span>), <span class="synType">&amp;char</span>)<span class="synStatement">&gt;</span> <span class="synStatement">=</span> p4 <span class="synStatement">+</span> p5 <span class="synStatement">+</span> p3; <span class="synComment">// ...</span> </pre> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/extension/parser/operator_parser.rs#L5"><code>operator_parser.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">OperatorParser</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>: ParserRunner<span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span> { <span class="synComment">// ...</span> <span class="synStatement">fn</span> <span class="synIdentifier">and_then</span><span class="synStatement">&lt;</span>B<span class="synStatement">&gt;</span>(<span class="synConstant">self</span>, other: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, (<span class="synType">Self</span><span class="synSpecial">::</span>Output, B)<span class="synStatement">&gt;</span> <span class="synStatement">where</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synComment">// ...</span> } </pre> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parser_impl/add_impl.rs#L6"><code>add_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span> Add<span class="synStatement">&lt;</span>Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;&gt;</span> <span class="synStatement">for</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synStatement">type</span> <span class="synIdentifier">Output</span> <span class="synStatement">=</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, (A, B)<span class="synStatement">&gt;</span>; <span class="synStatement">fn</span> <span class="synIdentifier">add</span>(<span class="synConstant">self</span>, rhs: Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output { <span class="synConstant">self</span>.<span class="synIdentifier">and_then</span>(rhs) } } </pre> <h3><code>Parser</code>型</h3> <p>パーサーを生成する関数も基本的な例を説明しましたが、そもそも<code>Parser</code>型とは何かという話をしていませんでした。パーサーのインスタンスを表す<a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/core/parser.rs">Parser</a>型は、簡単に言えばクロージャを内包する型です。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/core/parser.rs"><code>parser</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">type</span> <span class="synIdentifier">Parse</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">dyn</span> <span class="synType">Fn</span>(<span class="synType">&amp;</span>ParseState<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> ParseResult<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>; <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Parser</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> { <span class="synStatement">pub</span><span class="synSpecial">(</span><span class="synStatement">crate</span><span class="synSpecial">)</span> method: Rc<span class="synStatement">&lt;</span>Parse<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;&gt;</span>, } </pre> <p><code>Parse</code>型はクロージャ型のエイリアスで、引数に解析中の状態を表す<code>ParseState</code>型ととり、戻り値は解析結果を表す<code>ParseResult</code>型です。<code>Parser</code>型はそれを内部に保持しますが、<code>Rc</code>型でラップします。こうすることで、<code>Parser</code> 型のインスタンスがCloneされても同一インスタンスを参照できます。</p> <p>これをどう実行するかというと、以下のような実装になります。<code>parse</code>もしくは<code>run</code>メソッドを呼びだすと解析処理を行い解析結果を返します。クロージャ型は<code>Fn</code>型なので何度でも解析を実行できる必要があります。なので、クロージャ本体の処理でも所有権を一度消費して2回以上実行できない実装はコンパイルエラーになるので注意が必要です。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parser_impl/parser_runner_impl.rs"><code>parser_runner.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> ParserRunner<span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span> <span class="synStatement">for</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> { <span class="synStatement">type</span> <span class="synIdentifier">Input</span> <span class="synStatement">=</span> I; <span class="synStatement">type</span> <span class="synIdentifier">Output</span> <span class="synStatement">=</span> A; <span class="synStatement">type</span> <span class="synIdentifier">P</span><span class="synStatement">&lt;</span><span class="synSpecial">'m</span>, X, Y<span class="synStatement">&gt;</span> <span class="synStatement">where</span> X: <span class="synSpecial">'m</span>, <span class="synStatement">=</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'m</span>, X, Y<span class="synStatement">&gt;</span>; <span class="synStatement">fn</span> <span class="synIdentifier">parse</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, input: <span class="synType">&amp;</span><span class="synSpecial">'a</span> [<span class="synType">Self</span><span class="synSpecial">::</span>Input]) <span class="synStatement">-&gt;</span> ParseResult<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, <span class="synType">Self</span><span class="synSpecial">::</span>Output<span class="synStatement">&gt;</span> { <span class="synStatement">let</span> parse_state <span class="synStatement">=</span> <span class="synPreProc">ParseState</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(input, <span class="synConstant">0</span>); <span class="synConstant">self</span>.<span class="synIdentifier">run</span>(<span class="synType">&amp;</span>parse_state) } <span class="synStatement">fn</span> <span class="synIdentifier">run</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, param: <span class="synType">&amp;</span>ParseState<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> ParseResult<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">Self</span><span class="synSpecial">::</span>Input, <span class="synType">Self</span><span class="synSpecial">::</span>Output<span class="synStatement">&gt;</span> { (<span class="synConstant">self</span>.method)(param) } } </pre> <h3><code>elm_pred_ref</code>, <code>or</code>, <code>and_then</code>の実装</h3> <p><code>elm_pred_ref</code>, <code>or</code>, <code>and_then</code>の具体的な実装をみていきましょう。</p> <h4><code>elm_pred_ref</code>の実装</h4> <p><code>elm_pred_ref</code>の実装は以下です。</p> <p>入力値は<code>parse_state.input()</code>で配意として取得できます。このパーサーでは配列の先頭から1要素への参照を取得し<code>f</code>に渡します。<code>true</code>の場合はその参照を含む<code>ParseResult</code>を成功として返し、<code>false</code>の場合は<code>ParseResult</code>を失敗として返します。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parsers_impl/element_parsers_impl.rs#L8"><code>element_parsers_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl</span> ElementParsers <span class="synStatement">for</span> ParsersImpl { <span class="synStatement">fn</span> <span class="synIdentifier">elm_pred_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, F<span class="synStatement">&gt;</span>(f: F) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> F: <span class="synType">Fn</span>(<span class="synType">&amp;</span>I) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synPreProc">Parser</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>parse_state<span class="synStatement">|</span> { <span class="synStatement">let</span> input <span class="synStatement">=</span> parse_state.<span class="synIdentifier">input</span>(); <span class="synStatement">if</span> <span class="synStatement">let</span> <span class="synConstant">Some</span>(actual) <span class="synStatement">=</span> input.<span class="synIdentifier">get</span>(<span class="synConstant">0</span>) { <span class="synStatement">if</span> <span class="synIdentifier">f</span>(actual) { <span class="synStatement">return</span> <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span><span class="synIdentifier">successful</span>(actual, <span class="synConstant">1</span>); } } <span class="synStatement">let</span> offset <span class="synStatement">=</span> parse_state.<span class="synIdentifier">next_offset</span>(); <span class="synStatement">let</span> msg <span class="synStatement">=</span> <span class="synPreProc">format!</span>(<span class="synConstant">&quot;offset: {}&quot;</span>, offset); <span class="synStatement">let</span> ps <span class="synStatement">=</span> parse_state.<span class="synIdentifier">add_offset</span>(<span class="synConstant">1</span>); <span class="synStatement">let</span> pe <span class="synStatement">=</span> <span class="synPreProc">ParseError</span><span class="synSpecial">::</span><span class="synIdentifier">of_mismatch</span>(input, ps.<span class="synIdentifier">next_offset</span>(), <span class="synConstant">1</span>, msg); <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span><span class="synIdentifier">failed_with_uncommitted</span>(pe) }) } } </pre> <p><code>elm_pred_ref</code>は他の要素を解析するパーサーの基盤的実装になっています。<code>elm_pred_ref</code>さえ実装すれば他のバリエーションの実装が導出されるような設計になっています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">elm_digit_ref</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I<span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, <span class="synType">&amp;</span><span class="synSpecial">'a</span> I<span class="synStatement">&gt;</span> <span class="synStatement">where</span> I: Element <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">elm_pred_ref</span>(<span class="synPreProc">Element</span><span class="synSpecial">::</span>is_ascii_digit) } </pre> <h4><code>or</code>の実装</h4> <p><code>or</code>の実装は以下です。</p> <p>まず<code>parser1</code>を評価します。その結果が失敗なら<code>parser2</code>を評価しその解析結果を返し、そうでないなら<code>parser1</code>の解析結果を返します。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parsers_impl/operator_parsers_impl.rs#L36"><code>operator_parsers_impl</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">or</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>(parser1: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, parser2: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synSpecial">'a</span>, { <span class="synPreProc">Parser</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>parse_state<span class="synStatement">|</span> { <span class="synStatement">let</span> result <span class="synStatement">=</span> parser1.<span class="synIdentifier">run</span>(parse_state); <span class="synStatement">if</span> <span class="synStatement">let</span> <span class="synConstant">Some</span>(committed_status) <span class="synStatement">=</span> result.<span class="synIdentifier">committed_status</span>() { <span class="synStatement">if</span> committed_status.<span class="synIdentifier">is_uncommitted</span>() { <span class="synStatement">return</span> parser2.<span class="synIdentifier">run</span>(parse_state); } } result }) } </pre> <p>※コミットするかしないかは、<code>p1 | p2 | p3</code>のような複数の選択肢あるときのバックトラックと関係がありますが、本題ではないので説明は割愛します。FP in Scala本に簡単な説明があるので読んでみるとよいかもしません。</p> <h4><code>and_then</code>の実装</h4> <p><code>and_then</code>は以下のように実装されます。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/317e49964860dbdf5a0edc820aa3fd1f2168d298/parser/src/internal/parsers_impl/operator_parsers_impl.rs#L50"><code>operator_parsers_impl</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">and_then</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span>(parser1: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, parser2: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, (A, B)<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">flat_map</span>(parser1, <span class="synType">move</span> <span class="synStatement">|</span>a<span class="synStatement">|</span> <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">map</span>(parser2.<span class="synIdentifier">clone</span>(), <span class="synType">move</span> <span class="synStatement">|</span>b<span class="synStatement">|</span> (a.<span class="synIdentifier">clone</span>(), b))) } </pre> <p>二つのパーサーの処理結果をタプルで返すパーサーを作って返しているだけですが、<code>Self::flat_map</code>, <code>Self::map</code>について詳しく解説します。</p> <h3><code>flat_map</code>, <code>map</code>, <code>successful</code>, <code>failed</code> の実装</h3> <h4><code>ParsersImpl#flat_map</code></h4> <p><code>flat_map</code>はパーサーどうしの計算を結合するために使われるコンビネータです<a href="#f-a41f5fa8" name="fn-a41f5fa8" title="モナドの説明はしません…">*11</a>。その<code>flat_map</code>では<code>parser.run(&amp;parse_state)</code>によってパーサーの評価を行います。その結果が成功ならクロージャ<code>f</code>に結果を渡し、<code>f</code>が返すパーサーを実行し、その結果が失敗なら何もせずに失敗を返します。いずれの結果であっても、パーサー型が返ります。</p> <p>このコンビネータは、あるパーサーの解析結果を、別のパーサーの解析へ繋ぐ場合に使えます。<strong><code>flat_map</code>は代数的設計の中核を担うコンビネータです。</strong> 言いたいことの9割はこれです。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/0e9323a38f1a9445e61bf998ce3cc65925047d6a/parser/src/internal/parsers_impl.rs#L91"><code>parsers_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">fn</span> <span class="synIdentifier">flat_map</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B, F<span class="synStatement">&gt;</span>(parser: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, f: F) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> F: <span class="synType">Fn</span>(A) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, A: <span class="synSpecial">'a</span>, B: <span class="synSpecial">'a</span>, { <span class="synPreProc">Parser</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>parse_state<span class="synStatement">|</span> <span class="synStatement">match</span> parser.<span class="synIdentifier">run</span>(<span class="synType">&amp;</span>parse_state) { <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span>Success { value: a, length: n } <span class="synStatement">=&gt;</span> { <span class="synStatement">let</span> ps <span class="synStatement">=</span> parse_state.<span class="synIdentifier">add_offset</span>(n); <span class="synIdentifier">f</span>(a).<span class="synIdentifier">run</span>(<span class="synType">&amp;</span>ps).<span class="synIdentifier">with_committed_fallback</span>(n <span class="synStatement">!=</span> <span class="synConstant">0</span>).<span class="synIdentifier">with_add_length</span>(n) } <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span>Failure { error, committed_status: is_committed, } <span class="synStatement">=&gt;</span> <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span><span class="synIdentifier">failed</span>(error, is_committed), }) } </pre> <p>このコンビネータも、<code>parser1.flat_map(|result| ...)</code>のように記述できます。</p> <h4><code>ParsersImpl#map</code></h4> <p><code>map</code>はパーサーが解析した成功の結果を別の値に変換するコンビネータです。<code>map</code>は<code>flat_map</code>と<code>successful</code>を使って導出できます。<code>successful</code>は指定した値を返すパーサーを返す関数です。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/0e9323a38f1a9445e61bf998ce3cc65925047d6a/parser/src/internal/parsers_impl.rs#L108"><code>parsers_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">map</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B, F<span class="synStatement">&gt;</span>(parser: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, f: F) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> F: <span class="synType">Fn</span>(A) <span class="synStatement">-&gt;</span> B <span class="synStatement">+</span> <span class="synSpecial">'a</span>, A: <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">flat_map</span>(parser, <span class="synType">move</span> <span class="synStatement">|</span>e<span class="synStatement">|</span> <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">successful</span>(<span class="synIdentifier">f</span>(e))) } } </pre> <p>このコンビネータも、<code>parser1.map(|result| ...)</code>のように記述できます。</p> <h4><code>ParsersImpl#successful</code></h4> <p><code>ParsersImpl#map</code>で利用した、<code>ParsersImpl#successful</code>は、指定された値を返すパーサーを生成します。このパーサーはどんな入力であっても必ず成功するパーサーです。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/0e9323a38f1a9445e61bf998ce3cc65925047d6a/parser/src/internal/parsers_impl.rs#L35"><code>parsers_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">successful</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>(value: A) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synPreProc">Parser</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>_<span class="synStatement">|</span> <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span><span class="synIdentifier">successful</span>(value.<span class="synIdentifier">clone</span>(), <span class="synConstant">0</span>)) } </pre> <h4><code>ParsersImpl#failed</code></h4> <p><code>ParsersImpl#successful</code>より出番は少ないかもしれませんが、<code>ParsersImpl#failed</code>は指定されたエラーを返すパーサーを生成します。このパーサーはどんな入力であっても必ず失敗するパーサーです。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/0e9323a38f1a9445e61bf998ce3cc65925047d6a/parser/src/internal/parsers_impl.rs#L48"><code>parsers_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">failed</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>(value: ParseError<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I<span class="synStatement">&gt;</span>, committed: CommittedStatus) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> I: <span class="synType">Clone</span> <span class="synStatement">+</span> <span class="synSpecial">'a</span>, A: <span class="synSpecial">'a</span>, { <span class="synPreProc">Parser</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synType">move</span> <span class="synStatement">|</span>_<span class="synStatement">|</span> <span class="synPreProc">ParseResult</span><span class="synSpecial">::</span><span class="synIdentifier">failed</span>(value.<span class="synIdentifier">clone</span>(), committed.<span class="synIdentifier">clone</span>())) } </pre> <h4><code>SkipParsers#skip_left</code>, <code>skip_right</code>, <code>surround</code></h4> <p>実は、基本的なパーサーの合成は 前述のコンビネータがあれば実現できます。</p> <p>たとえば、<code>p1 + p2</code>のように<code>and_then</code>で繋がった二つのパーサーのうち、いずれかのパーサーの解析結果を捨てる実装は以下のようになります。<code>skip_left</code>は左のパーサーの解析結果を破棄し、<code>skip_right</code>は右のパーサーの解析結果を破棄します。<code>and_then</code>はタプルで解析結果を返しますが、<code>skip_left</code>, <code>skip_right</code>では単一の解析結果を返します。さらに、<code>p1 + p2 + p3</code>で<code>p2</code>の結果だけを返すパーサーは<code>surround</code>です。<code>skip_left</code>, <code>skip_right</code>の二つのコンビネータを使って簡単に実装できます。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/4809ef8ac55d2ee6ee41feb16117421f085ed6a6/parser/src/extension/parsers/skip_parsers.rs"><code>skip_parsers.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink> <span class="synStatement">fn</span> <span class="synIdentifier">skip_left</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span>(pa: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, pb: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">map</span>(<span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">and_then</span>(pa, pb), <span class="synStatement">|</span>(_, b)<span class="synStatement">|</span> b) } <span class="synStatement">fn</span> <span class="synIdentifier">skip_right</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span>(pa: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, pb: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">map</span>(<span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">and_then</span>(pa, pb), <span class="synStatement">|</span>(a, _)<span class="synStatement">|</span> a) } <span class="synStatement">fn</span> <span class="synIdentifier">surround</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A, B, C<span class="synStatement">&gt;</span>( left_parser: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span>, parser: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>, right_parser: <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, C<span class="synStatement">&gt;</span>, ) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>P<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, C: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">skip_left</span>(left_parser, <span class="synType">Self</span><span class="synSpecial">::</span><span class="synIdentifier">skip_right</span>(parser, right_parser)) } </pre> <p><code>or</code>, <code>and_then</code>同様に<code>skip_left</code>, <code>skip_righ</code>は演算子を再定義しているので、<code>surround(p1, p2, p3)</code>相当のことは<code>p1 * p2 - p3</code>と記述できます。</p> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/4809ef8ac55d2ee6ee41feb16117421f085ed6a6/parser/src/internal/parser_impl/mul_parser_impl.rs"><code>mul_parser_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span> Mul<span class="synStatement">&lt;</span>Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;&gt;</span> <span class="synStatement">for</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synStatement">type</span> <span class="synIdentifier">Output</span> <span class="synStatement">=</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>; <span class="synStatement">fn</span> <span class="synIdentifier">mul</span>(<span class="synConstant">self</span>, rhs: Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output { <span class="synConstant">self</span>.<span class="synIdentifier">skip_left</span>(rhs) } } </pre> <p><a href="https://github.com/j5ik2o/oni-comb-rs/blob/4809ef8ac55d2ee6ee41feb16117421f085ed6a6/parser/src/internal/parser_impl/sub_parser_impl.rs"><code>sub_parser_impl.rs</code></a></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl&lt;</span><span class="synSpecial">'a</span>, I, A, B<span class="synStatement">&gt;</span> Sub<span class="synStatement">&lt;</span>Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;&gt;</span> <span class="synStatement">for</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, A<span class="synStatement">&gt;</span> <span class="synStatement">where</span> A: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, B: <span class="synType">Clone</span> <span class="synStatement">+</span> Debug <span class="synStatement">+</span> <span class="synSpecial">'a</span>, { <span class="synStatement">type</span> <span class="synIdentifier">Output</span> <span class="synStatement">=</span> <span class="synType">Self</span>; <span class="synStatement">fn</span> <span class="synIdentifier">sub</span>(<span class="synConstant">self</span>, rhs: Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, I, B<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span><span class="synSpecial">::</span>Output { <span class="synConstant">self</span>.<span class="synIdentifier">skip_right</span>(rhs) } } </pre> <h3>URIのパスを解析するパーサー</h3> <p>実装例としてURIのパスを解析するパーサーを紹介します。</p> <p>URIのパスはほぼBNFに似た形でコードが書けます<a href="#f-70a25dc3" name="fn-70a25dc3" title="RFCの構文規則は結構あいまいに書かれているし、BNFよりPEGに書き直してから実装したほうがいいかもしれません">*12</a>。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// path = path-abempty ; begins with &quot;/&quot; or is empty</span> <span class="synComment">// / path-absolute ; begins with &quot;/&quot; but not &quot;//&quot;</span> <span class="synComment">// / path-noscheme ; begins with a non-colon segment</span> <span class="synComment">// / path-rootless ; begins with a segment</span> <span class="synComment">// / path-empty ; zero characters</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">path</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>() <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">char</span>, <span class="synType">Option</span><span class="synStatement">&lt;</span>Path<span class="synStatement">&gt;&gt;</span> { (<span class="synIdentifier">path_rootless</span>().<span class="synIdentifier">attempt</span>() <span class="synStatement">|</span> <span class="synIdentifier">path_abempty</span>(<span class="synConstant">true</span>).<span class="synIdentifier">attempt</span>() <span class="synStatement">|</span> <span class="synIdentifier">path_absolute</span>().<span class="synIdentifier">attempt</span>() <span class="synStatement">|</span> <span class="synIdentifier">path_noscheme</span>()) .<span class="synIdentifier">opt</span>() .<span class="synIdentifier">name</span>(<span class="synConstant">&quot;path&quot;</span>) } </pre> <p>このパーサーを構成する<code>path_abempty</code>も内部実装はファクトリやコンビネータを組み合わせます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// path-abempty = *( &quot;/&quot; segment )</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">path_abempty</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span><span class="synStatement">&gt;</span>(required: <span class="synType">bool</span>) <span class="synStatement">-&gt;</span> Parser<span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, <span class="synType">char</span>, Path<span class="synStatement">&gt;</span> { <span class="synStatement">let</span> n <span class="synStatement">=</span> <span class="synStatement">if</span> required { <span class="synConstant">1</span> } <span class="synStatement">else</span> { <span class="synConstant">0</span> }; ((<span class="synIdentifier">elm</span>(<span class="synConstant">'/'</span>) <span class="synStatement">+</span> <span class="synIdentifier">segment</span>()).<span class="synIdentifier">collect</span>()) .<span class="synIdentifier">map</span>(<span class="synType">String</span><span class="synSpecial">::</span>from_iter) .<span class="synIdentifier">repeat</span>(n..) .<span class="synIdentifier">map</span>(<span class="synStatement">|</span>e<span class="synStatement">|</span> <span class="synPreProc">Path</span><span class="synSpecial">::</span><span class="synIdentifier">of_abempty_from_strings</span>(<span class="synType">&amp;</span>e)) .<span class="synIdentifier">name</span>(<span class="synConstant">&quot;path-abempty&quot;</span>) } </pre> <p><code>(elm('/') + segment()).collect()</code>は、<code>collect()</code>はマッチすると入力の文字列への参照(&amp;str)をそのまま返します。ゼロコピーの解析処理を記述できます。<code>&amp;str</code>ままだと扱いづらいので<code>.map(String::from_iter)</code>で<code>String</code>に変換します。このパターンは複数回出現する想定なので、続く<code>repeat(n..)</code>でコレクションに変換します。</p> <h2>まとめ</h2> <p><code>Parser</code>型の設計は、まさに「小さいプログラムも大規模なプログラムも同じ概念で記述できるべきである」を表現しています。一つのことをうまくやる小さなパーサーどうしをコンビネータを使って合成することで、より複雑な問題を解決するパーサーを簡単に実装できる。これが代数的設計によってスケーラブルなプログラミングを実現する方法です。</p> <p>最後に、FP in Scala本以外で代数的設計を学べる本を以下に紹介しておきます。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/1617292249/j5ik2o.me-22/" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51e1HjMZ-RL._SL500_.jpg" class="hatena-asin-detail-image" alt="Functional and Reactive Domain Modeling" title="Functional and Reactive Domain Modeling"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/1617292249/j5ik2o.me-22/" target="_blank" rel="noopener">Functional and Reactive Domain Modeling</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Ghosh%2C%20Debasish" class="keyword">Ghosh, Debasish</a></li><li>Manning Publications</li></ul><a href="https://www.amazon.co.jp/exec/obidos/ASIN/1617292249/j5ik2o.me-22/" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>長々とお付き合いいただきありがとうございました。<code>flat_map</code>は偉大!</p> <div class="footnote"> <p class="footnote"><a href="#fn-c17b088c" name="f-c17b088c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">というか、オブジェクト指向言語や関数型言語の境界線が溶け込んで曖昧になってきているように思えます</span></p> <p class="footnote"><a href="#fn-b0f8c59c" name="f-b0f8c59c" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">丁寧に書くと大作になる。ボリュームが多いと読者は意図をくみとるのに失敗する確率が高くなる、ので難しい…。</span></p> <p class="footnote"><a href="#fn-fd00c03e" name="f-fd00c03e" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">このネットスラングを知らない方はこちらをご覧ください→<a href="https://togetter.com/li/1783989">https://togetter.com/li/1783989</a></span></p> <p class="footnote"><a href="#fn-a1c0a2fb" name="f-a1c0a2fb" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">高階関数のことです</span></p> <p class="footnote"><a href="#fn-1834878f" name="f-1834878f" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">さらにパーサーはパーサーを組み合わせることで実装可能なので下位層のWhatは上位層のHowになっていきます。下位層には、手続き型のコードはつきものですが、こういった全体の構造からみると一部になります。</span></p> <p class="footnote"><a href="#fn-d1617f78" name="f-d1617f78" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">実は<a href="https://enear.github.io/2016/03/31/parser-combinators">Scala Parser Combinators</a>も同じようなインターフェイスになっています。</span></p> <p class="footnote"><a href="#fn-9f321737" name="f-9f321737" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">空白の読み飛ばしをしないと実用的ではないですがここでは割愛</span></p> <p class="footnote"><a href="#fn-c95ea837" name="f-c95ea837" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">ちなみにこの程度ならpomでも同様に実装できます</span></p> <p class="footnote"><a href="#fn-451e29b9" name="f-451e29b9" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">文字をわざわざ要素と抽象的な書き方をしたのは、このパーサーが文字以外も解析できるような設計になっているからです。ある規則に沿ったバイト列上の要素であっても解析できます</span></p> <p class="footnote"><a href="#fn-4d3a0c49" name="f-4d3a0c49" class="footnote-number">*10</a><span class="footnote-delimiter">:</span><span class="footnote-text">演算子の再定義がScalaほど柔軟ではないので限界がありますが…</span></p> <p class="footnote"><a href="#fn-a41f5fa8" name="f-a41f5fa8" class="footnote-number">*11</a><span class="footnote-delimiter">:</span><span class="footnote-text">モナドの説明はしません…</span></p> <p class="footnote"><a href="#fn-70a25dc3" name="f-70a25dc3" class="footnote-number">*12</a><span class="footnote-delimiter">:</span><span class="footnote-text">RFCの構文規則は結構あいまいに書かれているし、BNFよりPEGに書き直してから実装したほうがいいかもしれません</span></p> </div> j5ik2o Rustで真に安全なプログラムを書く方法 hatenablog://entry/13574176438039529261 2021-12-08T00:00:00+09:00 2021-12-08T00:00:16+09:00 この記事はRust Advent Calendar 2021の12/8日の記事です。 Rust前提の記事として書きましたが、他の言語にも適用できる考え方なので、ほかの言語勢の方々もよければお付き合い下さい。 今回のテーマは「Rustで真に安全なプログラムを書く方法」についてです。 「真に安全なプログラム」の定義は以下とします。 挙動が安定し、結果が予測可能となる 正しさの基準に基づき、プログラムの間違いを検知することができる 「真に」とはドメイン知識に基づく正しさという意味です。詳しくは後述します。 それと「そもそもRustで実装されるプログラムは安全じゃないのか」という想定質問については「メ… <p>この記事は<a href="https://qiita.com/advent-calendar/2021/rust">Rust Advent Calendar 2021</a>の12/8日の記事です。</p> <p>Rust前提の記事として書きましたが、他の言語にも適用できる考え方なので、ほかの言語勢の方々もよければお付き合い下さい。</p> <p>今回のテーマは「Rustで真に安全なプログラムを書く方法」についてです。</p> <p>「真に安全なプログラム」の定義は以下とします。</p> <ul> <li>挙動が安定し、結果が予測可能となる</li> <li>正しさの基準に基づき、プログラムの間違いを検知することができる</li> </ul> <p>「真に」とはドメイン知識に基づく正しさという意味です。詳しくは後述します。</p> <p>それと「そもそもRustで実装されるプログラムは安全じゃないのか」という想定質問については「メモリの操作は安全。だが、それだけでは真に安全なプログラムにはならない」が答えになります。これについて興味がある方、ぜひ最後までお付き合いください。</p> <p>「真に安全なプログラム」を実現するレシピとしては「関数型プログラミング」「ドメイン・プリミティブ」「契約による設計」あたりを使ってみようと思います。</p> <h2>関数型はプログラミングスタイル</h2> <p>先日、関数型は不変が基本のプログラミングスタイルだという記事を書きました <a href="#f-e6d9e0c6" name="fn-e6d9e0c6" title="オブジェクト指向もプログラミングスタイルです">*1</a>。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fj5ik2o%2Farticles%2F97b458eff8283c783d9b" title="関数型はプログラミングスタイル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/j5ik2o/articles/97b458eff8283c783d9b">zenn.dev</a></cite></p> <p>プログラムが数学的特性を帯びることで、以下のような設計のよい効果が得られます。最初の項目は「真に安全なプログラム」の条件に含めています<a href="#f-acc6f98e" name="fn-acc6f98e" title="2も入るかなとは思いますが、改修の方法や手順に依存するかもしれないので今回は条件に含めず">*2</a>。</p> <ol> <li>挙動が安定し、結果が予測可能となる</li> <li>プログラムを改修した際、不具合を起こしにくい</li> <li>副作用が分離されているため、テストがしやすい</li> <li>宣言的で意図が理解しやすい</li> </ol> <p>この記事ではRustのコード例も記載していますが、Rustでも関数型プログラミングは可能なので同様の効果が見込めます。</p> <h2>ドメイン・プリミティブ(ドメイン固有型)</h2> <p>ドメイン固有型は以下の記事に詳しくまとめています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fj5ik2o%2Fitems%2F2ce5c1dafd2213ef4911" title="ドメインロジックはドメインオブジェクトに凝集させる - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/j5ik2o/items/2ce5c1dafd2213ef4911">qiita.com</a></cite></p> <p>関心の対象にあわせた独自の型は、一般的に「ドメイン固有型」や「ドメイン特化型」と呼ばれることがあります。</p> <p>「それがそんなに有用なの?」という疑問には、この探査機の教訓が答えてくれます。</p> <blockquote><p>1999年9月23日、火星探査機「マーズ・クライメイト・オーピター(MCO)」は火星を周回する軌道への突入に失敗し、燃え尽きました。3億2,730万ドルが失われた原因はソフトウェアのエラーでした。そのエラーは、具体的には「単位の混在」でした。同じ数値の単位を、地上のソフトウェアではポンドとしていたのに対し、宇宙船ではニュートンとしていたのです。その結果地上では、宇宙船のスラスタ推力を実際の約4.45分の1とみなしてしまうことになりました。</p></blockquote> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4873114799?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/511RPej0BNL._SL500_.jpg" class="hatena-asin-detail-image" alt="プログラマが知るべき97のこと" title="プログラマが知るべき97のこと"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4873114799?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">プログラマが知るべき97のこと</a></p><ul class="hatena-asin-detail-meta"><li>オライリージャパン</li></ul><a href="https://www.amazon.co.jp/dp/4873114799?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>単位の扱いを間違えないように、プリミティブ型ではなくドメイン固有型を使えば、探査機は燃え尽きることはなかったのではという話です。ドメイン固有型は、間違いを防止するだけでなく、可読性やテスタビリティが向上するという効能もあります。</p> <h3>なぜセキュア・バイ・デザインなのか</h3> <p>最近発売された『セキュア・バイ・デザイン』という書籍でも、ドメイン固有型が「ドメイン・プリミティブ」として取り上げられています。セキュリティを担保する強力なツールとして紹介されています(この記事でもドメイン固有型ではなく「ドメイン・プリミティブ」として表記を統一します)。和訳本が出てから、読書会が各所で立ち上がっているようで盛況ですね。</p> <p>ということで、少し本題から逸れますが、『セキュア・バイ・デザイン』の話にお付き合いください。</p> <p>僕も既にこの本を読む前からドメイン・プリミティブを採用して、脆弱性診断でも脆弱性0件を経験したことがあります。自慢?自慢ですね…。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/512NUC5YzPL._SL500_.jpg" class="hatena-asin-detail-image" alt="セキュア・バイ・デザイン" title="セキュア・バイ・デザイン"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">セキュア・バイ・デザイン</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Dan%20Bergh%20Johnsson" class="keyword">Dan Bergh Johnsson</a>,<a href="http://d.hatena.ne.jp/keyword/Daniel%20Deogun" class="keyword">Daniel Deogun</a>,<a href="http://d.hatena.ne.jp/keyword/Daniel%20Sawano" class="keyword">Daniel Sawano</a></li><li>マイナビ出版</li></ul><a href="https://www.amazon.co.jp/dp/B09F697K2V?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>さて、なぜ『セキュア・バイ・デザイン』がそこまで注目されるか。<strong>「ドメイン駆動設計」がセキュリティ対策にも大きなインパクトを与えるからです。</strong></p> <p>というのは、XSSやインジェクションなどの既知の攻撃を対策するだけがセキュリティ対策ではないからなんです。たとえば、オンライン書店で、既知の攻撃対策は完ぺきでしたが、-1冊の誤注文を受理し請求処理に失敗し返金処理まで至ったという実際の事故例が書籍中で紹介されています。このような問題は既知の攻撃対策では防げません。問題が対象ドメインのルールに依存するので、ドメインモデルに手を入れないと対策できません。</p> <p>『セキュア・バイ・デザイン』では、ドメイン知識による正しさから逸れた際に、プログラムを安全に停止させるという考え方があります <a href="#f-3b729d56" name="fn-3b729d56" title="これは代表的なパターン">*3</a>。これは後で説明しますが、「契約による設計」に基づいています。<strong>これを実現するには、ドメイン知識を設計に反映する必要があります。そう「ドメイン駆動設計」です。セキュリティの問題を解決したければ(セキュリティだけに注目するのではなく)ドメインにフォーカスしろというわけです<a href="#f-73fa6920" name="fn-73fa6920" title="『セキュア・バイ・デザイン』が既知のセキュリティ対策を軽視しているわけではありません">*4</a>。つまり「真に安全なプログラム」の「真に」とはドメイン知識に基づく正しさという意味になります。</strong></p> <p>というわけで、セキュリティ対策を口実にドメイン駆動設計をやれますね!(えっ手段の目的化…</p> <p>こういう話を聞くと「Rustを使ったら安全になるんじゃないの?」という疑問を持つかもしれませんが、メモリに関する操作は安全になりますが、そもそもビジネスルール上安全かは別問題です。明らかにレイヤーが違います。とはいえ、どちらの考え方も有用です。安全性は多層的に考えるべきなので、Rustのメモリ安全性に加えて『セキュア・バイ・デザイン』を適用すれば、真に安全なプログラムへ近づくのではないかと思います。</p> <p>攻撃を受けないスタンドアローンなシステム、それこそ探査機のシステムであっても<a href="#f-7ba39383" name="fn-7ba39383" title="探査機は地球と交信するので非スタンドアローンのでは…">*5</a>、自らのシステムが攻撃者になる可能性もゼロではありません。そんな自己矛盾が発生したときどう対処するのかも、『セキュア・バイ・デザイン』からヒントを得られるでしょう。</p> <h2>論よりコード</h2> <p>前置きが長くなりましたが、「関数型プログラミング」と「契約による設計」の観点を取り入れて、設計を考えてみたいと思います。何か題材が必要だと思うので『セキュア・バイ・デザイン』の例をRustで書いてみようと思います。</p> <h3>題材は子猫名リストオブジェクト</h3> <p>『セキュア・バイ・デザイン』の第4章に登場する、子猫の名前を集合として管理するオブジェクトの例です。本書内のコードはJavaですが、ざっとRustで書き直すと以下のような形になります。実はこれもドメイン・プリミティブです。<code>Vec&lt;String&gt;</code>型をそのまま利用せずに、有用なメソッドを介して利用する想定です。ただこのままだと問題が多そうです…。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CatNameList</span> { cat_names: <span class="synType">Vec</span><span class="synStatement">&lt;</span><span class="synType">String</span><span class="synStatement">&gt;</span>, } <span class="synStatement">impl</span> CatNameList { <span class="synSpecial">/// コンストラクタ相当</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">new</span>() <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synType">Self</span> { cat_names: <span class="synType">Vec</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>() } } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 子猫の名前には「s」が含まれていること</span> <span class="synSpecial">/// - まだ登録されていない名前であること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">queue_cat_name</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, name: <span class="synType">&amp;str</span>) { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">push</span>(name.<span class="synIdentifier">to_string</span>()); } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 候補となる子猫の名前が登録されていること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">next_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Option</span><span class="synStatement">&lt;</span><span class="synType">&amp;String</span><span class="synStatement">&gt;</span> { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">get</span>(<span class="synConstant">0</span>) } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 候補となる子猫の名前が登録されていること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">dequeue_cat_name</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>) { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">remove</span>(<span class="synConstant">0</span>); } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">size</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">usize</span> { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">len</span>() } } </pre> <h3>現状の何が問題なのか?</h3> <p>守るべき事前条件はコメントに記載しましたが、お気づきのとおり、この実装は契約を守っていません…。</p> <p>『オブジェクト指向入門 第2版 原則・コンセプト』の「契約による設計」では、欠陥(バグを含む)と判断する正しさとして、以下のような考え方が示されています。これもすごい鈍器本ですが、興味があればぜひ手にとってみてほしい…。</p> <ul> <li>正しさとは相対的な概念である</li> <li>ソフトウェア要素そのものが正しいかどうかではなく、対応する仕様と一致するかを論ずるべき(ソフトウェア要素と仕様のペアで考える)</li> <li>正しさを評価するには、 顧客(クライアント)と提供者(サービス)間で取り決める「契約」を定義し、その「契約」に沿っているか判断する</li> <li>「契約」に沿っているかは「表明」を使った仕様の記述方法を利用するのが一般的</li> </ul> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/4798111112?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/41A2yC7UpOL._SL500_.jpg" class="hatena-asin-detail-image" alt="オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)" title="オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/4798111112?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%D0%A1%BC%A5%C8%A5%E9%A5%F3%A5%C9%A1%A6%A5%E1%A5%A4%A5%E4%A1%BC" class="keyword">バートランド・メイヤー</a></li><li>翔泳社</li></ul><a href="https://www.amazon.co.jp/dp/4798111112?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>コード例で示した、守るべき事前条件は「契約」の一種です。</p> <p>ところで、このコードをみてどんな感想を持ちましたか?「えっ気にしたことがなかった」「バリデーションしてるから大丈夫」「テストしてるから大丈夫」とかでしょうか。本当にそう思いますか?</p> <p>例えば、プログラマーが正しくないロジックを書いた場合、プログラムは契約を違反したまま実行します。契約は守るべきルールですが、それが守られないまま実行します。つまり期待した挙動にはなりません。意図した振る舞いから逸れることになるので「欠陥」と言えます。バグもこの概念に含まれます。欠陥の詳細については以下の記事を見てもらうとよいと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fj5ik2o%2Farticles%2F6c4dbab802c9701fd878" title="Error, Defect, Fault, Failureの定義と解釈" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/j5ik2o/articles/6c4dbab802c9701fd878">zenn.dev</a></cite></p> <p>エラーはリカバリ可能ですが、欠陥は「通常ありえない状況」で、そのまま続行してもよいことはありませんし、リカバリするものではありません。『達人プログラマー ―熟達に向けたあなたの旅― 第2版』でもトラッシュよりクラッシュすべきと言われています。RustやGoではパニックさせることが多いでしょう。</p> <blockquote><p>「できるだけ早期に問題を検出すれば、早めにクラッシュ(停止)に持っていけるというメリットが出てきます。そして多くの場合、プログラムのクラッシュは最も正しい行いとなるのです。クラッシュさせずに放っておくのは、壊れたデータが重要なデータベースに格納されるのを指をくわえて見ていたり、衣類を 20回ほど連続して洗濯機にかけたりするのと同じことです。」</p></blockquote> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/dp/B08T9BXSVD?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="hatena-asin-detail-image-link" target="_blank" rel="noopener"><img src="https://m.media-amazon.com/images/I/51W3GJV1X-L._SL500_.jpg" class="hatena-asin-detail-image" alt="達人プログラマー ―熟達に向けたあなたの旅― 第2版" title="達人プログラマー ―熟達に向けたあなたの旅― 第2版"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/dp/B08T9BXSVD?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" target="_blank" rel="noopener">達人プログラマー ―熟達に向けたあなたの旅― 第2版</a></p><ul class="hatena-asin-detail-meta"><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/David%20Thomas" class="keyword">David Thomas</a>,<a href="http://d.hatena.ne.jp/keyword/Andrew%20Hunt" class="keyword">Andrew Hunt</a></li><li>オーム社</li></ul><a href="https://www.amazon.co.jp/dp/B08T9BXSVD?tag=j5ik2o.me-22&amp;linkCode=ogi&amp;th=1&amp;psc=1" class="asin-detail-buy" target="_blank" rel="noopener">Amazon</a></div></div></p> <p>あり得ない状況でも無理やり実行するコードを書いたことがありますが、障害の解析に凄く戸惑ってしまって無駄な時間を溶かした経験があります…。</p> <p>さらに、このような状況を放置するとよくないです。クライアント(呼出し元)がこのような不完全なモジュールを使うと、契約ではなく現状のサービス(呼出し先)の実装に依存します。そして、間違いに気づいて実装を修正しても、手遅れです。クライアント側が間違いに依存しているので壊れてしまう可能性があります。</p> <p>こういう話をすると、PHPのmt_rand()関数がすぐに修正できなかったことを思い出しますね。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Frsky%2Fitems%2F02aa42f81b2700ce9d74" title="なぜmt_rand()の誤った実装をサクッと修正できないのか - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/rsky/items/02aa42f81b2700ce9d74">qiita.com</a></cite></p> <p>本来であればクライアントも修正すべきですが、修正規模が大きすぎると難しいかもしれません。仕様に照らして明らかに間違っているのにそれを正せない。いつのまにかそれが正しいと言わざるを得ない状況になり、何が正しいか曖昧な状況になりかねません…。</p> <p>このような事態を避けるには、少なくとも開発者がドメイン知識に関心を持たないと難しいと思います…。</p> <h3>表明(値による表明)を導入する</h3> <p>ということで、事前条件を表明する実装に変更してみましょう。以下のように実装してみました<a href="#f-6602b57e" name="fn-6602b57e" title="外部クレートとして遅延初期化のためにonce_cell, 正規表現のためにregexを利用しています">*6</a>。事前条件から逸れたときは正しくない状況なので<code>panic!</code>するようにしました。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">use</span> <span class="synPreProc">once_cell</span><span class="synSpecial">::</span><span class="synPreProc">sync</span><span class="synSpecial">::</span>Lazy; <span class="synStatement">use</span> <span class="synPreProc">regex</span><span class="synSpecial">::</span>Regex; <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CatNameList</span> { cat_names: <span class="synType">Vec</span><span class="synStatement">&lt;</span><span class="synType">String</span><span class="synStatement">&gt;</span>, } <span class="synComment">// 正規表現型(Regex)のインスタンス</span> <span class="synType">static</span> REGEX: Lazy<span class="synStatement">&lt;</span>Regex<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synPreProc">Lazy</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synStatement">||</span> <span class="synPreProc">Regex</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synConstant">r&quot;.*s.*&quot;</span>).<span class="synIdentifier">unwrap</span>()); <span class="synStatement">impl</span> CatNameList { <span class="synSpecial">/// コンストラクタ相当</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">new</span>() <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synType">Self</span> { cat_names: <span class="synType">Vec</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>() } } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 子猫の名前には「s」が含まれていること</span> <span class="synSpecial">/// - まだ登録されていない名前であること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">queue_cat_name</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, name: <span class="synType">&amp;str</span>) { <span class="synStatement">if</span> <span class="synStatement">!</span>REGEX.<span class="synIdentifier">is_match</span>(name) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Must contain s&quot;</span>); } <span class="synStatement">if</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">contains</span>(<span class="synType">&amp;</span>name.<span class="synIdentifier">to_string</span>()) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Already queued&quot;</span>); } <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">push</span>(name.<span class="synIdentifier">to_string</span>()); } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 候補となる子猫の名前が登録されていること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">next_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Option</span><span class="synStatement">&lt;</span><span class="synType">&amp;String</span><span class="synStatement">&gt;</span> { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">get</span>(<span class="synConstant">0</span>) } <span class="synSpecial">/// - 事前条件</span> <span class="synSpecial">/// - 候補となる子猫の名前が登録されていること</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">dequeue_cat_name</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>) { <span class="synStatement">if</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">is_empty</span>() { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Must non empty&quot;</span>); } <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">remove</span>(<span class="synConstant">0</span>); } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">size</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">usize</span> { <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">len</span>() } } <span class="synPreProc">#[test]</span> <span class="synStatement">fn</span> <span class="synIdentifier">example</span>() { <span class="synStatement">let</span> <span class="synType">mut</span> cat_names <span class="synStatement">=</span> <span class="synPreProc">CatNameList</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(); cat_names.<span class="synIdentifier">queue_cat_name</span>(<span class="synConstant">&quot;cats&quot;</span>); <span class="synStatement">let</span> cat_name <span class="synStatement">=</span> cat_names.<span class="synIdentifier">next_cat_name</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_name); cat_names.<span class="synIdentifier">dequeue_cat_name</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_names); } </pre> <p>事前条件を表明するのは、<code>queue_cat_name</code> と <code>dequeue_cat_name</code> だけになりました。事前条件から逸れる=欠陥(バグを含む)です。その場合リカバリしても意味がありませんので、<code>panic!</code>でプログラムは早期に中断します。ここでは変数の値が正しいか判断するので「<strong>値による表明</strong>」と呼ぶことにします。</p> <p>この二つのメソッドに、意図した値による表明が組み込まれました。<code>next_cat_name</code>は要素サイズが0個であれば<code>None</code>を返し、要素が1個以上であれば<code>Some</code>を返すので表明を不要としました。使用例は<code>example</code>を参照してください。</p> <p>本題に関係ないところではありますが、内部データはVecを使っていますが、キューのような振る舞いであればVecDequeを使うほうがよいかもしれません。</p> <h3>可変と不変の使い分け</h3> <p><code>queue_cat_name</code>, <code>dequeue_cat_name</code>の二つのメソッドを可変ではなく不変にしたほうがいいのではという指摘もあると思います。</p> <p>Rust以外の言語では、可変と不変を別々の型で設計することが多いでしょう。例えばJavaの<code>String</code>と<code>StringBuilder</code>です。Rustでは、型(struct)が不変や可変を決めるのではなく、メソッド(関数)が決めます<a href="#f-6bb46780" name="fn-6bb46780" title="関数に記述されるselfの借用記述で決定します">*7</a>。可変か不変で型(struct)を別々に定義する必要はありません。問題はメソッドを、可変か不変のどちらにするか、という論点になります。</p> <p>インスタンスを定義するとき、<code>let</code>は不変で、<code>let mut</code>は可変になります。Rustでは、不変インスタンス時に可変操作をしようとすると以下のようにコンパイルエラーになります。<code>queue_cat_name</code>メソッド、<code>dequeue_cat_name</code>メソッドは<code>&amp;mut self</code>が必要なので、このコードではコンパイルできないわけです。不変インスタンス時に誤って可変操作できません。安全な可変操作ができます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synPreProc">#[test]</span> <span class="synStatement">fn</span> <span class="synIdentifier">example</span>() { <span class="synStatement">let</span> cat_names <span class="synStatement">=</span> <span class="synPreProc">CatNameList</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(); <span class="synComment">// 不変インスタンスの場合はコンパイルエラー</span> cat_names.<span class="synIdentifier">queue_cat_name</span>(<span class="synConstant">&quot;cats&quot;</span>); <span class="synStatement">let</span> cat_name <span class="synStatement">=</span> cat_names.<span class="synIdentifier">next_cat_name</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_name); <span class="synComment">// 不変インスタンスの場合はコンパイルエラー</span> cat_names.<span class="synIdentifier">dequeue_cat_name</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_names); } </pre> <p>ScalaやKotlinの可変性には豊富なバリエーションがあります。</p> <table> <thead> <tr> <th></th> <th>Immutable</th> <th>Mutable</th> </tr> </thead> <tbody> <tr> <td>val</td> <td>😁</td> <td>😕</td> </tr> <tr> <td>var</td> <td>😀</td> <td>😱</td> </tr> </tbody> </table> <p>Rustの場合はシンプルです。Rustの可変性は他の言語と違って安全に扱えるので、😱ではなく😀にしました。</p> <table> <thead> <tr> <th></th> <th>Immutable(&amp;self)</th> <th>Mutable(&amp;mut self)</th> </tr> </thead> <tbody> <tr> <td>let</td> <td>😄</td> <td>※</td> </tr> <tr> <td>let mut</td> <td>-</td> <td>😀</td> </tr> </tbody> </table> <p>※に近いことは、<code>T</code>型が<code>&amp;self</code>であっても<code>Rc&lt;RefCell&lt;T&gt;&gt;</code>, <code>Arc&lt;Mutex&lt;T&gt;&gt;</code>などの内部可変性を使うと可能です。</p> <p>さらに、ここでは詳しく述べませんが、可変参照には以下のような厳しい制約があります。</p> <ul> <li>一つのスコープで一つのインスタンスに対して、可変参照は一つしか取れません</li> <li>不変参照を借用中は可変参照を借用できません</li> </ul> <p>詳しくは<a href="https://doc.rust-jp.rs/book-ja/ch04-02-references-and-borrowing.html">公式ドキュメント</a>を参照してください。</p> <h3>メソッドを不変に変更する</h3> <p>2022/05/15: ここは&amp;mut selfのほうがよいかも。何でもかんでも不変にするとcloneだらけになるので注意してください</p> <p>可変でも安全に扱えますが、挙動を予測しやすくするにはやはり不変にした方がよいです。不変の例を以下に紹介します。</p> <p><code>&amp;mut self</code>を<code>&amp;self</code>に変更し、戻り値が<code>Self</code>に変わります。<code>self.cat_names</code>の複製に対して可変操作した結果を保持する新しいインスタンスを返します。当然、<code>example</code>を変わります。シャドーイングをうまく使ってすっきり書くことができます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// ...</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">queue_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, name: <span class="synType">&amp;str</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synStatement">if</span> <span class="synStatement">!</span>REGEX.<span class="synIdentifier">is_match</span>(name) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Must contain s&quot;</span>); } <span class="synStatement">if</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">contains</span>(<span class="synType">&amp;</span>name.<span class="synIdentifier">to_string</span>()) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Already queued&quot;</span>); } <span class="synComment">// 複製を作り変更を加え新しいインスタンスを返す</span> <span class="synStatement">let</span> <span class="synType">mut</span> cloned <span class="synStatement">=</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">clone</span>(); cloned.<span class="synIdentifier">push</span>(name.<span class="synIdentifier">to_string</span>()); <span class="synType">Self</span> { cat_names: cloned } } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">dequeue_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synStatement">if</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">is_empty</span>() { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Must non empty&quot;</span>); } <span class="synComment">// 複製を作り変更を加え新しいインスタンスを返す</span> <span class="synStatement">let</span> <span class="synType">mut</span> cloned <span class="synStatement">=</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">clone</span>(); cloned.<span class="synIdentifier">remove</span>(<span class="synConstant">0</span>); <span class="synType">Self</span> { cat_names: cloned } } } <span class="synPreProc">#[test]</span> <span class="synStatement">fn</span> <span class="synIdentifier">example</span>() { <span class="synStatement">let</span> cat_names <span class="synStatement">=</span> <span class="synPreProc">CatNameList</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(); <span class="synStatement">let</span> cat_names <span class="synStatement">=</span> cat_names.<span class="synIdentifier">queue_cat_name</span>(<span class="synConstant">&quot;cats&quot;</span>); <span class="synStatement">let</span> cat_name <span class="synStatement">=</span> cat_names.<span class="synIdentifier">next_cat_name</span>().<span class="synIdentifier">unwrap</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_name); <span class="synStatement">let</span> cat_names <span class="synStatement">=</span> cat_names.<span class="synIdentifier">dequeue_cat_name</span>(); <span class="synPreProc">println!</span>(<span class="synConstant">&quot;{:?}&quot;</span>, cat_names); } </pre> <p>この例では、状態を変えるような操作は常に新しいインスタンスを作ります。すべてのメソッドは引数に対して参照透明性があり、その挙動は予測可能になります。</p> <p>つまるところ、<code>self.cat_names</code>の複製をどう考えるかによって設計が変わります。共有される型なら原則 不変したほうがよいでしょう。性能上のペナルティが大きいなら<code>&amp;mut self</code>で局所的な可変を検討するのがよいでしょう。</p> <h3>何を検証すればいいのか</h3> <p>『セキュア・バイ・デザイン』では以下の項目と検証順序が提唱されています。詳しくは書籍を読んでみてください。</p> <ol> <li>オリジン(発生源) <ul> <li>正当な送信元から送信されたデータか?</li> </ul> </li> <li>サイズ <ul> <li>データのサイズは適切か</li> </ul> </li> <li>字句的内容 (lexical content) <ul> <li>データを構成する文字は正当な文字だけを使っており、正しくエンコードされているか?</li> </ul> </li> <li>構文 (syntax) <ul> <li>データは正しいフォーマットに沿っているか?</li> </ul> </li> <li>意味 (semantics) <ul> <li>そのデータは意味的に正しいものか?</li> </ul> </li> </ol> <p>今回の場合は、サイズや字句的内容や構文に関する表明を扱っていることがわかります。</p> <h3>型による表明</h3> <p><code>CatNameList</code>型をさらに改善してみます。</p> <h4>子猫の名前をドメイン・プリミティブ化する</h4> <p><code>name</code>を文字列型<code>&amp;str</code>ではなく、ドメイン・プリミティブに切り出してみます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CatNameList</span> { cat_names: <span class="synType">Vec</span><span class="synStatement">&lt;</span>CatName<span class="synStatement">&gt;</span>, } <span class="synStatement">impl</span> CatNameList { <span class="synComment">// ...</span> <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">queue_cat_name</span>(<span class="synType">&amp;mut</span> <span class="synConstant">self</span>, name: CatName) { <span class="synStatement">if</span> <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">contains</span>(<span class="synType">&amp;</span>name.<span class="synIdentifier">to_string</span>()) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Already queued&quot;</span>); } <span class="synConstant">self</span>.cat_names.<span class="synIdentifier">push</span>(name); } <span class="synComment">// ...</span> } </pre> <p>さらに、正規表現を使った表明を<code>CatName</code>側に移動させます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CatName</span>(<span class="synType">String</span>); <span class="synType">static</span> REGEX: Lazy<span class="synStatement">&lt;</span>Regex<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synPreProc">Lazy</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synStatement">||</span> <span class="synPreProc">Regex</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(<span class="synConstant">r&quot;.*s.*&quot;</span>).<span class="synIdentifier">unwrap</span>()); <span class="synStatement">impl</span> CatName { <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">new</span>(name: <span class="synType">&amp;str</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synStatement">if</span> <span class="synStatement">!</span>REGEX.<span class="synIdentifier">is_match</span>(name) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Must contain s&quot;</span>); } <span class="synType">Self</span>(name) } } </pre> <p><code>queue_cat_name</code>メソッドは<code>CatName</code>型を使います。<code>CatName</code>の<code>new</code>メソッドには表明があるのでインスタンス化できれば常に正しいインスタンスであると言えるので、<code>queue_cat_name</code>メソッドにあった<code>name</code>に関する表明は不要になります。ドメイン・プリミティブを引数に取ることは値による表明ではなく「<strong>型による表明</strong>」と呼ぶことにします<a href="#f-51b0d92d" name="fn-51b0d92d" title="静的型付き言語前提の解説になっていますが、動的型付き言語でも値の型を判定できるのであれば似たようなことはできるかもしれない…">*8</a>。</p> <h4>常に要素数1個以上となるCatNameList型</h4> <p>子猫の名前のリストが空かどうか、ある値の状態を表明するのは安全なのですが、そもそもそういった「値の表明」を利用せずに安全に操作できないものでしょうか。</p> <p><code>CatNameList</code>を、そもそも要素0個でインスタンス化できないように設計を変更します。これも「<strong>型による表明</strong>」の一種です。どういうことかみていきましょう。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CatNameList</span> { head: CatName, tail: <span class="synType">Option</span><span class="synStatement">&lt;</span>Rc<span class="synStatement">&lt;</span>CatNameList<span class="synStatement">&gt;&gt;</span> size: <span class="synType">usize</span>, } <span class="synStatement">impl</span> CatNameList { <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">new</span>(head: CatName) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synType">Self</span> { head, tail: <span class="synConstant">None</span>, size: <span class="synConstant">1</span>, } } <span class="synStatement">fn</span> <span class="synIdentifier">contains</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, name: <span class="synType">&amp;</span>CatName) <span class="synStatement">-&gt;</span> <span class="synType">bool</span> { <span class="synStatement">if</span> <span class="synConstant">self</span>.head <span class="synStatement">==</span> <span class="synType">*</span>name { <span class="synConstant">true</span> } <span class="synStatement">else</span> { <span class="synStatement">match</span> <span class="synType">&amp;</span><span class="synConstant">self</span>.tail { <span class="synConstant">None</span> <span class="synStatement">=&gt;</span> <span class="synConstant">false</span>, <span class="synConstant">Some</span>(next) <span class="synStatement">=&gt;</span> (<span class="synType">&amp;*</span>next).<span class="synIdentifier">contains</span>(name) } } } <span class="synStatement">fn</span> <span class="synIdentifier">combine</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, other: <span class="synType">Self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synStatement">let</span> t <span class="synStatement">=</span> <span class="synStatement">match</span> <span class="synConstant">self</span>.tail.<span class="synIdentifier">as_ref</span>() { <span class="synConstant">None</span> <span class="synStatement">=&gt;</span> <span class="synPreProc">Rc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(other), <span class="synConstant">Some</span>(t) <span class="synStatement">=&gt;</span> <span class="synPreProc">Rc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>((<span class="synType">&amp;*</span>t).<span class="synIdentifier">combine</span>(other)), }; <span class="synType">Self</span> { head: <span class="synConstant">self</span>.head.<span class="synIdentifier">clone</span>(), tail: <span class="synConstant">Some</span>(t), size: <span class="synConstant">1</span> <span class="synStatement">+</span> (<span class="synType">&amp;*</span>t).size } } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">queue_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>, name: CatName) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synStatement">if</span> <span class="synConstant">self</span>.<span class="synIdentifier">contains</span>(<span class="synType">&amp;</span>name) { <span class="synPreProc">panic!</span>(<span class="synConstant">&quot;Already queued&quot;</span>); } <span class="synConstant">self</span>.<span class="synIdentifier">combine</span>(<span class="synPreProc">CatNameList</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(name)) } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">next_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">&amp;</span>CatName { <span class="synType">&amp;</span><span class="synConstant">self</span>.head } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">dequeue_cat_name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">Option</span><span class="synStatement">&lt;</span><span class="synType">&amp;</span>Rc<span class="synStatement">&lt;</span>CatNameList<span class="synStatement">&gt;&gt;</span> { <span class="synConstant">self</span>.tail.<span class="synIdentifier">as_ref</span>() } <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">size</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">usize</span> { <span class="synConstant">self</span>.size } } </pre> <p><code>head</code>に必ず1要素を取るようにし、残りは<code>tail</code>に格納します。<code>tail</code>は<code>tail: Option&lt;Rc&lt;CatNameList&gt;&gt;</code>と少し複雑ですね。<code>Rc</code>はリファレンスカウントを扱う型です。<code>Rc</code>を使うとクローンしても参照のカウント値が加算されるだけでインスタンスのコピーは起きません。今回のケースでは、自己参照型を扱うために使っています。<code>size</code>は要素数を表現します。</p> <p>細かい変更点は以下です。</p> <ul> <li><code>contains</code>メソッドは、<code>head</code>を比較したのち、<code>tail</code>があれば委譲するだけです。</li> <li><code>queue_cat_name</code>メソッドは、<code>contains</code>メソッドで含まれていないことを確認したら、<code>combine</code>で要素をリストにして追加します。<code>combine</code>では現在の<code>tail</code>に引数で与えた新しいリストを<code>combine</code>したインスタンスを返します。<code>size</code>も同時に計算します。head, tailの構造では末尾に追加する効率が悪いですね…。課題が残りますが、今回の説明は本題がそこじゃないので…。</li> <li><code>next_cat_name</code>メソッドは、もはや<code>self.head</code>の参照を返すだけになりました。<strong>常に最初の要素があるので<code>Option</code>でラップしなくて済みました</strong></li> <li><code>dequeue_cat_name</code>メソッドも、<code>self.tail</code>の参照を返すだけです。要素数が1個以上か表明する必要もありませんし、後続がある場合でも<strong>後続の<code>CatNameList</code>のインスタンスを返すだけなので大きな削除コストはかからないでしょう</strong></li> <li><code>size</code>メソッドは<code>size</code>を返すだけです。この型は不変なので、要素数を一度計算すれば再計算は不要です。</li> </ul> <p>この変更によって、<code>CatNameList</code>が要素数0個で誤って利用されることがなくなりました。間違った操作がそもそもできない設計になりました。これも「型による表明」の効果です。 もちろん、「型による表明」は「値による表明」の下支えがないと実現が難しいのですが、重要な部分ではできる限り「型の表明」を適用することを考えたほうがよいでしょう。</p> <h2>まとめ</h2> <p>ところで、ヒューマンエラーの考え方には、フェイルセーフとフールプルーフという考え方があります。フェイルセーフは、システムが故障あるいはエラーを発生させても<strong>安全が維持できること</strong>です。フールプルーフは人間が誤った行為をしようとしても<strong>そもそも出来ないようにすること</strong>です。</p> <ul> <li><a href="https://seihin-sekkei.com/method/failsafe/">&#x30D5;&#x30A7;&#x30FC;&#x30EB;&#x30BB;&#x30FC;&#x30D5;&#xFF08;&#xFF11;&#xFF09; - &#x88FD;&#x54C1;&#x8A2D;&#x8A08;&#x77E5;&#x8B58;</a></li> <li><a href="https://seihin-sekkei.com/method/foolproof/">&#x30D5;&#x30FC;&#x30EB;&#x30D7;&#x30EB;&#x30FC;&#x30D5; - &#x88FD;&#x54C1;&#x8A2D;&#x8A08;&#x77E5;&#x8B58;</a></li> </ul> <p>「値による表明」は、フェイルセーフの考え方に分類されます。意図した振る舞いから逸れたとき、そのまま続行するとシステムがトラッシュします。それを防ぐために、早期にシステムを中断するからです。一方で「型による表明」は、そもそも危険な操作自体ができない(コンパイルエラーでプログラムとして実行できない)のでフールプルーフに分類されるでしょう。</p> <p>「値による表明」は実行すれば正しいことが分かる。「型による表明」はコンパイルすれば正しいことが分かる。後者が増えれば、テストを実行するまでもなく、間違いに気づく機会が増えるでしょう、きっと。この考え方が、Rustと合わされば鬼に金棒ですね。</p> <p>追記:12/8</p> <p>わかりにくかったかもしれませんが、ErrorとDefect(欠陥)で対応方法が異なります。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ErrorとDefectを勘違いされてると思います。 <a href="https://t.co/2OzBfyJNek">https://t.co/2OzBfyJNek</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1468513245019652099?ref_src=twsrc%5Etfw">2021年12月8日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">この記事ではErrorの話はしてません。Defect(欠陥)の話です。欠陥なのにResultで返してリカバリしてどうするのでしょう…。もちろん、仕様として、欠陥扱いするのではなく、Errorしてハンドリングしたいというのは設計の段階で精査すべきですね。</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1468515811438788610?ref_src=twsrc%5Etfw">2021年12月8日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>panic!するかResultを返すか公式のドキュメントも参照してみてください。Result型を返すからRustらしいは思考停止だと思います。</p> <ul> <li><a href="https://doc.rust-jp.rs/book-ja/ch09-01-unrecoverable-errors-with-panic.html">panic!&#x3067;&#x56DE;&#x5FA9;&#x4E0D;&#x80FD;&#x306A;&#x30A8;&#x30E9;&#x30FC; - The Rust Programming Language &#x65E5;&#x672C;&#x8A9E;&#x7248;</a></li> <li><a href="https://doc.rust-jp.rs/book-ja/ch09-02-recoverable-errors-with-result.html">Result&#x3067;&#x56DE;&#x5FA9;&#x53EF;&#x80FD;&#x306A;&#x30A8;&#x30E9;&#x30FC; - The Rust Programming Language &#x65E5;&#x672C;&#x8A9E;&#x7248;</a></li> <li><a href="https://doc.rust-jp.rs/book-ja/ch09-03-to-panic-or-not-to-panic.html">panic!&#x3059;&#x3079;&#x304D;&#x304B;&#x3059;&#x308B;&#x307E;&#x3044;&#x304B; - The Rust Programming Language &#x65E5;&#x672C;&#x8A9E;&#x7248;</a></li> </ul> <div class="footnote"> <p class="footnote"><a href="#fn-e6d9e0c6" name="f-e6d9e0c6" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">オブジェクト指向もプログラミングスタイルです</span></p> <p class="footnote"><a href="#fn-acc6f98e" name="f-acc6f98e" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">2も入るかなとは思いますが、改修の方法や手順に依存するかもしれないので今回は条件に含めず</span></p> <p class="footnote"><a href="#fn-3b729d56" name="f-3b729d56" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">これは代表的なパターン</span></p> <p class="footnote"><a href="#fn-73fa6920" name="f-73fa6920" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">『セキュア・バイ・デザイン』が既知のセキュリティ対策を軽視しているわけではありません</span></p> <p class="footnote"><a href="#fn-7ba39383" name="f-7ba39383" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">探査機は地球と交信するので非スタンドアローンのでは…</span></p> <p class="footnote"><a href="#fn-6602b57e" name="f-6602b57e" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">外部クレートとして遅延初期化のためにonce_cell, 正規表現のためにregexを利用しています</span></p> <p class="footnote"><a href="#fn-6bb46780" name="f-6bb46780" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">関数に記述されるselfの借用記述で決定します</span></p> <p class="footnote"><a href="#fn-51b0d92d" name="f-51b0d92d" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">静的型付き言語前提の解説になっていますが、動的型付き言語でも値の型を判定できるのであれば似たようなことはできるかもしれない…</span></p> </div> j5ik2o 「DDDで複数集約間の整合性を確保する方法 Rev2」に対する考察 hatenablog://entry/26006613706029349 2021-03-22T10:25:20+09:00 2021-03-22T10:25:20+09:00 どうも、かとじゅんです。 松岡さん(id:little_hands)が以下の記事を更新されたそうです。松岡さん自身が悩まれた中で検討したオプションであって、唯一の正解ではないと踏まえたうえで、率直な感想を述べたいと思います。結論からいうと、論旨は前回の記事と変わりませんが、コード例で具体的な考え方を示している点を工夫しています。 little-hands.hatenablog.com 前回の考察記事も古くなったので、最新の記事に併せて考察をまとめ直したいと思います。 blog.j5ik2o.me <p>どうも、かとじゅんです。</p> <p>松岡さん(<a href="http://blog.hatena.ne.jp/little_hands/">id:little_hands</a>)が以下の記事を更新されたそうです。松岡さん自身が悩まれた中で検討したオプションであって、唯一の正解ではないと踏まえたうえで、率直な感想を述べたいと思います。結論からいうと、論旨は前回の記事と変わりませんが、コード例で具体的な考え方を示している点を工夫しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flittle-hands.hatenablog.com%2Fentry%2F2021%2F03%2F08%2Faggregation" title="DDDで複数集約間の整合性を確保する方法(サンプルコードあり)[ドメイン駆動設計] - little hands&#39; lab" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://little-hands.hatenablog.com/entry/2021/03/08/aggregation">little-hands.hatenablog.com</a></cite></p> <p>前回の考察記事も古くなったので、最新の記事に併せて考察をまとめ直したいと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F2021%2F03%2F09%2F231332" title="「DDDで複数集約間の整合性を確保する方法」に対する考察 - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.j5ik2o.me/entry/2021/03/09/231332">blog.j5ik2o.me</a></cite></p> <h2>ドメインモデル</h2> <p>ドメインモデル図が追加されていますね。以下の3つの集約があるそうです。「一つの集約にまとめればいいよね」という提案はなしという前提で考えます。</p> <ul> <li>ユーザー</li> <li>タスク</li> <li>アクティビティ・レポート</li> </ul> <p>「アクティビティ・レポート」は「タスク」もしくは「ユーザー」に関連を持つようです。</p> <p>「これらのモデルをどう使いたいのか」が知れるともっと有益な議論ができそうです。ユースケースがないと使えるモデルかどうか判断できないと思います…。</p> <p>主なユースケースは想定で考えるなら以下とか?ドメインの振る舞いにフォーカスするために、意図的に「確認」もしくは「閲覧」するユースケースは省略しました。「アクティビティ・レポート」はユーザーが作るものではなく、システム内部で作られるものですよね、きっと。</p> <ul> <li>ユーザーがユーザーアカウントを更新する</li> <li>システムがユーザアカウントのアクティビティ・レポートを追加する</li> <li>ユーザーがタスクを作成する</li> <li>システムがタスクのアクティビティ・レポートを追加する</li> </ul> <p>このユースケースではCRUD臭しかしないので、もう少しモデルが成立するルールみたいなものがあれば面白いのですが…。</p> <p>以下のような集約ルートになるのでしょうか(コードはScalaです。<code>def</code>が<code>fun</code>に変わったぐらいなので読めると思います)。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> User(id: UserId, name: UserName, ...) { <span class="synComment">// ...</span> } <span class="synType">case</span> <span class="synType">class</span> Task(id: TaskId, name: TaskName, userId: UserId, ...) { <span class="synComment">// ...</span> } <span class="synType">case</span> <span class="synType">class</span> ActivityReport(id: ActivityId, taskId: Option[TaskId], userId: Option[UserId], ...) { <span class="synComment">// ...</span> } </pre> <h3>アクティビティ・レポートに対する違和感</h3> <p>何度かアクティビティ・レポートという名前を声に出してみる。文字で入力してみる。<code>ActivityReport</code>という名前は<code>ActivityReport = Activity + Report</code> に分解できそう。<code>Report</code>は何かしらのレポート形式を採用するのかもしれない。<code>Activity</code>を<code>Report</code>するのだから<code>ActivityReport</code>の前にまず<code>Activity</code>という概念はありそうだが…。それとも<code>ActivityReport</code>ではなく<code>Activity</code>という名前の方が適切?</p> <p>こういうふうに、ボケとツッコミでいうと、ツッコミをうまく使ってモデリングを深掘りしていきます。モデルの解釈を広げていくときはボケをうまく使う必要がありますが、今回はツッコミを入れてみました。このあたりの話は勉強の哲学にいろいろ書いてあるので参考にしてみてください。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B085979R8T/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/31mYFgzUuFL.jpg" class="hatena-asin-detail-image" alt="勉強の哲学 来たるべきバカのために 増補版 (文春文庫)" title="勉強の哲学 来たるべきバカのために 増補版 (文春文庫)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B085979R8T/j5ik2o.me-22/">勉強の哲学 来たるべきバカのために 増補版 (文春文庫)</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%C0%E9%CD%D5%20%B2%ED%CC%E9" class="keyword">千葉 雅也</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2020/03/10</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p><code>Activity</code>ならドメインに関連してそうだが、<code>ActivityReport</code>になると急にビューに見えてきます…。頭の中でこれは<code>Activity</code>だと思って話しを進めます(ところで、<code>Activity</code>って何?おそらく過去に起こった出来事?。ドメインイベントに似てますね…)。</p> <p>それにしても、<code>ActivityReport</code>の<code>taskId</code>, <code>userId</code> はどちらか一方しか使われないわけで、微妙ですね…。別々の型や集合に見える。<code>TaskActivity</code>や<code>UserActivity</code>のほうがわかりやすくないか。”なぜわかりやすいかどうか”は多分にユーザーの関心や利害に結び付きそう。 まずそれを知りたい…。</p> <p>などと、この時点で「そもそも論」を展開すると、本題に入れないので、このモデルでいいことにして話を進めします(笑)。</p> <h2>実装方法1. ユースケースで複数集約を更新する</h2> <h3>「複数集約を1ユースケースで更新して良いのか?」について</h3> <p>ユースケースはある機能を一連のプログラム(式次第のイメージ)として表現するものと理解しています。<strong>そのために、要件によっては複数の集約を呼び出すこともあるで、この件は問題がないという認識。というか、僕はこれ自体は否定はしていません。むしろ複雑なユースケースでは複数の型の集約を利用する必要があります。</strong> なので、このような実装になるだろうと思います。</p> <p>ただし、<strong>複数集約の更新を同一トランザクションに含めるかについては異論があります。集約の中では強い整合性(RDBのトランザクションなど)を、外では弱い整合性(結果整合性)を使うべきだからです。</strong>これは前回のブログ記事に書いた通りです。</p> <h3>メリット・デメリットについて</h3> <ul> <li>メリット <ul> <li>特に異論はないです。</li> </ul> </li> <li>デメリット <ul> <li>「アクティビティ・レポート」を「更新し忘れる」の件 <ul> <li>仕様を満たしていないプログラムなのでバグということかと思います。まずはそれをテストや表明で対策できないか。もちろん型で表明できればベストですがやり過ぎると代償もあるわけで。これについては他の実装方法と併せて後述します。</li> </ul> </li> <li>うっかり「更新し忘れる」以外のデメリットがあります(基本的に前回の記事で書いた通りです) <ul> <li>強い整合性の境界を持つ集約よりも、外側に二重に強い整合性の境界(Transactionalアノテーションで囲っている範囲)を設けているという点です。本来は集約とトランザクションの境界が一致していることが望ましいのですが、以下のようなユースケース毎に強い整合性の境界が設定されています。ユースケースの内側に張り巡らされた、集約の枠を飛び越えた強い整合性を基にロジックが組まれることになります…。集約はそれ単体で整合性が独立するはずですが、この設計では独立していないことになってしまうわけです。このような環境下では、二つの集約の関係は密結合になるリスクもあります。というのがDDDとしての立て付けだと思います。 <ul> <li>個人的には、今回のケースではRDB以外はない想定でしょうしTransactionalアノテーションをつけても大きな実害はなさそうです。DDDの集約の考え方に沿うなら結果整合でユースケースを実装するべきですが、もちろんそれ以外の設計の選択はあります<a href="#f-3f26fef9" name="fn-3f26fef9" title="わかりきったことでいちいち言及するべきことじゃないと思いますが…">*1</a>。つまるところ、どんな選択をするにしても設計(How)のコンテキスト(Why)に納得感があればよいのではないでしょうか。とはいえ、このように集約とは異なるものを「集約」と呼ぶのはいかがなものかと思っています。本来の集約とは違う解釈の「集約」が、割れ窓理論的に捉えられてしまうリスクを懸念します。</li> </ul> </li> </ul> </li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20210320/20210320231112.png" alt="f:id:j5ik2o:20210320231112p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>「実装方法2. ユースケースで複数集約を更新する」や「実装方法3. ドメインイベントを使用する」について</h2> <p><strong>結論からいうと、さじ加減が難しいところですが、実装方法2も実装方法3も「更新漏れを防ぐ」ために余計な複雑さを導入してしまっていると思います。</strong></p> <p>実装方法1であっても、以下の方法で正しさを検証すればよいのではないでしょうか</p> <ul> <li><strong>テストで振る舞いが正しいことを検証する</strong></li> <li><strong>ユースケースのexecuteメソッドの事後条件を表明する</strong></li> </ul> <p>もちろん、この場合でもテストや事後条件自体が間違っていたらどうするのか。テストが正しくても仕様が間違っていたらどうするのか、仕様が正しくても要件が間違っていたら…などと無限に続きます。松岡さん提案の方法でも、<code>TaskCreateParameter</code>を間違って実装して利用すると、結局期待した正しさを得ることができません。</p> <p><strong>つまるところソフトウェアのおける、正しさとは相対的な概念なのです(出典 オブジェクト指向入門 第11章 契約による設計:信頼性の高いソフトウェアを構築する)。正しさの証明は本来際限がないので、コストとリターンの均衡が取れる適当なところで絶妙に妥協する必要があります</strong>。ドメインオブジェクトの型として間違った使い方ができないように考えることは重要なことですが、そのために複雑なコードを組み込んでしまうぐらいなら、立ち止まってテストや表明の範囲に留めます。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798111112/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/41A2yC7UpOL.jpg" class="hatena-asin-detail-image" alt="オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)" title="オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798111112/j5ik2o.me-22/">オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%D0%A1%BC%A5%C8%A5%E9%A5%F3%A5%C9%A1%A6%A5%E1%A5%A4%A5%E4%A1%BC" class="keyword">バートランド・メイヤー</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2007/01/10</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>まぁ確かによいアイデアが思いついて、勢いでリファクタリングすることもありますが、そういうときほど、マクベス症候群(出典 レガシーソフトウェア改善ガイド 4.1 規律あるリファクタリング)を発症しないように、一度冷静になった方が良いと思っています。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798145149/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/516AmcmQGPL.jpg" class="hatena-asin-detail-image" alt="レガシーソフトウェア改善ガイド (Object Oriented Selection)" title="レガシーソフトウェア改善ガイド (Object Oriented Selection)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798145149/j5ik2o.me-22/">レガシーソフトウェア改善ガイド (Object Oriented Selection)</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A5%B9%A1%A6%A5%D0%A1%BC%A5%C1%A5%E3%A5%EB" class="keyword">クリス・バーチャル</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2016/11/11</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>「実装方法3. ドメインイベントを使用する」についても、「更新漏れを防ぐ」ことと天秤にかけてバランスできるでしょうか。僕はそう思えません。Event Sourcingをシステムの耐障害性やスケーラビリティを確保するために導入するなら、ドメインイベントを採用する価値はありそうですが…🤔</p> <h2>じゃぁどうするの</h2> <p>「更新漏れを防ぐ」は上記の方法でバランスをとることにして、ユースケース部分をどう実装したら良いかを考えてみました。大袈裟な仕組みを導入しないでも結果整合にする方法はあるという話です。</p> <h3>ユースケースをRDBのトランザクション境界にしない</h3> <p>ユースケースの<code>execute</code>は同一トランザクションに含めずに、以下のようにします。(1),(2)はそれぞれ独立したトランザクションになります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> CreateTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { <span class="synIdentifier"> def</span> execute(taskName: <span class="synConstant">String</span>): Unit = { <span class="synType">val</span> task = Task(taskName) taskRepository.store(task) <span class="synComment">// (1) リポジトリ内でトランザクションが閉じる</span> <span class="synType">val</span> activityReport = ActivityReport(task) activityReportRepository.store(activityReport) <span class="synComment">// (2) リポジトリ内でトランザクションが閉じる</span> } } </pre> <p>想定問答集は以下です。</p> <ul> <li>Q1, (1)→(2)の順に処理し(2)が失敗したらどうするの? <ul> <li>普通に 呼び出し元にはエラーを返します。クライアントにもエラーが返ります。</li> </ul> </li> <li>Q2, <code>Task</code>だけ保存されると<code>Task</code>と<code>ActivityReport</code>を結合するクエリがおかしくなるのでは? <ul> <li><code>Task LEFT JOIN ActivityReport</code>だとおかしくなりますよね。 (1)→(2)の順序なら<code>ActivityReport LEFT JOIN Task</code>的なクエリをすれば問題はおきません。</li> </ul> </li> <li>Q3, ユースケースは再実行すると同じタスクIDで<code>INSERT</code>するので失敗するのでは? <ul> <li>同じタスクIDなら、<code>INSERT</code> or <code>UPDATE</code>してください <ul> <li>つまりクライアントがユースケースを再実行できるようになっていないといけません。べき等性を担保する必要があります</li> </ul> </li> <li>都度ID生成するなら、ゴミ<code>Task</code>が残ることになります <ul> <li>失敗時のゴミレコード vs 同一トランザクションによってロジックが密結合になるリスクのトレードオフになります</li> </ul> </li> </ul> </li> </ul> <p>結果整合の場合は、すべての更新が完了すれば結果的に正しい状態に収束します。更新中は中間状態が見えるかもしれません。これによってビジネス上大きな問題が起きないように想定しておく必要があります。</p> <p>この考え方で実際に運用したことあるの?って話ですが、あります。認証/認可に関係する、とあるウェブアプリケーションもこの考え方で設計して数年運用していますが、実用上整合性に関する問題はありません。逆に何でもかんでもトランザクションに含め、ユースケースによってトランザクション境界がちぐはぐに異なるケースではいろいろな問題(一番ヤバいやつはデッドロック…)が起きることを体験しています…。</p> <h4>Taskを更新するユースケースでは</h4> <p>今回の場合は新規追加なのでレースコンディションは起こらないですが、既存の集約を更新するユースケースも考えてみます。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> RenameTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { <span class="synIdentifier"> def</span> execute(taskId: TaskId, taskName: TaskName): Unit = { <span class="synType">val</span> task = taskRepository.findById(taskId) <span class="synComment">// (F1)</span> <span class="synType">val</span> renamedTask = task.withTaskName(taskName) taskRepository.store(renamedTask) <span class="synComment">// (S2)</span> <span class="synType">val</span> activityReport = ActivityReport(task) activityReportRepository.store(activityReport) <span class="synComment">// (S3)</span> } } </pre> <p>上記のコードでは以下のようにAとBの並行処理がある場合、二つの操作に分離性や独立性がないので、期待した結果にはなりません。</p> <p>(A-F1) -> (A-S1) -> (B-F1) -> (B-S1) -> (B-S2) -> (A-S2) -> ...</p> <p>これはロックを導入すれば解決できます。クライアントサーバ型のシステムでは、<code>SELECT ... FROM ... FOR UPDATE</code>で悲観的ロックを利用することがありますが、入力→確認→更新するようなウェブのユースケースではリクエストを跨がってトランザクションを保持し続けるには無理があります。ということで、ウェブでは楽観ロックを使うことが多いです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> RenameTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { <span class="synIdentifier"> def</span> execute(taskId: TaskId, taskName: TaskName, taskVersion: TaskVersion): Unit = { <span class="synType">val</span> task = taskRepository.findById(taskId, taskVersion) <span class="synComment">// (F1)</span> <span class="synType">val</span> renamedTask = task.rename(taskName) taskRepository.store(renamedTask) <span class="synComment">// (S2)</span> <span class="synType">val</span> activityReport = ActivityReport(task) activityReportRepository.store(activityReport) <span class="synComment">// (S3)</span> } } </pre> <p>上記の場合は、BのF1はバージョンがすでに更新されているので<code>Task</code>が取得できない、もしくはBの(S2)でバージョンが進んでいるため保存に失敗するので、ユースケースの処理はエラーになりデータの不整合を防げます。 (S3)も楽観ロックを提供してもよいですが、常に追記になるなら不要でしょう。</p> <h4>RDB非依存の良さ</h4> <p>前述したように、<strong>ユースケースをRDB非依存として設計しておけば、リポジトリのストレージがRDBでもNoSQLでもよくなります。別のマイクロサービス上に存在する集約を呼び出すことも可能です。</strong>今どきの分散するシステムにおいては、RDBのトランザクションは一つの選択肢ですが、あらゆる局面で使えるわけではありません。</p> <p>逆を言えば、RDBしか使わないシステムなら上記は考慮せずに、ユースケースをRDBのトランザクション境界にしても問題ないでしょう。えっ、ほんとに?分散キャッシュを使いたいとかSQSを使いたいとか後から言わないでよ?って意味になります。</p> <p>実際、後からRedis, Memcachedなど分散キャッシュや、SQSやKafkaなどのメッセージング基盤などを必要することはよくあります。レビューで実際あったのは、RDB前提になってしまったユースケース・ロジックに、以下のようなコードを追加された事例がありました…。まぁキャッシュだけ残ることありますよね…。読者のみなさんはこんな間違いをしないと思いますが。RDBは便利なのですが、このケースでは使い方が間違っていると言わざるを得ません。前述したように結果整合の考え方でユースケースを書き直したほうが無難です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> RenameTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { @Transcational <span class="synIdentifier"> def</span> execute(taskId: TaskId, taskName: TaskName): Unit = { <span class="synType">val</span> task = taskRepository.findById(taskId) <span class="synType">val</span> renamedTask = task.rename(taskName) taskRepository.store(renamedTask) <span class="synComment">// (1), キャッシュとしてRedisに書き込んでいる。</span> <span class="synComment">// (2)でロールバックしてもRedisに永続化されたデータはロールバックできない…</span> taskCacheRepository.store(renamedTask) <span class="synType">val</span> activityReport = ActivityReport(task) <span class="synComment">// (2), DB I/Oエラーが発生しロールバックする</span> activityReportRepository.store(activityReport) } } </pre> <h3>(蛇足) アクティビティ・レポートはビューモデル?</h3> <p>言いたいことは以上なのですが、論旨が微妙にずれるアクティビティ・レポートについての考察です。蛇足なので時間がある方のみどうぞ。</p> <p>基本的な考え方は上記と変わりませんが、アクティビティ・レポートのモデルについて考えてみました。</p> <p>アクティビティ・レポートがどんな属性を持つかわからないのですが、ユースケースが閲覧のみならば、アクティビティ・レポートはビューモデルやリードモデルに見えます。冒頭でもいったように出来事(ドメインイベント)をうまく使えないか。集約内部で発生した出来事(ドメインイベント)をモデリングしてはどうか。振る舞いの結果は新しい状態を表現するインスタンスとドメインイベントを返すようにしました<a href="#f-eea24712" name="fn-eea24712" title="ドメインイベントを戻り値で返すのではなく、Pub/Subするという方法もありますが、意味としては同じなのでここでは戻り値で表現します">*2</a>。そして、ドメインイベントからアクティビティ・レポートを生成できればよさそうな気がします。まぁ機能的なロジックとしてはほぼ変わらないですが、アクティビティ・レポートにビューの知識があるならこのようにして分離することも可能ではという話です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Task(id: TaskId, name: TaskName, userId: UserId, ...) { <span class="synIdentifier"> def</span> rename(value: TaskName): (Task,TaskRenamed) = (copy(name = value), TaskRenamed(...)) <span class="synIdentifier"> def</span> complete: (Task, TaskCompleted) = (copy(status = Completed), TaskCompleted(...)) } <span class="synType">object</span> Task { <span class="synIdentifier"> def</span> apply(id: TaskId, name: TaskName, userId: UserId, ...): (Task, TaskCreated) = { (Task(...), TaskCreated(...)) } } <span class="synType">sealed</span> <span class="synType">trait</span> TaskEvent { <span class="synIdentifier"> def</span> id: TaskEventId <span class="synIdentifier"> def</span> taskId: TaskId } <span class="synType">case</span> <span class="synType">class</span> TaskCreated(id: TaskEventId, taskId: TaskId, ... ) <span class="synType">extends</span> TaskEvent <span class="synType">case</span> <span class="synType">class</span> TaskAssigned(id: TaskEventId, taskId: TaskId, assignerId: UserId, assigneeId: UserId, ... ) <span class="synType">extends</span> TaskEvent <span class="synType">case</span> <span class="synType">class</span> TaskRenamed(id: TaskEventId, taskId: TaskId, taskName: TaskName, ... ) <span class="synType">extends</span> TaskEvent <span class="synType">case</span> <span class="synType">class</span> TaskCompleted(id: TaskEventId, taskId: TaskId, ..., createdAt: Instant ) <span class="synType">extends</span> TaskEvent <span class="synType">class</span> CompletedTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { <span class="synIdentifier"> def</span> execute(taskId: TaskId, taskName: TaskName, taskVersion: TaskVersion): Unit = { <span class="synType">val</span> task = taskRepository.findById(taskId, taskVersion) <span class="synType">val</span> (completedTask, taskCompleted) = task.complete taskRepository.store(completedTask) taskEventRepository.store(taskCompleted) } } <span class="synType">class</span> ActivityReportDao { <span class="synComment">// ActivityReportはリードモデルとして扱う</span> <span class="synIdentifier"> def</span> findByTaskId(taskId: TaskId): Seq[TaskActivityReportDto] = { <span class="synComment">// TaskEventが保存されているテーブルからSELECT ... FROM ... JOIN ... するとか、</span> <span class="synComment">// TaskEventを基に別のデータを作成し、それをクエリで返すとか、方法はいくつかあります</span> } } </pre> <p><code>TaskEvent</code>, <code>UserEvent</code>のように型として分けるのか、<code>Event</code>としてまとめた型にするのか、そのあたりはよく分かりませんが、この設計だと<code>Task</code>のステートと<code>Task</code>のイベント(一つの集約と見なせる)をダブルライトすることになりますね。ここは妥協になってしまうかと。</p> <h4>Event Sourcingでは</h4> <p>ちなみに、Event Sourcingではステートはイベントから導出できるのでイベントしか書き込みません。以下のようなイメージになりますが、このままだといろいろ問題あります。(1)(2)で長大な<code>taskEvents</code>をリクエスト毎読み込むとパフォーマンスに相応のペナルティがあります。(3)の楽観ロックがないと(4)は無条件に追記されてしまう。これらの問題はAkkaなら簡単に解決できます。</p> <p>前述したように、システムの耐障害性やスケーラビリティを確保する必要があるならEvent Sourcingは投資対効果がバランスできるかもしれませんが、そういう目的がないなら手段として見合わないと思いますが、考え方は参考になると思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> CompletedTaskUseCase1( taskRepository: TaskRepository, activityReportRepository: ActivityReportRepository, ) { <span class="synIdentifier"> def</span> execute(taskId: TaskId, taskName: TaskName, taskVersion: TaskVersion): Unit = { <span class="synType">val</span> taskEvents = taskEventRepository.findById(taskId) <span class="synComment">// (1)</span> <span class="synType">val</span> task = Task.fromEvents(taskEvents) <span class="synComment">// (2)</span> <span class="synType">val</span> taskCompleted = task.complete(taskVersion) <span class="synComment">// (3) task内部のversionと引数のversionが一致するならcomplete状態に遷移できる</span> taskEventRepository.store(taskCompleted) <span class="synComment">// (4)</span> } } </pre> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B076Z8SK45/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/61O5YIgV0gL.jpg" class="hatena-asin-detail-image" alt="Akka実践バイブル アクターモデルによる並行・分散システムの実現" title="Akka実践バイブル アクターモデルによる並行・分散システムの実現"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B076Z8SK45/j5ik2o.me-22/">Akka実践バイブル アクターモデルによる並行・分散システムの実現</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%E2%A5%F3%A5%C9%A1%A6%A5%ED%A5%B9%A5%C6%A5%F3%A5%D0%A1%BC%A5%B0" class="keyword">レイモンド・ロステンバーグ</a>,<a href="http://d.hatena.ne.jp/keyword/%A5%ED%A5%D6%A1%A6%A5%D0%A5%C3%A5%AB%A1%BC" class="keyword">ロブ・バッカー</a>,<a href="http://d.hatena.ne.jp/keyword/%A5%ED%A5%D6%A1%A6%A5%A6%A5%A3%A5%EA%A5%A2%A5%E0%A5%BA" class="keyword">ロブ・ウィリアムズ</a>,<a href="http://d.hatena.ne.jp/keyword/%C1%B0%BD%D0%20%CD%B4%B8%E3" class="keyword">前出 祐吾</a>,<a href="http://d.hatena.ne.jp/keyword/%BA%AC%CD%E8%20%CF%C2%B5%B1" class="keyword">根来 和輝</a>,<a href="http://d.hatena.ne.jp/keyword/%C5%A3%B2%B0%20%C6%F3%CF%BA" class="keyword">釘屋 二郎</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/12/13</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <h2>まとめ</h2> <p>ということで、以下まとめです。</p> <ul> <li>今回のモデル構造自体に異論はない</li> <li>ユースケース内で複数の集約を利用することは問題ない。というか必要</li> <li>「更新し忘れ」対策はほどほどに</li> <li>原則はユースケースは弱い整合性の境界、集約は強い整合性の境界。前回書いた記事の主張どおりです</li> <li>それほどコストをかけずにユースケースを結果整合にする方法はある</li> </ul> <p>ご参考までに。</p> <p>補足: 3/22</p> <p>この記事に書いた考え方はマイクロサービスとかやる人向けですと言われることがあります。それはそのとおりですが、そんなにマイクロサービスってハードル高いですか?クラウドとDevOpsで簡単になったはずですよね。身近なものですよ。</p> <p>ということで、何か参考になる書籍を教えてほしいと言われたので以下の本がオススメです。</p> <p>「5.2 DDDのAggregateパターンを使ったドメインモデルの設計」で紹介されている「ルール3: 1つのトランザクションで1つのアグリゲートルートを作成または更新する」をぜひ読んでください。この記事と寸分違わない解説がなされています。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086JJNDKS/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/61Sj9ZggwML.jpg" class="hatena-asin-detail-image" alt="マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ" title="マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086JJNDKS/j5ik2o.me-22/">マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Chris%20Richardson" class="keyword">Chris Richardson</a>,<a href="http://d.hatena.ne.jp/keyword/%C4%B9%C8%F8%B9%E2%B9%B0" class="keyword">長尾高弘</a>,<a href="http://d.hatena.ne.jp/keyword/%C3%AE%DF%B7%B9%AD%B5%FC" class="keyword">樽澤広亨</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2020/03/23</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <div class="footnote"> <p class="footnote"><a href="#fn-3f26fef9" name="f-3f26fef9" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">わかりきったことでいちいち言及するべきことじゃないと思いますが…</span></p> <p class="footnote"><a href="#fn-eea24712" name="f-eea24712" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">ドメインイベントを戻り値で返すのではなく、Pub/Subするという方法もありますが、意味としては同じなのでここでは戻り値で表現します</span></p> </div> j5ik2o 「DDDで複数集約間の整合性を確保する方法」に対する考察 hatenablog://entry/26006613701373064 2021-03-09T23:13:32+09:00 2021-03-09T23:13:32+09:00 久しぶりにブログ記事を書きますか。 ということで、松岡さん(id:little_hands)のブログ記事に対する考察記事です。 この記事は古くなったので、ぜひ以下も参照してください。 blog.j5ik2o.me little-hands.hatenablog.com 題材も松岡さんのブログ記事と同じもので考えます。 「実装方法1. ユースケースで複数集約を更新する」について考察したいと思います。 注意事項)この記事で使うトランザクションという用語は単なる一連の手続きという意味ではなく、ACID特性を持つRDBのトランザクションという意味です。 class CreateTaskUseCase1… <p>久しぶりにブログ記事を書きますか。</p> <p>ということで、松岡さん(<a href="http://blog.hatena.ne.jp/little_hands/">id:little_hands</a>)のブログ記事に対する考察記事です。</p> <p><em>この記事は古くなったので、ぜひ以下も参照してください。</em></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F2021%2F03%2F22%2F102520" title="「DDDで複数集約間の整合性を確保する方法 Rev2」に対する考察 - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.j5ik2o.me/entry/2021/03/22/102520">blog.j5ik2o.me</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flittle-hands.hatenablog.com%2Fentry%2F2021%2F03%2F08%2Faggregation" title="DDDで複数集約間の整合性を確保する方法(サンプルコードあり)[ドメイン駆動設計] - little hands&#39; lab" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://little-hands.hatenablog.com/entry/2021/03/08/aggregation">little-hands.hatenablog.com</a></cite></p> <p>題材も松岡さんのブログ記事と同じもので考えます。</p> <p>「実装方法1. ユースケースで複数集約を更新する」について考察したいと思います。</p> <p>注意事項)この記事で使うトランザクションという用語は単なる一連の手続きという意味ではなく、ACID特性を持つRDBのトランザクションという意味です。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">class</span> CreateTaskUseCase1( <span class="synType">private</span> <span class="synType">val</span> taskRepository: TaskRepository, <span class="synType">private</span> <span class="synType">val</span> taskReportRepository: TaskReportRepository, ) { <span class="synIdentifier">@Transactional</span> <span class="synType">fun</span> execute(taskName: String) { <span class="synComment">// Taskの作成と保存</span> <span class="synType">val</span> task = Task(taskName) taskRepository.insert(task) <span class="synComment">// TaskReportの作成と保存</span> <span class="synType">val</span> taskReport = TaskReport(task) <span class="synComment">// 生成したTask経由でTaskReportを作成している</span> taskReportRepository.insert(task) } } </pre> <p>詳しくみていくと、@Transactionalで一つのトランザクションとしていて、TaskとTaskReportは一緒に更新されないといけないようですね。あれ、これってそもそも集約としておかしくないか?と思いました。</p> <h2>集約の外部では結果整合性を用いる</h2> <p>Evansの集約は</p> <blockquote><p>複数の集約にまたがるルールはどれも、常に最新の状態にあるということが期待できない。イベント処理やバッチ処理、その他の更新の仕組みを通じて、他の依存関係は一定の時間内に解消できる。[Evans](128ページ)</p></blockquote> <p>であると、実践ドメイン駆動設計の「10.5ルール:境界の外部では結果整合性を用いる」で紹介されています。</p> <blockquote><p>ひとつの集約上でコマンドを実行するときに、他の集約のコマンドも実行するようなビジネスルールが求められるのなら、その場合は結果整合性を使うこと。</p></blockquote> <p>この説明に従うと、以下のようになるはずです。一つのトランザクションにまとめあげません。集約どうしはそれぞれ結果整合性を使うので、強い整合性がないからです。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">class</span> CreateTaskUseCase1( <span class="synType">private</span> <span class="synType">val</span> taskRepository: TaskRepository, <span class="synType">private</span> <span class="synType">val</span> taskReportRepository: TaskReportRepository, ) { <span class="synType">fun</span> execute(taskName: String) { <span class="synComment">// Taskの作成と保存</span> <span class="synType">val</span> task = Task(taskName) taskRepository.insert(task) <span class="synComment">// Taskのトランザクション</span> <span class="synComment">// TaskReportの作成と保存</span> <span class="synType">val</span> taskReport = TaskReport(task) taskReportRepository.insert(task) <span class="synComment">// TaskReportのトランザクション</span> } } </pre> <p>もちろん、TaskReportの更新が失敗したらTaskのロールバックが面倒では?というのあります。とはいえ、集約としての定義はこうなるはずです。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00UX9VJGW/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/619Gh1s721L.jpg" class="hatena-asin-detail-image" alt="実践ドメイン駆動設計" title="実践ドメイン駆動設計"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00UX9VJGW/j5ik2o.me-22/">実践ドメイン駆動設計</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%F4%A5%A9%A1%BC%A5%F3%A1%A6%A5%F4%A5%A1%A1%BC%A5%CE%A5%F3" class="keyword">ヴォーン・ヴァーノン</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2015/03/19</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798121967/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/51f7WXHJYCL.jpg" class="hatena-asin-detail-image" alt="エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)" title="エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798121967/j5ik2o.me-22/">エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%EA%A5%C3%A5%AF%A1%A6%A5%A8%A5%F4%A5%A1%A5%F3%A5%B9" class="keyword">エリック・エヴァンス</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2011/04/09</li><li><span class="hatena-asin-detail-label">メディア:</span> 大型本</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>まぁ、たぶん、えーっそれは困るんです。Task集約とTaskReport集約は一緒に更新されないといけないんです、って話ですよね…。その理由がちょっとわからないから考えるのが難しい…。もう少し要件知りたい…。</p> <p><a href="https://academy.lightbend.com/">Lightbend Academy</a>でも集約について以下のように解説されています。</p> <blockquote><p>Transactions should not spend multiple aggregate roots.<br /> If you find yourself in a situation where you need to do a transaction that actually crosses aggregate roots, then you've either defined your aggregate roots incorrectly or there's a problem with your transaction and you might have to rethink it a little bit.<br /> トランザクションは、複数の集約ルートを費やすべきではありません。<br /> もし、実際に集約ルートを横断するトランザクションを行う必要がある状況に遭遇した場合は、集約ルートの定義が間違っているか、トランザクションに問題があり、少し考え直す必要があるかもしれません。</p></blockquote> <p>とあるので、TaskとTaskReportの場合も、そもそも集約の定義やトランザクションの扱いに問題あるということではないでしょうか。</p> <blockquote><p>Will a transaction span multiple entities? If the answer to that question is yes then we can safely say that we have got the wrong aggregate root.<br /> Because again a transaction should not span multiple aggregate root.<br /> トランザクションは複数のエンティティにまたがりますか? この質問の答えがイエスならば、間違った集約ルートを持っていると言っても良いでしょう。<br /> なぜなら、トランザクションは複数の集約ルートにまたがるべきではないからです。</p></blockquote> <p>あくまでEvansの集約の定義は、トランザクションなどの強い整合性が有効に働く範囲は集約内部だけという話です。逆に、集約間では結果整合性なので弱い整合性しか使えません。</p> <h2>モデリングに問題はないか</h2> <p>TaskとTaskReportがそれぞれ独立した集約というならば、ここに示されたようにトランザクションは独立していないとおかしいのです。なのに、<code>CreateTaskUseCase1#execute</code>ではこれらの集約を一つのトランザクションで更新している。強い整合性で結びつけているのです。</p> <p>そして”うっかりTaskReport作成を忘れてしまった”がまずいということは、TaskとTaskReportは独立して更新できないことを言っているに等しい。であれば、そもそもTaskとTaskReportは同じ一つの集約ではないのか…と考えます。仮に、そもそもすべてが集約内部に包含されれば、うっかり更新をミスっておかしな状態にならないわけです。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">val</span> task = Task(taskName, TaskReport(...)) taskRepository.insert(task) </pre> <p>というわけで、TaskとTaskReportは分かれていないとまずいんです!(再)と聞こえてきそうです(笑) これだと要求を満たせないというのであれば、モデリングに問題あるのでは感。他の実装方法も議論したいところですが、要件をもう少し確認したいなぁという感じ。</p> <p>追記:少しTwitterで松岡さんと話したので観点を追加(3/10)</p> <h2>集約の境界=強い整合性の境界</h2> <p>よくあるパターンとしては、Task : TaskReport = 1 : N の関係で、Nが一つのオブジェクトに内包できないぐらい大量にあるという話。 この場合は、とれる妥当なオプションとしては以下?</p> <p>Task : TaskReport = 1 : N の関係</p> <ol> <li>Nを十分に小さくできないか、要求を調整する。たとえば、注文伝票の注文詳細欄は1万行とかある?ないでしょって話</li> <li>Nを十分に大きく取らざるを得ないなら、別々の集約として独立させて結果整合を許容する</li> </ol> <p>1の場合、Task(TaskReport)だけじゃなくUser(UserReport)という構造もある場合、レポートの知識が分散してしまうとデメリットがあるのではないかという話。レポートがクエリ要件ならTaskReportとUserReportをマージしたリードモデルがあればよい。そうでなくドメインの知識なら型や集合として単一である必要があるのでReport型という独立した集約が必要なので2になる。あと、CQRSの観点でC側のドメインオブジェクトを最適化すれば1を満たせる場合がある。詳しくは → <a href="https://blog.j5ik2o.me/entry/2020/06/13/175448">CQRS/ES&#x306B;&#x3088;&#x3063;&#x3066;&#x96C6;&#x7D04;&#x306E;&#x5883;&#x754C;&#x5B9A;&#x7FA9;&#x3092;&#x898B;&#x76F4;&#x3059; - &#x304B;&#x3068;&#x3058;&#x3085;&#x3093;&#x306E;&#x6280;&#x8853;&#x65E5;&#x8A8C;</a></p> <p>基本的に1,2で考えれば、集約の構造をみれば整合性の境界が分かる。1対1に紐付く。ユースケースで複数の集約を同一トランザクションに入れてしまうとこれは不鮮明になる。</p> <p>実践ドメイン駆動設計では、複数の集約を同一のトランザクションで更新するリスクを以下のように述べている。</p> <blockquote><p>集約の境界が実際の業務の制約と一致していると仮定して、もしビジネスアナリストが図10‒4のような仕様を出してきたら、それは問題の元だ。考え得るコミット順を考慮していくと、三つのリクエストのうち二つが失敗するいう場合もあることがわかる。この指示が、あなたの設計にどんな影響をおよぼすのだろう?この問いに答えようとすると、ドメインについてのより深い理解が得られる。複数の集約のインスタンスの整合性を保ち続けなければいけないというのは、自分たちが不変条件を見落としているということを意味する。最終的には、その複数の集約をひとつの新たな概念にまとめて名前をつけて、この新たに発見した業務ルールに対応することになるだろう(そしてもちろん、今までの集約群を、この新しい概念に取り込むことになる)。</p></blockquote> <p>このようなトランザクションの競合問題を除けば、ほとんどの場合は、複数集約を同一トランザクションにまとめても問題ないだろう。普通に機能するし便利だと思われる…。つまるところ、集約が強い整合性の境界を作るのに対して、ユースケースクラス(アプリケーションサービス)では弱い整合性の境界を作っていると考えてもよい。整合性の境界が入れ子だとしても強い一貫性を発揮する境界は集約であることは変わりない。もちろん、個別ケースにおいてはリスクを取って同一トランザクションに含めることもあるが、原則論としてはこういう考え方になるのは妥当だと思う。</p> <p>ユースケースクラス(アプリケースサービス)で同一トランザクションにしないで複数の集約を更新する際、ダブルコミットの問題が生じるので、そのリカバリーが厄介になる。以下のように集約Bの更新が失敗した場合、集約Aはすでにコミットされているので、集約Bはリトライしなければならない。もしくは集約Bの更新が失敗しても、クライアントからユースケースAがべき等にリトライできるようにする必要がある。そもそも集約は独立した存在なので、集約Bの更新が失敗しても集約Aへの影響は大きくないはず。クエリサイドで集約Aと集約Bを合成した結果を取得する際は、集約Bだけ古いデータを見ることになるので、こういった制約を想定しておく必要がある。</p> <pre class="code" data-lang="" data-unlink>ユースケースA() { 集約A更新 集約B更新 }</pre> <h2>集約単位のトランザクションはモジュラリティ確保のために</h2> <p>まぁ、こういう想定をすると益々同一トランザクションに入れたくなるが…僕は入れません。</p> <p>集約Aと集約Bの更新を同一トランザクションにした場合、ユースケースAを後から分離することが難しくなります。スケーラビリティのために、集約Aと集約Bを別々のサーバで強い整合性を持って実行するということは無理に等しいです(そもそも分けて実行する要求がないと言い切れるなら無視していいことですが…)。両者の更新が常にセットで行われることを期待したロジックができあがり、そのロジックには依存関係が生じます。あとからトランザクションを分けようとすると、大部分の他のロジックが動作しなくなるということはざらにあります。もちろんコードの規模や複雑度にもよりますが…。</p> <pre class="code" data-lang="" data-unlink>// 同一トランザクション前提のロジックを、以下のように分割することはかなり難しい…。 サーバAのユースケースA() { 集約A更新 } サーバBのユースケースB() { // 集約Aが正しく更新されていることが前提だったが、まだ更新されていないかもしれない…。 集約B更新 }</pre> <p>あと、集約Aと集約BのストレージがRDBなら同一にできますが、それぞれ別々のDBであったり、NoSQLであったりする場合はそもそも無理ですね。つまり、@Transactionalなどの手法で一つのトランザクションすることには、同一DBインスタンスを利用するという暗黙的なコンテキストがある。DBを分割したり異種のDB I/Oというコンテキストは含まれない。やはりモジュール性に関する論点かもしれない。</p> <p>DBを例にしたが、マイクロサービスアーキテクチャの場合はどうだろう。こちらもユースケースで同一トランザクションにすることなどはそもそも無理だ。</p> <pre class="code" data-lang="" data-unlink>ユースケースA() { マイクロサービスAの集約A更新 自マイクロサービスの集約B更新 }</pre> <p>この場合でも、前節で述べたようにRDBのトランザクションには頼れないので、別のリカバリ方法が必要になります。ここでは詳しく述べませんが、Sagaというテクニックがあります。Sagaは、ACIDの分離性がないことによって引き起こされる並行性の問題を防止・軽減する設計テクニックです。興味がある人は以下の書籍の4章を読むとよいです。こちらの文脈を辿っていくとマイクロサービスなどの分散システムではACIDトランザクションではなく分散トランザクションが必要であることに気づくでしょう。逆にいえば、マイクロサービスではない環境下では、暗黙の大前提としてRDBだからDBのトランザクションを何の疑問もなく使えてしまうというのはあるのではないでしょうか。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086JJNDKS/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/61Sj9ZggwML.jpg" class="hatena-asin-detail-image" alt="マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ" title="マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086JJNDKS/j5ik2o.me-22/">マイクロサービスパターン[実践的システムデザインのためのコード解説] impress top gearシリーズ</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Chris%20Richardson" class="keyword">Chris Richardson</a>,<a href="http://d.hatena.ne.jp/keyword/%C4%B9%C8%F8%B9%E2%B9%B0" class="keyword">長尾高弘</a>,<a href="http://d.hatena.ne.jp/keyword/%C3%AE%DF%B7%B9%AD%B5%FC" class="keyword">樽澤広亨</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2020/03/23</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>長々と書きましたが、トランザクションを同じにするということはモジュールとしても強い依存関係をつくりあげてしまうということではないかと思います。モジュラリティとか疎結合のために集約単位にトランザクションを分けているといってもいいでしょう。</p> <p>追記: 2021/3/15</p> j5ik2o CQRSはなぜEvent Sourcingになってしまうのか hatenablog://entry/26006613629308594 2020-09-18T17:26:12+09:00 2020-09-18T17:26:12+09:00 CQRSはなぜEvent Sourcingになってしまうのか、まとめてみたいと思います。 なぜまとめるか、それはCQRSにとってEvent Sourcingはオプションだと誤解されている方が多いからです。この記事を書いてる本人も最初はそう思っていましたが、実際に開発・運用を経験してみるとCQRSにとってEvent Sourcingはほぼ必須で、認識を改めるべきだと気づきました。なので、原義に基づいたうえで、Event SourcingではないCQRSがなぜよくない設計になるのか解説します。 <p>CQRSはなぜEvent Sourcingになってしまうのか、まとめてみたいと思います。</p> <p>なぜまとめるか、それはCQRSにとってEvent Sourcingはオプションだと誤解されている方が多いからです。この記事を書いてる本人も最初はそう思っていましたが、実際に開発・運用を経験してみるとCQRSにとってEvent Sourcingはほぼ必須で、認識を改めるべきだと気づきました。なので、原義に基づいたうえで、Event SourcingではないCQRSがなぜよくない設計になるのか解説します。</p> <p>その前に松岡さんの記事について。</p> <h2>CQRSの領域ではモデルを完全に分ける</h2> <p>松岡さんの記事には”CQRSはモデルを完全に分ける必要はない”と書かれていますが、知識がないと誤解しがちですが文字のまま意味を取るといけません。こちらの言及は、システムのうち、モデルをC/Qに分割するCQRS領域とモデルを分割しない非CQRS領域に分けることができて、CQRSを部分導入できますよいう意味だと思います(下図参照)。モデルを分けなくていいのはあくまで非CQRS領域です。間違っても「CQRSはモデルを分割しなくてもいいんだ!」という解釈をしてはいけません。その解釈はもはやCQRSではありません。</p> <blockquote cite="https://little-hands.hatenablog.com/entry/2019/12/02/cqrs" data-uuid="26006613629285034"><p>部分的導入について</p><p> 重要なことですが、CQRSは部分的な導入が可能です。 つまり、「参照用モデルと更新用モデルを完全に分ける必要はない」ということです。 どちらかというと、「必要なところだけ参照に特化したモデルを導入する」といった使い方が適切でしょう。</p><cite><a href="https://little-hands.hatenablog.com/entry/2019/12/02/cqrs">CQRS実践入門 [ドメイン駆動設計] - little hands&#x27; lab</a></cite></blockquote> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200918/20200918095049.png" alt="f:id:j5ik2o:20200918095049p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>CQRSはモデルだけでなくモジュールも分割する</h2> <p>さらにモデルを分けるだけではなく、そのモデルを内包するトップレベルのモジュール同士を分離というか、隔離しなければなりません。という話はこちらの記事を参照してください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F2020%2F09%2F18%2F084914" title="CQRSはモデルだけでなくモジュールも分割する - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.j5ik2o.me/entry/2020/09/18/084914">blog.j5ik2o.me</a></cite></p> <p>シンプルなCQRSの概念図は以下です。上述したようにC/Qは隔離されています。データベースのところはRDB前提で書いていますが、実際の運用ではこうしません。想像しやすいように書いてるだけで、厳密には書いてないと思ってください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200918/20200918154402.png" alt="f:id:j5ik2o:20200918154402p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>Event SourcingではないCQRSを考える</h2> <p>この記事では、リポジトリからリード専用DAOまでの設計を考えていきましょう。最初はEvent Sourcingを考えずに素直に状態に基づくCRUD脳で考えていきます。</p> <p>具体的なコードのイメージは以下の記事を読むといいです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F2020%2F09%2F16%2F162037" title="具体的な実装コードからEvent Sourcingを理解する - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.j5ik2o.me/entry/2020/09/16/162037">blog.j5ik2o.me</a></cite></p> <p>対象ドメインはショッピングカート前提で考えます。</p> <h3>コマンドプロセッサ実装</h3> <p>コマンドプロセッサの実装例です。コマンド側のユースケースクラスだと思ってください。 カートオブジェクトとリポジトリの操作をCRUDで考えると以下のように実装になるでしょう。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> AddCartItemCommandProcessor(cartRepository: CartRepository) { <span class="synIdentifier"> def</span> execute(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synComment">// 最新の集約(グローバルなエンティティ)をストレージから取得する</span> <span class="synType">val</span> cart = cartRepository.findById(cartId) <span class="synComment">// ロジック実行: 予算超過ならカートオブジェクトが商品の追加を拒否する!</span> <span class="synType">val</span> newCart = cart.addItem(itemId, num) <span class="synComment">// 更新された最新状態をストレージに保存する</span> cartRepository.store(newCart) } } </pre> <p>まずC側のテーブル(A)とQ側のテーブル(B)は形式が異なるのですが、まずリポジトリによってカートオブジェクトは以下のテーブルに永続化されます。 (カート内のアイテムは別テーブルにマッピングされますが、リポジトリで<code>findAllByItemId</code>のような逆引き検索がなければ、子テーブルは不要かもしれませんが、一旦わかりやすさを優先して子テーブルを設けています)</p> <ul> <li>C側のテーブル(A) <ul> <li>カートテーブル <ul> <li>カートID(PK)</li> <li>顧客アカウントID</li> <li>上限予算金額</li> <li>作成日時</li> </ul> </li> <li>カートアイテムテーブル <ul> <li>カートアイテムID(PK)</li> <li>カートID(FK)</li> <li>商品ID</li> <li>数量</li> <li>作成日時</li> </ul> </li> </ul> </li> </ul> <p>このように永続化されますが、ビジネスロジックで計算する値もあります。カートの合計金額です。 あと、商品の単価は外部のオブジェクトから提供されるので、カートオブジェクト内部で保持してません。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Cart(id: CartId, userAccountId, upperLimitPrice: Price, items: CartItems, createAt: Instant) { <span class="synComment">// 合計金額計算</span> <span class="synIdentifier"> def</span> totalPrice(priceResolver: ItemId =&gt; Price): Price = { items.fold(Price.zero){ (t, item) =&gt; t + item.price(priceResolver) } } } <span class="synType">case</span> <span class="synType">class</span> CartItem(id: CartItemId, itemId: ItemId, quantity: Quantity, createAt: Instant) { <span class="synComment">// 価格計算メソッド</span> <span class="synIdentifier"> def</span> price(priceResolver: ItemId =&gt; Price): Price = priceResolver(itemId) * quantity } </pre> <h3>クエリプロセッサの実装例</h3> <p>クエリ側のユースケースクラス相当の実装です。 クエリ側のリード専用DAOとリードモデルであるDTOは以下のようなイメージになります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> GetCartQueryProcessor(cartDao: CartDao) { <span class="synIdentifier"> def</span> execute(cartId: Long): Vector[CartDto] = { cartDao.findByCartId(cartId) } } <span class="synType">case</span> <span class="synType">class</span> CartDto ( id: Long, userAccountId: Long, userAccountName: <span class="synConstant">String</span>, upperLimitPrice: <span class="synConstant">String</span>, totalPrice: <span class="synConstant">String</span> items: Seq[CartItemDto] ) <span class="synType">case</span> <span class="synType">class</span> CartItemDto( id: Long, itemId: Long, itemName: <span class="synConstant">String</span>, unitPrice: <span class="synConstant">String</span> quantity: Long, price: <span class="synConstant">String</span>) </pre> <p>対応するテーブルはDTOと同形で、C側と比べて非正規型になっています。C型と似てるようで似てない。別物です。あとで説明しますが、(★)のところが厄介ですね。</p> <ul> <li>Q側のテーブル(B) <ul> <li>カートテーブル <ul> <li>カートID(PK)</li> <li>顧客アカウントID</li> <li>顧客アカウント名(●)</li> <li>上限予算金額</li> <li>合計金額(★)</li> </ul> </li> <li>カートアイテムテーブル <ul> <li>カートアイテムID(PK)</li> <li>商品ID</li> <li>商品名(●)</li> <li>数量</li> <li>単価(★)</li> <li>価格(★)</li> </ul> </li> </ul> </li> </ul> <p>補足:</p> <p>(●)も面倒ですが、これは他の集約(エンティティ)のデータとして永続化されていて、SQLで解決できる可能性があります。</p> <p>めちゃくちゃ単純ですが、これでC/Qが独立しています。が、C/Qを連携させなければ更新されたデータを読み込むことができません。上図の(C)の部分です。</p> <h2>コマンド側からクエリ側に変更をどう通知するか</h2> <p>はい。やっと本題です。コマンド側からクエリ側に変更をどう通知するか、(C)部分の実装について以下に挙げてみます。</p> <ul> <li>テーブル(A)の更新トリガ時の変更を、SQLを使ってテーブル(B)を書き込む</li> <li>プログラムで、テーブル(A)を読み込み、その結果をテーブル(B)に書き込む</li> </ul> <h3>テーブル(A)の更新トリガ時の変更を、SQLを使ってテーブル(B)を書き込む</h3> <p>トリガー例はあえて書きませんが、更新データを受信してテーブル(B)のレコードを<strong>まるっと全カラム</strong>書くとよいですね。欲を言えば、更新されたレコードだけを検知したいですが、全カラム更新するしかないと思います。まぁリポジトリで集約(エンティティ)を保存する時点で全カラム更新なら、ここで差分更新を頑張っても釣り合わないですね。と、C側で静的に保持されているデータをQ側に書くだけならトリガーでよさそうです。</p> <p><strong>問題は(★)の部分。(★)はC側のデータベース上にはありません。ドメインオブジェクトの振る舞いによって計算される値だからです。</strong> まさかドメインロジックと同じ仕様のSQLを書くとかないですよね…。まぁ振る舞いのない貧血症オブジェクトしかないなら、特に問題はないでしょうね(皮肉) あ、例えトランザクションスクリプトであっても、ロジッククラスの助けなしに計算できない値があるとしたら?同じことですよね?</p> <p>苦肉の策としては計算した結果もC側のテーブルに書き込むという方法です。カートオブジェクトだと、<code>priceResolver</code>引数が外部のオブジェクトなので、まずこれが永続化時にないとこういったことはできません。リポジトリのI/Fを <code>def store(cart: Cart, priceResolver: ItemId =&gt; Price): Unit</code>するなど設計に歪みがでることがあります。つまり、リポジトリ内部でビジネスロジックを起動するということです。リポジトリの責務違反…</p> <p>そもそも、トリガーという時点でイベントに頼ってるような印象ですが、こういう計算で導出される値の扱いは難しいです。</p> <h3>プログラムで、テーブル(A)を読み込み、その結果をテーブル(B)に書き込む</h3> <p>(★)の問題があるから、一度ドメインオブジェクトに計算させてから、テーブル(B)に書けばよいことになります。これならQ側にもれなくデータを転送できそうです。が、変更のトリガーってどう検知しましょうか? まさか、C側のDBに変更されているかどうかわからないけど、ポーリングします?対象の集約はどうしますか?集約が大量にもあっても、それら全部ポーリングします?全然機能するイメージがないですね…。<strong>やっぱり最新状態を手に入れるにしても、更新イベントが必要なんです</strong>。</p> <p>補足:</p> <p>Q側のテーブルをC側のVIEWにすればよいのでは?というアイデアもあると思いますが、(★)の問題はSQLで解決できません。結局ドメインオブジェクトに頼ることになります。</p> <h2>結局 Event Sourcingにたどり着く</h2> <p>上述した2パターンのよいところを組み合わせるとよさそうです。C側のテーブル(A)以外に更新イベントを通知するキューを用意します。以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200918/20200918165640.png" alt="f:id:j5ik2o:20200918165640p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>リポジトリ側でC側のテーブル(A)を更新する以外に、更新イベントを通知するためにキューにもイベントを書き込みます。そのうえで、リードモデル更新プロセスがそのイベントを受け取り、テーブル(A)から集約(エンティティ)を再現し、Q側のテーブル(B)に書き込めばよいです。まぁ、いちいちC側のテーブル(A) を読まずに、変更内容を更新イベントに載せたらどうかという発想もありますね。</p> <p>一見、これで問題がないようにみえますが、C側のリポジトリは二つのストレージに書き込みを行っています。テーブル(A)はRDB、更新イベントを伝えるキューはRDBは不向きでしょう。AWSであればSQSのようなものを想像するでしょう。となると、同一トランザクションにはなりません。はい。<strong>高いコストを払う可能性がある、ダブルコミット問題が生じます。以下の記事でもダブルコミットは避けましょうという話をしました。</strong></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F2020%2F09%2F16%2F162037" title="具体的な実装コードからEvent Sourcingを理解する - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.j5ik2o.me/entry/2020/09/16/162037">blog.j5ik2o.me</a></cite></p> <p>ダブルコミットを避けるためにイベントを真のデータソースにします。C側のドメイン状態はイベントから作るようにします。そして、C側のDBはもはやRDBである必要はありません。上記記事にも書きましたが、集約で発生するイベントを追記したり、集約IDごとのイベント列を読み込めれば十分だからです。実際はNoSQL(KVS)を使うことが多いです。AWSのDynamoDBであればDynamoDB Streamsからスケーラブルにイベントを読み込めます。これがEvent Sourcingですね。結局ここにたどり着きます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200918/20200918170500.png" alt="f:id:j5ik2o:20200918170500p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>CQRSシステムのほとんどがEvent Sourcingを採用しているそうです(Lightbend社調べ)。その理由を分かっていただけたのではないでしょうか。</p> j5ik2o CQRSはモデルだけでなくモジュールも分割する hatenablog://entry/26006613629256780 2020-09-18T08:49:14+09:00 2020-09-18T08:49:14+09:00 掲題についての議論です。 僕の結論はこちら。"モジュール分割せずにモデルを分割するだけ"はCQRSと呼んでいけないのでは?まず原義からちゃんと把握しようというお話。 <p>掲題についての議論です。</p> <p>僕の結論はこちら。"モジュール分割せずにモデルを分割するだけ"はCQRSと呼んでいけないのでは?まず原義からちゃんと把握しようというお話。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">CQRSするにはC/Qは独立しないとダメなので、それをどう連携させるかすごい難しい課題。ESなしでは独立させて統合するのはほぼ難しいと思います <a href="https://t.co/Ta0CYujHJx">https://t.co/Ta0CYujHJx</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1306527489078902784?ref_src=twsrc%5Etfw">2020年9月17日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsipadan2003.blogspot.com%2F2013%2F12%2Fcqrs.html" title="CQRSの和訳" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://sipadan2003.blogspot.com/2013/12/cqrs.html">sipadan2003.blogspot.com</a></cite></p> <p>Gregさんの原義によると</p> <blockquote><p>Origins</p> <p>起源</p> <p>Command and Query Responsibility Segregation uses the same definition of Commands and Queries that Meyer used and maintains the viewpoint that they should be pure. The fundamental difference is that in CQRS objects are split into two objects, one containing the Commands one containing the Queries.</p> <p>コマンドとクエリの責任の分離は、Meyer氏が使用していたものと同じ定義を使用しており、純粋なものであるべきという視点を維持しています。根本的な違いは、CQRSではオブジェクトが2つのオブジェクトに分割され、1つはコマンドを含むオブジェクト、もう1つはクエリを含むオブジェクトに分割されます。</p></blockquote> <p>ここだけ読むと「そうかオブジェクトを分ければいいだけか」となりますが、</p> <blockquote><p>The Query Side</p> <p>クエリ側</p> <p>After CQRS has been applied there is a natural boundary. Separate paths have been made explicit. It makes a lot of sense now to not use the domain to project DTOs. Instead it is possible to introduce a new way of projecting DTOs</p> <p>CQRSを適用した後には、自然な境界があります。別々のパスが明示されています。DTOを投影するためにドメインを使用しないことは、今では非常に理にかなっています。その代わりに、DTOを投影する新しい方法を導入することができます。</p></blockquote> <p><strong>ドメインを利用するコマンド側とDTOを利用するクエリ側には境界があるとされている。原義のPDFでも大半の部分でこの隔離法を説いている。</strong>以下は抜粋。</p> <blockquote><p>In the “Stereotypical Architecture” the domain was handling both Commands and Queries, this caused many issues within the domain itself.</p> <p>ステレオタイプのアーキテクチャでは、ドメインはコマンドとクエリの両方を処理していたため、ドメイン自体に多くの問題が発生しました。</p> <p>Once the read layer has been separated the domain will only focus on the processing of Commands. These issues also suddenly go away. Domain objects suddenly no longer have a need to expose internal state, repositories have very few if any query methods aside from GetById, and a more behavioral focus can be had on Aggregate boundaries.</p> <p>読み取り層が分離されると、ドメインはCommandsの処理のみに集中するようになります。 これらの問題も突然なくなります。 ドメインオブジェクトは突然内部状態を公開する必要がなくなり、リポジトリにはGetById以外のクエリメソッドがある場合はほとんどありません。</p></blockquote> <p>まとめると、モジュールレベルでCとQが分かれているということです。簡単にCQRSとは何かを説明するなら以下となる。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">CQRSはCモジュールとQモジュールを完全に分離というより隔離。お互いに実装レベルでも知識レベルでも依存してはいけない。その上で中継役としての第3のモジュールで統合する。これに該当しないものはCQRSにあらず。モデルだけ分けるがCQRSじゃない</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1306577161499758594?ref_src=twsrc%5Etfw">2020年9月17日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>たぶん、C/Qをモジュールとして隔離しなくてもモデルだけ分ければいい派はいるでしょう。いてもいいですが、僕は<strong>それを原義に照らしてCQRSと言ったらダメでしょう派</strong>です。GregさんがSegregationという強い言葉をわざわざ選んだ理由を考えるべき。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">モデルを分ける・メソッドを分ける部分だけを初歩のCQRSと理解している派いそうですがSeparation(分離)じゃなくSegregation(隔離)ですよ。モデルを分ける程度ならこんな強い言葉使う必要ない。CQSでいい。原義の大部分でもこの隔離法を説いている。モジュール分割レベルで理解することが妥当だと思う</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1306717223604465664?ref_src=twsrc%5Etfw">2020年9月17日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>原義をこのように理解しているが、このままで終わると原理主義と言われるので、次の記事で背理法的にモジュール分割せずにモデルだけ分ける立場でそれが、なぜ良い設計にならないか書いてみよう。</p> j5ik2o 具体的な実装コードからEvent Sourcingを理解する hatenablog://entry/26006613628500217 2020-09-16T16:20:37+09:00 2020-09-16T16:20:37+09:00 DDD Community JPのほうでCQRS/Event Sourcingについて少し盛り上がったので、どういう議論をしたかまとめるのと同時に補足も追加しました。ちなみに、Event Sourcingが主題ですが、CQRSも前提として関係します。その想定で読んでいただければと。 発端はこのツイート。 これはEvent Sourcingじゃないと無理ですね。状態に基づく限り、ストリーム処理は難しいです https://t.co/prB16GJC5q— かとじゅん (@j5ik2o) 2020年9月14日 僕が引用したツイートは松岡さんの質問箱に対するリアクションです。その質問箱に寄せられた質… <p><a href="https://little-hands.hatenablog.com/entry/dddcj">DDD Community JP</a>のほうでCQRS/Event Sourcingについて少し盛り上がったので、どういう議論をしたかまとめるのと同時に補足も追加しました。ちなみに、Event Sourcingが主題ですが、CQRSも前提として関係します。その想定で読んでいただければと。</p> <p>発端はこのツイート。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これはEvent Sourcingじゃないと無理ですね。状態に基づく限り、ストリーム処理は難しいです <a href="https://t.co/prB16GJC5q">https://t.co/prB16GJC5q</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1305480847747735552?ref_src=twsrc%5Etfw">2020年9月14日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>僕が引用したツイートは<a href="https://twitter.com/little_hand_s">松岡さん</a>の質問箱に対するリアクションです。その質問箱に寄せられた質問は以下。</p> <blockquote><p>ストリームを開いてから閉じるまでのデータが変化する毎にUIで表示したい場合、DDDではどのように設計したら良いでしょうか? DDDのリポジトリは1つのリクエストに対して1つのリクエストを返すイメージがあり、ストリームをどのような形で扱ったら良いのかつかめずにいます。</p></blockquote> <p>かなり抽象的な質問なのでいろいろ確認しないとわからないのですが、CQRS/Event Sourcingならどう解決するか考えてみました。こういった説明が抽象的でわかりにくいという声をよく聞くので、具体的なコード<a href="#f-991131b9" name="fn-991131b9" title="とはいっても概念を説明するための疑似コードだと思ってください">*1</a>を交えた説明になっています。 注意事項としては、CQRS/Event Sourcingには実装コストが掛かります<a href="#f-28f6994d" name="fn-28f6994d" title="CQRS/Event Sourcingそのものというより、分散システムに起因する難しさですが…">*2</a>。それほどのコストを掛ける価値があるシステムなのかよく考えてくださいというのは前提としてありますので、その点は注意して読んでください。</p> <h2>CRUDは最新状態に基づく設計スタイル</h2> <p>ドメイン駆動設計を前提にAPIサーバなどを設計した場合、APIはリクエスト・レスポンスでCRUDすることが多いです。この場合、アプリケーション内部でリポジトリを使ってドメインオブジェクトを取り出すロジックになるのでリポジトリ操作もリクエスト・レスポンスに対応します。そしてCRUDは最新状態に基づく設計スタイルです。</p> <p>ショッピングカート<a href="#f-4994d46d" name="fn-4994d46d" title="データベースに永続化される前提のカートと思ってください">*3</a>に商品を追加するユースケースで考えると以下のようになります。ビジネスルールを守るのがドメインオブジェクトで重要な役割を持っています。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> AddCartItemUseCase(cartRepository: CartRepository) { <span class="synIdentifier"> def</span> execute(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synComment">// 最新の集約(グローバルなエンティティ)をストレージから取得する</span> <span class="synType">val</span> cart = cartRepository.findById(cartId) <span class="synComment">// ロジック実行: 予算超過ならカートオブジェクトが商品の追加を拒否する!</span> <span class="synType">val</span> newCart = cart.addItem(itemId, num) <span class="synComment">// 更新された最新状態をストレージに保存する</span> cartRepository.store(newCart) } } </pre> <p>カート内の商品を取得するユースケースは以下のようになるでしょう。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> GetCartItemsUseCase(cartRepository: CartRepository) { <span class="synIdentifier"> def</span> execute(userAccountId: UserAccountId): Vector[CartItem] = { cartRepository.findByUserAccountId(userAccountId).map{ cart =&gt; cart.items } } } </pre> <p>質問にあったように、リクエストに対してレスポンスを返すスタイルになっています。いわゆるPULL側のAPIです。REST APIであればこれで何ら問題でしょう。</p> <h2>イベントをPub/Subする</h2> <p>今回の質問は、ストリーム接続したいということです。漠然と「ストリームで」という話ですが、扱うデータは何でしょうか。上記の質問の場合は、ドメインの最新状態をリクエスト・レスポンス型で返しますが、ストリームでは最新状態を返すのでしょうか?ほとんどケースではそうではなく、そのとき起こった出来事であるイベントをクライアントに返すことになります。</p> <p>例えば、上記のAddCartItemUseCase#executeが実行されると、商品追加イベントが発生し、ストリームに接続するクライアントにそのイベントが通知されるという具合になります。サーバからクライアントへデータがPUSHされる形になり、通常はストリームの接続は永続的になります。</p> <p>サーバ側のエンドポイントの実装ではイベントのサブスクライバを作ります。サブスクライバでカートイベントを受信したら、ストリームに流すだけです。以下はakka-httpでSSE(Server Sent Event)を行う場合の例です。イベントどのサーバからでも受信できるようにストレージからWrite/Readすることになると思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>path(<span class="synConstant">&quot;events&quot;</span> / LongNumber ) { cartId =&gt; get { complete { cartEventSubscriber.subscribe(cartId) .map(event =&gt; ServerSentEvent(event)) .keepAlive(<span class="synConstant">1.</span>second, () =&gt; ServerSentEvent.heartbeat) } } } </pre> <p>さて、ドメイン状態が変化したときに、イベントをパブリッシュする実装はどうしたらよいか。ユースケースから上記で説明したストレージへイベントを書き込むことになると思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// CRUD前提のユースケース実装</span> <span class="synType">class</span> AddCartItemUseCase(cartRepository: CartRepository) { <span class="synIdentifier"> def</span> execute(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synType">val</span> cart = cartRepository.findById(cartId) <span class="synType">val</span> newCart = cart.addItem(itemId, num) cartRepository.store(newCart) <span class="synComment">// (1)</span> cartEventService.publish(CartItemAddedEvent(cartId, itemId, num)) <span class="synComment">// (2)</span> } } </pre> <p>これはいわゆるPub/Subの仕組みです。で、勘違いされている方が多いのですが、<strong>CQRS/Event Sourcingとは直接関係ない</strong>です。混同しないようにしましょう。上記はイベントを送受信しているだけでCQRS/Event Sourcingではありません。</p> <p>ところで、上記コードの(1)の状態更新に加え(2)のイベントの書き込みが増えたわけですが、ここで違和感を覚えます。(1)と(2)ってストレージがまず違うので同一トランザクションにできません。つまり<strong>2相コミット</strong>とか<strong>ダブルコミット</strong>というやつです。不整合が起きた場合、リカバリが面倒なやつです。例えば、(1)がコミットされたあとに、(2)が失敗したらどうなるか。(1)の完了した書き込みを削除するか、(2)の書き込みが成功するまでリトライするかです。複雑なリカバリ処理を自前で実装するハメになります。</p> <h3>ダブルコミットは避ける</h3> <p>なので、ダブルコミットはできる限り避けましょうです。以下は古典ですが読むことをお勧めします。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fameblo.jp%2Fouobpo%2Fentry-10070039150.html" title="『翻訳: スターバックスは2フェーズコミットを使わない』" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ameblo.jp/ouobpo/entry-10070039150.html">ameblo.jp</a></cite></p> <p>CQRS/Event Sourcingの考案者であるGregさんもダブルコミットを推奨していないようです:(興味あれば<a href="https://sipadan2003.blogspot.com/2013/12/cqrs.html">こちら</a>も参照ください)</p> <blockquote><p>The two-phase commit can be expensive but for low latency systems there is a larger problem when dealing with this situation. Generally the queue itself is persistent so the event becomes written on disk twice in the two-phase commit, once to the Event Storage and once to the persistent queue. Given for most systems having dual writes is not that important but if you have low latency requirements it can become quite an expensive operation as it will also force seeks on the disk.</p></blockquote> <p>”二相コミットはコストがかかりますが、低レイテンシのシステムでは、この状況を扱う際にはより大きな問題があります。一般的に、キュー自体は永続的なので、イベントは二段階のコミットで二度ディスク上に書き込まれます。ほとんどのシステムでは、二重書き込みを行うことはそれほど重要ではありませんが、もし低レイテンシの要件がある場合には、 ディスク上でのシークを強制的に行うことになるため、非常に高価な操作になる可能性があります。”</p> <blockquote><p>The database would insure that the values of sequence number would be unique and incrementing, this can be easily done using an auto-incrementing type. Because the values are unique and incrementing a secondary process can chase the Events table, publishing the events off to their queue. The chasing process would simply have to store the value of the sequence number of the last event it had processed, it could even update this value with a two-phase commit bringing the update and the publish to the queue into the same transaction.</p></blockquote> <p>”データベースは、シーケンス番号の値が一意でインクリメントされることを保証しますが、これはAUTO INCREMENT型を使用して簡単に行うことができます。値が一意でインクリメントされているので、セカンダリプロセスはイベントテーブルを追いかけることができ、イベントをキューに公開することができます。追いかけるプロセスは、単に最後に処理したイベントのシーケンス番号の値を保存しておく必要があります。 この値を更新するには、二段階のコミットで更新とキューへの公開を同じトランザクションにすることもできます。”</p> <p>ではどうしたらよいか。(1)と(2)は同時に二つ処理せずに、(2)のイベントだけを書き込むようにします。(1)の状態は(2)のイベントを基に別のプロセスで作り出します。これが<strong>CQRS/Event Sourcing</strong>で、すべての状態はイベントから導出できるようにします。やっと本題。</p> <p>しかし、以下の(0)から(1)の処理は、最新状態に基づいているのでそのままでは使えません。設計を見直す必要があります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synIdentifier">def</span> addCartItem(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synComment">// val cart = cartRepository.findById(cartId) // (0)</span> <span class="synComment">// val newCart = cart.addItem(itemId, num)</span> <span class="synComment">// cartRepository.store(newCart) // (1)</span> <span class="synComment">// イベントの書き込みはこれでよいが、↑の処理をどうしたらよいか </span> cartEventService.publish(ItemAdded(cartId, itemId, num)) <span class="synComment">// (2)</span> } </pre> <h2>Event Sourcingでプログラミングモデルがどう変わるか</h2> <p>前述しましたが、CQRS/Event Sourcingは、簡単にいうとイベントから状態を導出するアーキテクチャです。絵で説明したほうがわかりやすいので結論となる図は以下。どーん。「え、複雑だなぁ」と思いますよね。単純なCRUDよりはそりゃ難しくなりますよ…。その代わり耐障害性やスケーラビリティが向上するのです…。一つのアプリケーションのように見えますが、コマンド側とクエリ側は分離してデプロイすることが可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200916/20200916090416.png" alt="f:id:j5ik2o:20200916090416p:plain" title="f:id:j5ik2o:20200916090416p:plain" class="hatena-fotolife" itemprop="image"></span></p> <h3>イベントからドメインオブジェクトをリプレイする</h3> <p>ジャーナルDBと呼ばれるデータベースに、カートオブジェクトが発行したカートのイベント(以下、カートイベント)が永続化されます。このイベントを使ってコマンド側にドメインオブジェクトを、クエリ側にリードモデルを構築します。(リードモデルの形式は何でもOKです。リードDBはジャーナルDBと物理的に分けるかどうかはここでは問いません。まず概念的に理解したほうがいいでしょう)。つまり状態といえるものが2種類あるわけです。リードモデルは後述しますが、まずはドメインオブジェクトから。</p> <p>以下のように、永続された複数のイベントがあれば、最新のカートオブジェクトを作る(リプレイ)ことが可能です。(1)の部分でイベントを読み込み、(2)で最新のカートオブジェクトを作り出します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synIdentifier">def</span> addCartItem(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synType">val</span> allEvents = cartEventService.getAllEventsById(cartId) <span class="synComment">// (1)</span> <span class="synType">val</span> cart = replayFunction(allEvents) <span class="synComment">// (2)</span> <span class="synType">val</span> itemAdded = cart.addItem(itemId, num) cartEventService.publish(itemAdded) } </pre> <h3>スナップショットでリプレイ時間を短縮する</h3> <p>前述のコードみてこれはひどい設計だと思ったのではないでしょうか。わかります。僕も最初そう思いました…。容易に想像がつきますが、永続化されたカートイベントの件数に比例してリプレイのパフォーマンスが悪化します。なので、対策として、永続化するイベントN件ごとにカートオブジェクトの状態をスナップショットDBに保存しておき、カートオブジェクトをリプレイする際に、最新スナップショット+それ以降に発生した差分イベントを読み込んで、高速にリプレイします。つまり、N件以上イベントがあっても、最新のスナップショットと最大N - 1件分の読み込みで済ませることができます。とはいえ、Nを小さくすると読み込む差分イベントが少なくなる一方でイベント書き込み時のスナップショットを更新する頻度も高くなります。諸刃の剣ではありますが、バランスをうまくとる必要があります。Akka(akka-persistence)でもサポートされている機能でS3やDynamoDBをスナップショットDBとして使うことができます。</p> <p>以上のことを踏まえると以下のようなイメージになります。前提としてイベントやスナップショットには連番(seqNr)が振ってあるものとします。まず、最新のスナップショットとそのスナップショットのseqNr以降の差分イベントを取得します。それらを使って最新のカートオブジェクトをリプレイします。そしてオブジェクトオブジェクトの振る舞いを実行します。新しいカートオブジェクト、イベントを得ます。イベントはエンキューされますが、seqNrが1000件で割り切れるときに新しいカートオブジェクトを最新のスナップショットとして保存します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synIdentifier">def</span> addCartItem(cartId: CartId, itemId: ItemId, num: ItemNum): Unit = { <span class="synType">val</span> snapshot = loadSnapshot(cartId) <span class="synType">val</span> events = cartEventService.getEventsByIdWithSeqNr(cartId, snapshot.seqNr + <span class="synConstant">1</span>) <span class="synType">val</span> cart = replayFunction(snapshot, events) <span class="synType">val</span> (newCart, itemAdded) = cart.addItem(itemId, num) seqNr += <span class="synConstant">1</span> <span class="synStatement">if</span> (seqNr % <span class="synConstant">1000</span> == <span class="synConstant">0</span> ) { saveSnapshot(newCart, seqNr) } cartEventService.publish(itemAdded, seqNr) } </pre> <p>こうすることでリプレイ時間を短縮化できますが、CRUDのときと比較すると多くのデータを読み込んでいることがわかりますね…。もう少し効率的にならないかについては後ほど説明します。</p> <p>追記:</p> <p>ドメインオブジェクトはいちいちイベントからリプレイせずに、クエリ側のリードモデルをユースケースでうまく使えないの?と思うかもしれませんが、以下の問題があり難しいです。</p> <ol> <li>非正規化されたリードモデルは正規化されたドメインモデルではないので、代替はそもそも難しい。代替したとしたらDDDではなくなりそう</li> <li>C/Qがモジュールとして分離できなくなる。CとQが分離されていないならもはやCQRSではない?違う呼び方がよさそうです。</li> <li>リードDBへの書き込み時間分、最新状態を取得する時間が遅延する。つまり常に過去のデータをみていることになってしまいます。これは許容できる場合がありそう、要件によりますね</li> </ol> <h2>リードモデルの構築</h2> <p>コマンド側のドメインモデルの概要はつかめたと思いますが、クエリ側のリードモデルを考えてみましょう。</p> <p>このアーキテクチャパターンによって、イベントが唯一信頼できるデータソースとなり、そのイベントを基にクエリ側のリードモデルを構築します。イベントは不変という特徴を持ちます。なので、いつでも正しいリードモデルを作り出せます。極端な話、リードモデルの設計をミスってもイベントから作り直せます。もちろん、データ更新コストはかかりますが…。</p> <h3>コマンドとクエリで要件が非対称</h3> <p>そもそもなぜコマンドとクエリでモデルを分離するのか。理由は以下の表を参照してください。つまるところ要件が違うからです。人間が閲覧するようなシステム(SoEは特に顕著)だとまずクエリ側は非正規化データを求めます。</p> <table> <tr> <th width="20%">-</th><th width="40%">コマンド</th><th width="40%">クエリ</th> </tr> <tr> <th>一貫性/可用性</th><td>トランザクション整合性を使い一貫性を重視する</td><td>結果整合を使い可用性を重視する</td> </tr> <tr> <th>データ構造</th><td>トランザクション処理を行い正規化されたデータを保存することが好まれる(集約単位など)</td><td>非正規化したデータ形式を取得することが好まれる(クライント都合のレスポンスなど)</td> </tr> <tr> <th>スケーラビリティ</th><td>全体のリクエスト比率とごく少数のトランザクション処理しかしない。必ずしもスケーラビリティは重要ではない</td><td>全体のかなりのリクエスト比率を占める処理を行うため、クエリ側はスケーラビリティが重要</td> </tr> </table> <p>例えば、以下のような集約(グローバルなエンティティ)がある場合でも</p> <ul> <li>Employee { id, name, deptId }</li> <li>Department { id, name }</li> </ul> <p>画面は以下のように集約を二つ結合したようなデータを求めます。<code>deptId</code>だけではどの部署か分からないから名前も必要なのです。<a href="#f-0883fa5b" name="fn-0883fa5b" title="システムがユースケースのアクターならこういう考慮はいらないと思いますが、SoEだとどうしてもこういう要求は発生します">*4</a></p> <ul> <li>Employee { id, name, deptId, deptName }</li> </ul> <p>ドメインオブジェクトの構造にインパクトを与えるのは、このような正規と非正規の構造的な非対称性です。考えてみるとわかりますが、トランザクション処理と検索・レポートを両立するモデルの実現も理解も難しいのです。</p> <p>さて、コマンドとクエリを分離しないと実装上でどんな問題が起きるか気になるところですが、以下のようなものがあります。</p> <ul> <li>リポジトリのクエリメソッドが複雑になる</li> <li>N+1クエリが発生しやすい</li> <li>ドメインオブジェクトからDTOへの変換が非効率</li> </ul> <p>簡単に以下に説明します。</p> <h4>クエリ要件をリポジトリで満たそうとしてメソッドが複雑になる</h4> <p>集約(グローバルなエンティティ)は識別のためにIDを持つため、IDから集約本体を引き当てることができます。これはある意味正引きです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> employee = employeeRepository.findById(employeeId) </pre> <p>しかし、集約の他の属性(名前や他のIDなど)を使って集約(単体もしくは複数)を引き当てたい場合があります。セカンダリインデックスを使うような逆引きですね。</p> <p>以下のようなコードを書いたことがありませんか? 僕は散々書いてきました。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> employees = employeeRepository .findByDeptIdsWithEmpNamePatterns(deptIds, empNamePatterns) </pre> <p>ドメインの振る舞いを起こすためというより、クライアントにクエリのレスポンスを返すためによく使っていました。これは本当にドメインの責務でしょうか。ドメインは振る舞いを起こすことが責務と捉えると、ドメインの振る舞いが伴わないこのようなリポジトリメソッドは関心が分離できていないのかもしれません。前述した <code>GetCartItemsUseCase</code>クラスの実装も本当にリポジトリの責務でいいか考える必要がありそうです。本当に集約をそのまま返すならば問題ないかもしれません。次のN+1クエリを含むような問題に発展する可能性がある場合は要注意です。</p> <h4>リポジトリでレスポンスを組み立てるとN+1クエリが発生しやすい</h4> <p>APIのレスポンスでは非正規化データを返すことがほとんどです。例えば、ホテルの予約情報一覧を作るために、予約集約に関連するホテル集約、顧客集約を解決しなければならないことがあります。ここでもドメインの振る舞いは起きません。クエリだからです。これは本当にリポジトリがやるべき仕事でしょうか。僕は過去にDBAに申し訳ないと思いながらこういうN+1が大量に発生するコードを書いていました…。そもそもレスポンスとして表形式を期待するならSQLとRDBにやらせるべきではないでしょうか。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>reservationRepository.findByIds(ids).map { reservation =&gt; <span class="synType">val</span> hotel = hotelRepository.findById(reservation.hotelId) <span class="synType">val</span> customer = customerRepository.findById(reservation.customerId) <span class="synStatement">new</span> ReservationDto(reservation, hotel.name, customer.name) } </pre> <h4>ドメインオブジェクトからDTOへの変換が非効率</h4> <p>APIのレスポンスでは集約の一部だけを求めることがあります。以下は恣意的な例ですが、UIに併せて顧客名一覧を返す処理です。リポジトリで得た集約の大部分を捨てて名前だけのリストを作ります。クエリ要件を満たすために大部分のリード結果が捨てられることになります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> customerNames = customerRepository.findByIds(ids).map { customer =&gt; customer.name } </pre> <p>もちろん、概念モデルの粒度が大きいとI/Oコストも比例して大きくなるので、まず先に概念の大きさに目を向けるべきですが、コマンド側のドメインでクエリを頑張りすぎるとこういうことになります。</p> <h3>イベントからリードモデルを作る</h3> <p>RDBにリードモデルを構築するなら、以下のように永続化されたイベントを受信して、SQLを実行するだけです(実際にはこのようなストリーム処理は単発ではなく後に発生するワークロードに備えて常時起動させる必要があります)。これはこれで簡単ですが、cartIdを指定して読み込むので、スケールしにくいです。具体的なIDを指定しないので、ある程度の並列度を維持し永続化されたイベントを全順序に読み込む必要があります。また、こういった動作をするコンシューマは障害発生時のリバランスが必要になります。僕のお勧めはKafkaを使うことです。これはこれで一本ブログ記事が書けるぐらいの知識量なので、別の機会に触れます。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>cartEventSubscriber.subscribe(cartId).runWith(Sink.foreach{ <span class="synType">case</span> e: CartCreated =&gt; insertCartTable(e) <span class="synType">case</span> e: CartItemAdded =&gt; insertCartItemTable(e) <span class="synType">case</span> e: CartItemNumUpdated =&gt; updateCartItemTable(e) <span class="synType">case</span> e: CartItemRemoved =&gt; deleteCartItemTable(e) <span class="synType">case</span> e: CartRemoved =&gt; deleteCartTable(e) }) </pre> <h3>まとめ。そして課題はまだある</h3> <p>ということでだいたいの雰囲気はつかめたのではないかと思います。とはいえ実装するうえではまだ課題があります。</p> <p>たとえば、<code>addCartItem</code>内部の処理をもう少し効率化できないかの課題については、まさに分散システムの問題に直面します。</p> <p>考えられる対策としては、カートオブジェクトをユースケースのスコープから外に出して、ワークロードがある間だけ起動させてキャッシュさせておく方法があります。リクエストごとに集約をリプレイしなくなるのでオーハーヘッドが軽減します。が、これは自前で実装するのは大変です。たとえば、ウェブサーバがスケールアウトして、ロードバランサから同じカートID向けの異なるリクエストが、複数サーバに飛んだこと想定してみてください。同一カートオブジェクトが別々のサーバでリプレイされ、異なるコマンドが受理されることで、同一IDなのに別々の状態が作り出されてしまいます。ある意味スプリットブレインな状態になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200916/20200916113429.png" alt="f:id:j5ik2o:20200916113429p:plain" title="f:id:j5ik2o:20200916113429p:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>下図はイメージなので全然正確じゃないという前提ですが、前述のようなスプリットブレインにならないようにするには、集約(グローバルなエンティティ)を以下のようにシャーディングして、コマンドをルーティングするとよいわけです。つまり、同一IDの集約はクラスタ全体で1個しかないように配置すればよいでしょう…。クラスタ上で脳みそが一つしかないのでスプリットしようがないという話です。と、ここまで考えてめちゃくちゃ大変だと想像できたと思います。さらにサーバが故障した場合に別サーバに集約をテイクオーバさせるなど自前で実装したくない…。なので、AkkaやErlangなど分散システムのフレームワークなしでこういうことは辞めましょう…。AkkaではActorという軽量プロセスをクラスタ上に分散させることができます。こういう基盤なしに無茶は辞めよう…。<a href="#f-6d5cf271" name="fn-6d5cf271" title="分散システムに関係する話は途端に難しくなりがち。わかりやすく書いたつもりですが、十分に伝わらないかもしれません…ご容赦を…">*5</a></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200916/20200916165624.png" alt="f:id:j5ik2o:20200916165624p:plain" title="f:id:j5ik2o:20200916165624p:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>興味があれば以下参照。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fakka.io%2F" title="Akka: build concurrent, distributed, and resilient message-driven applications for Java and Scala | Akka" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://akka.io/">akka.io</a></cite></p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B076Z8SK45/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/61O5YIgV0gL._SL160_.jpg" class="hatena-asin-detail-image" alt="Akka実践バイブル アクターモデルによる並行・分散システムの実現" title="Akka実践バイブル アクターモデルによる並行・分散システムの実現"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B076Z8SK45/j5ik2o.me-22/">Akka実践バイブル アクターモデルによる並行・分散システムの実現</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%E2%A5%F3%A5%C9%A1%A6%A5%ED%A5%B9%A5%C6%A5%F3%A5%D0%A1%BC%A5%B0" class="keyword">レイモンド・ロステンバーグ</a>,<a href="http://d.hatena.ne.jp/keyword/%A5%ED%A5%D6%A1%A6%A5%D0%A5%C3%A5%AB%A1%BC" class="keyword">ロブ・バッカー</a>,<a href="http://d.hatena.ne.jp/keyword/%A5%ED%A5%D6%A1%A6%A5%A6%A5%A3%A5%EA%A5%A2%A5%E0%A5%BA" class="keyword">ロブ・ウィリアムズ</a>,<a href="http://d.hatena.ne.jp/keyword/%C1%B0%BD%D0%20%CD%B4%B8%E3" class="keyword">前出 祐吾</a>,<a href="http://d.hatena.ne.jp/keyword/%BA%AC%CD%E8%20%CF%C2%B5%B1" class="keyword">根来 和輝</a>,<a href="http://d.hatena.ne.jp/keyword/%C5%A3%B2%B0%20%C6%F3%CF%BA" class="keyword">釘屋 二郎</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/12/13</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>正直この分野は沼感がありますが、参考になれば幸いです。</p> <div class="footnote"> <p class="footnote"><a href="#fn-991131b9" name="f-991131b9" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">とはいっても概念を説明するための疑似コードだと思ってください</span></p> <p class="footnote"><a href="#fn-28f6994d" name="f-28f6994d" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">CQRS/Event Sourcingそのものというより、分散システムに起因する難しさですが…</span></p> <p class="footnote"><a href="#fn-4994d46d" name="f-4994d46d" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">データベースに永続化される前提のカートと思ってください</span></p> <p class="footnote"><a href="#fn-0883fa5b" name="f-0883fa5b" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">システムがユースケースのアクターならこういう考慮はいらないと思いますが、SoEだとどうしてもこういう要求は発生します</span></p> <p class="footnote"><a href="#fn-6d5cf271" name="f-6d5cf271" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">分散システムに関係する話は途端に難しくなりがち。わかりやすく書いたつもりですが、十分に伝わらないかもしれません…ご容赦を…</span></p> </div> j5ik2o 外部キー制約は何も考えずに適用するとよくない hatenablog://entry/26006613585633643 2020-06-16T10:53:11+09:00 2020-06-16T10:53:11+09:00 このブログが話題になってますね。制約を付けること自体はよいことだけど、無目的に適用すると害も生じると思います。 無目的という言い方はおかしいな…。外部キー制約をどのように使えばいいのか、逆にどんなときに使うとまずいのかを考えてみたいと思います。 tech.tabechoku.com 例えば、これ。外部キー制約はできるだけ付けるとか、何も考えずに付けるとよくないと思います。 外部キー制約は、可能な限りつけるようにしています。 DBが別れている場合、外部キーはもちろん貼れないのですが、そうでない場合はとにかく何も考えず貼っています。データベース設計の際に気をつけていること - 食べチョク開発者ブロ… <p>このブログが話題になってますね。制約を付けること自体はよいことだけど、<s>無目的に適用すると害も生じると思います。</s> 無目的という言い方はおかしいな…。外部キー制約をどのように使えばいいのか、逆にどんなときに使うとまずいのかを考えてみたいと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.tabechoku.com%2Fentry%2F2020%2F06%2F15%2F132518" title="データベース設計の際に気をつけていること - 食べチョク開発者ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.tabechoku.com/entry/2020/06/15/132518">tech.tabechoku.com</a></cite></p> <p>例えば、これ。外部キー制約はできるだけ付けるとか、何も考えずに付けるとよくないと思います。</p> <blockquote cite="https://tech.tabechoku.com/entry/2020/06/15/132518" data-uuid="26006613585633967"><p>外部キー制約は、可能な限りつけるようにしています。 DBが別れている場合、外部キーはもちろん貼れないのですが、そうでない場合はとにかく何も考えず貼っています。</p><cite><a href="https://tech.tabechoku.com/entry/2020/06/15/132518">データベース設計の際に気をつけていること - 食べチョク開発者ブログ</a></cite></blockquote> <h2>テーブル設計をシミュレーションする</h2> <p>いいたいことの結論はこれ。以上終了なのですが、もう少しわかりやすく書いてみよう。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">何も考えずに外部キーを貼るのは良くないな。トランザクション境界の外で結果整合性を使う場合は、外部制約はつけない。つける場合は一緒に削除されるものに限定する。つまるところ更新の境界の外と内を意識してる / “データベース設計の際に気をつけていること - 食べチョ…” <a href="https://t.co/1L7sKxrlFQ">https://t.co/1L7sKxrlFQ</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1272561505154678784?ref_src=twsrc%5Etfw">2020年6月15日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <h3>テーブル設計</h3> <p>以下のテーブルがあるとします。商品の売上を管理するテーブルです。とりあえず、何も考えずに外部キー制約をすべてに適用しています。(論理削除ではなく一旦物理削除で考えましょう。論理削除はこういった境界分析の邪魔になりますので)</p> <ul> <li>売上テーブル <ul> <li>売上ID(PK)</li> <li>売上日時</li> </ul> </li> <li>売上詳細テーブル <ul> <li>売上詳細ID(PK)</li> <li>売上ID(FK)</li> <li>商品ID(FK)</li> <li>数量</li> </ul> </li> <li>商品テーブル <ul> <li>商品ID(PK)</li> <li>商品名</li> <li>単価</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200616/20200616103927.png" alt="f:id:j5ik2o:20200616103927p:plain" title="f:id:j5ik2o:20200616103927p:plain" class="hatena-fotolife" itemprop="image"></span></p> <h3>想定ユースケース</h3> <ul> <li>商品情報を登録・更新・削除する</li> <li>売上を登録・更新・削除する <ul> <li>売上には売れた商品、数量、金額の売上詳細の概念が含まれる</li> </ul> </li> </ul> <h2>強い整合性と弱い整合性</h2> <p>ユースケースから考えて、整合性の境界を考えます。整合性には強いものと弱いものがあります。強い整合性は作成・更新・削除するときに一緒に行います。これは「トランザクション整合性」と呼ばれることがあります。弱い整合性は「結果整合性」と呼ばれることがあります。では、上記のテーブルで考えていきます。上記の図の点線はこの整合性の境界範囲を示しています。仮に全部が一つの境界内にあると考えシミュレーションします。</p> <h2>売上・売上詳細のトランザクション境界と外部キー制約</h2> <p>売上を登録するときに、一緒に個々の売上詳細を登録・更新・削除(物理削除)します。トランザクション整合性の境界(トランザクション境界)はどこからどこまでがよいでしょうか。売上には売上詳細の概念が含まれるとしているので、売上と売上詳細は同じ境界のほうが都合がよさそうです。つまり、売上と売上詳細が更新されるときは別個ではなく、不可分な一塊として扱われます。例えば、売上が先に作られて、売上詳細が後から作られることはありません。売上詳細だけが先に削除されることもなく、一緒に削除されます。</p> <p>さて、売上と売上詳細は不可分な一塊です。ある売上詳細Aが存在するとき、参照する売上Bは必ず存在します。つまり売上詳細Aの売上ID(FK)は存在する売上BのIDを参照します。売上Bが存在しないとき売上詳細Aも存在しません。この外部キー制約は機能しそうですね。データを保護できそうです。</p> <h2>売上・売上詳細と商品は同じトランザクション境界か?</h2> <p>ここからが問題です。商品ID(FK)はどうなんでしょうか。ここに外部キー制約があっても本当に大丈夫でしょうか?ユースケースを見るかぎり、商品と売上(売上詳細を含む)は別々に作成・更新・削除されます。存在する売上詳細Aが削除されるとき、参照する商品Aはまだ削除されません。<s>売上詳細Aの商品ID(FK)から商品AのIDに外部キー制約があると、売上詳細Aは削除できません。また、</s> ←(この表現は間違いでした。参照している側は削除できますね) 。外部キー制約の関係上、売上詳細が存在することで、商品単体での削除ができません。ユースケースを満たさなくなるので、同じトランザクション境界にできません。(え、普通は商品は削除しないだろうがという方、分かります。とりあえず最後まで読んでほしい)</p> <p>なので、以下のように境界が別個になるはずですが、外部キー制約があるほうが問題になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200616/20200616104018.png" alt="f:id:j5ik2o:20200616104018p:plain" title="f:id:j5ik2o:20200616104018p:plain" class="hatena-fotolife" itemprop="image"></span></p> <h3>整合性境界は物理削除で考えたほうがラク</h3> <p>「論理削除なら整合性の境界とか考える必要ないのでは?」という想定質問があるのですが、そんなことはないです。論理削除でも更新する境界がどこからどこまでかを考える必要があります。売上を消したつもりが商品も消されたら問題ですから。削除フラグが連動する範囲が整合性の境界になります。しかし、これを頭の中で整理して考えることが難しい。なので、分析時は物理削除で考えたほうが圧倒的にラクです。</p> <h2>売上・売上詳細と商品は結果整合性を使う</h2> <p>ということで、この場合どう考えるとよいか。トランザクション境界が異なるということはライフサイクルの境界が異なるわけです。売上詳細から参照する商品IDは存在するかもしれないし、すでに削除されているかもしれません。こういった弱い整合性を結果整合性といいます。ここには外部キー制約は適用できません。</p> <p>また、アプリケーションロジックで以下の売上合計金額を算出する場合、売上詳細からみて商品が結果整合性では、再計算できなくなってしまいます。</p> <ul> <li>売上合計金額=すべての売上詳細金額の合計</li> <li>売上詳細金額=商品IDの単価×数量</li> </ul> <p>これに対処するには、売上・売上詳細の境界内に再計算するための材料を保持する必要があります。もしくは計算結果を保持する方法もありそうですね。前者だとしたら、更新時にそのときの商品IDの単価をコピーする必要があります。(場合によっては商品名が更新されたり削除されるかもしれないので、商品名のコピーも必要になるかもしれません)</p> <ul> <li>売上テーブル <ul> <li>売上ID(PK)</li> <li>売上日時</li> </ul> </li> <li>売上詳細テーブル <ul> <li>売上詳細ID(PK)</li> <li>売上ID(FK)</li> <li>商品ID(※)</li> <li>単価(※)</li> <li>数量</li> </ul> </li> <li>商品テーブル <ul> <li>商品ID(PK)</li> <li>商品名</li> <li>単価</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20200616/20200616104057.png" alt="f:id:j5ik2o:20200616104057p:plain" title="f:id:j5ik2o:20200616104057p:plain" class="hatena-fotolife" itemprop="image"></span></p> <h2>これは集約という考え方</h2> <p>このような考え方は、<strong>ドメイン駆動設計の集約</strong> を学ぶとわかるようになるはずです。今回はテーブル設計の視点から説明してみましたが、ドメインモデルが整合性の境界を持つと考えるとわかりやすくなるかも。DDDの観点で簡単に言えば、<strong>強い整合性境界である集約の内側では外部キー制約に意味があり、集約の外には外部キー制約は不要です</strong>。興味がある方はぜひ学んでみたらいいと思います。</p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00GRKD6XU/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/6181Uutb1tL._SL160_.jpg" class="hatena-asin-detail-image" alt="エリック・エヴァンスのドメイン駆動設計" title="エリック・エヴァンスのドメイン駆動設計"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00GRKD6XU/j5ik2o.me-22/">エリック・エヴァンスのドメイン駆動設計</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Eric%20Evans" class="keyword">Eric Evans</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2013/11/20</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00UX9VJGW/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/619Gh1s721L._SL160_.jpg" class="hatena-asin-detail-image" alt="実践ドメイン駆動設計" title="実践ドメイン駆動設計"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00UX9VJGW/j5ik2o.me-22/">実践ドメイン駆動設計</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A5%F4%A5%A1%A1%BC%A5%F3%A1%A6%A5%F4%A5%A1%A1%BC%A5%CE%A5%F3" class="keyword">ヴァーン・ヴァーノン</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2015/03/19</li><li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>ということで、整合性の境界を無視して、外部キー制約を適用することはできません、ということで。</p> <p>併せて読みたい:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkbigwheel.hateblo.jp%2Fentry%2F2018%2F12%2F03%2Faggregate-and-consistency" title="集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話 - kbigwheelのプログラミング・ソフトウェア技術系ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://kbigwheel.hateblo.jp/entry/2018/12/03/aggregate-and-consistency">kbigwheel.hateblo.jp</a></cite></p> <p>追記:</p> <p>ブコメで例が悪いと指摘があったので、少し再考してみた。わかりにくいことは認める。言いたいことは結果整合性を求める用途では外部キー制約は使えない。トランザクション整合性のある境界内では使えるという主張だった。</p> <p>で、商品=今使える商品という意味で捉えてほしい。商品が廃盤になるとやはり物理削除して、過去に使えてた商品テーブルに移動するかもしれない。実際は、こんな難しいことをせずに、商品は削除せずに「廃盤状態」に遷移できるようにする方法もある。が、何が正しいかは要件による。ここでは仕様の善し悪しを議論したいわけではなく、仮に商品を消すことがある場合、売上詳細から商品ID(FK)は強い整合性を望むので使えないという意図だった。つまるところ、ライフサイクルの境界が異なるのだから、商品IDから商品に到達できるかはそのとき次第なので、以下のような考慮が必要かもしれない、ということ。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">商品IDだけでは困るケースがある。例えば、商品が廃盤になったり、単価改定など。過去に作った売上が変わってしまう。ライフサイクルが異なるので起きる。これを想定するなら、売上詳細側に商品名や単価などを取り込まないといけない <a href="https://t.co/tNSZJ5XgmB">https://t.co/tNSZJ5XgmB</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1273129076379848705?ref_src=twsrc%5Etfw">2020年6月17日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>とはいえ、もっとよい事例はないかと考えた。Slackのようなチャットサービスで、あるアカウントが投稿したメッセージは他のアカウントでも読める。しかし、ある投稿者のアカウントIDが退会したケースを考えてみよう。忘れられる権利に対応するために、アカウントは物理削除しなければならないとする(物理削除好きやな。まぁ例示のための仮定です)。しかし、退会アカウントのメッセージはタイムラインで、”退会済みアカウント”が投稿したメッセージとして、他のアカウントから閲覧できるものとする。この場合でも、メッセージとアカウントはライフサイクル、整合性の境界が独立している。メッセージからみてアカウントはあるかもしれないしないかもしれない。弱い整合性。この場合は、メッセージのアカウントIDからアカウントのアカウントIDに外部キー制約は適用できない。</p> <ul> <li>アカウント <ul> <li>アカウントID(PK)</li> <li>メールアドレスなどの個人情報など</li> </ul> </li> <li>メッセージ <ul> <li>メッセージID(PK)</li> <li>スレッドID</li> <li>メッセージ内容</li> <li>アカウントID(必ずしも存在するとは言えないので、外部キー制約は適用できない)</li> <li>作成日時</li> <li>更新日時</li> </ul> </li> </ul> j5ik2o CQRS/ESによって集約の境界定義を見直す hatenablog://entry/26006613584525947 2020-06-13T17:54:48+09:00 2020-06-13T17:54:48+09:00 peing.net メッセージングシステムのお題のようです。面白そうなのでちょっと考えてみよう。 問題提起 集約候補が以下の3つ。 ユーザー 企業 スレッド メッセージ スレッド集約はメッセージを複数保持するようです。 1000件のメッセージを保持するスレッド集約を更新した際、1000件のアップデートが行われる スレッド集約内部で更新された属性を把握していない場合は、リポジトリでは全メッセージ分の更新となる。これを避けるための仕組みはどう実装するのか? ということが指摘されている。まぁわかります。これはCQRS/ESなら解決できるよと言ってみる 問題の分析 で、僕ならどう考えて実装に落とすかつ… <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpeing.net%2Fja%2Fq%2F5b91f696-d030-4923-9325-80095cd3a14a" title="松岡@DDDブログ書いてますの質問箱です" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://peing.net/ja/q/5b91f696-d030-4923-9325-80095cd3a14a">peing.net</a></cite></p> <p>メッセージングシステムのお題のようです。面白そうなのでちょっと考えてみよう。</p> <h2>問題提起</h2> <p>集約候補が以下の3つ。</p> <ul> <li>ユーザー</li> <li>企業</li> <li>スレッド <ul> <li>メッセージ</li> </ul> </li> </ul> <p>スレッド集約はメッセージを複数保持するようです。</p> <ul> <li>1000件のメッセージを保持するスレッド集約を更新した際、1000件のアップデートが行われる</li> <li>スレッド集約内部で更新された属性を把握していない場合は、リポジトリでは全メッセージ分の更新となる。これを避けるための仕組みはどう実装するのか?</li> </ul> <p>ということが指摘されている。まぁわかります。これはCQRS/ESなら解決できるよと言ってみる</p> <h2>問題の分析</h2> <p>で、僕ならどう考えて実装に落とすかつらつらまとめてみよう。CQRS/ES前提です。Akkaの成分は少なめでScalaの擬似コードで解説します。コードはコンパイルしてないので…おかしなところあるかも。</p> <p>問題はスレッド集約がメッセージの集合を保持した場合、更新コストが大きくなりがち。さらに差分更新しようとしても集約の内部実装が複雑になるのではという論点。</p> <p>まず考えるのは、<strong>スレッドとメッセージの関係性に強い整合性は必要か?</strong> ということです。Noならメッセージを別の集約として切り出すのがよいとは思います。おそらく問題設定としてはYesになっている気がしますが…。 仮に契約プランによって書き込めるメッセージ数が変わるという場合は、スレッドとメッセージには強い整合性が必要かもしれません。別別の集約にした場合、トランザクションが別になり結果整合性になるため、インスタンス数(レコード数)を厳密に制御することはできませんね。ということで、強い整合性が必要という前提で進めます。</p> <p>あと、この問題だけではスレッド集約がどのような使われ方をするのが厳密にはわかりません。つまり振る舞いがイメージできません。たとえば、スレッドにメンバーとして参加したアカウントだけがメッセージを閲覧・追加・更新できるというシナリオがあればスレッド集約の構造が明確になります。メンバーの考慮が必要なスレッド集約は以下のようなイメージになるでしょう。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Message(id: MessageId, text: MessageText, senderId: AccountId, createdAt: Instant, updatedAt: Instant) <span class="synType">case</span> <span class="synType">class</span> Messages(values: List[Message]) { <span class="synIdentifier"> def</span> add(message: Message): Messages = ??? <span class="synIdentifier"> def</span> update(message: Message): Messages = ??? } <span class="synType">case</span> <span class="synType">class</span> Members(values: List[AccountId]) { <span class="synIdentifier"> def</span> add(accountId: AccountId): Members = ??? <span class="synIdentifier"> def</span> remove(accountId: AccountId): Members = ??? <span class="synIdentifier"> def</span> contains(accountId: AccountId): Boolean = ??? } <span class="synType">class</span> Thread(id: ThreadId, members: Members, messages: Messages, createdAt: Instant) { <span class="synIdentifier"> def</span> addMember(account: AccountId): Thread = copy(members = members.add(accountId)) <span class="synIdentifier"> def</span> addMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = { <span class="synStatement">if</span> (members.contains(senderId)) { Right(copy(messages = messages.add(Message(messageId, messageText, senderId, Instant.now, Instant.now)))) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> AddMessageError) } } <span class="synIdentifier"> def</span> updateMessage(messageId: MessageId, messageText: MessageText, sender: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (members.contains(senderId)) { Right(copy(messages = messages.updateMessage(Message(messageId, messageText, senderId, Instant.now, Instant.now)))) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> UpdateMessageError) } <span class="synIdentifier"> def</span> getMessages(accountId: Account): Either[ThreadError, Messages] = <span class="synStatement">if</span> (members.contains(accountId)) { Right(messages) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> ReadMessagesError) } } </pre> <p>課題に戻りますが、このクラスをリポジトリでCRUDすることを想像してみましょう。thread.messagesが1000件ある場合です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synStatement">for</span> { thread &lt;- threadRepository.findById(threadId) <span class="synComment">// 1000件メッセージを持つスレッドを取得</span> newThread &lt;- thread.addMessage(Message(...)) <span class="synComment">// 状態変更</span> _ &lt;- thradRepository.store(newThread) <span class="synComment">// 1001件メッセージを持つスレッドを更新</span> } <span class="synType">yield</span> () </pre> <p>スレッドのどのフィールドが更新されたかわからない場合は、スレッドのタイトル、説明、メッセージの本文などをすべての情報をSQLなどを使って更新するのでしょうか? これはあまりに非効率ではないか…。それを回避するにはThread集約内部にどのフィールドをDB上で更新すべきか把握する仕組みが必要なのではないかという話ですね。 あと、そもそもfindByIdの時点で、本文も含む1000件のメッセージをDBから取得するのは効率がよいとはいえませんね。</p> <h2>CQRS/ES流の設計</h2> <p>上記の問題をCQRS/ESで改善できるか考えてみよう。</p> <h3>コマンド側</h3> <p>集約はCQRSのC(コマンド)側に所属しますが、役割は副作用を起こすことを目的にします。 たとえば、メンバーの追加・削除、メッセージを追加・更新などです。メンバーの確認はメッセージの追加・更新に必要なので含めます。これら以外はクエリ側の責務とします。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> Thread(id: ThreadId, memberIds: MemberIds, messages: MessageIds, createdAt: Instant) { <span class="synIdentifier"> def</span> addMember(account: AccountId): Thread = copy(members = memberIds.add(accountId)) <span class="synIdentifier"> def</span> addMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { Right(copy(messages = messageIds.add(mesageId)) <span class="synComment">// 本文は保存しない</span> } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> AddMessageError) } <span class="synIdentifier"> def</span> updateMessage(messageId: MessageId, messageText: MessageText, sender: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { Right(<span class="synType">this</span>) <span class="synComment">// スレッドではメッセージIDしか管理しないのでメッセージ本文を更新できない</span> } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> UpdateMessageError) } } </pre> <p>本文を持たないので大分軽量化できたのではないでしょうか?(ただ、内部にID集合は保持するので無限に保持するわけには行かないとは思います。上限値は必要だと思います) さて、クエリ側にどうやって連携するかですね。メッセージの本文も読めないと意味がないですからね。</p> <p>ということで、擬似コードですが、以下のように副作用が起きたときにイベントをDBに追記します。このイベントは別のプロセスがコンシュームしてリードモデルを作る元ネタになります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">sealed</span> <span class="synType">trait</span> ThreadEvent <span class="synType">case</span> <span class="synType">class</span> MemeberAdded(threadId: ThreadId, accountId: AccountId, occurredAt: Instant) <span class="synType">extends</span> ThreadEvent <span class="synType">case</span> <span class="synType">class</span> MessageAdded(threadId: ThreadId, messageId: MessageId, messageText: MesssageText, senderId: AccountId, occurredAt: Instant) <span class="synType">extends</span> ThreadEvent <span class="synType">case</span> <span class="synType">class</span> MessageUpdated(threadId: ThreadId, messageId: MessageId, messageText: MesssageText, senderId: AccountId, occurredAt: Instant) <span class="synType">extends</span> ThreadEvent <span class="synType">class</span> Thread(id: ThreadId, memberIds: MemberIds, messages: MessageIds, createdAt: Instant) { <span class="synIdentifier"> def</span> addMember(accountId: AccountId): Thread = { persistEvent(MemberAdded(id, accountId, Instant.now)) copy(members = memberIds.add(accountId)) } <span class="synIdentifier"> def</span> addMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { persistEvent(MessageAdded(id, messageId, messageText, senderId, Instant.now)) Right(copy(messages = messageIds.add(mesageId)) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> AddMessageError) } <span class="synIdentifier"> def</span> updateMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { persistEvent(MessageUpdated(id, messageId, messageText, senderId, Instant.now)) Right(<span class="synType">this</span>) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> UpdateMessageError) } } </pre> <p>書き込みはこれでよいとして、集約をDBから再生する際はどうなるのか。DBに保存してあるイベント列を順番に空のThreadに適用するだけです。 これも疑似コードで説明。この考慮はあくまでコマンドを実行するための前提を整えるためのものです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">class</span> Thread(id: ThreadId, events: Seq[ThreadEvent]) { <span class="synType">private</span> <span class="synType">var</span> memberIds: MemberIds = MemberIds.empty <span class="synType">private</span> <span class="synType">var</span> messages: MessageIds = MessageIds.empty <span class="synComment">// イベントから最新状態を復元</span> events.foreach{ <span class="synType">case</span> MemberAdded(_, accountId, _) =&gt; members = memberIds.add(accountId) <span class="synType">case</span> MessageAdded(_, messageId, messageText, senderId, createdAt) =&gt; messages = messages.add(Message(messageId, messageText, senderId, createdAt) <span class="synType">case</span> MessageUpdated(_, messageId, messageText, senderId, updatedAt) =&gt; messages = messages.update(Message(messageId, messageText, senderId, updatedAt) } <span class="synIdentifier"> def</span> addMember(accountId: AccountId): Thread = { persistEvent(MemberAdded(id, accountId, Instant.now)) copy(members = memberIds.add(accountId)) } <span class="synIdentifier"> def</span> addMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { persistEvent(MessageAdded(id, messageId, messageText, senderId, Instant.now)) Right(copy(messages = messageIds.add(mesageId)) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> AddMessageError) } <span class="synIdentifier"> def</span> updateMessage(messageId: MessageId, messageText: MessageText, senderId: AccountId): Either[ThreadError, Thread] = <span class="synStatement">if</span> (memberIds.contains(senderId)) { persistEvent(MessageUpdated(id, messageId, messageText, senderId, Instant.now)) Right(<span class="synType">this</span>) } <span class="synStatement">else</span> { Left(<span class="synStatement">new</span> UpdateMessageError) } } </pre> <p>これでドメインを扱うコマンド側では、差分を表現するイベントをジャーナルとして追記保存すればいいことになります。また最新の集約の状態はコマンドを受け付けるために必要最低限のものしか持ちません。クエリをしないので、これで問題は起きません。当初想定していたモデルよりかなり小さくなります。コマンド側の集約はリプレイ後はルールをチェックしてパスしたらイベントを追記保存するだけなんです。</p> <h3>AkkaはEvent Sourcingを標準サポート</h3> <p>ここでは示してませんが、イベントの列が巨大だとリプレイに時間が掛かります。例えばイベントN件に1回スナップショットを保存しておき、リプレイ時に最新スナップショット+差分のイベントでリプレイを高速化することもできます。とはいえ、リクエストのたびにイベント列を読み込み・適用するのは効率が悪そうです。Akkaのアプローチでは軽量プロセスであるActorとしてスレッド集約を実装します。上記のクラスがActorとして実装されます(集約アクター)。メソッドコールがメッセージパッシングに変わります。すでにイベント列がある集約アクターはAkkaがストレージからイベントを読み込み、メッセージとして適用してくれます。また一定時間メッセージを受け付けていない集約アクターはランタイムから消えるように設定できます。まぁ、必要な機能が揃っているので確実に楽できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdoc.akka.io%2Fdocs%2Fakka%2Fcurrent%2Ftyped%2Fpersistence.html" title="Event Sourcing • Akka Documentation" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://doc.akka.io/docs/akka/current/typed/persistence.html">doc.akka.io</a></cite></p> <h2>クエリ側の設計</h2> <p>クエリ側のリードモデルはどうやって作るのか? 仮に上記のイベントがDynamoDBに書き込まれるとして、パーティションキーはThreadId, ソートキーはイベント番号(akkaでは自動採番してくれます)、イベント本体はJSONなどで格納されるイメージとします。これをDynamoDB Streamsなどでコンシュームします。コンシューム部分はLambdaでもKCLでもよいです。以下を参考にしてみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Famazondynamodb%2Flatest%2Fdeveloperguide%2FStreams.Lambda.html" title="DynamoDB ストリーム と AWS Lambda のトリガー - Amazon DynamoDB" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.html">docs.aws.amazon.com</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Famazondynamodb%2Flatest%2Fdeveloperguide%2FStreams.KCLAdapter.html" title="DynamoDB ストリーム Kinesis Adapter を使用したストリームレコードの処理 - Amazon DynamoDB" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.KCLAdapter.html">docs.aws.amazon.com</a></cite></p> <p>この方法でイベントが時系列順に手に入ります。リードモデルを作るにはこのイベントを順番にリードDBに反映します。 仮にRDBにTHREADテーブル、MEMBERテーブル、MESSAGEテーブルがある場合は以下のような処理になります。 consumeEventsByThreadIdFromDDBStreamsはDynamoDB Streamsからレコードを読み込んでイベントを返す関数です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>consumeEventsByThreadIdFromDDBStreams.foreach{ <span class="synType">case</span> ev: ThreadCreated =&gt; insertThread(ev) <span class="synComment">// 実際にはスレッドが作られたときのイベントも永続化する必要がある</span> <span class="synType">case</span> ev: MemberAdded =&gt; insertMember(ev) <span class="synType">case</span> ev: MessageAdded =&gt; insertMessage(ev) <span class="synType">case</span> ev: MessageUpdated =&gt; updateMessage(ev) } </pre> <p>最終的にリードモデルはレスポンスの形式に併せて定義するとよいでしょう。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> ThreadDto(id: Long, ...) <span class="synType">case</span> <span class="synType">class</span> MemberDto(id: Long, accountId: Long, createdAt: Int) <span class="synType">case</span> <span class="synType">class</span> MessageDto(id: Long, threadId: Long, text: <span class="synConstant">String</span>, senderId: Long, createdAt: Instant, updatedAt: Instant) </pre> <p>リードモデルはDAO内部でSQLを使って、部分集合を取り出したりできます。リードモデルはお好きなように…。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> messages: Seq[MessageDto] = MessageDao.findAllByThreadIdWithOffsetLimit(threadId, <span class="synConstant">0</span>, <span class="synConstant">100</span>) </pre> <h1>結局どうなるのか</h1> <p>最終的な疑似コードイメージです。 1000件のメッセージを保持するスレッドといってもIDしか持ちません。本文を持つより簡単にリプレイし一つのコマンドを付けて一つのイベントを永続化するだけです。1001件のメッセージは永続化しません。間違いなくスケールするのはこっちですね。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> thread = Thread(ThreadId(<span class="synConstant">1L</span>)) <span class="synComment">// 永続化されているイベント列があれば渡される。なければイベント列は空で作られる</span> thread.addMessage(...) <span class="synComment">// 1つのコマンドで1つのイベントが追記されるだけ。1001件も更新されません。</span> </pre> <h1>補足</h1> <p>上記の考え方がわかったとしても、CQRS/ESは自前でやるのは大変です。 たとえば、以下の処理が複数のサーバで起こった場合、ロックがなくても大丈夫でしょうか?ダメそうですねw そう書き込むためのトランザクション管理が必要なのです…。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> thread = Thread(ThreadId(<span class="synConstant">1L</span>)) thread.addMessage(...) </pre> <p>AkkaではThread部分がActorになりますが、複数のサーバで起動しても、ユニークになるように分散できます。つまり分散システム上で同じIDのActorがたかだか一つになるように配置してくれるので、トランザクションの問題は起きません。 こういったことを考えると、CQRS/ESをサポートしたフレームワークやライブラリを使うほうが圧倒的に楽です。Akka お勧めです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fakka.io%2F" title="Akka: build concurrent, distributed, and resilient message-driven applications for Java and Scala | Akka" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://akka.io/">akka.io</a></cite></p> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798153273/j5ik2o.me-22/"><img src="https://m.media-amazon.com/images/I/51DNj77500L._SL160_.jpg" class="hatena-asin-detail-image" alt="Akka実践バイブル アクターモデルによる並行・分散システムの実現" title="Akka実践バイブル アクターモデルによる並行・分散システムの実現"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798153273/j5ik2o.me-22/">Akka実践バイブル アクターモデルによる並行・分散システムの実現</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/Raymond%20Roestenburg" class="keyword">Raymond Roestenburg</a>,<a href="http://d.hatena.ne.jp/keyword/Rob%20Bakker" class="keyword">Rob Bakker</a>,<a href="http://d.hatena.ne.jp/keyword/Rob%20Williams" class="keyword">Rob Williams</a></li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/12/13</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> j5ik2o Error, Defect, Fault, Failureの定義と解釈 hatenablog://entry/26006613544775431 2020-03-09T16:40:24+09:00 2020-04-04T03:38:55+09:00 移動しました。 https://zenn.dev/j5ik2o/articles/6c4dbab802c9701fd878 <p>移動しました。</p> <p><a href="https://zenn.dev/j5ik2o/articles/6c4dbab802c9701fd878">https://zenn.dev/j5ik2o/articles/6c4dbab802c9701fd878</a></p> j5ik2o akka-cluster スプリットブレインリゾルバ OSS実装一覧 hatenablog://entry/26006613543956690 2019-06-16T00:30:23+09:00 2020-04-02T11:18:01+09:00 Akkaクラスターがネットワーク分断に遭遇した場合に、UnreachableメンバーをDown状態に遷移させるためのリゾルバのOSS実装を以下にまとめる。 ちなみに、このリゾルバがない場合はUnreachableのままだとリーダアクションが取れずにクラスターが機能不全状態なる。かといってAutodownを有効にするとスプリットブレインが発生する可能性がある。これを解決するのがスプリットブレインリゾルバで商用版はLightbend社から提供されている。スプリットブレインリゾルバの仕様はこちら git repo stars 備考 TanUkkii007/akka-cluster-custom-do… <p>Akkaクラスターがネットワーク分断に遭遇した場合に、UnreachableメンバーをDown状態に遷移させるためのリゾルバのOSS実装を以下にまとめる。</p> <p>ちなみに、このリゾルバがない場合はUnreachableのままだとリーダアクションが取れずにクラスターが機能不全状態なる。かといってAutodownを有効にするとスプリットブレインが発生する可能性がある。これを解決するのがスプリットブレインリゾルバで商用版はLightbend社から提供されている。スプリットブレインリゾルバの仕様は<a href="https://doc.akka.io/docs/akka-enhancements/current/split-brain-resolver.html">こちら</a></p> <table> <thead> <tr> <th>git repo</th> <th>stars</th> <th>備考</th> </tr> </thead> <tbody> <tr> <td><a href="https://github.com/TanUkkii007/akka-cluster-custom-downing">TanUkkii007/akka-cluster-custom-downing</a></td> <td>131</td> <td>OldestAutoDowning, QuorumLeaderAutoDowning, MajorityLeaderAutoDowningに対応している。OldestAutoDowningには不具合があるようだ。要修正</td> </tr> <tr> <td><a href="https://github.com/mbilski/akka-reasonable-downing">mbilski/akka-reasonable-downing</a></td> <td>85</td> <td>Static QuorumによるDowningにしか対応していない</td> </tr> <tr> <td><a href="https://github.com/arnohaase/simple-akka-downing">arnohaase/simple-akka-downing</a></td> <td>16</td> <td>static-quorum, keep-majority, keep-oldestに対応している</td> </tr> <tr> <td><a href="https://github.com/guangwenz/akka-down-resolver">guangwenz/akka-down-resolver</a></td> <td>5</td> <td>Static QuorumによるDowningにしか対応していない</td> </tr> </tbody> </table> j5ik2o sbtでビルドしたDockerイメージをECRにプッシュする方法 hatenablog://entry/26006613544777094 2019-05-26T11:20:43+09:00 2020-04-04T03:52:39+09:00 sbt-native-packagerとsbt-ecrを使って、ビルド→ECRログイン→ECRへのプッシュまで行います。 https://github.com/sbt/sbt-native-packager https://github.com/sbilinski/sbt-ecr ECRを作成する レジストリを作る。作成後 以下のようなURIが取得できます。 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/j5ik2o/thread-weaver-api-server sbtプラグインの設定の設定 project/plugins.sbt //… <p>sbt-native-packagerとsbt-ecrを使って、ビルド→ECRログイン→ECRへのプッシュまで行います。</p> <p><a href="https://github.com/sbt/sbt-native-packager">https://github.com/sbt/sbt-native-packager</a> <a href="https://github.com/sbilinski/sbt-ecr">https://github.com/sbilinski/sbt-ecr</a></p> <h1>ECRを作成する</h1> <p>レジストリを作る。作成後 以下のようなURIが取得できます。</p> <p><code>123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/j5ik2o/thread-weaver-api-server</code></p> <h1>sbtプラグインの設定の設定</h1> <p><code>project/plugins.sbt</code></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// ...</span> addSbtPlugin(<span class="synConstant">&quot;com.mintbeans&quot;</span> % <span class="synConstant">&quot;sbt-ecr&quot;</span> % <span class="synConstant">&quot;0.14.1&quot;</span>) addSbtPlugin(<span class="synConstant">&quot;com.typesafe.sbt&quot;</span> % <span class="synConstant">&quot;sbt-native-packager&quot;</span> % <span class="synConstant">&quot;1.3.10&quot;</span>) <span class="synComment">// ...</span> </pre> <h1>build.sbtの設定</h1> <p><code>repositoryName</code>は、上記で得られたURIのホスト名を除く文字列(<code>j5ik2o/thread-weaver-api-server</code>)を指定すること</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">import</span> com.amazonaws.regions.{Region, Regions} <span class="synType">val</span> ecrSettings = Seq( region in Ecr := Region.getRegion(Regions.AP_NORTHEAST_1), repositoryName in Ecr := <span class="synConstant">&quot;j5ik2o/thread-weaver-api-server&quot;</span>, repositoryTags in Ecr ++= Seq(version.value), <span class="synComment">// タグを付ける場合は指定する</span> localDockerImage in Ecr := <span class="synConstant">&quot;j5ik2o/&quot;</span> + (packageName in Docker).value + <span class="synConstant">&quot;:&quot;</span> + (version in Docker).value, push in Ecr := ((push in Ecr) dependsOn (publishLocal in Docker, login in Ecr)).value ) <span class="synType">val</span> `api-server` = (project in file(<span class="synConstant">&quot;api-server&quot;</span>)) .enablePlugins(AshScriptPlugin, JavaAgent, EcrPlugin) .settings(baseSettings) .settings(ecrSettings) .settings( name = <span class="synConstant">&quot;thread-weaver-api-server&quot;</span>, dockerBaseImage := <span class="synConstant">&quot;adoptopenjdk/openjdk8:x86_64-alpine-jdk8u191-b12&quot;</span>, maintainer in Docker := <span class="synConstant">&quot;Junichi Kato &lt;j5ik2o@gmail.com&gt;&quot;</span>, dockerUpdateLatest := <span class="synConstant">true</span>, dockerUsername := Some(<span class="synConstant">&quot;j5ik2o&quot;</span>), bashScriptExtraDefines ++= Seq( <span class="synConstant">&quot;addJava -Xms${JVM_HEAP_MIN:-1024m}&quot;</span>, <span class="synConstant">&quot;addJava -Xmx${JVM_HEAP_MAX:-1024m}&quot;</span>, <span class="synConstant">&quot;addJava -XX:MaxMetaspaceSize=${JVM_META_MAX:-512M}&quot;</span>, <span class="synConstant">&quot;addJava ${JVM_GC_OPTIONS:--XX:+UseG1GC}&quot;</span>, <span class="synConstant">&quot;addJava -Dconfig.resource=${CONFIG_RESOURCE:-application.conf}&quot;</span> ), <span class="synComment">// ...</span> ) </pre> <h1>実行</h1> <p>profileを使う場合は以下のようにする</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ <span class="synIdentifier">AWS_DEFAULT_PROFILE</span>=xxxxx sbt api-server/ecr:push </pre> j5ik2o Microsoft社のDDD, CQRSに関する記事一覧 hatenablog://entry/26006613544776374 2019-05-04T08:44:29+09:00 2020-04-04T03:46:33+09:00 CQRS+ESパターンの説明 コマンド クエリ責務分離 (CQRS) パターン ステートソーシング(CRUD)の短所としてあげているもの 読み取りと書き込みのデータ表現不一致問題 ステートの同時更新による競合問題 読み取りと書き込みが同じモデルによる、データの誤用問題 CRUDの単一データモデルより設計と実装が簡単になる。ただし、CRUDはスキャーフォルドメカニズムによって自動生成できない リードモデルはSQLビューもしくは即時プロジェクションを生成するか。 同じ物理ストアに格納する場合は、パフォーマンス、スケーラビリティ、セキュリティを最大化するため、データストアを物理的に分けるのが一般的 … <h2>CQRS+ESパターンの説明</h2> <ul> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/patterns/cqrs">コマンド クエリ責務分離 (CQRS) パターン</a> <ul> <li>ステートソーシング(CRUD)の短所としてあげているもの <ul> <li>読み取りと書き込みのデータ表現不一致問題</li> <li>ステートの同時更新による競合問題</li> <li>読み取りと書き込みが同じモデルによる、データの誤用問題</li> </ul> </li> <li>CRUDの単一データモデルより設計と実装が簡単になる。ただし、CRUDはスキャーフォルドメカニズムによって自動生成できない</li> <li>リードモデルはSQLビューもしくは即時プロジェクションを生成するか。</li> <li>同じ物理ストアに格納する場合は、パフォーマンス、スケーラビリティ、セキュリティを最大化するため、データストアを物理的に分けるのが一般的</li> </ul> </li> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/patterns/event-sourcing">イベント ソーシング パターン</a> <ul> <li>ステートソーシング(CRUD)の短所としてあげているもの <ul> <li>更新時のロックがパフォーマンスと応答性を低下させる</li> <li>単一データ項目への更新は競合が起きやすい(コラボレータが複数の場合)</li> <li>監査メカニズムがない限り、履歴が失われる</li> </ul> </li> <li>ESのメリット <ul> <li>イベントが不変であり、追記保存するだけ。イベントを処理するプロセスは非同期処理で問題が生じない</li> <li>イベントは、データストアを更新しない、シンプルなオブジェクト。</li> <li>イベントはドメインエキスパートの関心事。</li> <li>同時更新による競合の発生を防ぐことができる(ただし、ドメインオブジェクトが矛盾した状態にならないよう依然として保護が必要)</li> </ul> </li> </ul> </li> </ul> <h2>関連記事</h2> <h3>DDD,CQRS関連</h3> <ul> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns">マイクロサービスで DDD と CQRS パターンを使ってビジネスの複雑さに取り組む</a> <ul> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/apply-simplified-microservice-cqrs-ddd-patterns">マイクロサービスに簡略化された CQRS と DDD のパターンを適用する</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/cqrs-microservice-reads">CQRS マイクロサービスに読み取り/クエリを実装する</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/ddd-oriented-microservice">DDD 指向マイクロサービスの設計</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/microservice-domain-model">マイクロサービス ドメイン モデルの設計</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects">値オブジェクトを実装する</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/domain-model-layer-validations">ドメイン モデル レイヤーでの検証を設計する</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/domain-events-design-implementation">ドメイン イベント: 設計と実装</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/microservice-application-layer-web-api-design">マイクロサービス アプリケーション レイヤーと Web API を設計する</a></li> <li><a href="https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/microservice-application-layer-implementation-web-api">Web API を使用してマイクロサービス アプリケーション レイヤーを実装する</a></li> </ul> </li> </ul> <h3>マイクロサービス関連</h3> <p>マイクロサービス関連。DDDだと戦略的設計の部分。</p> <ul> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/microservices/introduction">マイクロサービス アーキテクチャの概要</a> <ul> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/microservices/model/domain-analysis">ドメイン分析を使用したマイクロサービスのモデル化</a></li> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/microservices/model/tactical-ddd">戦術的 DDD を使用したマイクロサービスの設計</a></li> <li><a href="https://docs.microsoft.com/ja-jp/azure/architecture/microservices/model/microservice-boundaries">マイクロサービス境界の識別</a></li> </ul> </li> </ul> j5ik2o Scala.js ウェブフレームワーク Facade 調査(2019年4月版) hatenablog://entry/26006613544930156 2019-04-17T09:16:17+09:00 2020-04-04T13:59:20+09:00 主要なウェブフレームワークのFacadeについて調査した(2019/4/17日時点)。 React.js Reactバージョン Scalaバージョン ライブラリ名 スター数 最終更新日時 タグ形式 ReactRouterサポート Reduxサポート 備考 16.6+ 2.12,2.11 japgolly/scalajs-react 1196 1時間前 Scalatags形式 Scala.js独自実装 サポートなし 16.8.1 2.12 shadaj/slinky 276 4日前 case class なし なし japgolly/scalajs-reactに依存, ReactNative対応… <p>主要なウェブフレームワークのFacadeについて調査した(2019/4/17日時点)。</p> <h2>React.js</h2> <table> <thead> <tr> <th style="text-align:left;">Reactバージョン</th> <th style="text-align:left;">Scalaバージョン</th> <th style="text-align:left;">ライブラリ名</th> <th style="text-align:left;">スター数</th> <th style="text-align:left;">最終更新日時</th> <th style="text-align:left;">タグ形式</th> <th style="text-align:left;"> ReactRouterサポート </th> <th style="text-align:left;"> Reduxサポート </th> <th style="text-align:left;">備考</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">16.6+</td> <td style="text-align:left;">2.12,2.11</td> <td style="text-align:left;"><a href="https://github.com/japgolly/scalajs-react">japgolly/scalajs-react</a></td> <td style="text-align:left;">1196</td> <td style="text-align:left;">1時間前</td> <td style="text-align:left;">Scalatags形式</td> <td style="text-align:left;">Scala.js独自実装</td> <td style="text-align:left;">サポートなし</td> <td></td> </tr> <tr> <td style="text-align:left;">16.8.1</td> <td style="text-align:left;">2.12</td> <td style="text-align:left;"><a href="https://github.com/shadaj/slinky">shadaj/slinky</a></td> <td style="text-align:left;">276</td> <td style="text-align:left;">4日前</td> <td style="text-align:left;">case class</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">japgolly/scalajs-reactに依存, ReactNative対応</td> </tr> <tr> <td style="text-align:left;">16.8.1+</td> <td style="text-align:left;">2.12</td> <td style="text-align:left;"><a href="https://github.com/aappddeevv/scalajs-reaction">aappddeevv/scalajs-reaction</a></td> <td style="text-align:left;">17</td> <td style="text-align:left;">25日前</td> <td style="text-align:left;">case class</td> <td style="text-align:left;">あり</td> <td style="text-align:left;">あり</td> <td></td> </tr> <tr> <td style="text-align:left;">15.5.3</td> <td style="text-align:left;">2.12</td> <td style="text-align:left;"><a href="https://github.com/shogowada/scalajs-reactjs">shogowada/scalajs-reactjs</a></td> <td style="text-align:left;">23</td> <td style="text-align:left;">2017/5</td> <td style="text-align:left;">Scalatags形式</td> <td style="text-align:left;">あり</td> <td style="text-align:left;">あり</td> <td></td> </tr> <tr> <td style="text-align:left;">0.11.0</td> <td style="text-align:left;">2.11</td> <td style="text-align:left;"><a href="https://github.com/xored/scala-js-react">xored/scala-js-react</a></td> <td style="text-align:left;">133</td> <td style="text-align:left;">2015/2</td> <td style="text-align:left;">scalax</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">なし</td> <td></td> </tr> </tbody> </table> <ul> <li>Reactコンポーネント関連 <ul> <li><a href="https://github.com/payalabs/scalajs-react-bridge">payalabs/scalajs-react-bridge</a></li> <li><a href="https://github.com/chandu0101/scalajs-react-components">chandu0101/scalajs-react-components</a></li> </ul> </li> </ul> <h3>所感</h3> <ul> <li><a href="https://github.com/aappddeevv/scalajs-reaction">aappddeevv/scalajs-reaction</a>もreact-router,redux対応しているので魅力的だが、スターと更新頻度的に<a href="https://github.com/japgolly/scalajs-react">japgolly/scalajs-react</a>が安心</li> <li><a href="https://github.com/japgolly/scalajs-react">japgolly/scalajs-react</a>のルーターは独自実装だが使えれば問題ない。Redux部分もScala.jsで関数型プログラミングすればなくても問題なさそう</li> </ul> <h2>Vue.js</h2> <table> <thead> <tr> <th style="text-align:left;">Vueバージョン</th> <th style="text-align:left;">Scalaバージョン</th> <th style="text-align:left;">ライブラリ名</th> <th style="text-align:left;">スター数</th> <th style="text-align:left;">最終更新日時</th> <th style="text-align:left;">Vue Routerサポート</th> <th style="text-align:left;">Vuexサポート</th> <th style="text-align:left;">備考</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">2.x</td> <td style="text-align:left;">2.11,2.12</td> <td style="text-align:left;"><a href="https://github.com/random-scalor/scala-js-vue">random-scalor/scala-js-vue</a></td> <td style="text-align:left;">3</td> <td style="text-align:left;">2018/2</td> <td style="text-align:left;">あり</td> <td style="text-align:left;">あり</td> <td></td> </tr> <tr> <td style="text-align:left;">2.x</td> <td style="text-align:left;">2.12</td> <td style="text-align:left;"><a href="https://github.com/massung/scala-js-vue">massung/scala-js-vue</a></td> <td style="text-align:left;">12</td> <td style="text-align:left;">2017/11</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">なし</td> <td></td> </tr> <tr> <td style="text-align:left;">2.x</td> <td style="text-align:left;">2.11</td> <td style="text-align:left;"><a href="https://github.com/fancellu/scalajs-vue">fancellu/scalajs-vue</a></td> <td style="text-align:left;">76</td> <td style="text-align:left;">2017/5</td> <td style="text-align:left;">なし</td> <td style="text-align:left;">なし</td> <td></td> </tr> </tbody> </table> <h3>所感</h3> <ul> <li>とはいえどれも古くなってきている…。</li> <li>まともに使えそうなものは<a href="https://github.com/random-scalor/scala-js-vue">random-scalor/scala-js-vue</a></li> </ul> <h2>Angular</h2> <ul> <li>最新のAngularを使えるFacadeを見つけることができなかった…。</li> </ul> <h2>まとめ</h2> <p>Scala.jsでウェブアプリケーションを作るなら<code>scalajs-react</code>一択…。他は自分でがっつりコントリビュートする気がないと、いろいろ大変そうです。 ということで、サンプルプロジェクトを二種類用意したので、興味があれば確認してみてください。</p> <p><a href="https://github.com/j5ik2o/scalajs-react-webpack4-example">https://github.com/j5ik2o/scalajs-react-webpack4-example</a> <a href="https://github.com/j5ik2o/scalajs-vuejs-webpack4-example">https://github.com/j5ik2o/scalajs-vuejs-webpack4-example</a></p> j5ik2o ID生成方法についてあれこれ hatenablog://entry/26006613544775676 2019-03-21T15:01:23+09:00 2020-04-04T03:41:09+09:00 引っ越しました。 ID生成方法についてあれこれ <p>引っ越しました。</p> <p><a href="https://zenn.dev/j5ik2o/articles/a085ab3e3d0f197f6559">ID&#x751F;&#x6210;&#x65B9;&#x6CD5;&#x306B;&#x3064;&#x3044;&#x3066;&#x3042;&#x308C;&#x3053;&#x308C;</a></p> j5ik2o akka-streamを使ってmemcachedクライアントを作るには hatenablog://entry/26006613543958288 2018-12-10T18:36:18+09:00 2020-04-02T11:23:46+09:00 かなり前になるのですが、akka-streamの習作課題として、Memcachedクライアントを実装したので、概要を解説する記事をまとめます。詳しくはgithubをみてください。 https://github.com/j5ik2o/reactive-memcached TCPのハンドリング方法 akkaでTCPをハンドリングするには、以下の二つになる。おそらく akka.actorとakka.ioのパッケージを使う方式。つまり、アクターでTCP I/Oを実装する方法。 akka.streamのTcp#outgoingConnectionというAPIを使う方法 今回は、akka-streamをベ… <p>かなり前になるのですが、akka-streamの習作課題として、<a href="https://github.com/j5ik2o/reactive-memcached">Memcachedクライアント</a>を実装したので、概要を解説する記事をまとめます。詳しくはgithubをみてください。</p> <p><a href="https://github.com/j5ik2o/reactive-memcached">https://github.com/j5ik2o/reactive-memcached</a></p> <h2>TCPのハンドリング方法</h2> <p>akkaでTCPをハンドリングするには、以下の二つになる。おそらく</p> <ol> <li><code>akka.actor</code>と<code>akka.io</code>のパッケージを使う方式。つまり、アクターでTCP I/Oを実装する方法。</li> <li><code>akka.stream</code>の<code>Tcp#outgoingConnection</code>というAPIを使う方法</li> </ol> <p>今回は、akka-streamをベースにしたかったので、2を採用しました。</p> <p><code>outgoingConnection</code>メソッドの戻り値の型は、<code>Flow[ByteString, ByteString, Future[OutgoingConnection]]</code>です。<code>ByteString</code>を渡せば、<code>ByteString</code>が返ってくるらしい。わかりやすいですね。そしてストリームが起動中であれば、コネクションは常時接続された状態になります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synIdentifier">def</span> outgoingConnection( remoteAddress : InetSocketAddress, localAddress : Option[InetSocketAddress], options : Traversable[SocketOption], halfClose : Boolean, connectTimeout : Duration, idleTimeout : Duration) : Flow[ByteString, ByteString, Future[Tcp.OutgoingConnection]] </pre> <h2>Memcachedのコネクションを表現するオブジェクトを実装する</h2> <p>では、早速 Memcachedのコネクションを表現するオブジェクトである <code>MemcachedConnection</code> を実装します。このオブジェクトを生成するとTCP接続され、破棄されると切断されます。そして、考えたインターフェイスは以下です(別にtrait切り出さなくてもよかったですが、説明のために分けました)。<code>send</code>メソッドにコマンドを指定して呼ぶとレスポンスが返る単純なものです。 ちなみに、<code>send</code>メソッドの戻り値の型は<code>monix.eval.Task</code>です。詳しくは <a href="https://monix.io/docs/3x/eval/task.html">https://monix.io/docs/3x/eval/task.html</a> をご覧ください。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">trait</span> MemcachedConnection { <span class="synIdentifier"> def</span> id: UUID <span class="synComment">// 接続ID</span> <span class="synIdentifier"> def</span> shutdown(): Unit <span class="synComment">// シャットダウン</span> <span class="synIdentifier"> def</span> peerConfig: Option[PeerConfig] <span class="synComment">// 接続先設定</span> <span class="synIdentifier"> def</span> send[C &lt;: CommandRequest](cmd: C): Task[cmd.Response] <span class="synComment">// コマンドの送信</span> <span class="synComment">// ...</span> } </pre> <p>実装はどうなるか。全貌は以下。先ほどの<code>Tcp#outgoingConnection</code>を使います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">private</span>[memcached] <span class="synType">class</span> MemcachedConnectionImpl(_peerConfig: PeerConfig, supervisionDecider: Option[Supervision.Decider])( implicit system: ActorSystem ) <span class="synType">extends</span> MemcachedConnection { <span class="synComment">// ...</span> <span class="synType">private</span> implicit <span class="synType">val</span> mat: ActorMaterializer = ... <span class="synType">protected</span> <span class="synType">val</span> tcpFlow: Flow[ByteString, ByteString, NotUsed] = RestartFlow.withBackoff(minBackoff, maxBackoff, randomFactor, maxRestarts) { () =&gt; Tcp() .outgoingConnection(remoteAddress, localAddress, options, halfClose, connectTimeout, idleTimeout) } <span class="synType">protected</span> <span class="synType">val</span> connectionFlow: Flow[RequestContext, ResponseContext, NotUsed] = Flow.fromGraph(GraphDSL.create() { implicit b =&gt; <span class="synPreProc">import</span> GraphDSL.Implicits._ <span class="synType">val</span> requestFlow = b.add( Flow[RequestContext] .map { rc =&gt; log.debug(s<span class="synConstant">&quot;request = [{}]&quot;</span>, rc.commandRequestString) (ByteString.fromString(rc.commandRequest.asString + <span class="synConstant">&quot;\r\n&quot;</span>), rc) } ) <span class="synType">val</span> responseFlow = b.add(Flow[(ByteString, RequestContext)].map { <span class="synType">case</span> (byteString, requestContext) =&gt; log.debug(s<span class="synConstant">&quot;response = [{}]&quot;</span>, byteString.utf8String) ResponseContext(byteString, requestContext) }) <span class="synType">val</span> unzip = b.add(Unzip[ByteString, RequestContext]()) <span class="synType">val</span> zip = b.add(Zip[ByteString, RequestContext]()) requestFlow.out ~&gt; unzip.in unzip.out0 ~&gt; tcpFlow ~&gt; zip.in0 unzip.out1 ~&gt; zip.in1 zip.out ~&gt; responseFlow.in FlowShape(requestFlow.in, responseFlow.out) }) <span class="synType">protected</span> <span class="synType">val</span> (requestQueue: SourceQueueWithComplete[RequestContext], killSwitch: UniqueKillSwitch) = Source .queue[RequestContext](requestBufferSize, overflowStrategy) .via(connectionFlow) .map { responseContext =&gt; log.debug(s<span class="synConstant">&quot;req_id = {}, command = {}: parse&quot;</span>, responseContext.commandRequestId, responseContext.commandRequestString) <span class="synType">val</span> result = responseContext.parseResponse <span class="synComment">// レスポンスのパース</span> responseContext.completePromise(result.toTry) <span class="synComment">// パース結果を返す</span> } .viaMat(KillSwitches.single)(Keep.both) .toMat(Sink.ignore)(Keep.left) .run() <span class="synType">override</span><span class="synIdentifier"> def</span> shutdown(): Unit = killSwitch.shutdown() <span class="synType">override</span><span class="synIdentifier"> def</span> send[C &lt;: CommandRequest](cmd: C): Task[cmd.Response] = Task.deferFutureAction { implicit ec =&gt; <span class="synType">val</span> promise = Promise[CommandResponse]() requestQueue .offer(RequestContext(cmd, promise, ZonedDateTime.now())) .flatMap { <span class="synType">case</span> QueueOfferResult.Enqueued =&gt; promise.future.map(_.asInstanceOf[cmd.Response]) <span class="synType">case</span> QueueOfferResult.Failure(t) =&gt; Future.failed(BufferOfferException(<span class="synConstant">&quot;Failed to send request&quot;</span>, Some(t))) <span class="synType">case</span> QueueOfferResult.Dropped =&gt; Future.failed( BufferOfferException( s<span class="synConstant">&quot;Failed to send request, the queue buffer was full.&quot;</span> ) ) <span class="synType">case</span> QueueOfferResult.QueueClosed =&gt; Future.failed(BufferOfferException(<span class="synConstant">&quot;Failed to send request, the queue was closed&quot;</span>)) } } } </pre> <p>TCPの送受信するには<code>tcpFlow</code>メソッドが返す<code>Flow</code>に必要な<code>ByteString</code>を流して、<code>ByteString</code>を受け取ればいいですが、リクエストとレスポンスを扱うためにもう少し工夫が必要です。そのために<code>connectionFlow</code>メソッドは<code>tcpFlow</code>メソッドを内部<code>Flow</code>として利用します。そして、Flowは<code>RequestContext</code>と<code>ResponseContext</code>の型を利用します。その実装は以下のようなシンプルなものです。キューである<code>requestQueue</code>は<code>connectionFlow</code>にリクエストを送信します。ストリームの後半で返ってきたレスポンスを処理するという流れになっています。キューへのリクエストのエンキューは<code>send</code>メソッドが行います。<code>send</code>メソッドが呼ばれると<code>RequestContext</code>を作って<code>requestQueue</code>にエンキュー、完了するとレスポンスを返します。<code>requestQueue</code>はストリームと繋がっていて、エンキューされた<code>RequestContext</code>は<code>connectionFlow</code>に渡され返ってきた<code>ResponseContext</code>がレスポンス内容をパースし、その結果をPromise経由で返すというものです<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>。エラーハンドリングについて、エラーを起こしやすいところに<code>RestartFlow.withBackoff</code>を入れています。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">final</span> <span class="synType">case</span> <span class="synType">class</span> RequestContext(commandRequest: CommandRequest, promise: Promise[CommandResponse], requestAt: ZonedDateTime) { <span class="synType">val</span> id: UUID = commandRequest.id <span class="synType">val</span> commandRequestString: <span class="synConstant">String</span> = commandRequest.asString } <span class="synType">final</span> <span class="synType">case</span> <span class="synType">class</span> ResponseContext(byteString: ByteString, requestContext: RequestContext, requestsInTx: Seq[CommandRequest] = Seq.empty, responseAt: ZonedDateTime = ZonedDateTime.now) <span class="synType">extends</span> ResponseBase { <span class="synType">val</span> commandRequest: CommandRequest = requestContext.commandRequest <span class="synIdentifier"> def</span> withRequestsInTx(values: Seq[CommandRequest]): ResponseContext = copy(requestsInTx = values) <span class="synComment">// レスポンスのパース</span> <span class="synIdentifier"> def</span> parseResponse: Either[ParseException, CommandResponse] = { requestContext.commandRequest match { <span class="synType">case</span> scr: CommandRequest =&gt; scr.parse(ByteVector(byteString.toByteBuffer)).map(_._1) } } } </pre> <p>コネクションオブジェクトの使い方は簡単です。ほとんどの場合は、このようにコネクションオブジェクトを直接操作せずに、高レベルなAPIを操作したいと思うはずです。そのためのクライアントオブジェクトはあとで述べます。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> connection = MemcachedConnection( PeerConfig(<span class="synStatement">new</span> InetSocketAddress(<span class="synConstant">&quot;127.0.0.1&quot;</span>, memcachedTestServer.getPort), backoffConfig = BackoffConfig(maxRestarts = <span class="synConstant">1</span>)), None ) <span class="synType">val</span> resultFuture = (<span class="synStatement">for</span> { _ &lt;- connection.send(SetRequest(UUID.randomUUID(), <span class="synConstant">&quot;key1&quot;</span>, <span class="synConstant">&quot;1&quot;</span>, <span class="synConstant">10</span> seconds)) _ &lt;- connection.send(SetRequest(UUID.randomUUID(), <span class="synConstant">&quot;key2&quot;</span>, <span class="synConstant">&quot;2&quot;</span>, <span class="synConstant">10</span> seconds)) gr1 &lt;- connection.send(GetRequest(UUID.randomUUID(), <span class="synConstant">&quot;key1&quot;</span>)) gr2 &lt;- connection.send(GetRequest(UUID.randomUUID(), <span class="synConstant">&quot;key2&quot;</span>)) } <span class="synType">yield</span> (gr1, gr2)).runAsync <span class="synType">val</span> result = Await.result(resultFuture, Duration.Inf) <span class="synComment">// (1, 2)</span> </pre> <h2>コマンドの実装</h2> <p>コマンドの一例はこんな感じです。 <code>asString</code>メソッドは文字通りMemcachedにコマンドを送信するときに利用される文字列です。<code>responseParser</code>はfastparseで実装されたレスポンスパーサを指定します。<code>parseResponse</code>メソッドはMemcachedからのレスポンスを表現する構文木をコマンドのレンスポンスに変換するための関数です。これは少し複雑なのであとで説明します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">final</span> <span class="synType">class</span> GetRequest <span class="synType">private</span> (<span class="synType">val</span> id: UUID, <span class="synType">val</span> key: <span class="synConstant">String</span>) <span class="synType">extends</span> CommandRequest <span class="synType">with</span> StringParsersSupport { <span class="synType">override</span> <span class="synType">type</span> Response = GetResponse <span class="synType">override</span> <span class="synType">val</span> isMasterOnly: Boolean = <span class="synConstant">false</span> <span class="synType">override</span><span class="synIdentifier"> def</span> asString: <span class="synConstant">String</span> = s<span class="synConstant">&quot;get $key&quot;</span> <span class="synComment">// レスポンスを解析するためのパーサを指定。レスポンス仕様に合わせて指定する</span> <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> responseParser: P[Expr] = P(retrievalCommandResponse) <span class="synComment">// ASTをコマンドレスポンスに変換する</span> <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> parseResponse: Handler = { <span class="synType">case</span> (EndExpr, next) =&gt; (GetSucceeded(UUID.randomUUID(), id, None), next) <span class="synType">case</span> (ValueExpr(key, flags, length, casUnique, value), next) =&gt; (GetSucceeded(UUID.randomUUID(), id, Some(ValueDesc(key, flags, length, casUnique, value))), next) <span class="synType">case</span> (ErrorExpr, next) =&gt; (GetFailed(UUID.randomUUID(), id, MemcachedIOException(ErrorType.OtherType, None)), next) <span class="synType">case</span> (ClientErrorExpr(msg), next) =&gt; (GetFailed(UUID.randomUUID(), id, MemcachedIOException(ErrorType.ClientType, Some(msg))), next) <span class="synType">case</span> (ServerErrorExpr(msg), next) =&gt; (GetFailed(UUID.randomUUID(), id, MemcachedIOException(ErrorType.ServerType, Some(msg))), next) } } </pre> <p>これはGetRequestの上位traitであるCommandRequestです。Memcachedのコマンド送信に必要なのは<code>asString</code>メソッドだけで、コマンドのレスポンスのパースには<code>parse</code>メソッドが利用されます。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">trait</span> CommandRequest { <span class="synType">type</span> Elem <span class="synType">type</span> Repr <span class="synType">type</span> P[+T] = core.Parser[T, Elem, Repr] <span class="synType">type</span> Response &lt;: CommandResponse <span class="synType">type</span> Handler = PartialFunction[(Expr, Int), (Response, Int)] <span class="synType">val</span> id: UUID <span class="synType">val</span> key: <span class="synConstant">String</span> <span class="synType">val</span> isMasterOnly: Boolean <span class="synIdentifier"> def</span> asString: <span class="synConstant">String</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> responseParser: P[Expr] <span class="synType">protected</span><span class="synIdentifier"> def</span> convertToParseSource(s: ByteVector): Repr <span class="synIdentifier"> def</span> parse(text: ByteVector, index: Int = <span class="synConstant">0</span>): Either[ParseException, (Response, Int)] = { responseParser.parse(convertToParseSource(text), index) match { <span class="synType">case</span> f @ Parsed.Failure(_, index, _) =&gt; Left(<span class="synStatement">new</span> ParseException(f.msg, index)) <span class="synType">case</span> Parsed.Success(value, index) =&gt; Right(parseResponse((value, index))) } } <span class="synType">protected</span><span class="synIdentifier"> def</span> parseResponse: Handler } </pre> <h2>レスポンス・パーサの実装</h2> <p>レスポンスパーサは、Memcachedの仕様に合わせて以下の定義から適切なものを選んで利用します。詳しくは<a href="http://www.lihaoyi.com/fastparse/">公式ドキュメント</a>をみてください。標準のパーサーコンビネータと少し違うところがありますが、概ね似たような感じで書けます。Memcachedのレスポンスのルールは単純で左再帰除去などもしなくていいので、パーサーコンビネータのはじめての題材にはよいと思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> StringParsers { <span class="synType">val</span> digit: P0 = P(CharIn('<span class="synConstant">0</span>' to '<span class="synConstant">9</span>')) <span class="synType">val</span> lowerAlpha: P0 = P(CharIn('a' to 'z')) <span class="synType">val</span> upperAlpha: P0 = P(CharIn('A' to 'Z')) <span class="synType">val</span> alpha: P0 = P(lowerAlpha | upperAlpha) <span class="synType">val</span> alphaDigit: P0 = P(alpha | digit) <span class="synType">val</span> crlf: P0 = P(<span class="synConstant">&quot;\r\n&quot;</span>) <span class="synType">val</span> error: P[ErrorExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;ERROR&quot;</span> ~ crlf).map(_ =&gt; ErrorExpr) <span class="synType">val</span> clientError: P[ClientErrorExpr] = P(<span class="synConstant">&quot;CLIENT_ERROR&quot;</span> ~ (!crlf ~/ AnyChar).rep(<span class="synConstant">1</span>).! ~ crlf).map(ClientErrorExpr) <span class="synType">val</span> serverError: P[ServerErrorExpr] = P(<span class="synConstant">&quot;SERVER_ERROR&quot;</span> ~ (!crlf ~/ AnyChar).rep(<span class="synConstant">1</span>).! ~ crlf).map(ServerErrorExpr) <span class="synType">val</span> allErrors: P[Expr] = P(error | clientError | serverError) <span class="synType">val</span> end: P[EndExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;END&quot;</span> ~ crlf).map(_ =&gt; EndExpr) <span class="synType">val</span> deleted: P[DeletedExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;DELETED&quot;</span> ~ crlf).map(_ =&gt; DeletedExpr) <span class="synType">val</span> stored: P[StoredExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;STORED&quot;</span> ~ crlf).map(_ =&gt; StoredExpr) <span class="synType">val</span> notStored: P[NotStoredExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;NOT_STORED&quot;</span> ~ crlf).map(_ =&gt; NotStoredExpr) <span class="synType">val</span> exists: P[ExistsExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;EXISTS&quot;</span> ~ crlf).map(_ =&gt; ExistsExpr) <span class="synType">val</span> notFound: P[NotFoundExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;NOT_FOUND&quot;</span> ~ crlf).map(_ =&gt; NotFoundExpr) <span class="synType">val</span> touched: P[TouchedExpr.<span class="synType">type</span>] = P(<span class="synConstant">&quot;TOUCHED&quot;</span> ~ crlf).map(_ =&gt; TouchedExpr) <span class="synType">val</span> key: P[<span class="synConstant">String</span>] = (!<span class="synConstant">&quot; &quot;</span> ~/ AnyChar).rep(<span class="synConstant">1</span>).! <span class="synType">val</span> flags: P[Int] = digit.rep(<span class="synConstant">1</span>).!.map(_.toInt) <span class="synType">val</span> bytes: P[Long] = digit.rep(<span class="synConstant">1</span>).!.map(_.toLong) <span class="synType">val</span> casUnique: P[Long] = digit.rep(<span class="synConstant">1</span>).!.map(_.toLong) <span class="synType">val</span> incOrDecCommandResponse: P[Expr] = P(notFound | ((!crlf ~/ AnyChar).rep.! ~ crlf).map(StringExpr) | allErrors) <span class="synType">val</span> storageCommandResponse: P[Expr] = P((stored | notStored | exists | notFound) | allErrors) <span class="synType">val</span> value: P[ValueExpr] = P(<span class="synConstant">&quot;VALUE&quot;</span> ~ <span class="synConstant">&quot; &quot;</span> ~ key ~ <span class="synConstant">&quot; &quot;</span> ~ flags ~ <span class="synConstant">&quot; &quot;</span> ~ bytes ~ (<span class="synConstant">&quot; &quot;</span> ~ casUnique).? ~ crlf ~ (!crlf ~/ AnyChar).rep.! ~ crlf) .map { <span class="synType">case</span> (key, flags, bytes, cas, value) =&gt; ValueExpr(key, flags, bytes, cas, value) } <span class="synType">val</span> version: P[VersionExpr] = P(<span class="synConstant">&quot;VERSION&quot;</span> ~ <span class="synConstant">&quot; &quot;</span> ~ alphaDigit.rep(<span class="synConstant">1</span>).!).map(VersionExpr) <span class="synType">val</span> retrievalCommandResponse: P[Expr] = P(end | value | allErrors) <span class="synType">val</span> deletionCommandResponse: P[Expr] = P(deleted | notFound | allErrors) <span class="synType">val</span> touchCommandResponse: P[Expr] = P(touched | notFound | allErrors) <span class="synType">val</span> versionCommandResponse: P[Expr] = P(version | allErrors) } </pre> <h2>クライアントの実装</h2> <p>次はクライアント実装。<code>MemcachedClient#send</code>メソッドはコマンドを受け取り、<code>ReaderTTaskMemcachedConnection[cmd.Response]</code>を返します。<code>ReaderTTaskMemcachedConnection</code>はMemcachedのために特化したReaderTで、コネクションを受け取り、レスポンスを返す<code>Task</code>を返します。Memcachedのための高レベルなAPIはこの<code>send</code>メソッドを利用して実装されます。<code>get</code>,<code>set</code>などのメソッドの内部でコマンドを作成し、レスポンスを戻り値にマッピングするだけで、使いやすい高レベルAPIになります。また、<code>ReaderT</code>を返すためfor式での簡潔な記述もできます。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span><span class="synType"> object</span> memcached { <span class="synType">type</span> ReaderTTask[C, A] = ReaderT[Task, C, A] <span class="synType">type</span> ReaderTTaskMemcachedConnection[A] = ReaderTTask[MemcachedConnection, A] <span class="synType">type</span> ReaderMemcachedConnection[M[_], A] = ReaderT[M, MemcachedConnection, A] } </pre> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">final</span> <span class="synType">class</span> MemcachedClient()(implicit system: ActorSystem) { <span class="synIdentifier"> def</span> send[C &lt;: CommandRequest](cmd: C): ReaderTTaskMemcachedConnection[cmd.Response] = ReaderT(_.send(cmd)) <span class="synIdentifier"> def</span> get(key: <span class="synConstant">String</span>): ReaderTTaskMemcachedConnection[Option[ValueDesc]] = send(GetRequest(UUID.randomUUID(), key)).flatMap { <span class="synType">case</span> GetSucceeded(_, _, result) =&gt; ReaderTTask.pure(result) <span class="synType">case</span> GetFailed(_, _, ex) =&gt; ReaderTTask.raiseError(ex) } <span class="synIdentifier"> def</span> set[A: Show](key: <span class="synConstant">String</span>, value: A, expireDuration: Duration = Duration.Inf, flags: Int = <span class="synConstant">0</span>): ReaderTTaskMemcachedConnection[Int] = send(SetRequest(UUID.randomUUID(), key, value, expireDuration, flags)).flatMap { <span class="synType">case</span> SetExisted(_, _) =&gt; ReaderTTask.pure(<span class="synConstant">0</span>) <span class="synType">case</span> SetNotFounded(_, _) =&gt; ReaderTTask.pure(<span class="synConstant">0</span>) <span class="synType">case</span> SetNotStored(_, _) =&gt; ReaderTTask.pure(<span class="synConstant">0</span>) <span class="synType">case</span> SetSucceeded(_, _) =&gt; ReaderTTask.pure(<span class="synConstant">1</span>) <span class="synType">case</span> SetFailed(_, _, ex) =&gt; ReaderTTask.raiseError(ex) } <span class="synComment">// ...</span> } </pre> <ul> <li>for式での利用例</li> </ul> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> client = <span class="synStatement">new</span> MemcachedClient() <span class="synType">val</span> resultFuture = (<span class="synStatement">for</span> { _ &lt;- client.set(key, <span class="synConstant">&quot;1&quot;</span>) r1 &lt;- client.get(key) _ &lt;- client.set(key, <span class="synConstant">&quot;2&quot;</span>) r2 &lt;- client.get(key) } <span class="synType">yield</span> (r1, r2)).run(connection).runAsync <span class="synType">val</span> result = Await.result(resultFuture, Duration.Inf) <span class="synComment">// (1, 2)</span> </pre> <h2>コネクションプール</h2> <p>コネクションとクライアントを実装できたら、次はコネクションプールも欲しくなります。<code>MemcachedConnection</code>をプーリングして選択できればいいわけなので、<code>commons-pool</code>など使えば楽ですね。コネクションをプールするアルゴリズム(HashRingなど)も複数考えられるので、工夫するとなかなか面白いです。</p> <ul> <li><p>プールの実装 <a href="https://github.com/j5ik2o/reactive-memcached/blob/master/core/src/main/scala/com/github/j5ik2o/reactive/memcached/pool/MemcachedConnectionPoolActor.scala">https://github.com/j5ik2o/reactive-memcached/blob/master/core/src/main/scala/com/github/j5ik2o/reactive/memcached/pool/MemcachedConnectionPoolActor.scala</a> <a href="https://github.com/j5ik2o/reactive-memcached/blob/master/pool-commons/src/main/scala/com/github/j5ik2o/reactive/memcached/CommonsPool.scala">https://github.com/j5ik2o/reactive-memcached/blob/master/pool-commons/src/main/scala/com/github/j5ik2o/reactive/memcached/CommonsPool.scala</a></p></li> <li><p>利用例</p></li> </ul> <pre class="code lang-scala" data-lang="scala" data-unlink>implicit <span class="synType">val</span> system = ActorSystem() <span class="synType">val</span> peerConfig = PeerConfig(remoteAddress = <span class="synStatement">new</span> InetSocketAddress(<span class="synConstant">&quot;127.0.0.1&quot;</span>, <span class="synConstant">6379</span>)) <span class="synType">val</span> pool = MemcachedConnectionPool.ofSingleRoundRobin(sizePerPeer = <span class="synConstant">5</span>, peerConfig, RedisConnection(_)) <span class="synComment">// powered by RoundRobinPool</span> <span class="synType">val</span> connection = MemcachedConnection(connectionConfig) <span class="synType">val</span> client = MemcachedClient() <span class="synComment">// ローンパターン形式</span> <span class="synType">val</span> resultFuture1 = pool.withConnectionF{ con =&gt; (<span class="synStatement">for</span>{ _ &lt;- client.set(<span class="synConstant">&quot;foo&quot;</span>, <span class="synConstant">&quot;bar&quot;</span>) r &lt;- client.get(<span class="synConstant">&quot;foo&quot;</span>) } <span class="synType">yield</span> r).run(con) }.runAsync <span class="synComment">// モナド形式</span> <span class="synType">val</span> resultFuture2 = (<span class="synStatement">for</span> { _ &lt;- ConnectionAutoClose(pool)(client.set(<span class="synConstant">&quot;foo&quot;</span>, <span class="synConstant">&quot;bar&quot;</span>).run) r &lt;- ConnectionAutoClose(pool)(client.get(<span class="synConstant">&quot;foo&quot;</span>).run) } <span class="synType">yield</span> r).run().runAsync </pre> <h2>まとめ</h2> <p>というわけでこんな風にakka-streamを使えばTCPクライアントも比較的簡単に実装できると思います。参考にしてみてください。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> <p>バックプレッシャを適切にハンドリングするには、ストリームを常に起動した状態にしておく必要があります、要求ごとにストリームを起動していると効果的ではありません))<a href="#fnref:1" rev="footnote">&#8617;</a></p></li> </ol> </div> j5ik2o 「エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か」の感想 hatenablog://entry/26006613543955858 2018-12-05T13:08:46+09:00 2020-04-02T11:15:20+09:00 毎日、ドメイン駆動設計というか、設計の話が投稿されると、楽しくなりますね。 さて、今日の話題は、以下です! エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か ”稀によくあるサンプル”。多分これ僕が書いた事例ですね。ということで、なぜそうしたか、理由など書いておきたいと思います。 なぜこうしたか equalsの責務は以下のとおりで、オブジェクトが等しいかどうかを示すものです。エンティティの等価判定の基準に、識別子以外の属性は含めていません。 これにはトレードオフがあります。 https://docs.oracle.com/javase/jp/8/docs/api/ja… <p>毎日、ドメイン駆動設計というか、設計の話が投稿されると、楽しくなりますね。</p> <p>さて、今日の話題は、以下です!</p> <p><a href="https://yoskhdia.hatenablog.com/entry/2018/12/05/002626">エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か</a></p> <p>”稀によくあるサンプル”。多分これ僕が書いた事例ですね。ということで、なぜそうしたか、理由など書いておきたいと思います。</p> <h2>なぜこうしたか</h2> <p><code>equals</code>の責務は以下のとおりで、オブジェクトが等しいかどうかを示すものです。<strong>エンティティの等価判定の基準に、識別子以外の属性は含めていません。</strong> これにはトレードオフがあります。</p> <blockquote><p><a href="https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html">https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html</a> Object#equalsの責務は、「このオブジェクトと他のオブジェクトが等しいかどうかを示します。」</p></blockquote> <p>以下のように実装したと場合に、(ID以外の)属性が変わったエンティティを特定することが可能です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>scala&gt; <span class="synType">case</span> <span class="synType">class</span> EmployeeId(value: Int) <span class="synType">extends</span> Identifier defined <span class="synType">class</span> EmployeeId scala&gt; <span class="synType">case</span> <span class="synType">class</span> Employee(identifier: EmployeeId, name: <span class="synConstant">String</span>) <span class="synType">extends</span> Entity[EmployeeId] defined <span class="synType">class</span> Employee scala&gt; <span class="synType">val</span> list = Seq(Employee(EmployeeId(<span class="synConstant">1</span>), <span class="synConstant">&quot;yamada taro&quot;</span>), Employee(EmployeeId(<span class="synConstant">2</span>), <span class="synConstant">&quot;yamada hanako&quot;</span>)) list: Seq[Employee] = List(Employee(EmployeeId(<span class="synConstant">1</span>),yamada taro), Employee(EmployeeId(<span class="synConstant">2</span>),yamada hanako)) <span class="synComment">// &quot;yamada hanako&quot;が結婚して名前が変わるイベントが発生、リストの内容も合わせて変更される</span> scala&gt; list.contains(Employee(EmployeeId(<span class="synConstant">2</span>), <span class="synConstant">&quot;yamada hanako&quot;</span>)) res0: Boolean = <span class="synConstant">true</span> </pre> <p><code>Entity#sameIdentityAs</code>の場合は、 コレクションのメソッドに組み込むことはできないのでそれなりに実装を工夫する必要がありますね。</p> <h2>論理的等価性検査としてのequals</h2> <p>上記のメリットはよいとして、一体<code>equals</code>メソッドとして何を求めるているのか。DDDの観点があろうがなかろうか、<code>equals</code>の契約を逸脱する設計をしないようにすべきと考えます。久しぶりに、Effective Javaを開いてみると、equalsは「論理的等価性」検査の責務を持つというような記述はあるものの、「論理的等価性」が具体的に何かは明記されていませんでした。</p> <p>ちなみに<code>java.lang.Object</code>のデフォルト実装は以下となっています。多くの場合はサブクラスで固有のequalsメソッドとしてオーバーライドされますが、されない場合もあります。<code>java.util.Random</code>は、同じ乱数列を生成するかで等価判定できたはずですが、クライアントがそれを求めてなかったので、デフォルト実装のままとなっているようです。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">boolean</span> equals(Object obj) { <span class="synStatement">return</span> (<span class="synType">this</span> == obj); } </pre> <h2>契約プログラミングの観点から</h2> <p>契約プログラミングの観点では、<code>java.lan.Object#equals</code>のJavadocに書かれていること以上のことは求めてはいけません。つまり、一般契約に従っている以上は「オブジェクトが等しいかどうか」の仕様を満たしているはずです。</p> <blockquote><p>DDDの文脈では、等価性は(エンティティが持つ)値がすべて同じであればtrueとみなし、同一性は識別子が同じであればtrueとみなすものと解釈できそうです。つまり、エンティティの定義である「同一性によって定義されるオブジェクト」に照らすと equals をオーバーライドすることは適切ではありません。 この点では、本来オーバーライドすべきは eq と言えるかもしれません。</p></blockquote> <p>このアイデアは有益ですしこの設計を取ってもよいと思っていますが、<code>java.lan.Object#equals</code>の契約に照らして考えるに「適切ではありません」とまでは言い切れないと考えています。</p> <h2>結局どうすべきか</h2> <p>まぁ身の蓋もないですが、こうすべきだと思っています。(ここでオーバーライドするという意味は、IDのみの等価判断する実装としてオーバーライドするかしないかという意味です)</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">まぁ、オーバーライドするかしないかは、トレードオフがあるので、プロジェクトやチームで判断してくださいというスタンスかな、僕は。</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1070118543033651200?ref_src=twsrc%5Etfw">2018年12月5日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> <p>また、<code>equals</code>や<code>hashCode</code>のようにもともと抽象度の高いインターフェイスは意図がわかりにくいことがあります。対策としては、ドキュメンテーションコメントに自然言語できちんと設計の意図を記述するべきで、利用者も求められる契約が何かを理解すべきだと思っています。(自壊の念を込めて)</p> <blockquote><p>実装を追わなければ equals がオーバーライドされていることが分からないことは、余分な意識コストとなってしまうでしょう。</p></blockquote> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">DbCの観点でいえば、equals, hashCodeはDDDでいう意図の明白なインターフェイスではありませんね。コードだけでは設計の意図がわからない場合は、javadocなりscaladocに自然言語で仕様を記述すべきですね。</p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1070133509807734784?ref_src=twsrc%5Etfw">2018年12月5日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> j5ik2o akka-httpにswagger2.xを組み込む方法 hatenablog://entry/26006613543957686 2018-10-01T09:05:25+09:00 2020-04-02T11:21:40+09:00 akka-httpにswagger2.xを組み込む方法を以下に示します。 ※DIコンテナであるAirframeを使っていますが、もちろん必須ではありません。適宜読み替えてくだだい。 ライブラリの依存関係 swagger-akka-httpを追加します。javax.ws.rs-apiはアノテーションを利用するために追加します。akka-http-corsは必要に応じて追加してください。 libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-http" % "10.1.5", "com.github.swagger-akka-http"… <p><code>akka-http</code>に<code>swagger2.x</code>を組み込む方法を以下に示します。</p> <p>※DIコンテナであるAirframeを使っていますが、もちろん必須ではありません。適宜読み替えてくだだい。</p> <h2>ライブラリの依存関係</h2> <p><a href="https://github.com/swagger-akka-http/swagger-akka-http">swagger-akka-http</a>を追加します。<code>javax.ws.rs-api</code>はアノテーションを利用するために追加します。<code>akka-http-cors</code>は必要に応じて追加してください。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>libraryDependencies ++= Seq( <span class="synConstant">&quot;com.typesafe.akka&quot;</span> %% <span class="synConstant">&quot;akka-http&quot;</span> % <span class="synConstant">&quot;10.1.5&quot;</span>, <span class="synConstant">&quot;com.github.swagger-akka-http&quot;</span> %% <span class="synConstant">&quot;swagger-akka-http&quot;</span> % <span class="synConstant">&quot;2.0.0&quot;</span> <span class="synConstant">&quot;javax.ws.rs&quot;</span> % <span class="synConstant">&quot;javax.ws.rs-api&quot;</span> % <span class="synConstant">&quot;2.0.1&quot;</span> <span class="synConstant">&quot;ch.megard&quot;</span> %% <span class="synConstant">&quot;akka-http-cors&quot;</span> % <span class="synConstant">&quot;0.3.0&quot;</span> <span class="synComment">// ...</span> ) </pre> <h2>コントローラの例</h2> <p>コントローラに相当するクラスにアクションを作って、そのメソッドにアノテーションを割り当てます。別にコントローラを定義しなくとも、エンドポイントごとにRoute定義が分かれていて、アノテーションが付与できればよいです。</p> <p>アノテーションの使い方は、<a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations">Swagger 2.X Annotations</a>を読んでください。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> spetstore.interface.api.controller <span class="synPreProc">import</span> java.time.ZonedDateTime <span class="synPreProc">import</span> akka.http.scaladsl.server.{Directives, Route} <span class="synPreProc">import</span> de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ <span class="synPreProc">import</span> io.circe.generic.auto._ <span class="synPreProc">import</span> io.swagger.v3.oas.annotations.Operation <span class="synPreProc">import</span> io.swagger.v3.oas.annotations.media.{Content, Schema} <span class="synPreProc">import</span> io.swagger.v3.oas.annotations.parameters.RequestBody <span class="synPreProc">import</span> io.swagger.v3.oas.annotations.responses.ApiResponse <span class="synPreProc">import</span> javax.ws.rs._ <span class="synPreProc">import</span> monix.eval.Task <span class="synPreProc">import</span> monix.execution.Scheduler <span class="synPreProc">import</span> org.hashids.Hashids <span class="synPreProc">import</span> org.sisioh.baseunits.scala.money.Money <span class="synPreProc">import</span> spetstore.domain.model.basic.StatusType <span class="synPreProc">import</span> spetstore.domain.model.item._ <span class="synPreProc">import</span> spetstore.interface.api.model.{CreateItemRequest, CreateItemResponse, CreateItemResponseBody} <span class="synPreProc">import</span> spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC <span class="synPreProc">import</span> spetstore.interface.repository.ItemRepository <span class="synPreProc">import</span> wvlet.airframe._ <span class="synPreProc">import</span> scala.concurrent.Future @Path(<span class="synConstant">&quot;/items&quot;</span>) @Consumes(<span class="synConstant">Array</span>(<span class="synConstant">&quot;application/json&quot;</span>)) @Produces(<span class="synConstant">Array</span>(<span class="synConstant">&quot;application/json&quot;</span>)) <span class="synType">trait</span> ItemController <span class="synType">extends</span> Directives { <span class="synType">private</span> <span class="synType">val</span> itemRepository: ItemRepository[Task] = bind[ItemRepository[Task]] <span class="synType">private</span> <span class="synType">val</span> itemIdGeneratorOnJDBC: ItemIdGeneratorOnJDBC = bind[ItemIdGeneratorOnJDBC] <span class="synType">private</span> <span class="synType">val</span> hashids = bind[Hashids] <span class="synIdentifier"> def</span> route: Route = create <span class="synType">private</span><span class="synIdentifier"> def</span> convertToAggregate(id: ItemId, request: CreateItemRequest): Item = Item( id = id, status = StatusType.Active, name = ItemName(request.name), description = request.description.map(ItemDescription), categories = Categories(request.categories), price = Price(Money.yens(request.price)), createdAt = ZonedDateTime.now(), updatedAt = None ) @POST @Operation( summary = <span class="synConstant">&quot;Create item&quot;</span>, description = <span class="synConstant">&quot;Create Item&quot;</span>, requestBody = <span class="synStatement">new</span> RequestBody(content = <span class="synConstant">Array</span>(<span class="synStatement">new</span> Content(schema = <span class="synStatement">new</span> Schema(implementation = classOf[CreateItemRequest])))), responses = <span class="synConstant">Array</span>( <span class="synStatement">new</span> ApiResponse(responseCode = <span class="synConstant">&quot;200&quot;</span>, description = <span class="synConstant">&quot;Create response&quot;</span>, content = <span class="synConstant">Array</span>(<span class="synStatement">new</span> Content(schema = <span class="synStatement">new</span> Schema(implementation = classOf[CreateItemResponse])))), <span class="synStatement">new</span> ApiResponse(responseCode = <span class="synConstant">&quot;500&quot;</span>, description = <span class="synConstant">&quot;Internal server error&quot;</span>) ) ) <span class="synIdentifier"> def</span> create: Route = path(<span class="synConstant">&quot;items&quot;</span>) { post { extractActorSystem { implicit system =&gt; implicit <span class="synType">val</span> scheduler: Scheduler = Scheduler(system.dispatcher) entity(as[CreateItemRequest]) { request =&gt; <span class="synType">val</span> future: Future[CreateItemResponse] = (<span class="synStatement">for</span> { itemId &lt;- itemIdGeneratorOnJDBC.generateId() _ &lt;- itemRepository.store(convertToAggregate(itemId, request)) } <span class="synType">yield</span> CreateItemResponse(Right(CreateItemResponseBody(hashids.encode(itemId.value))))).runAsync onSuccess(future) { result =&gt; complete(result) } } } } } <span class="synComment">// ...</span> } </pre> <h2>swagger-ui</h2> <p><a href="https://github.com/swagger-api/swagger-ui">swagger-ui</a>の<code>dist</code>を<code>src/main/resource/swagger</code>としてコピーしてください。</p> <h2>SwaggerHttpService</h2> <p>次にSwaggerHttpServiceの実装を用意します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> spetstore.interface.api <span class="synPreProc">import</span> com.github.swagger.akka.SwaggerHttpService <span class="synPreProc">import</span> com.github.swagger.akka.model.Info <span class="synType">class</span> SwaggerDocService(hostName: <span class="synConstant">String</span>, port: Int, <span class="synType">val</span> apiClasses: Set[Class[_]]) <span class="synType">extends</span> SwaggerHttpService { <span class="synType">override</span> <span class="synType">val</span> host = s<span class="synConstant">&quot;127.0.0.1:$port&quot;</span> <span class="synComment">//the url of your api, not swagger's json endpoint</span> <span class="synType">override</span> <span class="synType">val</span> apiDocsPath = <span class="synConstant">&quot;api-docs&quot;</span> <span class="synComment">//where you want the swagger-json endpoint exposed</span> <span class="synType">override</span> <span class="synType">val</span> info = Info() <span class="synComment">//provides license and other description details</span> <span class="synType">override</span> <span class="synType">val</span> unwantedDefinitions = Seq(<span class="synConstant">&quot;Function1&quot;</span>, <span class="synConstant">&quot;Function1RequestContextFutureRouteResult&quot;</span>) } </pre> <h2>routeの設定</h2> <p>akka-httpのRouteは以下を参考にしてください。SwaggerDocServiceとコントローラをrouteに加えます。また、CORSが必要なら、"ch.megard" %% "akka-http-cors" % "0.3.0" を使うとよいと思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> spetstore.interface.api <span class="synPreProc">import</span> akka.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpResponse } <span class="synPreProc">import</span> akka.http.scaladsl.server.{ Directives, Route, StandardRoute } <span class="synPreProc">import</span> wvlet.airframe._ <span class="synPreProc">import</span> ch.megard.akka.http.cors.scaladsl.CorsDirectives._ <span class="synPreProc">import</span> spetstore.interface.api.controller.ItemController <span class="synType">trait</span> Routes <span class="synType">extends</span> Directives { <span class="synType">private</span> lazy <span class="synType">val</span> itemController = bind[ItemController] <span class="synType">private</span> lazy <span class="synType">val</span> swaggerDocService = bind[SwaggerDocService] <span class="synType">private</span><span class="synIdentifier"> def</span> index(): StandardRoute = complete( HttpResponse( entity = HttpEntity( ContentTypes.`text/plain(UTF-<span class="synConstant">8</span>)`, <span class="synConstant">&quot;Wellcome to API&quot;</span> ) ) ) <span class="synIdentifier"> def</span> routes: Route = cors() { pathEndOrSingleSlash { index() } ~ path(<span class="synConstant">&quot;swagger&quot;</span>) { getFromResource(<span class="synConstant">&quot;swagger/index.html&quot;</span>) } ~ getFromResourceDirectory(<span class="synConstant">&quot;swagger&quot;</span>) ~ swaggerDocService.routes ~ itemController.route } } </pre> <h2>ブートストラップ</h2> <p>akka-httpの起動部分のコードです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> spetstore.interface.api <span class="synPreProc">import</span> akka.actor.ActorSystem <span class="synPreProc">import</span> akka.http.scaladsl.Http <span class="synPreProc">import</span> akka.http.scaladsl.Http.ServerBinding <span class="synPreProc">import</span> akka.http.scaladsl.settings.ServerSettings <span class="synPreProc">import</span> akka.stream.ActorMaterializer <span class="synPreProc">import</span> wvlet.airframe._ <span class="synPreProc">import</span> scala.concurrent.Future <span class="synPreProc">import</span> scala.util.{ Failure, Success } <span class="synType">trait</span> ApiServer { implicit <span class="synType">val</span> system = bind[ActorSystem] implicit <span class="synType">val</span> materializer = ActorMaterializer() implicit <span class="synType">val</span> executionContext = system.dispatcher <span class="synType">private</span> <span class="synType">val</span> routes = bind[Routes].routes <span class="synIdentifier"> def</span> start(host: <span class="synConstant">String</span>, port: Int, settings: ServerSettings): Future[ServerBinding] = { <span class="synType">val</span> bindingFuture = Http().bindAndHandle(handler = routes, interface = host, port = port, settings = settings) bindingFuture.onComplete { <span class="synType">case</span> Success(binding) =&gt; system.log.info(s<span class="synConstant">&quot;Server online at http://${binding.localAddress.getHostName}:${binding.localAddress.getPort}/&quot;</span>) <span class="synType">case</span> Failure(ex) =&gt; system.log.error(ex, <span class="synConstant">&quot;occurred error&quot;</span>) } sys.addShutdownHook { bindingFuture .flatMap(_.unbind()) .onComplete { _ =&gt; materializer.shutdown() system.terminate() } } bindingFuture } } </pre> <p>アプリケーションのブートストラップ部分です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> spetstore.api <span class="synPreProc">import</span> akka.actor.ActorSystem <span class="synPreProc">import</span> akka.http.scaladsl.settings.ServerSettings <span class="synPreProc">import</span> monix.eval.Task <span class="synPreProc">import</span> org.hashids.Hashids <span class="synPreProc">import</span> slick.basic.DatabaseConfig <span class="synPreProc">import</span> slick.jdbc.JdbcProfile <span class="synPreProc">import</span> spetstore.domain.model.item.ItemId <span class="synPreProc">import</span> spetstore.interface.api.controller.ItemController <span class="synPreProc">import</span> spetstore.interface.api.{ApiServer, Routes, SwaggerDocService} <span class="synPreProc">import</span> spetstore.interface.generator.IdGenerator <span class="synPreProc">import</span> spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC <span class="synPreProc">import</span> spetstore.interface.repository.{ItemRepository, ItemRepositoryBySlick} <span class="synPreProc">import</span> wvlet.airframe._ <span class="synComment">/**</span> <span class="synComment"> * http://127.0.0.1:8080/swagger</span> <span class="synComment"> */</span> <span class="synType">object</span> Main { <span class="synIdentifier"> def</span> main(args: <span class="synConstant">Array</span>[<span class="synConstant">String</span>]): Unit = { <span class="synType">val</span> parser = <span class="synStatement">new</span> scopt.OptionParser[AppConfig](<span class="synConstant">&quot;spetstore&quot;</span>) { opt[<span class="synConstant">String</span>]('h', <span class="synConstant">&quot;host&quot;</span>).action((x, c) =&gt; c.copy(host = x)).text(<span class="synConstant">&quot;host&quot;</span>) opt[Int]('p', <span class="synConstant">&quot;port&quot;</span>).action((x, c) =&gt; c.copy(port = x)).text(<span class="synConstant">&quot;port&quot;</span>) } <span class="synType">val</span> system = ActorSystem(<span class="synConstant">&quot;spetstore&quot;</span>) <span class="synType">val</span> dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig[JdbcProfile](path = <span class="synConstant">&quot;spetstore.interface.storage.jdbc&quot;</span>, system.settings.config) parser.parse(args, AppConfig()) match { <span class="synType">case</span> Some(config) =&gt; <span class="synType">val</span> design = newDesign .bind[Hashids].toInstance(<span class="synStatement">new</span> Hashids(system.settings.config.getString(<span class="synConstant">&quot;spetstore.interface.hashids.salt&quot;</span>))) .bind[ActorSystem].toInstance(system) .bind[JdbcProfile].toInstance(dbConfig.profile) .bind[JdbcProfile#Backend#Database].toInstance(dbConfig.db) .bind[Routes].toSingleton .bind[SwaggerDocService].toInstance( <span class="synStatement">new</span> SwaggerDocService(config.host, config.port, Set(classOf[ItemController])) ) .bind[ApiServer].toSingleton .bind[ItemRepository[Task]].to[ItemRepositoryBySlick] .bind[IdGenerator[ItemId]].to[ItemIdGeneratorOnJDBC] .bind[ItemController].toSingleton design.withSession { session =&gt; <span class="synType">val</span> system = session.build[ActorSystem] session.build[ApiServer].start(config.host, config.port, settings = ServerSettings(system)) } <span class="synType">case</span> None =&gt; println(parser.usage) } } } </pre> <h2>まとめ</h2> <p>最低限、考慮すべきこととしては、akka-httpのroute dslはエンドポイントごとに分割してswaggerアノテーションを割り当てれるようにしてください。これ以外はswaggerの一般的な使い方と変わりません。</p> j5ik2o Getter/Setterを避けて役に立つドメインオブジェクトを作る hatenablog://entry/10257846132610343174 2018-08-14T13:41:25+09:00 2018-08-14T13:41:25+09:00 Clean Architecture 達人に学ぶソフトウェアの構造と設計を読んでます。モデリングに関しては成分薄めですが、よい本だと思います。はい。 Clean Architecture 達人に学ぶソフトウェアの構造と設計作者: Robert C.Martin,角征典,高木正弘出版社/メーカー: KADOKAWA発売日: 2018/07/27メディア: 単行本この商品を含むブログを見る 本書の大筋から少し逸れるが、「5章 オブジェクト指向プログラミング」の「カプセル化」が面白かったので、これを切り口にモデリングについて考えてみる。 <p><a href="http://d.hatena.ne.jp/asin/4048930656/j5ik2o.me-22">Clean Architecture 達人に学ぶソフトウェアの構造と設計</a>を読んでます。モデリングに関しては成分薄めですが、よい本だと思います。はい。</p> <p><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4048930656/j5ik2o.me-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51LkcwTMC8L._SL160_.jpg" class="hatena-asin-detail-image" alt="Clean Architecture 達人に学ぶソフトウェアの構造と設計" title="Clean Architecture 達人に学ぶソフトウェアの構造と設計"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4048930656/j5ik2o.me-22/">Clean Architecture 達人に学ぶソフトウェアの構造と設計</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> Robert C.Martin,角征典,高木正弘</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> KADOKAWA</li><li><span class="hatena-asin-detail-label">発売日:</span> 2018/07/27</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本</li><li><a href="http://d.hatena.ne.jp/asin/4048930656/j5ik2o.me-22" target="_blank">この商品を含むブログを見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>本書の大筋から少し逸れるが、「5章 オブジェクト指向プログラミング」の「カプセル化」が面白かったので、これを切り口にモデリングについて考えてみる。</p> <h2>OO言語のカプセル化はすでに弱体化している</h2> <p>オブジェクト指向の三大要素の一つである、カプセル化について、以下のようなことが書いてあります。</p> <blockquote><p>「カプセル化」がOOの定義の一部となっているのは、OO言語がデータと関数のカプセル化を簡単かつ効果的なものにしているからだ。それによって、データと関数の周囲に線を引くことができる。その線の外側にはデータが見えないようになっていて、一部の関数だけが見えるようになっている。</p></blockquote> <p>本書に登場するある座標を表すオブジェクトをJavaでいつも通り書いてみた。フィールドはプライベートで、直接アクセスできないようにGetterもあるし、距離を計算するメソッドもある。カプセル化の例としてわかりやすいものだが、本章ではカプセル化に問題があると言及している。日常的にこういうコードを書いている人にとって、これの何が悪いのか全くわからない…。僕もそのひとりだった。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> Point { <span class="synType">public</span> Point(<span class="synType">double</span> x, <span class="synType">double</span> y) { <span class="synType">this</span>.x = x; <span class="synType">this</span>.y = y; } <span class="synComment">// 距離を測る</span> <span class="synType">public</span> <span class="synType">double</span> distance(Point p) { <span class="synType">double</span> dx = x - p.x; <span class="synType">double</span> dy = y - p.y; <span class="synStatement">return</span> Math.sqrt(dx * dx + dy * dy); } <span class="synType">public</span> <span class="synType">double</span> getX() { <span class="synStatement">return</span> x; } <span class="synType">public</span> <span class="synType">double</span> getY() { <span class="synStatement">return</span> y; } <span class="synType">private</span> <span class="synType">double</span> x; <span class="synType">private</span> <span class="synType">double</span> y; } </pre> <p>早速、本書で紹介されている例を写経してみたので、みてみよう。C言語でPointオブジェクトを実装すると以下のようになると紹介されている。</p> <p>まずpoint.hだ。久しぶりにC言語で書いた…。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#ifndef OOP_POINT_H</span> <span class="synPreProc">#define OOP_POINT_H</span> <span class="synType">struct</span> Point; <span class="synType">typedef</span> <span class="synType">struct</span> Point* PPoint; PPoint makePoint(<span class="synType">double</span> x, <span class="synType">double</span> y); <span class="synType">double</span> distance(PPoint p1, PPoint p2); <span class="synPreProc">#endif</span> <span class="synComment">//OOP_POINT_H</span> </pre> <p>次はpoint.cだ。構造体の宣言が.cファイルに書ける。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#include </span><span class="synConstant">&quot;point.h&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;stdlib.h&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;math.h&gt;</span> <span class="synType">struct</span> Point { <span class="synType">double</span> x,y; }; PPoint makePoint(<span class="synType">double</span> x, <span class="synType">double</span> y) { PPoint p = malloc(<span class="synStatement">sizeof</span>(<span class="synType">struct</span> Point)); p-&gt;x = x; p-&gt;y = y; <span class="synStatement">return</span> p; } <span class="synType">double</span> distance(PPoint p1, PPoint p2) { <span class="synType">double</span> dx = p1-&gt;x - p2-&gt;x; <span class="synType">double</span> dy = p1-&gt;y - p2-&gt;y; <span class="synStatement">return</span> sqrt(dx*dx + dy*dy); } </pre> <p>このようにPoint構造体をインスタンスと見立てて、内部状態をカプセル化しそれらを利用する関数群というのはよく見られる設計です。本章の解説は以下のとおり。</p> <blockquote><p>point.hのユーザは、struct Pointのメンバーにアクセスできない。makePoint()とdistance()は呼び出せるが、Pointのデータ構造や関数の実装については何も知らない。 これは(非OO言語による)完ぺきなカプセル化である。</p></blockquote> <p>ヘッダにはPoint構造体の名前しかないので、ヘッダ利用者からはこの構造体の内部へアクセスができない。というか、ヘッダ利用者は <strong>構造体の内部構造さえ知ることができない</strong> 。これが、 <strong>完璧なカプセル化</strong> だと言及されている。</p> <p>そういえば、C言語でライブラリを開発していた頃はこういうコードをよく書いていて、メンバーは存在自体を隠蔽していた記憶がある。</p> <p>そして、著者はC++では、そういう完璧さが失われてしまったとしている。</p> <blockquote><p>だがその後、C++というOO言語が登場し、C言語の完璧なカプセル化が破られてしまった。C++のコンパイラの技術的な理由から、クラスのメンバー変数をヘッダファイルに宣言する必要があった。</p></blockquote> <p>C++では以下のようなコードになる。Cの例と比べると、 <strong>メンバ変数x, yがヘッダ利用者から見えてる</strong> 。もちろん、アクセス修飾子を使えば、コンパイラによる防御は可能だが、 <strong>ヘッダ利用者に内部に隠蔽すべき知識が、(ソースコードレベルでは)外部に暴露していることになる</strong> 。本章では、これを「<strong>カプセル化が壊れている</strong>」の根拠としている。</p> <p>こういう視点からみると、C++だけではなく、Java, C#などヘッダと実装を分離しない言語ではカプセル化は弱体化していることになる<a href="#f-1e2e884e" name="fn-1e2e884e" title="コメントもらったけど、確かに Javaなら.class, Javadocのみの提供は完ぺきなカプセル化ですね。誤って*-source.jarを参照しないように…">*1</a>。</p> <pre class="code lang-cpp" data-lang="cpp" data-unlink><span class="synPreProc">#ifndef OOP_POINT_HPP</span> <span class="synPreProc">#define OOP_POINT_HPP</span> <span class="synType">class</span> Point { <span class="synStatement">public</span>: Point(<span class="synType">double</span> x, <span class="synType">double</span> y); <span class="synType">double</span> distance(<span class="synType">const</span> Point&amp; p) <span class="synType">const</span>; <span class="synStatement">private</span>: <span class="synType">double</span> x; <span class="synType">double</span> y; }; <span class="synPreProc">#endif</span> <span class="synComment">//OOP_POINT_HPP</span> </pre> <pre class="code lang-cpp" data-lang="cpp" data-unlink><span class="synPreProc">#include </span><span class="synConstant">&quot;point.hpp&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;math.h&gt;</span> Point::Point(<span class="synType">double</span> x, <span class="synType">double</span> y) : x(x), y(y) { } <span class="synType">double</span> Point::distance(<span class="synType">const</span> Point &amp;p) <span class="synType">const</span> { <span class="synType">double</span> dx = x - p.x; <span class="synType">double</span> dy = y - p.y; <span class="synStatement">return</span> sqrt(dx * dx + dy * dy) } </pre> <p>追記:C++でもPimplイディオムを使えば完ぺきなカプセル化ができるようです。コメントいただき、ありがとうございました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fflat-leon.hatenablog.com%2Fentry%2Fcpp_pimpl" title="【C++ イディオム】Pimplイディオムを使って真のprivateを実現する - Flat Leon Works" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://flat-leon.hatenablog.com/entry/cpp_pimpl">flat-leon.hatenablog.com</a></cite></p> <h2>カプセル化を破るGetter/Setter</h2> <p>いまや完ぺきなカプセル化ができなくなってしまったが、今の問題は残念ながらそこじゃない。C++の例のようにprivate フィールドのままならよいが、安易にGetter, Setterを追加すると、知識のカプセル化が破られる。この問題はあまりに日常的に起こっていて気づきにくい<a href="#f-fbea624d" name="fn-fbea624d" title="いや、この問題もヘッダと実装の統合がカプセル化を破る温床となった可能性があるのではないか。わざわざ、外部に存在を知らせなくてもよい知識を暴露しやすくなったのではないか、そういう個人的な疑念を持っている">*2</a>。そして、そのGetter, Setterによって、ドメインオブジェクトが骨抜きになることはよくある。いわゆる貧血症オブジェクトを生み出してしまう問題だ。</p> <p>たとえば、先ほど示したJavaのPointクラスのgetX(), getY()のGetterはカプセル化を弱めてしまっている。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> Point { <span class="synType">public</span> Point(<span class="synType">double</span> x, <span class="synType">double</span> y) { <span class="synType">this</span>.x = x; <span class="synType">this</span>.y = y; } <span class="synType">public</span> <span class="synType">double</span> distance(Point p) { <span class="synType">double</span> dx = x - p.x; <span class="synType">double</span> dy = y - p.y; <span class="synStatement">return</span> Math.sqrt(dx * dx + dy * dy); } <span class="synType">public</span> <span class="synType">double</span> getX() { <span class="synStatement">return</span> x; } <span class="synType">public</span> <span class="synType">double</span> getY() { <span class="synStatement">return</span> y; } <span class="synType">private</span> <span class="synType">double</span> x; <span class="synType">private</span> <span class="synType">double</span> y; } </pre> <p>そして、以下のようにPointクラスが担うべき距離計算処理は、getX(), getY()などのGetterによってカプセル化が壊れているので、Pointクラスの責務に逆らって他のクラスに実装できてしまう。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synType">public</span> <span class="synType">class</span> Point { <span class="synType">public</span> Point(<span class="synType">double</span> x, <span class="synType">double</span> y) { <span class="synType">this</span>.x = x; <span class="synType">this</span>.y = y; } <span class="synComment">// カプセル化が弱いと、このメソッドがあってなくてもPointの責務を無視できる</span> <span class="synComment">// public double distance(Point p) </span> <span class="synType">public</span> <span class="synType">double</span> getX() { <span class="synStatement">return</span> x; } <span class="synType">public</span> <span class="synType">double</span> getY() { <span class="synStatement">return</span> y; } <span class="synType">private</span> <span class="synType">double</span> x; <span class="synType">private</span> <span class="synType">double</span> y; } <span class="synType">public</span> <span class="synType">class</span> PointLogic { <span class="synType">public</span> <span class="synType">double</span> distance(Point p1, Point p2) { <span class="synType">double</span> dx = p1.getX() - p2.getX(); <span class="synType">double</span> dy = p1.getY() - p2.getY(); <span class="synStatement">return</span> Math.sqrt(dx * dx + dy * dy); } } </pre> <h2>Getter, Setter禁止とTell, Don't Ask原則</h2> <p>前述のような状況を改善させるには、「<a href="http://d.hatena.ne.jp/asin/487311389X/j5ik2o.me-22">ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション</a>」の「オブジェクト指向エクササイズ」には、「ルール9:Getter,Setter、プロパティを使用しないこと」という一見過激なルールが効果的だ。だいたい、初見で学ぶとやりすぎじゃねーの?と思うやつです。でも、やってみると意外と効果がある。</p> <p><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/487311389X/j5ik2o.me-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51FOBZPjz-L._SL160_.jpg" class="hatena-asin-detail-image" alt="ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション" title="ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/487311389X/j5ik2o.me-22/">ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> ThoughtWorks Inc.,株式会社オージス総研オブジェクトの広場編集部</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> オライリージャパン</li><li><span class="hatena-asin-detail-label">発売日:</span> 2008/12/27</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><span class="hatena-asin-detail-label">購入</span>: 14人 <span class="hatena-asin-detail-label">クリック</span>: 323回</li><li><a href="http://d.hatena.ne.jp/asin/487311389X/j5ik2o.me-22" target="_blank">この商品を含むブログ (81件) を見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>このルール9にはこんなことが書いてある。</p> <blockquote><p>インスタンス変数の適切な集合をカプセル化してもまだ設計がぎこちないときは、もっと直接的なカプセル化の違反がないかチェックしましょう。振る舞いがその場で簡単に値を求められるようになっていると、その振る舞いはインスタンス変数の後を付いてきません。(中略) これは後に、重複の大幅な削減や、新機能を実現するための修正の局所化、といった大きな効果をもたらします。このルールは「求めるな、命じよ」として一般的に言われています。</p></blockquote> <p>このルールを知らなくても、「求めるな、命じよ」(Tell, Don't Ask)という言葉を知ってる人は多いでしょう。</p> <p>このGetter, Setterの不要・必要論はかなり昔からあって、以下のQiita記事がわかりやすいので読むとよいです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fkatolisa%2Fitems%2F6cfd1a2a87058678d646" title="結局のところgetter/setterは要るのか?要らないのか? - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/katolisa/items/6cfd1a2a87058678d646">qiita.com</a></cite></p> <p>記事中で、「求めるな、命じよ」(Tell, Don't Ask)というオブジェクト指向の原則がわかりやすく解説されている。</p> <blockquote><p>ある処理をする際、その処理に必要な情報をオブジェクトから引き出さないで、情報を持ったオブジェクトにその処理をさせろということ</p></blockquote> <p>Point#getX(), #getY()は、まさに情報を引き出す行為だ。Point#getX(), #getY()を使って構成されるロジックは、Pointが持つべきロジックかもしれない。</p> <blockquote><p>あるクラスで他クラスのgetterを呼び出すような処理を実装している場合、その処理は本来呼び出されるクラス側で実装されるべきだということ</p></blockquote> <p>せっかくPoint#distanceメソッドが実装されていても、getX(), getY()が使えるのでdistanceを迂回することができる。これらのGetterの使い方によっては、Pointは台無しになる可能性がある。</p> <p>もともとこれは知ってはいたけど、実践的なノウハウは増田さんの本 <a href="http://d.hatena.ne.jp/asin/477419087X/j5ik2o.me-22">現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法</a> から学んだ。判断/加工/計算を持たない、Getter/Setterのみのデータクラスは諸悪の根源であり、「第10章 オブジェクト指向設計の学び方と教え方」の「古い習慣から抜け出すちょっと過激なコーディング規則」では、そのGetterとSetterに以下の弊害があると述べられている。なぜデータクラスを使うとダメなのかは、「第3章 業務ロジックをわかりやすく整理する」を読むのがよいでしょう。</p> <blockquote><p>データクラスは諸悪の根源です。(中略)メソッドは、何らかの判断/加工/計算をしなければなりません。インスタンス変数をそのまま返すだけのgetterを書いてはいけません。インスタンス変数を書き換えるsetterはプログラムの挙動を不安定にし、バグの原因になります。</p></blockquote> <p><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/477419087X/j5ik2o.me-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51fm-EVWsnL._SL160_.jpg" class="hatena-asin-detail-image" alt="現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法" title="現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/477419087X/j5ik2o.me-22/">現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> 増田亨</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> 技術評論社</li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/07/05</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><a href="http://d.hatena.ne.jp/asin/477419087X/j5ik2o.me-22" target="_blank">この商品を含むブログ (1件) を見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <p>確かに、Getter, Setterは、ほとんどの場合で判断/加工/計算をしない。Pointで例でもそうだ。逆に、Point#distanceメソッドは計算をしている。ドメインオブジェクトはこういう分析の視点が反映されているべきだ。</p> <p>この記事では副作用の制御が難しいSetterについて特に触れていませんが、こちらを読んでください。(Scalaのようなイミュータブルが基本の言語では、Setterはほとんど書かないので…厄介なのはGetterなのだ…)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.j5ik2o.me%2Fentry%2F20110211%2F1297442876" title=" 副作用を最小限に抑えるために必要なこと - かとじゅんの技術日誌" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://blog.j5ik2o.me/entry/20110211/1297442876">blog.j5ik2o.me</a></cite></p> <p>Getter, Setterを書かないのは現実的じゃないと、まだ思っているならばActorプログラミングのコードをみてみるといい。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> Point { <span class="synIdentifier"> def</span> props(x: Double, y: Double): Props = Props(<span class="synStatement">new</span> Point(x, y)) <span class="synType">case</span> <span class="synType">class</span> Distance(x: Double, y: Double) <span class="synType">case</span> <span class="synType">class</span> DistanceResult(value: Double) } <span class="synType">class</span> Point(<span class="synType">val</span> x: Double, <span class="synType">val</span> y: Double) <span class="synType">extends</span> Actor { <span class="synComment">// 本来 val は不要</span> <span class="synComment">// val state: State = ...</span> <span class="synType">override</span><span class="synIdentifier"> def</span> receive: Receive = { <span class="synType">case</span> Distance(x, y) =&gt; <span class="synType">val</span> dx = <span class="synType">this</span>.x - x <span class="synType">val</span> dy = <span class="synType">this</span>.y - y sender() ! DistanceResult(Math.sqrt(dx * dx + dy * dy)) } } </pre> <p>そもそもアクタープログラミングでアクターの内部状態にアクセスできない。つまり、Askしにくい。プロトコルとしてのメッセージを定義してアクターに文字通りTellすることになる。<a href="#f-61259cef" name="fn-61259cef" title="まぁ、アクタープログラミングでも、内部状態を取得するプロトコルを実装してしまうと同様の問題を抱えることになる…。要注意。しかし、GetするにはGet, GetResultみたいなプロトコルが必要で実装が面倒。そんなことやるぐらいなら、判断/加工/計算するプロトコルを実装した方がコスパがよい、というのはある">*3</a></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> Main { <span class="synIdentifier"> def</span> props(pointRef: ActorRef): Props = Props(<span class="synStatement">new</span> Main(pointRef)) <span class="synType">case object</span> Start } <span class="synType">class</span> Main(pointRef: ActorRef) <span class="synType">extends</span> Actor { <span class="synType">override</span><span class="synIdentifier"> def</span> receive: Receive = { <span class="synType">case</span> Start =&gt; pointRef ! Distance(<span class="synConstant">2</span>, <span class="synConstant">1</span>) <span class="synComment">// !メソッドは、tellメソッドの別名!</span> <span class="synType">case</span> DistanceResult(value) =&gt; println(<span class="synConstant">&quot;result = $value&quot;</span>) } } <span class="synType">val</span> system: ActorSystem = ActorSystem() <span class="synType">val</span> pointRef: ActorRef = system.actorOf(Point.props(<span class="synConstant">1</span>, <span class="synConstant">2</span>)) <span class="synType">val</span> mainRef: ActorRef = system.actorOf(Main.props(pointRef)) <span class="synComment">// pointRef.x pointRef.yはコンパイルエラー</span> <span class="synComment">// pointRef.state にもアクセスできない</span> <span class="synComment">// そもそもActorRefからはActorの内部状態にアクセスできない</span> mainRef ! Start sys.addShutdownHook{ system.terminate() Await.result(system.whenTerminated, <span class="synConstant">60</span> seconds) } </pre> <p>追記: 「point1.distance(point2) で距離を取得できていたのだから、pointRef1 ! Distance(PointRef2)ではないと不公平ではないです?」という意見をもらいました。確かにそのとおり。少し書き直してみた。Getterは必要なのですが、Publicにする必要がなかったりします。</p> <p><a href="https://gist.github.com/j5ik2o/b4512b2e2e6b5343ee8ac06cdb16aa90">https://gist.github.com/j5ik2o/b4512b2e2e6b5343ee8ac06cdb16aa90</a></p> <h2>データクラスはAskの温床となりやすい</h2> <p>Tell, Don't Askのもう少しわかりやすい例を考えてみた。いつもの銀行口座の例はやめてチャットルームの例で説明してみよう。</p> <p>内部にメンバ情報を持つチャットルームをモデルとして考えるとき以下のような構造を考えるだろう。これはScalaの例だけど、case classはgetterをコンパイラが自動生成なので、この例は見えないところにgetterが書かれていると思って読めばいい。そう、これは諸悪の根源 データクラス。少なくともドメインオブジェクトとしては表現力が乏しく、何が出てきて何ができないかがわからない。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Room(id: Long, members: Set[Member]) </pre> <p>では重要なメンバーを追加するシナリオを考えてみよう。利用者はroomのmembersに自由にアクセスできるので、以下のように状態を変えた新しいインスタンスを生成できる(copyメソッドは一部の属性を置き換えるために使います)。これはAskの例です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> newRoom: Room = room.copy(members = room.members ++ members) </pre> <p>上記のようなコードが責務に含まれるという前提で考えると、ドメインオブジェクトの外に知識が分散するリスクがある。このような重要なコードは、Roomクラスのすぐ近くではなく、遠く離れたインターフェイス層やユースケース層などに現れる。つまり、Roomの全貌を理解するには、これらの散らばったコードを全て探して回り、頭の中ですべてを繋ぎ合わせる必要がある。これでは、非効率でわかりにくいコードになってしまう。</p> <p>ということで、Room#addMembersメソッドを追加することになるはずだ。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Room(id: Long, <span class="synType">private</span> <span class="synType">val</span> members: Set[Member]) { <span class="synIdentifier"> def</span> addMembers(values: Member*): Room = copy(members = <span class="synType">this</span>.members ++ values) } </pre> <p>先ほどのコードと打って変わって、呼び出し側はaddMembersと命じる(Tell)だけだ。Whatを公開してHowは隠されていてAskの例よりわかりやすい。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> newRoom = room.addMembers(newMembers) </pre> <p>これは頭でわかっていても、Getter,Setterを安易に記述してしまうと、それに頼ってしまい、カプセル化を破ってドメインモデルを貧血症にしてしまう。だから、ドメインモデルを設計するときは、publicなGetter/Setterを書かない方が無難でしょう。</p> <p>追記:メンバーの一覧表示のユースケースがある場合は、もちろんmembersはprivateにできず、publicなGetterは必要です。そういうユースケースもない状況で、無条件にGetter/Setterを定義すると、上記のような状況に陥りやすいという主張です。</p> <h2>Getter/Setterを禁止するのが難しい場合</h2> <p>Getter/Setter禁止が現実的ではない場合、どうするとよいか。それは、このブログ記事に書きましたが、プロパティに長い名前を付けることです。例えば、breachEncapsulationOfなどのプレフィックスを付ける例です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fcreators-note.chatwork.com%2Fentry%2F2017%2F12%2F10%2F160210" title="ドメインモデルの根拠とドメインモデル貧血症の対策について - ChatWork Creator&#39;s Note" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://creators-note.chatwork.com/entry/2017/12/10/160210">creators-note.chatwork.com</a></cite></p> <p>実はこの方法は、Ericさんが過去に関わっていたTime and Moneyのコードから借用したアイデアです。</p> <p><a href="http://timeandmoney.sourceforge.net/">Time &amp; Money Code Library</a></p> <p>例えば、CalendarDateには、カプセル化を破るが内部状態にアクセスできる<a href="https://sourceforge.net/p/timeandmoney/code/HEAD/tree/timeandmoney/trunk/src/main/java/com/domainlanguage/time/CalendarDate.java#l207-l220">メソッド</a>があります。</p> <ul> <li>breachEncapsulationOf_day</li> <li>breachEncapsulationOf_month</li> <li>breachEncapsulationOf_year</li> </ul> <p>Roomの例で説明すると、以下のようになる。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> Room(id: Long, breachEncapsulationOfMembers: Set[Member]) </pre> <p>以下のようなAskするコードは書けるけど、明らかに可読性が落ちるので、Roomの責務に応じてTellするためのメソッドを定義しよう。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> newRoom: Room = room.copy(breachEncapsulationOfMembers = room.breachEncapsulationOfMembers ++ members) <span class="synComment">// Tell形式に書き換える → val newRoom = room.addMembers(members)</span> </pre> <p>とはいえ、ドメインオブジェクトをI/Oする際は、Getterがあった方がよいかもしれません。フレームワークやライブラリの都合でこのような妥協も必要な場合があります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> json = room.breachEncapsulationOfMembers.asJson </pre> <h2>まとめ</h2> <p>ということで、今日から ドメインオブジェクトに安易にGetter/Setterを書くのをやめてみよう。</p> <div class="footnote"> <p class="footnote"><a href="#fn-1e2e884e" name="f-1e2e884e" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">コメントもらったけど、確かに Javaなら.class, Javadocのみの提供は完ぺきなカプセル化ですね。誤って*-source.jarを参照しないように…</span></p> <p class="footnote"><a href="#fn-fbea624d" name="f-fbea624d" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">いや、この問題もヘッダと実装の統合がカプセル化を破る温床となった可能性があるのではないか。わざわざ、外部に存在を知らせなくてもよい知識を暴露しやすくなったのではないか、そういう個人的な疑念を持っている</span></p> <p class="footnote"><a href="#fn-61259cef" name="f-61259cef" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">まぁ、アクタープログラミングでも、内部状態を取得するプロトコルを実装してしまうと同様の問題を抱えることになる…。要注意。しかし、GetするにはGet, GetResultみたいなプロトコルが必要で実装が面倒。そんなことやるぐらいなら、判断/加工/計算するプロトコルを実装した方がコスパがよい、というのはある</span></p> </div> j5ik2o DDDリポジトリを楽に実装するライブラリ hatenablog://entry/26006613543959599 2018-07-28T21:34:04+09:00 2020-04-02T11:29:00+09:00 DDDのリポジトリを実装するのがダルいので、ライブラリ化したというか前から書いていたけど、Redis, Memcachedなどの実装も追加したので、簡単に説明を書いてみる。 プロジェクトなどで自前で実装する際も、参照実装として参考になると思います。Scalaの例だけど、他の言語でも参考にはなるんじゃないかと勝手に想像してます。 https://github.com/j5ik2o/scala-ddd-base デフォルトで対応している永続化技術は、以下。 JDBC SkinnyORM Slick3 NOSQL Memcached Redis Guava Cache Freeモナド こちらは永続化… <p>DDDのリポジトリを実装するのがダルいので、ライブラリ化したというか前から書いていたけど、Redis, Memcachedなどの実装も追加したので、簡単に説明を書いてみる。 プロジェクトなどで自前で実装する際も、参照実装として参考になると思います。Scalaの例だけど、他の言語でも参考にはなるんじゃないかと勝手に想像してます。</p> <p><a href="https://github.com/j5ik2o/scala-ddd-base">https://github.com/j5ik2o/scala-ddd-base</a></p> <p>デフォルトで対応している永続化技術は、以下。</p> <ul> <li>JDBC <ul> <li>SkinnyORM</li> <li>Slick3</li> </ul> </li> <li>NOSQL <ul> <li>Memcached</li> <li>Redis</li> <li>Guava Cache</li> </ul> </li> <li>Freeモナド <ul> <li>こちらは永続化そのものは行いません。上記どれかの実装に委譲することになります。</li> </ul> </li> </ul> <p>何が楽になるかというと、上記向けのリポジトリの実装があるので、Daoだけ用意すればリポジトリが実装できるようになります。</p> <h2>中核になるトレイト</h2> <p>中核となる抽象トレイトはこれ。実装はついてないです。</p> <p><a href="https://github.com/j5ik2o/scala-ddd-base/tree/master/core/src/main/scala/com/github/j5ik2o/dddbase">https://github.com/j5ik2o/scala-ddd-base/tree/master/core/src/main/scala/com/github/j5ik2o/dddbase</a></p> <p>IOするのはAggregateです。リポジトリによっては実装するメソッドが異なるのでトレイトは細かく分かれています。Mは型コンストラクタです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">trait</span> AggregateSingleReader[M[_]] <span class="synType">extends</span> AggregateIO[M] { <span class="synIdentifier"> def</span> resolveById(id: IdType): M[AggregateType] } </pre> <h2>SkinnyORM向け実装トレイト</h2> <p>一例としてSkinnyORM向けに実装を提供するトレイトの説明を簡単にします。<code>M</code>はここでは、<code>ReaderT[Task, DBSession, A]</code>としています。<code>Task</code>は<code>monix.eval.Task</code>です。この型は実装ごとに変わります。たとえば、Redis向けの実装では、<code>ReaderT[Task, RedisConnection, A]</code>になります。</p> <p>実装を提供するトレイトは<code>AggregateIOBaseFeature</code>を継承しますが、ほとんどのロジックで<code>SkinnyDaoSupport#Dao</code>を実装したオブジェクトに委譲します。つまり、リポジトリを作る場合は、これらのトレイトをリポジトリのクラスにミックスインして、Daoの実装を提供するだけでよいことになります。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> AggregateIOBaseFeature { <span class="synType">type</span> RIO[A] = ReaderT[Task, DBSession, A] } <span class="synType">trait</span> AggregateIOBaseFeature <span class="synType">extends</span> AggregateIO[RIO] { <span class="synType">override</span> <span class="synType">type</span> IdType &lt;: AggregateLongId <span class="synType">type</span> RecordType &lt;: SkinnyDaoSupport#Record <span class="synType">type</span> DaoType &lt;: SkinnyDaoSupport#Dao[RecordType] <span class="synType">protected</span> <span class="synType">val</span> dao: DaoType } <span class="synType">trait</span> AggregateSingleReadFeature <span class="synType">extends</span> AggregateSingleReader[RIO] <span class="synType">with</span> AggregateBaseReadFeature { <span class="synType">override</span><span class="synIdentifier"> def</span> resolveById(id: IdType): RIO[AggregateType] = <span class="synStatement">for</span> { record &lt;- ReaderT[Task, DBSession, RecordType] { implicit dbSession: DBSession =&gt; Task { dao.findBy(byCondition(id)).getOrElse(<span class="synStatement">throw</span> AggregateNotFoundException(id)) } } aggregate &lt;- convertToAggregate(record) } <span class="synType">yield</span> aggregate } </pre> <h2>実装サンプル</h2> <p>実装する際は、<code>Aggregate*Feature</code>のトレイトをミックスしてください(UserAccountRepositoryは実装を持たない抽象型です)。あとはDaoの実装の提供だけです。以下の例では、<code>UserAccountComponent</code>が<code>UserAccountRecord</code>, <code>UserAccountDao</code>を提供します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> UserAccountRepository { <span class="synType">type</span> BySlick[A] = Task[A] <span class="synIdentifier"> def</span> bySkinny: UserAccountRepository[BySkinny] = <span class="synStatement">new</span> UserAccountRepositoryBySkinny } <span class="synType">class</span> UserAccountRepositoryBySkinny <span class="synType">extends</span> UserAccountRepository[BySkinny] <span class="synType">with</span> AggregateSingleReadFeature <span class="synType">with</span> AggregateSingleWriteFeature <span class="synType">with</span> AggregateMultiReadFeature <span class="synType">with</span> AggregateMultiWriteFeature <span class="synType">with</span> AggregateSingleSoftDeleteFeature <span class="synType">with</span> AggregateMultiSoftDeleteFeature <span class="synType">with</span> UserAccountComponent { <span class="synType">override</span> <span class="synType">type</span> RecordType = UserAccountRecord <span class="synType">override</span> <span class="synType">type</span> DaoType = UserAccountDao.<span class="synType">type</span> <span class="synType">override</span> <span class="synType">protected</span> <span class="synType">val</span> dao: UserAccountDao.<span class="synType">type</span> = UserAccountDao <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> convertToAggregate: UserAccountRecord =&gt; RIO[UserAccount] = { record =&gt; ReaderT { _ =&gt; <span class="synComment">// このメソッド内部でDBを利用したり、非同期タスクを実行する可能性もあるので、RIO形式を取っている</span> Task.pure { UserAccount( id = UserAccountId(record.id), status = Status.withName(record.status), emailAddress = EmailAddress(record.email), password = HashedPassword(record.password), firstName = record.firstName, lastName = record.lastName, createdAt = record.createdAt, updatedAt = record.updatedAt ) } } } <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> convertToRecord: UserAccount =&gt; RIO[UserAccountRecord] = { aggregate =&gt; ReaderT { _ =&gt; Task.pure { UserAccountRecord( id = aggregate.id.value, status = aggregate.status.entryName, email = aggregate.emailAddress.value, password = aggregate.password.value, firstName = aggregate.firstName, lastName = aggregate.lastName, createdAt = aggregate.createdAt, updatedAt = aggregate.updatedAt ) } } } } </pre> <p>DaoはSkinnyDaoSupport#Daoを実装する形式になります。これはSkinnyCRUDMapperの派生型です。 僕の場合は、<a href="https://qiita.com/j5ik2o/items/f2098dbaf99d4f10c921">ここで紹介した方法</a>でスキーマから自動生成しています。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> skinny { <span class="synPreProc">import</span> com.github.j5ik2o.dddbase.skinny.SkinnyDaoSupport <span class="synPreProc">import</span> scalikejdbc._ <span class="synPreProc">import</span> _root_.skinny.orm._ <span class="synType">trait</span> UserAccountComponent <span class="synType">extends</span> SkinnyDaoSupport { <span class="synType">case</span> <span class="synType">class</span> UserAccountRecord( id: Long, status: <span class="synConstant">String</span>, email: <span class="synConstant">String</span>, password: <span class="synConstant">String</span>, firstName: <span class="synConstant">String</span>, lastName: <span class="synConstant">String</span>, createdAt: java.time.ZonedDateTime, updatedAt: Option[java.time.ZonedDateTime] ) <span class="synType">extends</span> Record <span class="synType"> object</span> UserAccountDao <span class="synType">extends</span> Dao[UserAccountRecord] { <span class="synType">override</span><span class="synIdentifier"> def</span> useAutoIncrementPrimaryKey: Boolean = <span class="synConstant">false</span> <span class="synType">override</span> <span class="synType">val</span> tableName: <span class="synConstant">String</span> = <span class="synConstant">&quot;user_account&quot;</span> <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> toNamedValues(record: UserAccountRecord): Seq[(Symbol, Any)] = Seq( 'status -&gt; record.status, 'email -&gt; record.email, 'password -&gt; record.password, 'first_name -&gt; record.firstName, 'last_name -&gt; record.lastName, 'created_at -&gt; record.createdAt, 'updated_at -&gt; record.updatedAt ) <span class="synType">override</span><span class="synIdentifier"> def</span> defaultAlias: Alias[UserAccountRecord] = createAlias(<span class="synConstant">&quot;u&quot;</span>) <span class="synType">override</span><span class="synIdentifier"> def</span> extract(rs: WrappedResultSet, s: ResultName[UserAccountRecord]): UserAccountRecord = autoConstruct(rs, s) } } } </pre> <p>利用するときは、以下のような感じでリポジトリが返す<code>ReaderT[Task, DBSession, UserAccount]#run</code>に<code>DBSession</code>を渡すと<code>Task</code>が返ってきます。 それを<code>runAsync</code>すると<code>Future[UserAccount]</code>が取得できます。他の実装でもほとんど同じように使えます。ご参考までに。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> resultFuture: Future[UserAccount] = (<span class="synStatement">for</span> { _ &lt;- repository.store(userAccount) r &lt;- repository.resolveById(userAccount.id) } <span class="synType">yield</span> r).run(AutoSession).runAsync </pre> j5ik2o sbtでDAOを自動生成する方法 hatenablog://entry/26006613544776747 2018-07-04T09:23:40+09:00 2020-04-04T03:49:18+09:00 DDDのリポジトリを実装する際、ほとんどのケースでDAOが必要になります。が、ボイラープレートが多く、自動生成したいところです。というわけで作りました。 どうやって自動化するか septeni-original/sbt-dao-generator 指定されたスキーマのJDBCメタ情報とテンプレートをマージさせて、ソースコードを出力します。その機能を提供するのがsbt-dao-generator1です。つまり、sbtからコマンド一発でこういうことができるようになるわけですが、 DBのインスタンスが立ち上がっていて、スキーマ情報が組み込まれた状態でないと使えません。 chatwork/sbt-wi… <p>DDDのリポジトリを実装する際、ほとんどのケースでDAOが必要になります。が、ボイラープレートが多く、自動生成したいところです。というわけで作りました。</p> <h2>どうやって自動化するか</h2> <h3><a href="https://github.com/septeni-original/sbt-dao-generator">septeni-original/sbt-dao-generator</a></h3> <p>指定されたスキーマのJDBCメタ情報とテンプレートをマージさせて、ソースコードを出力します。その機能を提供するのが<a href="https://github.com/septeni-original/sbt-dao-generator">sbt-dao-generator</a><sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>です。つまり、<code>sbt</code>からコマンド一発でこういうことができるようになるわけですが、 DBのインスタンスが立ち上がっていて、スキーマ情報が組み込まれた状態でないと使えません。</p> <h3><a href="https://github.com/chatwork/sbt-wix-embedded-mysql">chatwork/sbt-wix-embedded-mysql</a></h3> <p><code>sbt</code>から組み込みMySQLを起動するプラグインです。MySQL固定です…。</p> <h3><a href="https://github.com/flyway/flyway-sbt">flyway/flyway-sbt</a></h3> <p>こちらは言わずもがな、有名なsbtプラグイン。組み込みMySQL上にスキーマを自動作成するために使います。</p> <h2>環境構築手順</h2> <p>実際のサンプルコードは、<a href="https://github.com/j5ik2o/scala-ddd-base">j5ik2o/scala-ddd-base</a>をみてください。 <code>flyway</code>を扱うプロジェクトflywayとDAOを自動生成するプロジェクトexampleはわけています。</p> <h3><code>project/plugins.sbt</code></h3> <p>プラグインを追加しましょう</p> <pre class="code lang-scala" data-lang="scala" data-unlink>addSbtPlugin(<span class="synConstant">&quot;com.chatwork&quot;</span> % <span class="synConstant">&quot;sbt-wix-embedded-mysql&quot;</span> % <span class="synConstant">&quot;1.0.9&quot;</span>) addSbtPlugin(<span class="synConstant">&quot;jp.co.septeni-original&quot;</span> % <span class="synConstant">&quot;sbt-dao-generator&quot;</span> % <span class="synConstant">&quot;1.0.8&quot;</span>) addSbtPlugin(<span class="synConstant">&quot;io.github.davidmweber&quot;</span> % <span class="synConstant">&quot;flyway-sbt&quot;</span> % <span class="synConstant">&quot;5.0.0&quot;</span>) </pre> <h3>テンプレートを作りましょう</h3> <p>FTLでDAOのテンプレートを書きます。以下はSkinnyORMのための例です。レコードクラスとDAOクラスです。</p> <pre class="code lang-scala" data-lang="scala" data-unlink> <span class="synType">case</span> <span class="synType">class</span> ${className}Record( &lt;#list primaryKeys as primaryKey&gt; ${primaryKey.propertyName}: ${primaryKey.propertyTypeName}&lt;#<span class="synStatement">if</span> primaryKey_has_next&gt;,&lt;/#<span class="synStatement">if</span>&gt;&lt;/#list&gt;&lt;#<span class="synStatement">if</span> primaryKeys?has_content&gt;,&lt;/#<span class="synStatement">if</span>&gt; &lt;#list columns as column&gt; &lt;#<span class="synStatement">if</span> column.columnName == <span class="synConstant">&quot;status&quot;</span>&gt; &lt;#assign softDelete=<span class="synConstant">true</span>&gt; &lt;/#<span class="synStatement">if</span>&gt; &lt;#<span class="synStatement">if</span> column.nullable&gt; ${column.propertyName}: Option[${column.propertyTypeName}]&lt;#<span class="synStatement">if</span> column_has_next&gt;,&lt;/#<span class="synStatement">if</span>&gt; &lt;#<span class="synStatement">else</span>&gt; ${column.propertyName}: ${column.propertyTypeName}&lt;#<span class="synStatement">if</span> column_has_next&gt;,&lt;/#<span class="synStatement">if</span>&gt; &lt;/#<span class="synStatement">if</span>&gt; &lt;/#list&gt; ) <span class="synType">extends</span> Record <span class="synType"> object</span> ${className}Dao <span class="synType">extends</span> Dao[${className}Record] { <span class="synType">override</span><span class="synIdentifier"> def</span> useAutoIncrementPrimaryKey: Boolean = <span class="synConstant">false</span> <span class="synType">override</span> <span class="synType">val</span> tableName: <span class="synConstant">String</span> = <span class="synConstant">&quot;${tableName}&quot;</span> <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> toNamedValues(record: ${className}Record): Seq[(Symbol, Any)] = Seq( &lt;#list columns as column&gt; '${column.name} -&gt; record.${column.propertyName}&lt;#<span class="synStatement">if</span> column.name?ends_with(<span class="synConstant">&quot;id&quot;</span>) || column.name?ends_with(<span class="synConstant">&quot;Id&quot;</span>)&gt;.value&lt;/#<span class="synStatement">if</span>&gt;&lt;#<span class="synStatement">if</span> column_has_next&gt;,&lt;/#<span class="synStatement">if</span>&gt; &lt;/#list&gt; ) <span class="synType">override</span><span class="synIdentifier"> def</span> defaultAlias: Alias[UserAccountRecord] = createAlias(<span class="synConstant">&quot;${className[0]?lower_case}&quot;</span>) <span class="synType">override</span><span class="synIdentifier"> def</span> extract(rs: WrappedResultSet, s: ResultName[${className}Record]): ${className}Record = autoConstruct(rs, s) } </pre> <p>特定のDAOに依存しないので、ほとんどのものに対応できるはず。以下は、Slick用とSkinny用の両方に対応したテンプレート例です。どちらでも好きなORMを使ってください。</p> <p><a href="https://github.com/j5ik2o/scala-ddd-base/blob/reboot/example/templates/template.ftl">https://github.com/j5ik2o/scala-ddd-base/blob/reboot/example/templates/template.ftl</a></p> <p>テンプレートの書き方は<a href="https://github.com/septeni-original/sbt-dao-generator">こちら</a>参照。カラム名をあらかじめプロパティ名としてテンプレートコンテキストに含めているので、簡単に書けるはずです。</p> <h3><code>build.sbt</code></h3> <ul> <li>flywayプロジェクト</li> </ul> <p><a href="https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L114-L136">https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L114-L136</a></p> <p>このプロジェクトではchatwork/sbt-wix-embedded-mysqlとflyway/flyway-sbtを使って自動的にスキーマを作ります。<code>flywayMigrate := (flywayMigrate dependsOn wixMySQLStart).value</code> としているので、<code>sbt flyway/flywayMigrate</code>する前に組み込みMySQLが起動します。</p> <ul> <li>exampleプロジェクト</li> </ul> <p><a href="https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L138-L188">https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L138-L188</a></p> <p>JDBCの接続先設定は、<code>flyway</code>プロジェクトと同じ設定を指定してください。</p> <p>このプロジェクトでは、septeni-original/sbt-dao-generatorを使ってJDBCメタ情報とテンプレートをマージして、DAOクラスのソースコードを生成します。 今回は生成物をGitで管理したかったので、以下のようにして通常のソースコードと同じパスに出力していますが、<code>(sourceManaged in Compile).value</code>を使って<code>target/src_managed</code>に出力することも可能です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink>outputDirectoryMapper in generator := { <span class="synType">case</span> s <span class="synStatement">if</span> s.endsWith(<span class="synConstant">&quot;Spec&quot;</span>) =&gt; (sourceDirectory in Test).value <span class="synType">case</span> s =&gt; <span class="synStatement">new</span> java.io.File((scalaSource in Compile).value, <span class="synConstant">&quot;/com/github/j5ik2o/dddbase/example/dao&quot;</span>) }, </pre> <pre class="code lang-scala" data-lang="scala" data-unlink>outputDirectoryMapper in generator := { className: <span class="synConstant">String</span> =&gt; (sourceManaged in Compile).value }, </pre> <p>コンパイル時にソースコード生成するには以下のようにしてください。コンパイルと無関係に生成タスクを実行したい場合は<code>sbt generator::generateAll</code>としてください。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// sourceGenerators in Compile時に出力</span> sourceGenerators in Compile += (generateAll in generator).value </pre> <p>コンパイルより前に出力したい場合は以下でも動作します。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// コンパイルより前に出力</span> compile in Compile := ((compile in Compile) dependsOn (generateAll in generator)).value </pre> <p>あとで<code>example</code>プロジェクトから<code>flyway</code>プロジェクトに依存することをお忘れ無く</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">val</span> example = ... dependsOn(..., flyway) </pre> <h3>生成</h3> <p>コンパイル時に自動的に以下が行われます。</p> <ol> <li>組み込みMySQLの起動</li> <li>flyway マイグレーション実行</li> <li>JDBCメタ情報とテンプレートのマージとソースファイル出力</li> <li>コンパイル</li> </ol> <p>実際に生成されたソースコードはこちら。</p> <p><a href="https://github.com/j5ik2o/scala-ddd-base/blob/master/example/src/main/scala/com/github/j5ik2o/dddbase/example/dao/UserAccount.scala">https://github.com/j5ik2o/scala-ddd-base/blob/master/example/src/main/scala/com/github/j5ik2o/dddbase/example/dao/UserAccount.scala</a></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">package</span> com.github.j5ik2o.dddbase.example.dao <span class="synPreProc">package</span> slick { <span class="synPreProc">import</span> com.github.j5ik2o.dddbase.slick.SlickDaoSupport <span class="synType">trait</span> UserAccountComponent <span class="synType">extends</span> SlickDaoSupport { <span class="synPreProc">import</span> profile.api._ <span class="synType">case</span> <span class="synType">class</span> UserAccountRecord( id: Long, status: <span class="synConstant">String</span>, email: <span class="synConstant">String</span>, password: <span class="synConstant">String</span>, firstName: <span class="synConstant">String</span>, lastName: <span class="synConstant">String</span>, createdAt: java.time.ZonedDateTime, updatedAt: Option[java.time.ZonedDateTime] ) <span class="synType">extends</span> SoftDeletableRecord <span class="synType">case</span> <span class="synType">class</span> UserAccounts(tag: Tag) <span class="synType">extends</span> TableBase[UserAccountRecord](tag, <span class="synConstant">&quot;user_account&quot;</span>) <span class="synType">with</span> SoftDeletableTableSupport[UserAccountRecord] { <span class="synComment">// def id = column[Long](&quot;id&quot;, O.PrimaryKey)</span> <span class="synIdentifier"> def</span> status = column[<span class="synConstant">String</span>](<span class="synConstant">&quot;status&quot;</span>) <span class="synIdentifier"> def</span> email = column[<span class="synConstant">String</span>](<span class="synConstant">&quot;email&quot;</span>) <span class="synIdentifier"> def</span> password = column[<span class="synConstant">String</span>](<span class="synConstant">&quot;password&quot;</span>) <span class="synIdentifier"> def</span> firstName = column[<span class="synConstant">String</span>](<span class="synConstant">&quot;first_name&quot;</span>) <span class="synIdentifier"> def</span> lastName = column[<span class="synConstant">String</span>](<span class="synConstant">&quot;last_name&quot;</span>) <span class="synIdentifier"> def</span> createdAt = column[java.time.ZonedDateTime](<span class="synConstant">&quot;created_at&quot;</span>) <span class="synIdentifier"> def</span> updatedAt = column[Option[java.time.ZonedDateTime]](<span class="synConstant">&quot;updated_at&quot;</span>) <span class="synType">override</span><span class="synIdentifier"> def</span> * = (id, status, email, password, firstName, lastName, createdAt, updatedAt) &lt;&gt; (UserAccountRecord.tupled, UserAccountRecord.unapply) } <span class="synType"> object</span> UserAccountDao <span class="synType">extends</span> TableQuery(UserAccounts) } } <span class="synPreProc">package</span> skinny { <span class="synPreProc">import</span> com.github.j5ik2o.dddbase.skinny.SkinnyDaoSupport <span class="synPreProc">import</span> scalikejdbc._ <span class="synPreProc">import</span> _root_.skinny.orm._ <span class="synType">trait</span> UserAccountComponent <span class="synType">extends</span> SkinnyDaoSupport { <span class="synType">case</span> <span class="synType">class</span> UserAccountRecord( id: Long, status: <span class="synConstant">String</span>, email: <span class="synConstant">String</span>, password: <span class="synConstant">String</span>, firstName: <span class="synConstant">String</span>, lastName: <span class="synConstant">String</span>, createdAt: java.time.ZonedDateTime, updatedAt: Option[java.time.ZonedDateTime] ) <span class="synType">extends</span> Record <span class="synType"> object</span> UserAccountDao <span class="synType">extends</span> Dao[UserAccountRecord] { <span class="synType">override</span><span class="synIdentifier"> def</span> useAutoIncrementPrimaryKey: Boolean = <span class="synConstant">false</span> <span class="synType">override</span> <span class="synType">val</span> tableName: <span class="synConstant">String</span> = <span class="synConstant">&quot;user_account&quot;</span> <span class="synType">override</span> <span class="synType">protected</span><span class="synIdentifier"> def</span> toNamedValues(record: UserAccountRecord): Seq[(Symbol, Any)] = Seq( 'status -&gt; record.status, 'email -&gt; record.email, 'password -&gt; record.password, 'first_name -&gt; record.firstName, 'last_name -&gt; record.lastName, 'created_at -&gt; record.createdAt, 'updated_at -&gt; record.updatedAt ) <span class="synType">override</span><span class="synIdentifier"> def</span> defaultAlias: Alias[UserAccountRecord] = createAlias(<span class="synConstant">&quot;u&quot;</span>) <span class="synType">override</span><span class="synIdentifier"> def</span> extract(rs: WrappedResultSet, s: ResultName[UserAccountRecord]): UserAccountRecord = autoConstruct(rs, s) } } } </pre> <h2>まとめ</h2> <p>このプロジェクト構成は、仕事でも結構がっつり使っていて気に入っています。スキーマ変更が起こっても、DAOは一瞬で自動生成できるので楽になると思います。興味あれば使ってみてください!</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> <p>僕とセプテーニさんとコラボして作りました。<a href="#fnref:1" rev="footnote">&#8617;</a></p></li> </ol> </div> j5ik2o jupyter-scala の セットアップ方法と使い方 hatenablog://entry/26006613544776035 2018-06-18T09:48:50+09:00 2020-04-04T03:44:00+09:00 セットアップ $ pyenv install 3.6.5 $ pyenv virtualenv 3.6.5 jupyter-notebook $ cd jupyter-scala $ pyenv local jupyter-notebook $ pip install --upgrade pip $ pip install jupyter-notebook $ wget https://raw.githubusercontent.com/alexarchambault/jupyter-scala/master/jupyter-scala $ sh ./jupyter-scala wgetしたj… <h2>セットアップ</h2> <pre class="code sh" data-lang="sh" data-unlink>$ pyenv install 3.6.5 $ pyenv virtualenv 3.6.5 jupyter-notebook $ cd jupyter-scala $ pyenv local jupyter-notebook $ pip install --upgrade pip $ pip install jupyter-notebook $ wget https://raw.githubusercontent.com/alexarchambault/jupyter-scala/master/jupyter-scala $ sh ./jupyter-scala</pre> <p><code>wget</code>した<code>jupyter-scala</code>の<code>SCALA_VERSION</code>は以下のように2.11系なので、2.12を使う場合はコメントどおりに修正する。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synIdentifier">SCALA_VERSION</span>=<span class="synConstant">2</span>.<span class="synConstant">11</span>.<span class="synConstant">11</span> <span class="synComment"># Set to 2.12.2 for Scala 2.12</span> </pre> <h2>起動</h2> <pre class="code lang-sh" data-lang="sh" data-unlink>$ jupyter notebook </pre> <p>デフォルトブラウザ上で、<code>http://localhost:8888/tree</code>が開かれます。</p> <h2>使い方</h2> <p><code>New</code>→<code>Scala</code>をクリックします。</p> <p><img src="https://qiita-image-store.s3.amazonaws.com/0/8329/3d4f5de5-9a54-3d51-eb8d-ccfb7c3539f5.png" alt="jn-home.png" /></p> <p>Hello Worldをやってみましょう。コードを入力したらShift+Enterで実行することができます。typoしてもセル内のコードを修正して実行し直すことができる</p> <p><img src="https://qiita-image-store.s3.amazonaws.com/0/8329/8e82449e-fbd4-554b-c897-d4bdda8d07bc.png" alt="jn-helloworld.png" /></p> <h3>外部ライブラリへの依存関係を追加する</h3> <p><code>build.sbt</code>の<code>libraryDependencies</code>に書く情報を以下の形式にして入力・評価するだけでダウンロードされます。他に依存関係を除外する<code>$execlude</code>もあるようです。<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synPreProc">import</span> $ivy.`org.typelevel::cats-core:<span class="synConstant">1.1.0</span>` </pre> <p><img src="https://qiita-image-store.s3.amazonaws.com/0/8329/2acf3a4a-fd9e-2a83-8e75-a2c774d3df61.png" alt="jn-import.png" /></p> <h2>NOTE</h2> <p><code>load.resolver</code>に相当するAPIがよくわからなかった。<a href="https://github.com/jupyter-scala/jupyter-scala/blob/98bac7034f07e3e51d101846953aecbdb7a4bb5d/jupyter-scala#L37">ここ</a>にResolverを追加すればよさそうではあるが、まだ試していない。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> <p><a href="https://github.com/jupyter-scala/jupyter-scala/issues/4">このイシュー</a> によると<code>load.ivy</code>や<code>classpath.add</code>など古いAPIとのこと。<a href="#fnref:1" rev="footnote">&#8617;</a></p></li> </ol> </div> j5ik2o FluxとDDDの統合方法 hatenablog://entry/10328749687182036058 2016-09-09T20:06:43+09:00 2016-09-09T20:06:43+09:00 おはこんばんにちは、かとじゅんです。 久しぶりにブログを書く…。最近、趣味でAngular2やらReactやらやっています。やっとWebpackになれました…。 さて、今回のお題は「FluxとDDDの統合方法」について。Angular2を先に触っていましたが、FluxといえばやはりReactだろうということで途中で浮気してReactで考えています。Angular2でもできるはずですが、今回はReactで統合方法*1について考えてみたいと思います。一つ断っておくと、FluxはDDDと統合することを想定していない設計パターンなんで云々とかはここでは考えていません。それはこのブログ記事を読む読まない… <p>おはこんばんにちは、かとじゅんです。 久しぶりにブログを書く…。最近、趣味でAngular2やらReactやらやっています。やっとWebpackになれました…。</p> <p>さて、今回のお題は「FluxとDDDの統合方法」について。Angular2を先に触っていましたが、FluxといえばやはりReactだろうということで途中で浮気してReactで考えています。Angular2でもできるはずですが、今回はReactで統合方法<a href="#f-ca189aa6" name="fn-ca189aa6" title="今回はEvent Sourcingではありません。State Sourcingです。機会があれば実装例を作ります">*1</a>について考えてみたいと思います。一つ断っておくと、FluxはDDDと統合することを想定していない設計パターンなんで云々とかはここでは考えていません。それはこのブログ記事を読む読まないに関わらずご自身で判断されてください。ソースコードについては、Githubへのリンクを一番下に書いてあるので興味がある人は参考にしてみてください。</p> <h2>Fluxって何?</h2> <p>まず基礎ということで、Flux is 何から。</p> <p><a href="https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png" class="http-image" target="_blank"><img src="https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png" class="http-image" alt="https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png"></a></p> <p>本家曰く、クライアントサイドアプリケーションを構築するためにFacebookが使っているアプリケーションアーキテクチャ。図を見ればわかるように一方向のデータフローを提供するのが特徴です。Facebookが開発しているので、Reactと一緒に使われることが多いと思いますが、フレームワークというより設計パターンのようなものです。</p> <p>では、Fluxの各コンポーネントの僕の解釈からということで以下を参照。</p> <p><a href="https://facebook.github.io/flux/docs/overview.html">Flux | Application Architecture for Building User Interfaces</a></p> <p>Viewはわかると思うので、Action, Action Creator, Dispatcher, Storeをみていきましょう。特にStoreってなんぞやって話が多いので整理します。Fluxについて解釈が間違っているところがあれば指摘してもらえるとうれしいです。</p> <h2>Action with Action Creator</h2> <p>Action Creatorはメソッドのパラメータからアクションを生成する、ヘルパーメソッドの集合。Actionにはタイプがアサインされ、それをDispatcherに提供する。</p> <h2>Dispatcher</h2> <p>すべてのActionは、StoreがDispathcerに登録したコールバックを経由してすべてのStoreに送られる。</p> <ul> <li>すべてのActionはStoreがDispatcherに登録したコールバックを経由してStoreに送られる。</li> <li>Dispatcherは、Fluxアプリケーションのためのデータフローを管理する中央ハブ的な存在で、アプリケーション固有の要件を含まないシンプルな配送システム。</li> <li>Action Creatorは新しいActionをDispatcherに提供する。</li> </ul> <p>DispatcherはPublisher -> (Subscriber - Publisher) -> Subscriber みたいなものですね。技術的な要件しか含まなそう。</p> <h2>Store</h2> <p>はい。よくわかんねーと言われるもの。</p> <p>ストアがアクションへのレスポンス処理でストア自身を更新した後、変更イベントを送信する。</p> <ul> <li>Storeはアプリケーション状態とアプリケーションロジックを表すもの。</li> <li>MVCのモデル相当だが、たくさんのオブジェクト状態を管理する。ORMモデルような単一のモデルを表現しない。</li> <li>TodoStoreはTodoアイテムのコレクションを管理するものに似ている。Storeはモデルのコレクションと論理領域のシングルトンなモデルの両方の表現する。</li> <li>StoreはDispatcherを使って自分自身の登録とコールバックを提供する。コールバックはパラメータとしてActionを受け取る。</li> <li>Storeに登録されたコールバックの内部にある、アクションタイプに基づくswitch文がActionを解釈するために使われ、適切なフックを提供する。これによって、Dispatcher経由でStoreが持つアプリケーション状態を更新できるようになる。</li> <li>Storeが更新された後は、更新イベントがブロードキャストされ、ビューが新しい状態をStoreに問い合わせたり、ビュー自体を更新するかもしれない。</li> </ul> <p>ここでわかることは、StoreはMVCのモデル相当ということと、アプリケーション状態とアプリケーションロジックを含むということです。このモデルの振る舞いはコールバックの中にあるということになります。</p> <p>まとめると、ActionはメッセージとしてDispatcherに渡すと、ActionはDispatcher経由でStoreに送られる。Storeはアプリケーション状態を持っていて、Actionに応じた振る舞いを起こし状態を変化させる。状態変化が起こるとViewに通知される。更新の通知を受け取ったViewはStoreから状態を取り出したり、自分自身の状態を更新する可能性がある。ということになると思います。</p> <h2>DDDではどう考えるか?</h2> <p>第2部 モデル駆動設計の構成要素 から考えてみます。</p> <p>レイヤー化アーキテクチャ</p> <blockquote><p>ドメイン層 ビジネスの概念と、ビジネスが置かれた状況に関する情報、およびビジネスルールを表す責務を負う。ビジネスの状況を反映する状態はここで制御され使用されるが、それを格納するという技術的な詳細は、インフラストラクチャに委譲される。この層がビジネスソフトウェアの核心である。</p></blockquote> <p>ビジネスという言葉は少し大げさに聞こえるかもしれません。簡単に言い換えるならば、そのソフトウェアで解決すべき関心事とか知識という意味です。Todoアプリケーションならば、Todoのタイトル、期限、担当者、重要度、優先度などが当てはまるかもしれません。自分が担当しているTodoが今どれだけあるか、期限切れしているTodoは何かなどの状態を保持しています(状態の保持についての技術的都合はこの層の責務外)。そういう意味では、ソフトウェアのコアはドメインです。もう少し詳しくいうと、他のレイヤーのすべてのコンポーネントが直接的か間接的かによらずドメイン層に依存します。AngularやReactなどのフレームワークによって実装されるアプリケーションであっても、ドメイン層に従います。</p> <blockquote><p>アプリケーション層 ソフトウェアが行うことになっている仕事を定義し、表現力豊かなドメインオブジェクトが問題を解決するように導く。このレイヤが責務を負う作業は、ビジネス にとって意味があるものか、あるいは他システムのアプリケーション層と相互作用するのに必要なものである。このレイヤは薄く保たれる。ビジネスルールや知識を含まず、やるべき作業を調整するだけで、実際の処理は、ドメインオブジェクトによって直下のレイヤで実行される共同作業に委譲する。ビジネスの状況を反映する状態は持たないが、ユーザやプログラムが行う作業の進捗を反映する状態を持つことはできる。</p></blockquote> <p>アプリケーション層もわかりにくいものの一つかもしれませんが、ドメインモデルの状態や振る舞いを協調動作させてアプリケーション上の一つのユースケースを実現するものです。例えば、チャットの部屋で、誰かにメンションすると、チャットというドメインモデルを更新するだけではなく、To先のユーザアカウントのデバイスセッションを探し、通知しなければならないかもしれません。これがアプリケーションロジックと言われるもので、多くの場合はユースケースを担うアプリケーションサービスの責務となります。</p> <p>DDDではアプリケーション状態はドメイン層が握っていますが、もう少し詳細に述べると、集約とリポジトリというものが担っています。集約は内部にドメインモデルが複数格納されていて、それらの複数の関連をひとまとまりとして表現しています。そして、集約を永続化する際はリポジトリに依頼します。一般的に実装されるインターフェイス形式としてはput(todo: Todo)やgetByid(id: TodoId), getAll(): List[Todo] などがあります。対応する永続化デバイスもRDB版、KVS版、API版など様々ですが、外からみるとMapのように見えるインターフェイスを持っています。わざわざリポジトリが存在する理由としては、ドメインモデルはソフトウェアで解決する関心事をそのまま表現するため、永続化などの技術都合はリポジトリに移譲すべきという考えからきています。</p> <h2>FluxへのDDDの統合方法</h2> <p>では、Fluxとの統合について話しを先に進めます。FluxのStoreはリポジトリに似たような責務を持っているのは上記で述べた通りです。どちらもアプリケーション状態を保持する責務を担います。また、FluxのStoreにはアプリケーションロジックも含まれています。一方、リポジトリには集約の永続化責務しかありません。DDDではアプリケーションロジックはドメインモデルを協調動作させるアプリケーションサービスの役割でした。つまり、Flux Store = リポジトリ + 集約の協調動作(=アプリケーションサービス)と考えることができます。この時点でStoreはアプリケーション層にあると考えられます。ビジネスロジック付きの永続化アプリケーションサービスのようなものとして解釈することができます。</p> <h3>全体像</h3> <p>単純に統合するならば、FluxのStore内部でリポジトリと集約を使って、アプリケーション状態とアプリケーションロジックを実装すれば、FluxやDDDの考え方から逸脱せずに統合することが可能です。以下が統合イメージです。最初に考えたものから少し変わっていますが、根本は変わっていません。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/j5ik2o/20160909/20160909185652.png" alt="f:id:j5ik2o:20160909185652p:plain" title="f:id:j5ik2o:20160909185652p:plain" class="hatena-fotolife" itemprop="image"></span></p> <p>UI層にはUIに関連するActionとAction CreatorとViewを、アプリケーション層にはDispatcherとStoreを、ドメイン層にはビジネス上の概念を表す集約(ドメインモデルをカプセル化したもの)とリポジトリを、それ以外の汎用的なものはインフラストラクチャ層に配置します。アプリケーション状態を更新したり、取得したりする際に、Store経由でリポジトリと集約を利用します。それ以外の要件(たとえばドメインモデルとは関係がないRPC呼び出しなど)は別の方法で実現することを考えていますが、長くなるので後日にします。</p> <p>では、Fluxを踏まえて各層をどのように実装するか提案します。</p> <h3>ドメイン層</h3> <p>まず最初に集約を定義します。この集約は、ビジネス上の利害関係者の頭の中にある概念なので、実現方法である技術要素からできる限り独立している方がよいです。なので、ここではインフラストラクチャ(要件の影響を受けない汎用的技術基盤)となる言語機能にしか依存しないように設計します。しかしながら、今回のドメインモデルはただのデータの入れ物と化していて貧血症になっています。本来はビジネス上の豊富な知識を投影したドメインモデルとなるようにしなければいけませんが、今回の趣旨とは外れるのでご容赦くださし…。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoAggregate <span class="synIdentifier">{</span> <span class="synStatement">constructor(public</span> id: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> text: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> createAt: <span class="synSpecial">Date</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>次にこの集約の永続化を担うリポジトリを実装します。リポジトリは集約のインスタンスを保存したり検索したりすることができます<a href="#f-da04a1a3" name="fn-da04a1a3" title="ActiveRecordのようにドメインモデルが永続化機能を持たないのはなぜか?という疑問があるかもしれません。ドメインモデルはビジネス上の概念と紐付くものなので、なるべく固有の技術から依存性を排除しビジネス上の知識を表現します">*2</a>。インターフェイスだけを見ると集約のコレクションのように見え、内部の実装はMapであったり、ローカルストレージであったり、APIサーバであったり様々なものが実装できます。REST APIサーバから集約をI/Oしたいなら、内部の実装をHTTPクライアントに切り替える必要があるでしょう。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoRepository <span class="synIdentifier">{</span> <span class="synStatement">private</span> _todos: <span class="synIdentifier">{[</span>id: <span class="synType">string</span><span class="synIdentifier">]</span>: TodoAggregate<span class="synIdentifier">}</span> <span class="synStatement">=</span> <span class="synIdentifier">{}</span><span class="synStatement">;</span> <span class="synStatement">constructor(</span>aggreates: TodoAggregate<span class="synIdentifier">[]</span> <span class="synStatement">=</span> <span class="synIdentifier">[]</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>.storeMulti<span class="synStatement">(</span>aggreates<span class="synStatement">);</span> <span class="synIdentifier">}</span> store<span class="synStatement">(</span>aggregate: TodoAggregate<span class="synStatement">)</span>: <span class="synType">void</span> <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>._todos<span class="synIdentifier">[</span>aggregate.<span class="synSpecial">id</span><span class="synIdentifier">]</span> <span class="synStatement">=</span> _.cloneDeep<span class="synStatement">(</span>aggregate<span class="synStatement">);</span> <span class="synIdentifier">}</span> storeMulti<span class="synStatement">(</span>aggreates: TodoAggregate<span class="synIdentifier">[]</span><span class="synStatement">)</span>: <span class="synType">void</span> <span class="synIdentifier">{</span> aggreates.forEach<span class="synStatement">((</span>a<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">this</span>.store<span class="synStatement">(</span>a<span class="synStatement">));</span> <span class="synIdentifier">}</span> resoleBy<span class="synStatement">(</span>id: <span class="synType">string</span><span class="synStatement">)</span>: TodoAggregate <span class="synIdentifier">{</span> <span class="synStatement">return</span> _.cloneDeep<span class="synStatement">(</span><span class="synIdentifier">this</span>._todos<span class="synIdentifier">[</span>id<span class="synIdentifier">]</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> resolveAll<span class="synStatement">()</span>: TodoAggregate<span class="synIdentifier">[]</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synSpecial">Object</span>.keys<span class="synStatement">(</span><span class="synIdentifier">this</span>._todos<span class="synStatement">)</span>.map<span class="synStatement">((</span>id<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">this</span>.resoleBy<span class="synStatement">(</span>id<span class="synStatement">));</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <h3>アプリケーション層</h3> <p>Storeの前にStateについて説明します。Stateには画面の入力状態以外にビューに出力するための状態として、先ほど定義したリポジトリを含めるようにします<a href="#f-4b91bcee" name="fn-4b91bcee" title="Scalaのケースクラスのcopyメソッドがほしい…。インスタンスの入れ替えとか面倒なので">*3</a>。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoState <span class="synIdentifier">{</span> <span class="synStatement">constructor(public</span> currentTodo: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">private</span> _repository: TodoRepository<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">}</span> getRepository <span class="synStatement">=</span> <span class="synStatement">()</span>: TodoRepository <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> _.cloneDeep<span class="synStatement">(</span><span class="synIdentifier">this</span>._repository<span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synIdentifier">}</span> </pre> <p>次はStoreです。今回はFluxUtilsのReduceStoreで実装します。肝心な部分はreduceメソッドとcreateTodoメソッドです。このコードから、アプリケーション状態はリポジトリが担っていることがわかります。今回のTodoは振る舞いがないので微妙ですが、複雑な要件であればこのドメインモデルに設計の意図を十分に表せる振る舞いが実装されるはずです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoStore <span class="synStatement">extends</span> FluxUtils.ReduceStore<span class="synStatement">&lt;</span>TodoState<span class="synStatement">,</span> TodoAction<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">constructor(</span>dispatcher: Flux.Dispatcher<span class="synStatement">&lt;</span>TodoAction<span class="synStatement">&gt;)</span> <span class="synIdentifier">{</span> <span class="synStatement">super(</span>dispatcher<span class="synStatement">);</span> <span class="synIdentifier">}</span> getInitialState<span class="synStatement">()</span>: TodoState <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">new</span> TodoState<span class="synStatement">(</span><span class="synConstant">''</span><span class="synStatement">,</span> <span class="synStatement">new</span> TodoRepository<span class="synStatement">());</span> <span class="synIdentifier">}</span> reduce<span class="synStatement">(</span>state: TodoState<span class="synStatement">,</span> action: TodoAction<span class="synStatement">)</span>: TodoState <span class="synIdentifier">{</span> <span class="synStatement">switch</span> <span class="synStatement">(</span>action.<span class="synStatement">type)</span> <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">'CreateTodo'</span>: <span class="synStatement">return</span> <span class="synIdentifier">this</span>.createTodo<span class="synStatement">(</span>state<span class="synStatement">,</span> action <span class="synStatement">as</span> CreateTodo<span class="synStatement">);</span> <span class="synStatement">default</span>: <span class="synSpecial">throw</span> <span class="synSpecial">Error</span><span class="synStatement">(</span><span class="synConstant">'no match error'</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">private</span> createTodo<span class="synStatement">(</span>state: TodoState<span class="synStatement">,</span> action: CreateTodo<span class="synStatement">)</span>: TodoState <span class="synIdentifier">{</span> console.log<span class="synStatement">(</span>action<span class="synStatement">);</span> <span class="synStatement">const</span> currentTodo <span class="synStatement">=</span> state.currentTodo<span class="synStatement">;</span> <span class="synStatement">const</span> repository <span class="synStatement">=</span> <span class="synStatement">new</span> TodoRepository<span class="synStatement">(</span>state.getRepository<span class="synStatement">()</span>.resolveAll<span class="synStatement">());</span> <span class="synStatement">const</span> aggregate <span class="synStatement">=</span> <span class="synStatement">new</span> TodoAggregate<span class="synStatement">(new</span> Guid<span class="synStatement">()</span>.toString<span class="synStatement">(),</span> action.text<span class="synStatement">,</span> <span class="synStatement">new</span> <span class="synSpecial">Date</span><span class="synStatement">());</span> repository.store<span class="synStatement">(</span>aggregate<span class="synStatement">);</span> <span class="synStatement">return</span> <span class="synStatement">new</span> TodoState<span class="synStatement">(</span>currentTodo<span class="synStatement">,</span> repository<span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">const</span> todoStore <span class="synStatement">=</span> <span class="synStatement">new</span> TodoStore<span class="synStatement">(</span>todoDispatcher<span class="synStatement">);</span> </pre> <p>Dispatcherが抜けていた…。以下の一行です。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">const</span> todoDispatcher <span class="synStatement">=</span> <span class="synStatement">new</span> Flux.Dispatcher<span class="synStatement">&lt;</span>TodoAction<span class="synStatement">&gt;();</span> </pre> <h2>UI層</h2> <p>いよいよ、Reactのコンポーネントの結合ですが、最初にビューモデルを作ります。このモデルはビューに関連する知識を表しています。集約などのドメインモデルは、実際のビジネスなどの問題領域の概念と対応付きますが、ビューでは異なる表現形式が必要になる場合があります。集約のモデルを画面によって形式を変えて出力する場合などです。ビューモデルの生成はいろいろやり方がありますが、ここではConverterを使って集約から変換して導出します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoViewModel <span class="synIdentifier">{</span> <span class="synStatement">constructor(public</span> key: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> text: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> dateString: <span class="synType">string</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>Converterのコードは以下。リポジトリが持つ集約の集合をビューの要件に合わせてmapしているだけです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoViewModelConverter <span class="synIdentifier">{</span> <span class="synStatement">constructor(private</span> _repository: TodoRepository<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">}</span> getTodoVMs <span class="synStatement">=</span> <span class="synStatement">()</span>: TodoViewModel<span class="synIdentifier">[]</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">this</span>._repository.resolveAll<span class="synStatement">()</span>.map<span class="synStatement">((</span>a<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">new</span> TodoViewModel<span class="synStatement">(</span>a.<span class="synSpecial">id</span><span class="synStatement">,</span> a.text<span class="synStatement">,</span> a.createAt.toLocaleDateString<span class="synStatement">()</span> + <span class="synConstant">&quot; &quot;</span> + a.createAt.toLocaleTimeString<span class="synStatement">());</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synIdentifier">}</span> </pre> <p>最後に、React.Componentです。このコンポーネントはAction CreatorとStore(リポジトリ含む)にしか依存していません。 View自身も状態を持っていますが、UI上の何かのイベントが起こるとAction Creatorを使ってActionをDispatcherに送信します。すると、Storeで振る舞いを起こし状態変化が起こり、ViewがStoreにあらかじめ登録したハンドラが呼ばれてView自身の状態を書き換えます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> TodoComponent <span class="synStatement">extends</span> React.Component<span class="synStatement">&lt;</span><span class="synIdentifier">{}</span><span class="synStatement">,</span> TodoState<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">private</span> listenerSubscription: <span class="synIdentifier">{</span> remove: <span class="synSpecial">Function</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synStatement">constructor(</span>props: <span class="synIdentifier">{}</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">super(</span>props<span class="synStatement">);</span> <span class="synIdentifier">this</span>.state <span class="synStatement">=</span> <span class="synStatement">new</span> TodoState<span class="synStatement">(</span><span class="synConstant">''</span><span class="synStatement">,</span> <span class="synStatement">new</span> TodoRepository<span class="synStatement">());</span> <span class="synIdentifier">}</span> componentDidMount<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>.listenerSubscription <span class="synStatement">=</span> todoStore.addListener<span class="synStatement">(</span><span class="synIdentifier">this</span>.handleStateChange.bind<span class="synStatement">(</span><span class="synIdentifier">this</span><span class="synStatement">));</span> <span class="synIdentifier">}</span> componentWillUnmount<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>.listenerSubscription.remove<span class="synStatement">();</span> <span class="synIdentifier">}</span> handleStateChange<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synStatement">const</span> storeState <span class="synStatement">=</span> todoStore.getState<span class="synStatement">();</span> <span class="synStatement">const</span> newRepository <span class="synStatement">=</span> <span class="synStatement">new</span> TodoRepository<span class="synStatement">(</span>storeState.getRepository<span class="synStatement">()</span>.resolveAll<span class="synStatement">());</span> <span class="synStatement">const</span> newState <span class="synStatement">=</span> <span class="synStatement">new</span> TodoState<span class="synStatement">(</span>storeState.currentTodo<span class="synStatement">,</span> newRepository<span class="synStatement">);</span> <span class="synIdentifier">this</span>.setState<span class="synStatement">(</span>newState<span class="synStatement">);</span> <span class="synIdentifier">}</span> handleValueChange<span class="synStatement">(</span><span class="synConstant">event</span>: React.SyntheticEvent<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synStatement">const</span> todoText <span class="synStatement">=</span> <span class="synStatement">(</span><span class="synConstant">event</span>.<span class="synSpecial">target</span> <span class="synStatement">as</span> HTMLInputElement<span class="synStatement">)</span>.value<span class="synStatement">;</span> <span class="synIdentifier">this</span>.setState<span class="synStatement">(new</span> TodoState<span class="synStatement">(</span>todoText<span class="synStatement">,</span> <span class="synIdentifier">this</span>.state.getRepository<span class="synStatement">()));</span> <span class="synIdentifier">}</span> handleClick<span class="synStatement">()</span> <span class="synIdentifier">{</span> TodoActionCreator.createTodo<span class="synStatement">(</span><span class="synIdentifier">this</span>.state.currentTodo<span class="synStatement">);</span> <span class="synIdentifier">}</span> render<span class="synStatement">()</span>: JSX.<span class="synType">Element</span> <span class="synIdentifier">{</span> <span class="synStatement">const</span> todos <span class="synStatement">=</span> <span class="synStatement">new</span> TodoViewModelConverter<span class="synStatement">(</span><span class="synIdentifier">this</span>.state.getRepository<span class="synStatement">())</span>.getTodoVMs<span class="synStatement">();</span> <span class="synStatement">return</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>input <span class="synStatement">type=</span><span class="synConstant">'input'</span> value<span class="synStatement">=</span><span class="synIdentifier">{this</span>.state.currentTodo<span class="synIdentifier">}</span> onChange<span class="synStatement">=</span><span class="synIdentifier">{this</span>.handleValueChange.bind<span class="synStatement">(</span><span class="synIdentifier">this</span><span class="synStatement">)</span><span class="synIdentifier">}</span>/<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>button <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{this</span>.handleClick.bind<span class="synStatement">(</span><span class="synIdentifier">this</span><span class="synStatement">)</span><span class="synIdentifier">}</span><span class="synStatement">&gt;</span>Update<span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synIdentifier">{</span>todos.map<span class="synStatement">((</span>a<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">&lt;</span>p key<span class="synStatement">=</span><span class="synIdentifier">{</span>a.key<span class="synIdentifier">}</span><span class="synStatement">&gt;</span><span class="synIdentifier">{</span>a.text<span class="synIdentifier">}</span> : <span class="synIdentifier">{</span>a.dateString<span class="synIdentifier">}</span><span class="synStatement">&lt;</span>/p<span class="synStatement">&gt;;</span> <span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span> <span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;;</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>コードは<a href="https://github.com/j5ik2o/webpack-typescript-flux-react/tree/master/src/main/ts/todo">Github</a>にあります。リポジトリがメモリ版なのでそれをAPI版に返れば複数ユーザで特定のアプリケーション状態を共有できるようになるはずです。</p> <p>以上、長くなりましたが、ReactでのFluxとDDDの統合方法の解説でした!統合方法は他にもいろいろあると思いますが、僕が考える一例を示してみました。</p> <p>次はAngular2版をまとめてみます。</p> <div class="footnote"> <p class="footnote"><a href="#fn-ca189aa6" name="f-ca189aa6" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">今回はEvent Sourcingではありません。State Sourcingです。機会があれば実装例を作ります</span></p> <p class="footnote"><a href="#fn-da04a1a3" name="f-da04a1a3" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">ActiveRecordのようにドメインモデルが永続化機能を持たないのはなぜか?という疑問があるかもしれません。ドメインモデルはビジネス上の概念と紐付くものなので、なるべく固有の技術から依存性を排除しビジネス上の知識を表現します</span></p> <p class="footnote"><a href="#fn-4b91bcee" name="f-4b91bcee" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">Scalaのケースクラスのcopyメソッドがほしい…。インスタンスの入れ替えとか面倒なので</span></p> </div> j5ik2o 混乱しがちなサービスという概念について hatenablog://entry/10328537792365977040 2016-03-07T03:46:46+09:00 2016-03-07T03:46:46+09:00 社内でサービスがよくわからないという話になったので、考察を少しまとめておきます。 過去のエントリでも以下のように触れましたが、もう少しかみ砕いてみよう。 サービスという言葉はあいまい まず、簡単に前提の整理から。単に"サービス"って言葉が何を指すのか結構曖昧です。 サービスは簡単にいうと手続きとか振る舞いのことですが、細かくいうと、PofEAAでいうサービスと、DDDいうサービスは、目的が異なります。前者はアプリケーションのためにドメインモデルを再利用可能にするためのものです。後者はドメインの知識を表している振る舞いです。これはのちほど詳しく説明します。 まぁこのあたりは具体例がないと理解しが… <p>社内でサービスがよくわからないという話になったので、考察を少しまとめておきます。</p> <p>過去のエントリでも以下のように触れましたが、もう少しかみ砕いてみよう。</p> <blockquote cite="http://blog.j5ik2o.me/entry/2014/01/03/051100" data-uuid="10328537792365976097"><p>サービスという言葉はあいまい まず、簡単に前提の整理から。単に&quot;サービス&quot;って言葉が何を指すのか結構曖昧です。 サービスは簡単にいうと手続きとか振る舞いのことですが、細かくいうと、PofEAAでいうサービスと、DDDいうサービスは、目的が異なります。前者はアプリケーションのためにドメインモデルを再利用可能にするためのものです。後者はドメインの知識を表している振る舞いです。これはのちほど詳しく説明します。 まぁこのあたりは具体例がないと理解しがたいですが、レイヤーの違いによって責務が異なるという感じです。DDDのサービスの章では、サービスには、アプリケーション層、ドメイン層、インフラストラクチャ層と、複数のレイヤーに存在すると言及されています。PofEAAのService Layerは、DDDでいうアプリケーション層のサービス(以下 アプリケーションサービス)に相当すると思います。</p><cite><a href="http://blog.j5ik2o.me/entry/2014/01/03/051100">ServiceとDCIについて - かとじゅんの技術日誌</a></cite></blockquote> <p>サービスは抽象的でわかりにくい。特にDDDのレイヤー化アーキテクチャのレイヤー分割という概念を踏まえないと混乱する原因になりますので、レイヤーの定義から入りましょう。</p> <h2>レイヤー化アーテクチャの目的</h2> <p>第2部 第4章 ドメインを隔離するからの引用。</p> <p>まず、問題提起から</p> <blockquote> <p>UI, DBおよびその他の補助的なコードがビジネスオブジェクトに直接書かれることがしばしばある。また、ビジネスロジックが新たに追加される時には、UIウィジットやデータベーススクリプトのふるまいに組み込まれてしまう。こういうことが起きるのは、短期的に見ると、動くようにするには最も簡単な方法だからだ。 ドメイン関連のコードがそうした膨大な他のコードの中に拡散してしまうと、コードを見て意味を理解するのがきわめて困難になる。</p> </blockquote> <p>解決方法は以下。</p> <blockquote> <p>複雑なプログラムはレイヤーに分割すること。各レイヤで設計を進め、凝集度を高めて下位層にだけで依存するようにすること。<snip> ドメインモデルに関係するコード全部を1つの層に集中させ、UI、アプリケーション、インフラストラクチャのコードから分離すること。表示や格納、アプリケーションタスク管理などの責務から解放されることで、ドメインオブジェクトはドメインモデルを表現するという責務に専念できる。これによて、モデルは十分豊かで明確になるように進化し、本質的なビジネスの知識をとらえて、それを機能させることができるようになる。</p> </blockquote> <p>ここでは簡単にいうとドメインを隔離することが第一義的と言っています。</p> <h2>レイヤーの内訳</h2> <p>レイヤーは大きく分けて以下に分かれます。</p> <ul> <li>インターフェイス層(UIなど)</li> <li>アプリケーション層</li> <li>ドメイン層</li> <li>インフラストラクチャ層</li> </ul> <p>ここでは、同じく引用からアプリケーション層とドメイン層、インフラストラクチャ層の責務の違いを説明します。</p> <p>アプリケーション層とは</p> <blockquote><p>ソフトウェアが行うことになっている仕事を定義し、表現力豊かなドメインオブジェクトが問題を解決するように導く。このレイヤが責務を負う作業は、ビジネスにとって意味があるものか、あるいは他システムのアプリケーション層と相互作用するのに必要なものである。このレイヤは薄く保たれる。ビジネスルールや知識を含まず、やるべき作業を調整するだけで、実際の処理は、ドメインオブジェクトによって直下のレイヤで実行される共同作業に移譲する。ビジネスの状況を反映する状態は持たないが、ユーザやプログラムが行う作業の進捗を反映する状態を持つことはできる</p> </blockquote> <p>ドメイン層とは</p> <blockquote> <p>ビジネスの概念と、ビジネスが置かれた状況に関する情報、およびビジネスルールを表す責務を負う。ビジネスの状況を反映する状態はここで制御され使用されるが、これを格納するという技術的な詳細は、インフラストラクチャに委譲される。この層がビジネスソフトウェアの核心である</p> </blockquote> <p>インフラストラクチャ層とは</p> <blockquote> <p>上位のレイヤーを支える一般的な技術的な機能を提供する。これには、アプリケーションのためのメッセージ送信、ドメインのための永続化、ユーザインターフェイスのためのウィジット描画などがある。インフラストラクチャ層は、ここで示す4層間における相互作用のパターンも、アーキテクチャフレームワークを通じてサポートすることがある。 </p> </blockquote> <h2>ドメインサービスとは</h2> <p>それでは、各レイヤーのサービスについて説明する前に、ドメイン層のドメインサービスの概念について触れておきます。あくまでドメインサービスの話です。</p> <p>問題提起:</p> <blockquote><p>ドメインから生まれる概念の中には、オブジェクトとしてモデル化すると不自然なものもある。こうしたドメインで必要な機能をエンティティや値オブジェクトの責務として押し付けると、モデルに基づくオブジェクトの定義を歪めるか、意味のない不自然なオブジェクトを追加することになる</p> </blockquote> <p>解決方法:</p> <blockquote> <p>ドメインにおける重要なプロセスや変換処理が、エンティティや値オブジェクトの自然な責務でない場合は、その操作は、サービスとして宣言される独立したインターフェイスとしてモデルに追加すること。モデルの言語を用いてインターフェイスを定義し、操作名が必ずユビキタス言語の一部になるようにすること。サービスには状態を持たせないこと。</p> </blockquote> <p>具体的な例で考えてみましょう。コード例は、以前のエントリから引用します。口座間転送のサービスです。転送メソッドは、MoneyにもBankAccountにも従属できないので、ドメインサービスとしています。あまり適切な例ではないかもしれませんが、万人ウケする事例はないので…。</p> <blockquote cite="http://blog.j5ik2o.me/entry/2014/01/03/051100" data-uuid="10328537792365977572"><p>口座間送金 サービス</p><cite><a href="http://blog.j5ik2o.me/entry/2014/01/03/051100">ServiceとDCIについて - かとじゅんの技術日誌</a></cite></blockquote> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">object</span> TransferDomainService { <span class="synComment">// 送金する</span> <span class="synComment">// - 振る舞いの名前はユビキタス言語と対応する</span> <span class="synComment">// - 原則的にステートレスであること</span> <span class="synIdentifier"> def</span> transfer(money: Money, from: BankAccount, to: BankAccount): (BankAccount, BankAccount) = { require(from.balance &gt;= money) <span class="synType">val</span> newFrom = from.decrease(money) <span class="synType">val</span> newTo = to.increase(money) (newFrom, newTo) } } </pre> <p>呼び出し例</p> <pre class="code lang-scala" data-lang="scala" data-unlink>TransferDomainService.transfer(money, from, to) </pre> <p>このコードを見てもらうと、"なんだ ドメインモデルを入出力にとる関数じゃないか" と思うでしょう。そのとおりです。最初はその程度の認識でよいと思いますが、ここで一点だけいいたいのは、乱用は禁止ということです。ドメインサービスは、単に関数を導入をすればいいというものでありません。ドメインで必要な振る舞いであるが、無理矢理エンティティや値オブジェクトの振る舞いとすることで、ビジネス上の知見を歪める設計になるということです。また、逆に、従属するエンティティや値オブジェクトがないということで早期あきらめてしまい、なんでもかんでもドメインサービスにするというのもの違うのです。後者の場合は、振る舞いがあるべきドメインモデルから振る舞いを奪うことになるので、ドメインモデル貧血症の温床になる可能性があるのです。いずれにしても、ユビキタス言語のセマンティクスに従う必要があるということですね。十分な考慮なしに乱用は厳禁です。</p> <h2>アプリケーションサービスの事例</h2> <p>前置きはさておき、アプリケーション層のアプリケーションサービスの違いを考えてみましょう<a href="#f-c192bc0e" name="fn-c192bc0e" title="混同の対象はアプリケーションサービスとドメインサービスとしているので、インフラストラクチャ層のインフラストラクチャサービスは特に触れません。">*1</a>。 以下の例は、上記のドメインサービスを利用したアプリケーションサービスの例です。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synType">case</span> <span class="synType">class</span> TransferDto(money: Money, fromId: BankAccountId, toId: BankAccountId) <span class="synType">object</span> TransferApplicationService { <span class="synIdentifier"> def</span> transfer(transferDto: TransferDto): Try[Unit] = { DB.localTx{ implicit tx =&gt; <span class="synStatement">for</span> { Seq(from, to) &lt;- BackAccountRepository.resolveBy(transferDto.fromId, transferDto.toId) (newFrom, newTo) = TransferDomainService.transfer(money, from, to) storeResult = BankAccountRepository.store(newFrom, newTo) Seq(fromUA, toUA) &lt;- UserAccountRepository.resolveByBankAccountIds(transferDto.fromId, transferDto.toId) result &lt;- NotifyInfrastructureService.notify(storeResult, fromUA.mailAddress, toUA.mailAddress) } <span class="synType">yield</span> result } } } </pre> <h2>ドメインサービスとアプリケーションサービスの違い</h2> <p>ドメインサービスはドメインモデルを入出力にとる関数ですが、それだけではアプリケーション要件は実現できません。そもそもアプリケーションサービスの入出力が違うわけです。アプリケーションサービスはI/F層から利用される前提であるため、TransferDtoはユースケースに強く依存することになります。また、I/Oの整合性を保証するためのトランザクション制御や、特定の処理完了を告げるためにプッシュ通知サービスを呼び出したりすることがあります<a href="#f-1a87bf44" name="fn-1a87bf44" title="この例では、DBやNotifyInfrastructureServiceが、アプリケーション固有ではなく一般的な技術基盤としてのインフラストラクチャサービスと位置づけています">*2</a>。もちろん、単純なユースケースであれば、コントローラなどにこのようなロジックを直接書くかもしれないが、意図をより明確にするには別の名前をつけたメソッドに切り出したりします。この考え方をより推し進めたのがアプリケーションサービスです。</p> <p>DDDでは、ドメインサービスはユビキタス言語に基づくビジネスロジックを表現しています。一方、アプリケーションサービスはビジネスロジックそのものではないという定義です<a href="#f-58177ff8" name="fn-58177ff8" title="ビジネスにとっては必要不可欠であることは間違いないですが。">*3</a>。あくまで、ドメインモデルやインフラストラクチャサービスなどのやるべき作業を調整し進捗管理をするためだけの存在なのです。ユビキタス言語上の振る舞いと、アプリケーション上のユースケースという関係性とでも言えばよいでしょうか?この言葉だけでも、粒度が違うことが理解できると思います。この例ではリポジトリを使って現在の集約(=グローバルな識別子を持つエンティティ)を読み込んでドメインサービスの結果をまた保存しています。このI/Oはドメインモデル(エンティティ, 値オブジェクト, ドメインサービス)自体ので責務<a href="#f-cb41fc06" name="fn-cb41fc06" title="ドメインモデルはユビキタス言語に即した表現を行うのが責務なので永続化は責務対象外としている">*4</a>ではなく、リポジトリの責務なのでドメインサービスが担えないと考えています。</p> <p>というわけで、ドメインサービスとアプリケーションサービスは、役割からして全く違うものです。</p> <p>ドメインサービスのメソッドはユビキタス言語に結びついていることが前提です。ビジネス上の知識が単に振る舞いとなっているだけですが、重要なのは名前だけでなくその裏に秘められた不変条件です。口座間送金の場合はfromからtoに必ずお金が移動することです。</p> <p>仮に <code>TransferDomainService.transfer(money, from, to)</code> メソッドの呼び出し部分が</p> <pre class="code lang-scala" data-lang="scala" data-unlink>newFrom = from.decrease(money) newTo = to.increase(money) </pre> <p>であってもこのようなアプリケーションサービスは必要になるかもしれない。僕の経験ではほとんど必要になります。繰り返しになりますが、役割が違うから必要になるというわけです。もし、ドメインサービスにプッシュ通知などの通知機能が含まれているとしたら、それはドメイン知識と関係があるのか?それはユビキタス言語に含まれるのか?という問いをした方がよいでしょう。仮に、混同しているようなら、ドメインは隔離できていないということになります。</p> <h2>Akka で 番外編</h2> <p>ここから番外編。AkkaでのCQRS+ESを前提にしているので理解しにくいと思った方は無理に読まなくてもよいと思います。</p> <p>PersistentActorを使って 集約やドメインサービス、アプリケーションサービスを実装した場合の擬似コードを書いてみた。あくまで概念を理解してもらうためのコードなので、すべてのコードは記載していませんし、コンパイルできないとか、集約がなぜActorRefなのかなど、そういう細かい点は無視して下さい。</p> <p>CQRS + ESになるとドメインモデル群は書き込み系にしか登場しませんが、読み込みは別の系になります。集約を読み込むことを考えると伝統的なスタイルではリポジトリを実装することになりますが、CQRSでは集約は基本的にキーバリューのデータ構造として保存できればいいと見なすことができます。</p> <p>AkkaではPersistentActorにコマンドを投げてその時の状態変化の記録としてドメインイベントを追記保存していくことになります<a href="#f-474c2fd1" name="fn-474c2fd1" title="エラー時のロールバックはこの例では考慮していません">*5</a>。蛇足ですが、集約はドメインモデルなのになぜ永続化機能も持つの?という疑問にだけ答えておくと、集約内部のルートとなるエンティティに代表されるドメインモデルには永続化機能がなくて、ライフサイクルを司る集約にだけ永続化機能があるという考え方です。以下の例では、開始と終了イベントしか永続化していません<a href="#f-363fb431" name="fn-363fb431" title="開始イベントは利用してないのでいらないかも。">*6</a>が、リポジトリでいうstoreの機能はAkkaが担っていることになります。</p> <p>そうなるとドメインサービスとアプリケーションサービスはほぼ同じようなものになるのでは?という見方になると思いますが、微妙に要件(以下の例では通知サービスの呼び出しがある)が異なるのでそうもいかないというのがわかると思います。</p> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// ドメインサービス</span> <span class="synType">class</span> TransferDomainService <span class="synType">extends</span> PersistentActor { <span class="synType">var</span> state: Option[TransferState] = None <span class="synComment">// snip</span> <span class="synType">override</span><span class="synIdentifier"> def</span> receiveCommand = { <span class="synType">case</span> BankAccountIncreased(toRef) =&gt; <span class="synType">val</span> (commandId, fromRef, toRef, money) = state.map( e =&gt; (e.fromRef, e.toRef, e.money) ).get persist(DomainTransferFinished(EventId(), commandId, fromRef, toRef, money)) { ev =&gt; eventBus.unsubscribe(Topic(classOf[BankAccountIncreased], toRef)) eventBus.publish(Topic(ev, Some(ev.commandId)) <span class="synComment">// 終わったら完了イベントを発火</span> } <span class="synType">case</span> BankAccountDecreased(fromRef) =&gt; eventBus.unsubscribe(Topic(classOf[BankAccountDecreased], fromRef)) <span class="synType">val</span> toRef = state.map(_.toRef).get toRef ! Increase(state.map(_.money).get) <span class="synComment">// BankAccount に Increase メッセージをパッシング。状態の永続化も行われます。</span> <span class="synType">case</span> Transfer(commandId, money, fromRef, toRef) =&gt; persist(DomainTransferStarted(EventId(), commandId, fromRef, toRef, money)) { ev =&gt; state = Some(TransferState(commandId, fromRef, toRef, money)) eventBus.subscribe(Topic(classOf[BankAccountDecreased], fromRef)) eventBus.subscribe(Topic(classOf[BankAccountIncreased], toRef)) fromRef ! Decrease(money) <span class="synComment">// BankAccount に Decrease メッセージをパッシング。状態の永続化も行われます</span> eventBus.publish(ev) <span class="synComment">// 開始イベントを発火</span> } } } </pre> <pre class="code lang-scala" data-lang="scala" data-unlink><span class="synComment">// アプリケーションサービス</span> <span class="synType">class</span> TransferApplicationService <span class="synType">extends</span> PersistentActor { <span class="synComment">// snip</span> <span class="synType">override</span><span class="synIdentifier"> def</span> receiveCommand = { <span class="synType">case</span> NotifyCompleted(_, commandId) =&gt; persist(ApplicationTransferFinished(EventId(), commandId, fromId, toId, money)) { ev =&gt; eventBus.unsubscribe(Topic(classOf[NotifyCompleted], commandId)) eventBus.publish(ev) <span class="synComment">// 終了イベントを発火</span> } <span class="synType">case</span> DomainTransferFinished(_, commandId, fromId, toId, money) =&gt; <span class="synComment">// ドメインロジックが完了したら</span> eventBus.unsubscribe(Topic(classOf[TransferFinished], commandId)) eventBus.subscribe(Topic(classOf[NotifyCompleted], commandId)) notifyService ! Notify(commandId, fromId, toId, money) <span class="synComment">// 通知サービスを呼び出す</span> <span class="synType">case</span> ApplicationTransfer(commandId, money, fromId, toId) =&gt; persist(ApplicationTransferStarted(EventId(), commandId, fromId, toId, money)) { ev =&gt; eventBus.subscribe(Topic(classOf[TransferFinished], commandId)) <span class="synType">val</span> fromRef = context.actorSelection(s<span class="synConstant">&quot;/user/${fromId}&quot;</span>).resolveOne() <span class="synComment">// リポジトリから参照を取得する処理に相当する</span> <span class="synType">val</span> toRef = context.actorSelection(s<span class="synConstant">&quot;/user/${toId}&quot;</span>).resolveOne() <span class="synComment">// リポジトリから参照を取得する処理に相当する</span> transferRef ! Transfer(commandId, money, fromRef, toRef) <span class="synComment">// ドメインサービスを呼び出す</span> eventBus.publish(ev) <span class="synComment">// 開始イベントを発火</span> } } } </pre> <div class="footnote"> <p class="footnote"><a href="#fn-c192bc0e" name="f-c192bc0e" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">混同の対象はアプリケーションサービスとドメインサービスとしているので、インフラストラクチャ層のインフラストラクチャサービスは特に触れません。</span></p> <p class="footnote"><a href="#fn-1a87bf44" name="f-1a87bf44" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">この例では、DBやNotifyInfrastructureServiceが、アプリケーション固有ではなく一般的な技術基盤としてのインフラストラクチャサービスと位置づけています</span></p> <p class="footnote"><a href="#fn-58177ff8" name="f-58177ff8" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">ビジネスにとっては必要不可欠であることは間違いないですが。</span></p> <p class="footnote"><a href="#fn-cb41fc06" name="f-cb41fc06" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">ドメインモデルはユビキタス言語に即した表現を行うのが責務なので永続化は責務対象外としている</span></p> <p class="footnote"><a href="#fn-474c2fd1" name="f-474c2fd1" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">エラー時のロールバックはこの例では考慮していません</span></p> <p class="footnote"><a href="#fn-363fb431" name="f-363fb431" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">開始イベントは利用してないのでいらないかも。</span></p> </div> j5ik2o