Mobile Factory Tech Blog 技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします! 2024-03-12T16:00:00+09:00 mobile-factory Hatena::Blog hatenablog://blog/17391345971642619799 Mapbox GL JS でresize animationを実装する hatenablog://entry/6801883189089944451 2024-03-12T16:00:00+09:00 2024-03-12T16:00:12+09:00 みなさん、こんにちは。新卒エンジニアの id:matsuda0528 です。 今日は、Mapbox GL JS を使用して地図の描画領域を変更するアニメーションを実装する方法についてお話します。 TL;DR 以下のように、setInterval() 関数を用いて resize() 関数を繰り返し実行する方法で実装しました。 const onClickMapResizeButton = () => { clearInterval(mapResizer) mapResizer = setInterval(() => { map.value.resize() }) } const onTransit… <p>みなさん、こんにちは。新卒エンジニアの <a href="http://blog.hatena.ne.jp/matsuda0528/">id:matsuda0528</a> です。 今日は、Mapbox GL JS を使用して地図の描画領域を変更するアニメーションを実装する方法についてお話します。</p> <h1 id="TLDR">TL;DR</h1> <p>以下のように、<code>setInterval()</code> 関数を用いて <code>resize()</code> 関数を繰り返し実行する方法で実装しました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> onClickMapResizeButton = () =&gt; <span class="synIdentifier">{</span> clearInterval(mapResizer) mapResizer = setInterval(() =&gt; <span class="synIdentifier">{</span> map.value.resize() <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> <span class="synStatement">const</span> onTransitionend = () =&gt; <span class="synIdentifier">{</span> clearInterval(mapResizer) <span class="synIdentifier">}</span> </pre> <h1 id="駅メモの地図について">駅メモの地図について</h1> <p>駅メモでは、Mapbox GL JS を使用していくつかの地図表示機能を実装しています。 最近ではスタンプラリーイベントで新たに地図表示機能が追加されました。 今回は、スタンプラリーイベントで新しく実装した<strong>地図の大きさを変更するアニメーション</strong>について解説します。</p> <h1 id="地図のサイズ変更アニメーション">地図のサイズ変更アニメーション</h1> <p>下記のような実装を用意します。 その上で <code>&lt;div class="map"&gt;</code> の大きさを変更する処理を追加すれば、地図のサイズ変更アニメーションを実装することが可能です。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">div</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">&quot;map&quot;</span><span class="synIdentifier">&gt;</span> <span class="synComment">&lt;!-- 地図を描画するコンポーネント --&gt;</span> <span class="synIdentifier">&lt;</span>v-<span class="synStatement">map</span><span class="synIdentifier"> ... /&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> </pre> <pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.map</span> <span class="synIdentifier">{</span> <span class="synType">transition</span>: <span class="synType">height</span> <span class="synConstant">0.5s</span> <span class="synConstant">ease</span>; <span class="synIdentifier">}</span> </pre> <p>しかし、Mapbox GL JS の地図は描画領域に関する情報を保持していて、CSS アニメーションだけでは地図本体が追従しません。 実際に行ってみると、以下の図のように地図を囲む要素の大きさは変わるものの、地図自体の描画領域は変化しません。</p> <p><figure class="figure-image figure-image-fotolife" title="サイズ変更前、サイズ変更後の地図"><div class="images-row mceNonEditable"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20240312/20240312160006.png" width="784" height="700" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20240312/20240312160003.png" width="422" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></div><figcaption>サイズ変更前、サイズ変更後の地図</figcaption></figure></p> <p>地図の描画領域も同時に動かすためには、<code>resize()</code> 関数を用いて随時地図領域を更新する必要があります。 今回は、地図の外枠に対して transition でアニメーションを行いつつ、その間中に <code>resize()</code> を実行し続けることで対応しました。</p> <p>以下に Vue3 での実装例を挙げています:</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>&lt;template&gt; &lt;main&gt; &lt;div <span class="synStatement">class</span>=<span class="synConstant">&quot;map&quot;</span> :<span class="synStatement">class</span>=<span class="synConstant">&quot;mapSize&quot;</span> @transitionend=<span class="synConstant">&quot;onTransitionend&quot;</span>&gt; &lt;v-map ref=<span class="synConstant">&quot;map&quot;</span> ... /&gt; &lt;/div&gt; &lt;v-button @click=<span class="synConstant">&quot;onClickMapResizeButton&quot;</span>&gt;サイズ変更&lt;/v-button&gt; &lt;/main&gt; &lt;/template&gt; &lt;script setup&gt; <span class="synStatement">import</span> <span class="synIdentifier">{</span> ref <span class="synIdentifier">}</span> from <span class="synConstant">'vue'</span>; <span class="synStatement">const</span> map = ref(<span class="synStatement">null</span>); <span class="synStatement">const</span> mapSize = ref(<span class="synConstant">'map-size-normal'</span>); <span class="synStatement">const</span> mapResizer; <span class="synStatement">const</span> onClickMapResizeButton = () =&gt; <span class="synIdentifier">{</span> mapSize.value = <span class="synConstant">'map-size-large'</span>; mapResizer = setInterval(() =&gt; <span class="synIdentifier">{</span> map.value.resize(); <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>; <span class="synStatement">const</span> onTransitionend = () =&gt; <span class="synIdentifier">{</span> clearInterval(mapResizer); <span class="synIdentifier">}</span>; &lt;/script&gt; &lt;style&gt; .map <span class="synIdentifier">{</span> transition: height 0.5s ease; <span class="synIdentifier">}</span> .map-size-normal <span class="synIdentifier">{</span> height: 50px; <span class="synIdentifier">}</span> .map-size-large <span class="synIdentifier">{</span> height: 100px; <span class="synIdentifier">}</span> &lt;/style&gt; </pre> <h1 id="clearInterval-に届かない可能性を考える">clearInterval に届かない可能性を考える</h1> <p>この実装の懸念点は、<code>setInterval()</code> から <code>clearInterval()</code> までに別のイベント( <code>onTransitionend</code> )を経由するため、<code>clearInterval()</code> が必ず実行される保証がないことです。 たとえば、一度「サイズ変更ボタン」を押した後、 transition の途中で「サイズ変更ボタン」をもう一度押してしまうと、<code>onTransitionend</code> が発火しないまま新しく <code>setInterval()</code> が実行されてしまいます。 これによって、最初の <code>setInterval()</code> の <code>intervalID</code> が失われてしまい、インターバルの処理が終わらなくなってしまう可能性があります。 この問題への対応策として、transition が中断する可能性のあるアクションの前に <code>clearInterval()</code> を実行します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> onClickMapResizeButton = () =&gt; <span class="synIdentifier">{</span> mapSize.value = <span class="synConstant">&quot;map-size-large&quot;</span> clearInterval(mapResizer) mapResizer = setInterval(() =&gt; <span class="synIdentifier">{</span> map.value.resize() <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> </pre> <h1 id="まとめ">まとめ</h1> <p>今回紹介した方法では、<code>setInterval()</code> を使用して <code>resize()</code> 関数を繰り返し実行することで、地図のサイズ変更アニメーションを実現しました。 ただし、この方法では <code>clearInterval()</code> が常に適切に実行される保証がないため、使う際には注意が必要です。</p> <h1 id="参考サイト">参考サイト</h1> <ul> <li>Mapbox GL JS | Mapbox. (<a href="https://docs.mapbox.com/mapbox-gl-js">https://docs.mapbox.com/mapbox-gl-js</a>)</li> <li>javascript - MapBox Smooth Transition of Resizing Map - Stack Overflow. (<a href="https://stackoverflow.com/questions/61490901/mapbox-smooth-transition-of-resizing-map">https://stackoverflow.com/questions/61490901/mapbox-smooth-transition-of-resizing-map</a>)</li> </ul> matsuda0528 コスト削減のため Redis の sorted sets で実装していたランキング処理を MySQL に移行しました hatenablog://entry/6801883189078940693 2024-01-29T16:30:00+09:00 2024-01-29T16:30:10+09:00 駅メモ!チームエンジニアの id:yumlonne です。 この記事では Redis の sorted sets で実装していたランキング処理を MySQL に移行した仕組みを紹介します。 背景 駅メモ!には複数のランキングがあり、Redis の sorted sets を使うことでパフォーマンスの高いランキング処理を実現していました。 中にはリリースからの全期間に渡るデータを利用するランキングもあり、Redis のメモリ使用率は日に日に増えていく一方でした。 何度か Redis をスケールアップしてメモリを増やすことで対応していましたが、根本的に対応しなければ今後も Redis をスケールア… <p>駅メモ!チームエンジニアの <a href="http://blog.hatena.ne.jp/yumlonne/">id:yumlonne</a> です。</p> <p>この記事では Redis の sorted sets で実装していたランキング処理を MySQL に移行した仕組みを紹介します。</p> <h2 id="背景">背景</h2> <p>駅メモ!には複数のランキングがあり、Redis の sorted sets を使うことでパフォーマンスの高いランキング処理を実現していました。 中にはリリースからの全期間に渡るデータを利用するランキングもあり、Redis のメモリ使用率は日に日に増えていく一方でした。 何度か Redis をスケールアップしてメモリを増やすことで対応していましたが、根本的に対応しなければ今後も Redis をスケールアップもしくはスケールアウトさせ続けるしか選択肢がなく、コストが増え続けてしまう状況でした。</p> <p>調査したところ、一部のランキングがメモリ使用率の 2/3 程度を占めていることが判明しました。 そこで、その巨大なランキングを Redis から MySQL に移行させることを考えました。</p> <h2 id="Redis-とデータベースの詳細">Redis とデータベースの詳細</h2> <ul> <li>Redis: Amazon ElastiCache Redis 7.x</li> <li>データベース: Amazon RDS Aurora v2 (MySQL 5.7 InnoDB)</li> </ul> <h2 id="ランキングについて">ランキングについて</h2> <p>以下は対象となるランキングの要件と仕様です。</p> <p>要件 1. 上位数件のユーザとそのスコアを取得できる<br/> 要件 2. ユーザは自分自身の順位を取得できる<br/> 要件 3. 順位は同率を考慮する(スコア 100,90,90,80 とある場合、順位は 1,2,2,4 となる)<br/> 要件 4. 上記の処理をそれぞれ 10ms 程度で実行できる</p> <p>仕様 1. 古いデータを削除することはない<br/> 仕様 2. スコアの更新は増加のみで、減少させるような更新はない</p> <p>また、ランキングのスコアにはかなりの偏りがあります。 以下は偏りのイメージです。</p> <p>縦軸はユーザ数ですが偏りが酷いので対数目盛にしています。<br/> 横軸はスコアです。イメージなので数値は表示していません。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20240129/20240129163004.png" width="800" height="445" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="MySQL-でのランキング処理の課題">MySQL でのランキング処理の課題</h2> <p>「要件 1. 上位数件のユーザとそのスコアを取得できる」 はスコアにインデックスを張っておけば 以下のように指定するだけで高速に取得できるため問題ありません。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> * <span class="synSpecial">FROM</span> ranking <span class="synSpecial">ORDER</span> <span class="synSpecial">BY</span> score <span class="synSpecial">DESC</span> LIMIT <span class="synConstant">10</span>; </pre> <p>問題は 「要件 2. ユーザは自分自身の順位を取得できる」 で、こちらはインデックスだけでは解決できません。ユーザの順位を求めるためには、そのユーザのスコアより高いスコアのユーザ数を知る必要があります。 下記 SQL にて自分よりスコアが高いユーザ数を取得できますが、スコアが低いユーザの場合、カウント対象行が多くなるためインデックスを有効活用しづらくなります。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> <span class="synIdentifier">COUNT</span>(*) <span class="synSpecial">FROM</span> ranking <span class="synSpecial">WHERE</span> score &gt; <span class="synConstant">50</span>; </pre> <p>この問題を回避するためにカウント対象行を絞る対応を考えました。</p> <h2 id="ランキングを区切る">ランキングを区切る</h2> <p>順位を出すときにカウント対象行が多くなりうる問題を解消するため、ランキングを区切ることを考えてみます。</p> <p>例えば、事前に「1000 位のユーザーのスコアが 500」だと集計できているとします。 スコアが 450 のユーザーの順位は、「スコアが 450 より大きく 500 以下」を満たすレコード数に 1000 (位)を加えることで求められます。</p> <table> <thead> <tr> <th> 順位 </th> <th> スコア </th> </tr> </thead> <tbody> <tr> <td> 1000 </td> <td> 500 </td> </tr> <tr> <td> 2000 </td> <td> 340 </td> </tr> <tr> <td> 3000 </td> <td> 250 </td> </tr> <tr> <td> 4000 </td> <td> 210 </td> </tr> <tr> <td> ... </td> <td> ... </td> </tr> </tbody> </table> <p>上の表のように順位を等間隔で区切ることにより、いかなる順位でもカウント対象をほぼ一定に保つことができ、パフォーマンスが安定します。 逆に、スコアを等間隔で区切るとランキングデータの偏りの影響でカウント対象がばらつくため不採用としました。</p> <p>説明のため、以降はランキングを区切るデータのことをランキングインデックスと表記します。</p> <h3 id="サンプルスキーマ">サンプルスキーマ</h3> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">CREATE</span> <span class="synSpecial">TABLE</span> `ranking` ( `id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> AUTO_INCREMENT, `user_id` <span class="synType">int</span>(<span class="synConstant">10</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span>, `score` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span>, PRIMARY KEY (`id`), <span class="synSpecial">UNIQUE</span> KEY `user_uniq` (`user_id`), KEY `score_idx` (`score`) ) </pre> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">CREATE</span> <span class="synSpecial">TABLE</span> `ranking_index` ( `id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> AUTO_INCREMENT, `<span class="synIdentifier">rank</span>` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span>, `score` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span>, PRIMARY KEY (`id`), KEY `rank_idx` (`<span class="synIdentifier">rank</span>`), KEY `score_idx` (`score`) ) </pre> <h3 id="順位取得処理のサンプルコード">順位取得処理のサンプルコード</h3> <p>順位取得処理のサンプルコードです。シンプルなコードなので Perl がわからない方でも流れを理解できると思います。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">sub </span><span class="synIdentifier">get_rank_by_score </span>{ <span class="synComment"># rankを算出するスコアを受け取る</span> <span class="synStatement">my</span> (<span class="synIdentifier">$score</span>) = <span class="synIdentifier">@_</span>; <span class="synComment"># $scoreに近いranking_indexを探す</span> <span class="synStatement">my</span> <span class="synIdentifier">$ranking_index</span> = get_ranking_index_by_score(<span class="synIdentifier">$score</span>); <span class="synStatement">if</span> (<span class="synStatement">defined</span> <span class="synIdentifier">$ranking_index</span>) { <span class="synStatement">if</span> (<span class="synIdentifier">$ranking_index-&gt;{</span><span class="synConstant">score</span><span class="synIdentifier">}</span> == <span class="synIdentifier">$score</span>) { <span class="synComment"># ranking_indexのスコアがrankを求めたいスコアと同じだった場合はcountするまでもなく順位がわかる</span> <span class="synStatement">return</span> <span class="synIdentifier">$ranking_index-&gt;{</span><span class="synConstant">rank</span><span class="synIdentifier">}</span>; } <span class="synComment"># ranking_indexのscoreと$scoreの間に存在するレコードをカウント</span> <span class="synStatement">my</span> <span class="synIdentifier">$cnt</span> = exec_sql(<span class="synConstant">&quot;SELECT COUNT(*) AS cnt FROM ranking WHERE </span><span class="synIdentifier">$score</span><span class="synConstant"> &lt; score AND score &lt;= </span><span class="synIdentifier">$ranking_index-&gt;{</span><span class="synConstant">score</span><span class="synIdentifier">}</span><span class="synConstant">&quot;</span>)-&gt;{cnt}; <span class="synStatement">return</span> <span class="synIdentifier">$ranking_index-&gt;{</span><span class="synConstant">rank</span><span class="synIdentifier">}</span> + <span class="synIdentifier">$cnt</span>; } <span class="synStatement">else</span> { <span class="synComment"># ランキング上位の場合はranking_indexが存在しない</span> <span class="synComment"># この場合$scoreより高いスコアを持つレコードが少ないので素直にカウントする</span> <span class="synComment"># 順位は1からスタートするので+1して返す</span> <span class="synStatement">my</span> <span class="synIdentifier">$cnt</span> = exec_sql(<span class="synConstant">&quot;SELECT COUNT(*) AS cnt FROM ranking WHERE </span><span class="synIdentifier">$score</span><span class="synConstant"> &lt; score&quot;</span>)-&gt;{cnt}; <span class="synStatement">return</span> <span class="synIdentifier">$cnt</span> + <span class="synConstant">1</span>; } } <span class="synStatement">sub </span><span class="synIdentifier">get_ranking_index_by_score </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$score</span>) = <span class="synIdentifier">@_</span>; <span class="synComment"># 与えられた$score以上のscoreのうち最も$scoreに近いレコードを取得する</span> <span class="synStatement">return</span> exec_sql(<span class="synConstant">&quot;SELECT * FROM ranking_index WHERE score &gt;= </span><span class="synIdentifier">$score</span><span class="synConstant"> ORDER BY score ASC LIMIT 1&quot;</span>); } <span class="synStatement">sub </span><span class="synIdentifier">exec_sql </span>{ <span class="synComment"># SQLを受けとって実行結果を返す</span> <span class="synComment"># サンプルコードをシンプルにする便宜上の関数</span> <span class="synComment"># 本来はSQLインジェクション対策をすべきだが単純化のため省略</span> } </pre> <h2 id="ランキングインデックスの管理">ランキングインデックスの管理</h2> <p>ランキングを区切ることによって順位計算時のカウント対象を減らし、パフォーマンスを向上させることができます。 しかし、ランキングを区切ったことで新たにランキングインデックスを管理する必要が出てきます。これを適切に更新しなければ、ユーザに誤った順位を返してしまうことになります。</p> <h3 id="更新">更新</h3> <p>ランキングインデックスは以下の条件に当てはまるものを更新する必要があります。</p> <p><code>更新前スコア(新規作成の場合は0) &lt;= ランキングインデックスのスコア &lt; 更新後スコア</code></p> <p>例えば以下のようなランキングインデックスがあったとします。</p> <table> <thead> <tr> <th> 順位 </th> <th> スコア </th> </tr> </thead> <tbody> <tr> <td> 1000 </td> <td> 500 </td> </tr> <tr> <td> 2000 </td> <td> 340 </td> </tr> <tr> <td> 3000 </td> <td> 250 </td> </tr> <tr> <td> 4000 </td> <td> 210 </td> </tr> <tr> <td> ... </td> <td> ... </td> </tr> </tbody> </table> <p>とあるユーザのスコアを 250 から 500 に更新する場合、ランキングインデックスは以下のように更新します。</p> <table> <thead> <tr> <th> 順位 </th> <th> スコア </th> <th> 備考 </th> </tr> </thead> <tbody> <tr> <td> 1000 </td> <td> 500 </td> <td> 同率順位のデータが増えただけなので影響しない </td> </tr> <tr> <td> 2000 + 1 </td> <td> 340 </td> <td> スコアが 340 を超えるレコードが増えたので順位が下がる </td> </tr> <tr> <td> 3000 + 1 </td> <td> 250 </td> <td> スコアが 250 を超えるレコードが増えたので順位が下がる </td> </tr> <tr> <td> 4000 </td> <td> 210 </td> <td> スコア 210 より上のデータが動いただけなので影響しない </td> </tr> <tr> <td> ... </td> <td> ... </td> <td> </td> </tr> </tbody> </table> <p>順位を <code>+1</code> するのがポイントです。 スコアは更新しないので他のトランザクションの更新対象に影響を与えず、順位は <code>順位 = 順位 + 1</code> のようにすれば更新前の状態でロックを取っておく必要はありません。 これによりランキングインデックスのロック時間を最小限に抑えることができるようになります。</p> <h3 id="順位取得処理のサンプルコード-1">順位取得処理のサンプルコード</h3> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">sub </span><span class="synIdentifier">update_score </span>{ <span class="synComment"># 更新するユーザとそのスコアを受け取る</span> <span class="synStatement">my</span> (<span class="synIdentifier">$user_id</span>, <span class="synIdentifier">$score</span>) = <span class="synIdentifier">@_</span>; with_transaction(<span class="synStatement">sub </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$user_ranking</span> = exec_sql(<span class="synConstant">&quot;SELECT * FROM ranking WHERE user_id = </span><span class="synIdentifier">$user_id</span><span class="synConstant"> FOR UPDATE&quot;</span>); <span class="synStatement">my</span> <span class="synIdentifier">$before_score</span>; <span class="synStatement">if</span> (<span class="synStatement">defined</span> <span class="synIdentifier">$user_ranking</span>) { <span class="synComment"># すでにuser_idに対応するrankingレコードがある場合は更新</span> <span class="synComment"># 更新前スコアを保持しておき、ranking_indexの更新範囲決定に使う</span> exec_sql(<span class="synConstant">&quot;UPDATE ranking SET score = </span><span class="synIdentifier">$score</span><span class="synConstant"> WHERE id = </span><span class="synIdentifier">$user_ranking-&gt;{</span><span class="synConstant">id</span><span class="synIdentifier">}</span><span class="synConstant">&quot;</span>); <span class="synIdentifier">$before_score</span> = <span class="synIdentifier">$user_ranking-&gt;{</span><span class="synConstant">score</span><span class="synIdentifier">}</span>; } <span class="synStatement">else</span> { <span class="synComment"># rankingレコードがない場合はレコードを作る</span> <span class="synComment"># 更新前スコアは0とすることで$score未満の全てのranking_indexを更新対象にする</span> exec_sql(<span class="synConstant">&quot;INSERT INTO ranking(user_id, score) VALUES (</span><span class="synIdentifier">$user_id</span><span class="synConstant">, </span><span class="synIdentifier">$score</span><span class="synConstant">)&quot;</span>); <span class="synIdentifier">$before_score</span> = <span class="synConstant">0</span>; } <span class="synComment"># スコアの更新幅に含まれるranking_indexを更新する</span> increase_ranking_index(<span class="synIdentifier">$before_score</span>, <span class="synIdentifier">$score</span>); }); <span class="synStatement">return</span>; } <span class="synStatement">sub </span><span class="synIdentifier">increase_ranking_index </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$before_score</span>, <span class="synIdentifier">$after_score</span>) = <span class="synIdentifier">@_</span>; exec_sql(<span class="synConstant">&quot;UPDATE ranking_index SET rank = rank + 1 WHERE </span><span class="synIdentifier">$before_score</span><span class="synConstant"> &lt;= score AND score &lt; </span><span class="synIdentifier">$after_score</span><span class="synConstant">&quot;</span>); <span class="synStatement">return</span>; } <span class="synStatement">sub </span><span class="synIdentifier">exec_sql </span>{ <span class="synComment"># SQLを受けとって実行結果を返す</span> <span class="synComment"># サンプルコードをシンプルにする便宜上の関数</span> <span class="synComment"># 本来はSQLインジェクション対策をすべきだが単純化のため省略</span> } <span class="synStatement">sub </span><span class="synIdentifier">with_transaction </span>{ <span class="synComment"># 与えられたコードブロックをDBのトランザクション内で実行する関数</span> } </pre> <h3 id="定期実行処理">定期実行処理</h3> <p>ランキング更新によってランキングインデックスの順位がずれていってしまうため、間隔が一定に保てなくなり、徐々にパフォーマンスが劣化していってしまいます。 これを防ぐため、ランキングインデックスが順位で等間隔になるよう定期的に調整する必要があります。</p> <p>上記のランキング更新の都合で、ランキングインデックスのスコアは更新できないため、新しいランキングインデックスを挿入し古いランキングインデックスを削除する実装にしました。 これによりランキングの更新や順位取得に影響を与えずにランキングインデックスの間隔調整ができます。</p> <p>駅メモ!ではランキングインデックスの間隔調整処理を毎日実行しています。ランキングの更新頻度やスコア分布などの特性によってパフォーマンス劣化のスピードは変わるため、適切な頻度を見極めて実行する必要があります。</p> <h3 id="テーブルのメンテナンス">テーブルのメンテナンス</h3> <p>定期実行スクリプトではレコードの作成と削除をしているため、ランキングインデックステーブルが断片化してしまいます。 サービスのメンテナンス時にランキングインデックステーブルに対して OPTIMIZE TABLE を実行することで対応しています。</p> <h2 id="まとめ">まとめ</h2> <p>Redis の sorted sets で実装していたランキングを MySQL に移行するため、データベースでランキング処理をするようにしてみました。 その結果、Redis のメモリ使用量を元の 1/3 程度まで減らすことができ、Amazon ElastiCache Redis のスペックを下げることができました!</p> <p>Redis 内で大きくなり続けるランキングにお困りの際はデータベースに処理を移すことを検討してみてはいかがでしょうか。</p> yumlonne dayjsで相対時間を操る hatenablog://entry/6801883189069623710 2023-12-26T16:00:00+09:00 2023-12-26T16:00:04+09:00 皆さんこんにちは、最近ずっとポットのお湯を沸かし続けないと寒くて耐えられないエンジニアの id:Dozi0116 です。 今回は、 dayjs で相対時間を求める方法、自由自在に操る方法を紹介します。 TL; DR 以下は今日紹介する出力をいじるための設定と、利用例です。 import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime.js" import updateLocale from "dayjs/plugin/updateLocale.js" import "dayjs/locale/ja.j… <p>皆さんこんにちは、最近ずっとポットのお湯を沸かし続けないと寒くて耐えられないエンジニアの <a href="http://blog.hatena.ne.jp/Dozi0116/">id:Dozi0116</a> です。 今回は、 <code>dayjs</code> で相対時間を求める方法、自由自在に操る方法を紹介します。</p> <h1 id="TL-DR">TL; DR</h1> <p>以下は今日紹介する出力をいじるための設定と、利用例です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">import</span> relativeTime from <span class="synConstant">&quot;dayjs/plugin/relativeTime.js&quot;</span> <span class="synStatement">import</span> updateLocale from <span class="synConstant">&quot;dayjs/plugin/updateLocale.js&quot;</span> <span class="synStatement">import</span> <span class="synConstant">&quot;dayjs/locale/ja.js&quot;</span> <span class="synComment">// 基準になる時刻を調整</span> <span class="synComment">// l: relativeTimeで使うkey</span> <span class="synComment">// r: dで判定した時、この値以下までがこの閾値になる</span> <span class="synComment">// d: 判定に利用する時間単位、省略した場合は直前の判定に用いた単位</span> <span class="synStatement">const</span> relativeTimeConfig = <span class="synIdentifier">{</span> thresholds: <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;s&quot;</span>, r: 59, d: <span class="synConstant">&quot;second&quot;</span> <span class="synIdentifier">}</span>, <span class="synComment">// 0〜59秒</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;m&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synComment">// 1分表示用: 単数用の処理があるため、これを書かないと1分が1秒と表示されてしまう</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;m&quot;</span>, r: 59, d: <span class="synConstant">&quot;minute&quot;</span> <span class="synIdentifier">}</span>, <span class="synComment">// 1分〜59分</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;mm&quot;</span>, r: 60 * 24 - 1 <span class="synIdentifier">}</span>, <span class="synComment">// 1時間〜23時間59分</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;d&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synComment">// 1日</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;d&quot;</span>, d: <span class="synConstant">&quot;day&quot;</span> <span class="synIdentifier">}</span>, <span class="synComment">// 2日〜</span> <span class="synIdentifier">]</span>, rounding: Math.floor, <span class="synComment">// 閾値判定時に用いる丸め関数</span> <span class="synIdentifier">}</span> <span class="synComment">// 日本語で扱えるように</span> dayjs.locale(<span class="synConstant">&quot;ja&quot;</span>) <span class="synComment">// プラグインを利用する</span> dayjs.extend(updateLocale) dayjs.extend(relativeTime, relativeTimeConfig) <span class="synComment">// 相対時間の表示ルール設定</span> dayjs.updateLocale(<span class="synConstant">&quot;ja&quot;</span>, <span class="synIdentifier">{</span> relativeTime: <span class="synIdentifier">{</span> future: <span class="synConstant">&quot;%s後&quot;</span>, past: <span class="synConstant">&quot;%s前&quot;</span>, s: <span class="synConstant">&quot;数秒&quot;</span>, <span class="synComment">// %d を時間に置換して表示。%dがなくてもOK</span> m: <span class="synConstant">&quot;%d分&quot;</span>, mm: (abs) =&gt; <span class="synIdentifier">{</span> <span class="synComment">// 関数でカスタマイズも可能</span> <span class="synStatement">if</span> (abs % 60 === 0) <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synConstant">`</span><span class="synSpecial">${abs / 60}</span><span class="synConstant">時間`</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synConstant">`</span><span class="synSpecial">${Math.floor(abs / 60)}</span><span class="synConstant">時間</span><span class="synSpecial">${abs % 60}</span><span class="synConstant">分`</span> <span class="synIdentifier">}</span>, d: <span class="synConstant">&quot;%d日&quot;</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>) <span class="synComment">////////////////////</span> <span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2021-01-01 00:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2021-01-01 00:00:01&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2021-01-01 00:01:00&quot;</span>) <span class="synStatement">const</span> targetTime3 = dayjs(<span class="synConstant">&quot;2021-01-01 00:59:59&quot;</span>) <span class="synStatement">const</span> targetTime4 = dayjs(<span class="synConstant">&quot;2021-01-01 01:00:00&quot;</span>) <span class="synStatement">const</span> targetTime5 = dayjs(<span class="synConstant">&quot;2021-01-01 01:20:30&quot;</span>) <span class="synStatement">const</span> targetTime6 = dayjs(<span class="synConstant">&quot;2021-01-02 00:00:00&quot;</span>) <span class="synStatement">const</span> targetTime7 = dayjs(<span class="synConstant">&quot;2021-02-01 00:00:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// 数秒後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// 1分後</span> console.log(targetTime3.from(baseTime)) <span class="synComment">// 59分後</span> console.log(targetTime4.from(baseTime)) <span class="synComment">// 1時間後</span> console.log(targetTime5.from(baseTime)) <span class="synComment">// 1時間20分後</span> console.log(targetTime6.from(baseTime)) <span class="synComment">// 1日後</span> console.log(targetTime7.from(baseTime)) <span class="synComment">// 31日後</span> </pre> <h1 id="動作環境">動作環境</h1> <p>今日紹介するコードは</p> <ul> <li>node v20.1.0</li> <li>dayjs v1.11.0</li> </ul> <p>で動作確認をしています。</p> <h1 id="dayjs-とは"><code>dayjs</code> とは</h1> <p><a href="https://day.js.org/">https://day.js.org/</a></p> <p>dayjs とは、 <a href="https://momentjs.com/">Moment.js</a>という非推奨になってしまった時刻を扱うパッケージと同じインターフェースを備えているかつ、Moment.js より軽い構造になっていることが特徴のパッケージです。</p> <p>以下のように書くことで、簡単に時刻を用意して扱うことが可能です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">const</span> now = dayjs() console.log(now.format(<span class="synConstant">&quot;YYYY-MM-DD(ddd)&quot;</span>)) <span class="synComment">// -&gt; 2023-12-22(Fri) (今日の日付)</span> </pre> <h2 id="オプション-dayjs-で日本語表示をする">(オプション) <code>dayjs</code> で日本語表示をする</h2> <p><code>dayjs</code> は言語のデフォルトが英語になっているため、相対時間を表示しようとすると英語で表示されてしまいます。 今回の記事では日本語で相対時間を操るため、日本語のセットアップをしておきます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">import</span> <span class="synConstant">&quot;dayjs/locale/ja.js&quot;</span> <span class="synComment">// 表示言語を日本語に設定</span> dayjs.locale(<span class="synConstant">&quot;ja&quot;</span>) <span class="synStatement">const</span> time = dayjs(<span class="synConstant">&quot;2023-12-01&quot;</span>) console.log(time.format(<span class="synConstant">&quot;ddd&quot;</span>)) <span class="synComment">// 金</span> </pre> <p>以降この記事では特に書かれていないところでも locale を ja に設定して進めていきます。</p> <h1 id="dayjs-で相対時間を扱うための準備"><code>dayjs</code> で相対時間を扱うための準備</h1> <h2 id="relativeTime-プラグインの準備"><code>relativeTime</code> プラグインの準備</h2> <p><code>dayjs</code> は必要最低限の機能のみの実装でコードを小さくしているため、 <code>dayjs</code> の import だけでは相対時間を求められません。 しかし、公式がパッケージと共に提供しているプラグイン <a href="https://github.com/iamkun/dayjs/tree/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime"><code>relativeTime</code></a>を import することによって相対時間を扱えるようになります。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">import</span> relativeTime from <span class="synConstant">&quot;dayjs/plugin/relativeTime.js&quot;</span> dayjs.extend(relativeTime) </pre> <p>実際に時刻を計算する場合は <code>to</code> もしくは <code>from</code> というメソッドを使うだけです。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-01&quot;</span>) <span class="synStatement">const</span> targetTime = dayjs(<span class="synConstant">&quot;2023-12-05&quot;</span>) console.log(targetTime.from(baseTime)) <span class="synComment">// -&gt; 4日後</span> </pre> <p><code>fromNow</code> や <code>toNow</code> というメソッドを使えば現在時刻からの相対時間を求められます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> time = dayjs(<span class="synConstant">&quot;2023-12-01&quot;</span>) console.log(time.fromNow()) <span class="synComment">// -&gt; 21日前 (今日の日付依存)</span> console.log(time.toNow()) <span class="synComment">// -&gt; 21日後 (今日の日付依存)</span> </pre> <h1 id="おめでとうございます">おめでとうございます!…?</h1> <p>これだけで相対時間を扱えるようになりました! 他の日付も試してみましょう。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-01 12:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2023-12-02 09:00:00&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2023-12-02 10:00:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// -&gt; 21時間後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// -&gt; 1日後</span> </pre> <p>21 時間を境に、1 日前という判定になってしまいました。</p> <h1 id="厳密に判定するには">厳密に判定するには</h1> <p>実は<code>dayjs</code>の<code>relativeTime</code>プラグインは結構アバウトな時間管理を行なっています。</p> <p><a href="https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js#L24-L36">実装ロジック</a>を実際に見てみると、このような判定になっていることがわかります。</p> <pre class="code" data-lang="" data-unlink>d: 判定に利用する時間単位 r: dで判定した時、この値以下までがこの閾値になる</pre> <table> <thead> <tr> <th> 範囲 </th> <th> 表示 </th> </tr> </thead> <tbody> <tr> <td> 〜44 秒 </td> <td> n 秒 </td> </tr> <tr> <td> 45〜89 秒 </td> <td> 1 分 </td> </tr> <tr> <td> 90 秒〜44 分 </td> <td> n 分 </td> </tr> <tr> <td> 45 分〜89 分 </td> <td> 1 時間 </td> </tr> <tr> <td> 90 分〜21 時間 </td> <td> n 時間 </td> </tr> <tr> <td> 22 時間〜35 時間 </td> <td> 1 日 </td> </tr> <tr> <td> 36 時間〜25 日 </td> <td> n 日 </td> </tr> <tr> <td> 26 日〜45 日 </td> <td> 1 ヶ月 </td> </tr> <tr> <td> 46 日〜10 ヶ月 </td> <td> n ヶ月 </td> </tr> <tr> <td> 11 ヶ月〜17 ヶ月 </td> <td> 1 年 </td> </tr> <tr> <td> 18 ヶ月〜 </td> <td> n 年 </td> </tr> </tbody> </table> <p>そのため、先ほどの例では 21 時間後と 22 時間後で表示が異なってしまったのでした。</p> <p>これを厳密に扱いたい場合は公式が example を出してくれているように設定する必要があります。</p> <p><a href="https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding">https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding</a></p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">import</span> relativeTime from <span class="synConstant">&quot;dayjs/plugin/relativeTime.js&quot;</span> <span class="synComment">// from: https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding</span> <span class="synStatement">const</span> thresholds = <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;s&quot;</span>, r: 59, d: <span class="synConstant">&quot;second&quot;</span> <span class="synIdentifier">}</span>, <span class="synComment">// ここだけ微調整: r: 59 / d: 'second' としないと秒周りがおかしくなるので注意!</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;m&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;mm&quot;</span>, r: 59, d: <span class="synConstant">&quot;minute&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;h&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;hh&quot;</span>, r: 23, d: <span class="synConstant">&quot;hour&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;d&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;dd&quot;</span>, r: 29, d: <span class="synConstant">&quot;day&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;M&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;MM&quot;</span>, r: 11, d: <span class="synConstant">&quot;month&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;y&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;yy&quot;</span>, d: <span class="synConstant">&quot;year&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">]</span> dayjs.extend(relativeTime, <span class="synIdentifier">{</span> thresholds <span class="synIdentifier">}</span>) <span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-01 12:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2023-12-02 09:00:00&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2023-12-02 10:00:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// -&gt; 21時間後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// -&gt; 22時間後</span> </pre> <h2 id="表示部分より細かいところも厳密にする">表示部分より細かいところも厳密にする</h2> <p>もっと細かい表示を見てみます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-02 10:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2023-12-02 14:00:00&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2023-12-02 14:29:59&quot;</span>) <span class="synStatement">const</span> targetTime3 = dayjs(<span class="synConstant">&quot;2023-12-02 14:30:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// -&gt; 4時間後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// -&gt; 4時間後</span> console.log(targetTime3.from(baseTime)) <span class="synComment">// -&gt; 5時間後</span> </pre> <p>4 時間 30 分を境に 4 時間後 → 5 時間後という切り替わりが起こっています。 これはデフォルトの diff を求めるロジックが <code>Math.round</code> であることに由来しており、4.5 時間の diff が丸められて 5 時間になっているためです。</p> <p>これを解消するのも config が活躍してくれます。</p> <p>config の <code>rounding</code> という項目に、判定に用いる関数を渡してあげることで、丸め方を指示できます。 今回は切り捨てである <code>Math.floor</code> を指定しました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">// 小数切り捨てのdiffで計算する</span> dayjs.extend(relativeTime, <span class="synIdentifier">{</span> rounding: Math.floor <span class="synIdentifier">}</span>) <span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-02 10:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2023-12-02 14:00:00&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2023-12-02 14:29:59&quot;</span>) <span class="synStatement">const</span> targetTime3 = dayjs(<span class="synConstant">&quot;2023-12-02 14:30:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// -&gt; 4時間後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// -&gt; 4時間後</span> console.log(targetTime3.from(baseTime)) <span class="synComment">// -&gt; 4時間後</span> </pre> <h1 id="r-1-の意味"><code>r: 1</code> の意味</h1> <p>先ほど参考にした <code>thresholds</code> では、 <code>r: 1</code> という設定がありました。 これは、他の言語用などに用意されている単数系の表示をするために用意されています。</p> <p><a href="https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js#L52C11-L52C11">判定ロジック</a>を見てみると、「diff が 1 以下の場合、index が 1 つ手前の閾値を採用する」という処理になっています。 そのため単数系の区別がない日本語などでも、 <code>r: 1</code> という設定がないと 1 つ前の設定、つまり 1 分を出すはずが 1 秒という表示になってしまうので注意してください。</p> <h1 id="カスタマイズしたい">カスタマイズしたい!</h1> <p>ここまで来ると、閾値を自由に操って表示を切り替えられるようになったはずです。しかしこのままではまだ完全に操れるようになったとは言えないでしょう。 なぜならこのプラグインの力だけでは、表示形式を変えることはできないからです。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-01 12:00:00&quot;</span>) <span class="synStatement">const</span> targetTime = dayjs(<span class="synConstant">&quot;2023-12-01 13:30:00&quot;</span>) <span class="synComment">// 1時間後や2時間後だったり、90分後だったりはできるけど 1時間30分後という表示にできない!</span> console.log(targetTime.from(baseTime)) </pre> <p>これを叶えてくれるのが <a href="https://day.js.org/docs/en/customization/relative-time"><code>updateLocale</code></a> プラグインです。(リンク先は relativeTime ですが、こちらの方に細かい使い方が載っています) これを使うと、求めた閾値に応じた出力をカスタマイズできます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> dayjs from <span class="synConstant">&quot;dayjs&quot;</span> <span class="synStatement">import</span> relativeTime from <span class="synConstant">&quot;dayjs/plugin/relativeTime.js&quot;</span> <span class="synStatement">import</span> updateLocale from <span class="synConstant">&quot;dayjs/plugin/updateLocale.js&quot;</span> <span class="synStatement">import</span> <span class="synConstant">&quot;dayjs/locale/ja.js&quot;</span> <span class="synStatement">const</span> thresholds = <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;s&quot;</span>, r: 59, d: <span class="synConstant">&quot;second&quot;</span> <span class="synIdentifier">}</span>, <span class="synComment">// ここは r: 59 / d: 'second' としないと秒周りがおかしくなるので注意!</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;m&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;mm&quot;</span>, r: 59, d: <span class="synConstant">&quot;minute&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;mmm&quot;</span>, r: 60 * 24 - 1 <span class="synIdentifier">}</span>, <span class="synComment">// n時間を廃止して、1439分まで見れるようにした</span> <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;d&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;dd&quot;</span>, r: 29, d: <span class="synConstant">&quot;day&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;M&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;MM&quot;</span>, r: 11, d: <span class="synConstant">&quot;month&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;y&quot;</span>, r: 1 <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> l: <span class="synConstant">&quot;yy&quot;</span>, d: <span class="synConstant">&quot;year&quot;</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">]</span> dayjs.locale(<span class="synConstant">&quot;ja&quot;</span>) dayjs.extend(updateLocale) dayjs.extend(relativeTime, <span class="synIdentifier">{</span> thresholds, rounding: Math.floor <span class="synIdentifier">}</span>) <span class="synComment">// 出力のカスタマイズ</span> <span class="synComment">// カスタマイズしないところも書く必要がある</span> dayjs.updateLocale(<span class="synConstant">&quot;ja&quot;</span>, <span class="synIdentifier">{</span> relativeTime: <span class="synIdentifier">{</span> <span class="synComment">// 未来の場合、過去の場合につける文言のカスタマイズ</span> future: <span class="synConstant">&quot;%s後&quot;</span>, past: <span class="synConstant">&quot;%s前&quot;</span>, <span class="synComment">// 時間出力のカスタマイズ</span> <span class="synComment">// thresholdsで指定した `l` の値に応じた出力をする</span> s: <span class="synConstant">&quot;数十秒&quot;</span>, <span class="synComment">// 1分未満は全部数十秒と表示させる</span> m: <span class="synConstant">&quot;%d分&quot;</span>, mm: <span class="synConstant">&quot;%d分&quot;</span>, mmm: (abs) =&gt; <span class="synIdentifier">{</span> <span class="synComment">// 関数で指定することも可能。第一引数にはdiffの値がそのまま来る</span> <span class="synStatement">if</span> (abs % 60 === 0) <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synConstant">`</span><span class="synSpecial">${abs / 60}</span><span class="synConstant">時間`</span> <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synConstant">`</span><span class="synSpecial">${Math.floor(abs / 60)}</span><span class="synConstant">時間</span><span class="synSpecial">${abs % 60}</span><span class="synConstant">分`</span> <span class="synIdentifier">}</span>, d: <span class="synConstant">&quot;%d日&quot;</span>, dd: <span class="synConstant">&quot;%d日&quot;</span>, M: <span class="synConstant">&quot;%dヶ月&quot;</span>, MM: <span class="synConstant">&quot;%dヶ月&quot;</span>, y: <span class="synConstant">&quot;%d年&quot;</span>, yy: <span class="synConstant">&quot;%d年&quot;</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>) </pre> <p>updateLocale プラグインは、<code>s</code> <code>m</code> などの thresholds で設定した <code>l</code> の値ごとに、出力する内容を設定できます。 その時に string を設定すれば、 %d -> diff に変換したものが、function を設定すれば diff を受け取ってカスタマイズした返り値を出力することができます。</p> <p>このように指定することで、出力のカスタマイズも可能になりました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> baseTime = dayjs(<span class="synConstant">&quot;2023-12-01 12:00:00&quot;</span>) <span class="synStatement">const</span> targetTime1 = dayjs(<span class="synConstant">&quot;2023-12-01 12:00:30&quot;</span>) <span class="synStatement">const</span> targetTime2 = dayjs(<span class="synConstant">&quot;2023-12-01 13:30:00&quot;</span>) console.log(targetTime1.from(baseTime)) <span class="synComment">// 数十秒後</span> console.log(targetTime2.from(baseTime)) <span class="synComment">// 1時間30分後</span> </pre> <h1 id="まとめ">まとめ</h1> <p>だいぶ長くなってしまいましたが、 <code>dayjs</code> というライブラリと、それを用いた相対時刻の操作について</p> <ul> <li><code>relativeTime</code> プラグインを用いて相対時刻の判定ができる <ul> <li>厳密に判定したい場合は config の <code>thresholds</code> と <code>rounding</code> を調整</li> <li>単数系の処理に注意</li> </ul> </li> <li><code>updateLocale</code> プラグインを用いて相対時刻の表示方法をカスタマイズできる <ul> <li>string を渡して簡易テンプレートを作ったり、関数を渡してより細かい制御をしたりできる</li> </ul> </li> </ul> <p>ことを紹介しました。</p> <p>この記事が誰かの助けになれば幸いです。みなさまもよき時刻判定ライフを〜</p> <h1 id="参考にしたサイト">参考にしたサイト</h1> <p>とても参考になりました、ありがとうございました!</p> <p>Day.js で相対日時を厳密に表示する(thresholds) <a href="https://zenn.dev/catnose99/articles/ba540f5c233847">https://zenn.dev/catnose99/articles/ba540f5c233847</a></p> <p>dayjs - RelativeTime <a href="https://day.js.org/docs/en/plugin/relative-time">https://day.js.org/docs/en/plugin/relative-time</a></p> <p>dayjs - Relative Time <a href="https://day.js.org/docs/en/customization/relative-time">https://day.js.org/docs/en/customization/relative-time</a></p> <p>GitHub - dayjs/src/plugin/relativeTime/index.js <a href="https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js">https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js</a></p> Dozi0116 緊急度が低く重要度が高く、そして重いプロジェクトを進める hatenablog://entry/6801883189068878714 2023-12-25T16:00:00+09:00 2023-12-25T16:00:03+09:00 こんにちは!ブロックチェーンチームでエンジニアをしている id:dorapon2000 です。寒暖ある中でインフルも流行っているようで、私も咳がなかなか収まらず困っています。皆様におかれましても体調にはお気をつけください。 今回はタイトルの通り「緊急度が低く重要度が高く、そして重いプロジェクト」を私が担当した際に感じたことや学んだことをまとめた記事になります。前半で私の担当したプロジェクトの経過について感じたことをお話して、後半で学びをまとめます。 第二領域 重い第二領域の問題 私の場合 初期 中期 後期 重い第二領域プロジェクトを通しての学び まとめ 第二領域 「緊急度が低いが重要度が高いタ… <p>こんにちは!ブロックチェーンチームでエンジニアをしている <a href="http://blog.hatena.ne.jp/dorapon2000/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/dorapon2000/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:dorapon2000</a> です。寒暖ある中でインフルも流行っているようで、私も咳がなかなか収まらず困っています。皆様におかれましても体調にはお気をつけください。</p> <p>今回はタイトルの通り「緊急度が低く重要度が高く、そして重いプロジェクト」を私が担当した際に感じたことや学んだことをまとめた記事になります。前半で私の担当したプロジェクトの経過について感じたことをお話して、後半で学びをまとめます。</p> <ul class="table-of-contents"> <li><a href="#第二領域">第二領域</a></li> <li><a href="#重い第二領域の問題">重い第二領域の問題</a></li> <li><a href="#私の場合">私の場合</a><ul> <li><a href="#初期">初期</a></li> <li><a href="#中期">中期</a></li> <li><a href="#後期">後期</a></li> </ul> </li> <li><a href="#重い第二領域プロジェクトを通しての学び">重い第二領域プロジェクトを通しての学び</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="第二領域">第二領域</h2> <p>「緊急度が低いが重要度が高いタスク」では記述にあたり少し長いので、ここでは第二領域<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>のタスクと呼ぶことにします。今すぐではないがいつかは必ずやらねばならないタスク。例えば OS やライブラリのアップデート、開発負債の解消などが第二領域のタスクとして考えられます。緊急度が低いこともあって後回しにされがちという傾向があり、みなさんも心当たりが大いにありますよね。</p> <p>では、どうやって第二領域のタスクに着手するのか。第二領域のタスクは必ず実施しないとプロダクトに致命的な影響を与えます。重要度が高いとはそういうことです。そのため、後回しになりすぎないように、全体のスケジュールに合わせて事前に期日を決めるという対策は定番です。</p> <h2 id="重い第二領域の問題">重い第二領域の問題</h2> <p>軽い第二領域のタスクは着手が一番の問題で、着手したあとは完了を待つのみです。一方で、重い第二領域のタスク(プロジェクト)は着手してからも問題がおきます。自分が考える問題をここにあげておきましょう。</p> <ul> <li>単純に重いタスクであるため、重いタスクにまつわる問題を引き継ぐ <ul> <li>学習時間、調査、検証、動作確認、反映をする時間を見積もりに含め忘れる</li> <li>単純に見積もりが難しい</li> <li>もうお手上げだと思われる状況に何度もあたるとメンタルがすり減る</li> <li>長期に渡るプロジェクトだと、モチベーションを保ち続けられないことがある</li> <li>属人化</li> </ul> </li> <li>長期に渡る第二領域のプロジェクトは、差し込まれる緊急度の高いタスクに優先度を何度も譲らなくてはいけない <ul> <li>スケジュールが立てづらい</li> <li>再着手するときに過去のことを忘れている</li> </ul> </li> </ul> <p>これらを踏まえて私の担当したプロジェクトについてお聞きください。</p> <h2 id="私の場合">私の場合</h2> <p>私が前のチームにいたときの話です。プロダクトで運用しているサーバの OS のアップデートをするというプロジェクト担当になりました。というよりも、自分がやってみたいと手をあげました。これがまさに重い第二領域のプロジェクトになります。主な担当は自分で、サブで当時の上司にサポートをしていただきました。自分は難しくてもチャレンジさせてもらえるならチャレンジしたい性格なので、大変だとはわかりつつ、楽しみにもしていました。</p> <h3 id="初期">初期</h3> <p>ほかチームでも OS のアップデートプロジェクトはすでに進行しており、知見を共有してもらいながらの進行でした。まず自分のチームではどのような作業が必要か洗い出し、誰と協力しなくてはいけないのか、どれくらい工数がかかりそうか、計画を立てました。この計画は今振り返るとまったく未熟で、全体の作業の 2 割程度しか網羅できていなかった気がします。計画を考えるために必要な AWS の知識が足りていなかったため、サーバを安全にアップデートするための具体的な手順と OS アップデートに伴う広い影響範囲をイメージできていませんでした。</p> <p>不十分な計画ながらも、とりあえず最初にすべき検証や準備を少しずつ進めていました。 しかし、途中でサポート担当の上司がチーム異動をすることになりました。もちろん他チームのヘルプは借りつつも、これ以降はチーム内で自分だけがプロジェクト担当になります。1 人での対応に不安はありつつ、ヘルプを出すのは得意な方なので、とにかく自分がやり切るしかないという気持ちでした。</p> <h3 id="中期">中期</h3> <p>自分以外のチームメンバーもそれぞれ異なるプロジェクトを持っており、それらは OS のアップデートよりも優先度が高かったです。そのため、ユーザーさんからのお問い合わせ調査やバグの修正など突発的なタスクは、第二領域のプロジェクトを進行していた自分がよく持っていました。また、ほかプロジェクトが遅延していたときに、自分がよくヘルプに向かいました。その度に自分のタスクは中断しています。数ヶ月中断せざるを得なかったことも数回ありました。</p> <p>中断した数だけ自分のプロジェクトの完了は遅れるので、バッファありのガントチャートを組んでプロジェクトによりコミットできるよう工夫しました。しかし、これは結果的に自分のメンタルを責める原因になってしまいました。ただでさえ自分にとって未知の領域で見積もり通りにいかない、差し込みが多数発生する、その度にガントチャートを引き直す必要がありました。タスク単体の見積もりの曖昧さはバッファが吸収しますが、いつくるかわからない差し込みタスクをバッファは吸収しきれません。引き直すたびに完了予定が後ろ倒しになり、申し訳ない気持ちになりました。</p> <p>ガントチャートは長期な第二領域のプロジェクトと相性がよくないのだと学びました。</p> <p>それ以外に、このあたりから OS アップデートで更新したインフラ周りの知識が自分に属人化していることに気づき始めました。ですが、今からほかメンバーに参加してもらうにも逆に工数が膨らみそうであったため、作業ログを大量に残しつつ自分ひとりで進行を続けました。大量の作業ログは中断後の再開で記憶を取り戻すのにとても役に立ちました。</p> <p>このあたりから気持ちは淀んできます。</p> <h3 id="後期">後期</h3> <p>前述の件があったため、ガントチャートでの計画はやめて、各タスクに掛かった日数や工数の管理だけはちゃんと記録するようにしました。のちに振り返りの材料になります。</p> <p>自分で調べたり周りに聞いたりしてもなかなか解決できない問題に何度も遭遇して、技術力不足なのかメンタルが淀んでいるのかわからなかったりしました。もしプロジェクトにほかメンバーがいれば、実は進め方に問題がある、あるいは今からでもプロジェクトメンバーを 1 人追加したほうがいい、などというような指摘を貰えるかもしれません。しかし、担当が自分だけであるので、それを自問自答しなくてはいけないことも辛かったです。このような場合、基本に立ち返ってほうれんそうが大事だと思いました。</p> <p>初めてのカナリアリリースもうまくいき、いざ本番リリースと臨んだところでリリース手順中のスクリプトがうまく動かず延期ということもありましたが、最後は無事 OS アップデートすることができました。</p> <p>憑き物が取れたような気持ちでした。プロジェクト開始から 1 年 8 ヶ月経っていました。</p> <h2 id="重い第二領域プロジェクトを通しての学び">重い第二領域プロジェクトを通しての学び</h2> <p>プロジェクトでは自分自身の経験不足なところが多くありました。その中で学んだことをあげます。</p> <ul> <li>ビッグバンリリースを避ける <ul> <li>ビッグバンリリースは避けるべきとわかっていても、細かいリリースに分けるだけの知識と経験がなかった</li> <li>そのために周りをもっと頼ることができたかもしれない</li> </ul> </li> <li>重い第二領域プロジェクトはガントチャートと相性が悪い <ul> <li>他のプロジェクトと兼任せず差し込み対応をしなくてよいならガントチャートは強力なツール</li> <li>ガントチャートを組まないにしても、各タスクに掛かった時間は記録して振り返りの材料にする</li> </ul> </li> <li>重い第二領域プロジェクトは 1 人で進行しない <ul> <li>一緒にコミットして助け合う仲間が必要</li> <li>属人化させない</li> </ul> </li> <li>振り返りは短いスパンと長いスパンの両方でする <ul> <li>ガントチャート周りは短いスパンの振り返りで気づいた</li> </ul> </li> <li>再計画とは別に再見積もりもする <ul> <li>手を動かしながら解像度が上がることで、より見積もり精度を高めることができる</li> <li>初期の見積もりの精度を高めるより、その後に再見積もりが必要だと気づくことのほうが大切</li> </ul> </li> </ul> <p>特にガントチャートとの相性がよくなかったことは自分にとって印象的な発見でした。もちろん、常に相性が悪いわけではなく、今回は差し込みが多分に起きる状況だったため相性がよくなかったと思います。差し込みが多いというチームの状況がよくなかったという見方もできるかもしれません。よりよいプロジェクト管理のために、日々考え抜かなくてはいけません。</p> <h2 id="まとめ">まとめ</h2> <p>記事を読んでいただきありがとうございます。緊急度が低いが重要度が高いそして重いタスクには特有の問題があることを学びました。1 年 8 ヶ月という期間はあまりに長く(もちろん途中で他のプロジェクトにスイッチすることもありつつ)、今の自分ならもっとスマートに完了できるのだろうかと空想するところです。</p> <p>私の学びが他の方の学びになればと思います。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 『7 つの習慣』という本で分類される呼び方を拝借しています。<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> dorapon2000 Ethereumにおけるアップグレード可能なコントラクトの開発 hatenablog://entry/6801883189068122218 2023-12-20T16:00:00+09:00 2023-12-20T16:00:03+09:00 こんにちは、ブロックチェーンチームの id:charines です。 今回はアップグレード可能なスマートコントラクトの開発事例について紹介します。 コントラクト開発者のみなさんの参考になればと思います。 アップグレード機能の必要性 アップグレード可能なコントラクトの仕組み 今回行った実装 コントラクト実装 プロキシをデプロイする仕組みの実装 まとめ アップグレード機能の必要性 ブロックチェーン上に展開されたコントラクトはオフチェーンのアプリケーションと異なり、通常は後から実装を修正したり機能を追加することはできません。しかしアップグレード可能なコントラクトとして設計することで、変更の余地を持た… <p>こんにちは、ブロックチェーンチームの <a href="http://blog.hatena.ne.jp/charines/">id:charines</a> です。 今回はアップグレード可能なスマートコントラクトの開発事例について紹介します。 コントラクト開発者のみなさんの参考になればと思います。</p> <ul class="table-of-contents"> <li><a href="#アップグレード機能の必要性">アップグレード機能の必要性</a></li> <li><a href="#アップグレード可能なコントラクトの仕組み">アップグレード可能なコントラクトの仕組み</a></li> <li><a href="#今回行った実装">今回行った実装</a><ul> <li><a href="#コントラクト実装">コントラクト実装</a></li> <li><a href="#プロキシをデプロイする仕組みの実装">プロキシをデプロイする仕組みの実装</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="アップグレード機能の必要性">アップグレード機能の必要性</h2> <p>ブロックチェーン上に展開されたコントラクトはオフチェーンのアプリケーションと異なり、通常は後から実装を修正したり機能を追加することはできません。しかしアップグレード可能なコントラクトとして設計することで、変更の余地を持たせることが可能になります。</p> <p>例えば弊チームでは NFT をウェブコンソールから生成できる、「ユニキスガレージ」というサービスを開発・運用しています。 以前このブログでも紹介した <a href="https://tech.mobilefactory.jp/entry/2022/01/18/103000">ERC-2981 への対応</a> では、このサービスからデプロイされるコントラクトに、NFT が他のマーケットプレイスで再販売されたときロイヤリティの情報をマーケットプレイスへ提供するという機能を追加しました。 しかしこの機能追加以前にコントラクトをデプロイしたクライアントは、この機能の恩恵を受けることができません。 このようなケースでもアップグレード機能を実装していれば、今後デプロイされるコントラクトが常に最新の機能を利用できるようになります。</p> <h2 id="アップグレード可能なコントラクトの仕組み">アップグレード可能なコントラクトの仕組み</h2> <p>アップグレード可能なコントラクトはプロキシと呼ばれる仕組みによって実装され、これは機能実装にあたるロジックコントラクトと、ロジックコントラクトへの参照やストレージを持つプロキシコントラクトの組によって構成されます。 プロキシコントラクトはロジックコントラクトの関数を <code>delegatecall</code> することで自身がその関数を実装しているかのように振る舞うため、参照先のロジックコントラクトを変更することでアップデートが可能になります。</p> <p>ただしアップグレード可能であることはその性質上、コントラクトの中央集権性を高めてしまうため慎重に行う必要もあります。例えば ERC-721 コントラクトの実装において、NFT の移転に関する関数をアップグレードすれば、NFT の所有権を操作することも可能になってしまいます。 弊チームではプロダクトの性質を考慮した上で、それでもユーザに利便性を提供することに価値があると考え今回の実装を行うことにしました。</p> <h2 id="今回行った実装">今回行った実装</h2> <p>実は弊チームではコントラクトをデプロイする際のガス代を節約するために、アップグレード可能ではなかったものの以前からプロキシの仕組みを使用していました。 このコントラクトは古いバーションの OpenZeppelin で実装されていたため、今回もチームで利用実績のある OpenZeppelin をベースにしつつ、現行のバージョンで書き直す方針としました。</p> <h3 id="コントラクト実装">コントラクト実装</h3> <p>OpenZeppelin はアップグレード可能な ERC-721 コントラクトを<a href="https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC721/ERC721Upgradeable.sol">ERC721Upgradeable</a>として公開しています。さらに<a href="https://wizard.openzeppelin.com/#erc721">OpenZeppelin Contracts Wizard</a>というウェブアプリケーションが提供されており、アップグレード機能を含め追加したい機能を選択していくだけで基本的な ERC-721 コントラクトを作れるため、これでベースを作っていきます。</p> <p>ここに独自機能を追加していくのですが、ベース部分は今後の機能追加などを行った際にも書き直さずに使い回したいので、今回は Abstract Contracts として実装することとしました。 以下は OpenZeppelin Contracts Wizard で生成された実装を Abstract Contracts 化するにあたっての主要な差分です。</p> <pre class="code lang-diff" data-lang="diff" data-unlink><span class="synSpecial">- contract BaseERC721V1 is Initializable, ERC721Upgradeable, ERC721PausableUpgradeable, AccessControlUpgradeable, ERC721BurnableUpgradeable, UUPSUpgradeable {</span> <span class="synIdentifier">+ abstract contract BaseERC721V1 is Initializable, ERC721Upgradeable, ERC721PausableUpgradeable, AccessControlUpgradeable, ERC721BurnableUpgradeable, UUPSUpgradeable {</span> bytes32 public constant PAUSER_ROLE = keccak256(&quot;PAUSER_ROLE&quot;); bytes32 public constant MINTER_ROLE = keccak256(&quot;MINTER_ROLE&quot;); bytes32 public constant UPGRADER_ROLE = keccak256(&quot;UPGRADER_ROLE&quot;); <span class="synSpecial">- /// @custom:oz-upgrades-unsafe-allow constructor</span> <span class="synSpecial">- constructor() {</span> <span class="synSpecial">- _disableInitializers();</span> <span class="synSpecial">- }</span> <span class="synSpecial">- function initialize(address defaultAdmin, address pauser, address minter, address upgrader)</span> <span class="synSpecial">- initializer public</span> <span class="synIdentifier">+ function __BaseERC721V1(address defaultAdmin, address pauser, address minter, address upgrader)</span> <span class="synIdentifier">+ internal onlyInitializing</span> { ... </pre> <p>また同様に独自機能についても、独立した各機能をアップグレード後も再利用できるように Abstract Contracts として実装し、それらを継承したロジックコントラクトを最終的にデプロイする構造にしました。 ディレクトリ構成は以下のようになります。</p> <pre class="code" data-lang="" data-unlink>contracts ├── tokens │ └── BaseERC721V1.sol ├── features │ ├── FeatureA.sol │ └── FeatureB.sol └── logics └── ERC721LogicV1.sol</pre> <p>デプロイするロジックコントラクトは <code>logics/ERC721LogicV1.sol</code> で、 <code>tokens/</code> や <code>features/</code> の各機能を継承しています。</p> <h3 id="プロキシをデプロイする仕組みの実装">プロキシをデプロイする仕組みの実装</h3> <p>OpenZeppelin はプロキシコントラクトを安全にデプロイするために Hardhat や Truffle 向けの<a href="https://docs.openzeppelin.com/upgrades-plugins/1.x/">プラグイン</a>を使用することを推奨しています。しかし弊チームのプロダクトが SaaS である都合上 HTTP サーバからのデプロイが必要であり、開発環境として構成されている Hardhat などとの相性はあまり良くありません。 そこで今回はプラグインを利用せずに、 <code>@openzeppelin/upgrades-core</code> で提供されているバイトコードをそのまま使用してプロキシをデプロイする方針にしました。これは OpenZeppelin が提供するプラグインの内部で利用されているのと同じものです。</p> <p>一方でプラグインにはデプロイ時に実装が安全にアップグレードできることを検証したり、アップグレード時に以前の実装と互換性があることを検証するなどの機能があり、プラグインを全く利用しない場合これらの恩恵を受けられません。そこでコントラクト開発のリポジトリのテストでのみプラグインを使用したデプロイやアップグレードのテストを行うことで、これらの安全性を担保できるようにしました。</p> <h2 id="まとめ">まとめ</h2> <p>アップグレード可能なコントラクトの仕組みや設計、デプロイ方法など開発の一連の流れを紹介しました。</p> <p>コントラクトのアップグレードは中央集権的になってしまうなどの側面もあるので良く考えて導入する必要がある技術ではありますが、新しい機能を後から追加できるというのは利便性の面で非常に大きなメリットです。 ぜひコントラクト開発を行う際は検討してみて下さい。</p> charines Git の Squash マージをやめた話 hatenablog://entry/6801883189062334318 2023-11-29T16:00:00+09:00 2023-11-29T17:05:16+09:00 こんにちは!ブロックチェーンチームでエンジニアをしている id:dorapon2000 です。最近買ってよかったものは「潮の華 あおさといわしふりかけ」です。 今回は Git の Squash マージについての知見を共有したいと思います。端的に言うと、 チーム開発で Non Fast-Forward マージをやめて Squash マージを採用し、再び Non Fast-Forward マージに戻した経緯の説明です。Squash マージを運用に導入するか考えたことがある方の参考になればと思います。 Squash マージとは マージには 3 種類ありますね。みなさんはトピックブランチを main … <p>こんにちは!ブロックチェーンチームでエンジニアをしている <a href="http://blog.hatena.ne.jp/dorapon2000/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/dorapon2000/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:dorapon2000</a> です。最近買ってよかったものは「潮の華 あおさといわしふりかけ」です。</p> <p>今回は Git の Squash マージについての知見を共有したいと思います。端的に言うと、 チーム開発で Non Fast-Forward マージをやめて Squash マージを採用し、再び Non Fast-Forward マージに戻した経緯の説明です。Squash マージを運用に導入するか考えたことがある方の参考になればと思います。</p> <h2 id="Squash-マージとは">Squash マージとは</h2> <p>マージには 3 種類ありますね。みなさんはトピックブランチを main へマージする際にどのマージ方法を利用していますか?</p> <ul> <li>Fast-Forward マージ <ul> <li>git merge --ff-only</li> </ul> </li> <li>Non Fast-Forward マージ <ul> <li>git merge --no-ff</li> </ul> </li> <li>Squash マージ <ul> <li>git merge --squash</li> </ul> </li> </ul> <p>GitHub 上のマージボタンではそれぞれ</p> <ul> <li>Rebase and merge</li> <li>Create a merge commit</li> <li>Squash and merge</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231129/20231129160004.png" width="343" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>に対応します。今回注目する Squash マージは、複数コミットを単一のコミットにまとめてしまうマージ方法です。これらを説明するわかりやすい記事は多くあるため、そちらに説明を譲ります。</p> <h2 id="なぜ-Squash-マージをやってみたのか">なぜ Squash マージをやってみたのか</h2> <p>それぞれのマージ方法にはメリット・デメリットがあります。私達が利用していた Non Fast-Forward マージと Squash マージであげると以下のとおりです。</p> <ul> <li>Non Fast-Forward マージ <ul> <li>メリット <ul> <li>マージコミットができるので、緊急時に Revert しやすい</li> <li>すべてのコミットログが残るため、コードの意図の調査をしやすい</li> </ul> </li> <li>デメリット <ul> <li>マージコミットが大量に発生する <ul> <li>GitHub Flow において、main を feature へマージするときと feature を main へマージするときにマージコミットが発生する</li> </ul> </li> <li>WIP なコミットや add 忘れなどの雑なコミットまで残る</li> </ul> </li> </ul> </li> <li>Squash マージ <ul> <li>メリット <ul> <li>マージコミットができるので、緊急時に Revert しやすい</li> <li>プルリク内のコミットは単一のコミットにまとめられてスッキリする</li> </ul> </li> <li>デメリット <ul> <li>詳細なコミット履歴が失われる</li> </ul> </li> </ul> </li> </ul> <p>私達のチームで利用していた Non Fast-Forward マージでも開発において不都合はありませんでした。しかし、マージコミットが大量に発生する点が気になっていました。実際に Non Fast-Forward マージで運用している現在の main ブランチの様子です。マージコミットだらけです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231129/20231129160007.png" width="499" height="315" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>メリットの中で最も重要な点は「緊急時に Revert しやすい」です。それは Squash マージにもあります。そして Non Fast-Forward マージのデメリットが目についたとき、Squash マージが魅力的に映りました。</p> <p>それから私達のチームは Squash マージを採用しました。具体的には、main → feature は従来どおり Non Fast-Forward マージで、feature → main へのマージが Squash マージです。</p> <h2 id="やってみてつらかったこと">やってみてつらかったこと</h2> <p>Squash マージは 3 ヶ月間運用しました。しかし、運用の中で Non Fast-Forward マージのときには想像していなかったコンフリクトの問題が大量に現れました。</p> <ul> <li>main から feature/α ブランチ (子) を切る</li> <li>feature/α にコミットハッシュ A を push</li> <li>feature/α から feature/β ブランチ (孫) を切る</li> <li>feature/β にコミットハッシュ B を push (画像 1 枚目) <ul> <li>A と B はコンフリクトの関係にあるとする (補足参照)</li> </ul> </li> <li>feature/α を main に Squash マージ (画像 2 枚目) <ul> <li>ここで Squash されるので main にはコミットハッシュ A が入らない (コミットハッシュ C として追加)</li> <li>feature/β のベースブランチは main に切り替わる</li> </ul> </li> <li>(最新にするため) main を feature/β へ Non Fast-Forward マージする (画像 3 枚目) <ul> <li>main 中にコミットハッシュ A がないことで、 feature/β のコミット A+B と main の C の解決がうまくできずコンフリクトする</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231129/20231129160010.png" width="800" height="214" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231129/20231129160013.png" width="800" height="214" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231129/20231129160015.png" width="800" height="214" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Squash マージのデメリットである「詳細なコミット履歴が失われる」がコンフリクトという形で問題になりました。この状況によるコンフリクトを Squash コンフリクトと命名して説明を続けます。</p> <h2 id="やめた">やめた</h2> <p>コンフリクトが辛かったため、チームで相談して対応を 4 つ考えました。</p> <ul> <li>① 気合で Squash コンフリクトを解消する</li> <li>② Squash コンフリクトが発生する場合のみ、Non Fast-Forward でマージする</li> <li>③ Squash コンフリクトが発生しないように、派生する feature ブランチがすべてマージされていることを確認してから Squash マージする</li> <li>④ Squash マージをやめる</li> </ul> <p>本記事のタイトルにもある通り、採用したのは ④ の Squash マージをやめることです。つまり、従来の Non Fast-Forward マージの運用に戻しました。</p> <p>①〜③ を採用しなかった理由をそれぞれ説明します。</p> <p>① は現実的でないと判断されました。もしロックファイル(pnpm-lock.yml など)でコンフリクトが起きてしまったときにあまりに悲惨です。</p> <p>② は運用でカバーする方法です。しかし、本来であれば認識する必要のない自身の子ブランチに合わせて 2 種類のマージを使い分ける必要があります。また、運用していると Non Fast-Forward マージすべきところをうっかり Squash マージしてしまったというミスが起きてしまいそうです。議論の余地なく不採用でした。</p> <p>③ も運用でカバーする方法です。Squash コンフリクトが起きる状況を作らないように、feature/A を feature/B より先にマージしなければいいのです。しかし、feature/A に依存する子ブランチが多いと、あるいは他の子ブランチがマージされるのを待っているうちに新しい子ブランチができるなど、いつまで経っても feature/A を main にマージできません。そして、十分にその状況がありえます。</p> <p>④ はもともと運用していた方法であり懸念はありません。</p> <h2 id="考察">考察</h2> <p>当時のチームは新規プロダクトの開発初期段階にあり、 feature/B (孫ブランチ) のようなブランチがよく作成されていました。そのため Squash マージとの相性が良くなかったのだと考えられます。feature/B があまり発生しない状況や環境であれば Squash マージもよい選択肢になるかと思います。</p> <h2 id="まとめ">まとめ</h2> <p>記事を読んでいただきありがとうございます。最後にまとめます。</p> <ul> <li>Non Fast-Forward マージ戦略にはマージコミットが大量に発生するという問題がある</li> <li>問題の解決のために Squash マージで運用した</li> <li>特定の状況でコンフリクトが頻繁に発生した</li> <li>結局 Non Fast-Forward マージ戦略に戻した</li> </ul> <h2 id="補足-コンフリクトの関係">(補足) コンフリクトの関係</h2> <p>Squash コンフリクトの例中で以下のように説明した箇所があります。</p> <blockquote><p>A と B はコンフリクトの関係にある</p></blockquote> <p>適切な用語を思いつかなかったためこのような説明になっていますが、内容はシンプルです。Squash コンフリクトの例を引き継いで、コードで説明します。</p> <p>コミット A</p> <pre class="code lang-diff" data-lang="diff" data-unlink>import bisect <span class="synIdentifier">+ import collections</span> </pre> <p>コミット B</p> <pre class="code lang-diff" data-lang="diff" data-unlink>import bisect import collections <span class="synIdentifier">+ import math</span> </pre> <p>Squash マージコミット C は</p> <pre class="code lang-diff" data-lang="diff" data-unlink>import bisect <span class="synIdentifier">+ import collections</span> </pre> <p>のようになり、コミット A+B とコミット C が Squash コンフリクトを起こす、ということです。</p> dorapon2000 MemcachedとRedisの統合によるコスト削減の紹介 hatenablog://entry/6801883189054881731 2023-11-01T16:30:00+09:00 2024-01-29T16:33:15+09:00 駅メモ!チームエンジニアの id:yumlonne です。 この記事では駅メモ!で使っていた Memcached を廃止し Redis に統合した経緯や流れを紹介します。 記事内で提供するサンプルコードは、駅メモ!の実装に合わせ Perl となってます。 簡単なコードなので Perl に詳しく無い方でも十分理解できると思います。 KVS 統合の背景 駅メモ!は AWS を使ってサービスを提供しています。 統合前は Amazon ElastiCache で Memcached と Redis の両方を運用していました。 Memcached はプライマリノードのみ、Redis はプライマリノードと… <p>駅メモ!チームエンジニアの <a href="http://blog.hatena.ne.jp/yumlonne/">id:yumlonne</a> です。</p> <p>この記事では駅メモ!で使っていた Memcached を廃止し Redis に統合した経緯や流れを紹介します。</p> <p>記事内で提供するサンプルコードは、駅メモ!の実装に合わせ Perl となってます。 簡単なコードなので Perl に詳しく無い方でも十分理解できると思います。</p> <h2 id="KVS-統合の背景">KVS 統合の背景</h2> <p>駅メモ!は AWS を使ってサービスを提供しています。</p> <p>統合前は Amazon ElastiCache で Memcached と Redis の両方を運用していました。 Memcached はプライマリノードのみ、Redis はプライマリノードとレプリカノードそれぞれ 1 台の構成でした。 それとほとんど同じ構成が他に 2 セットあるため、全体を見ると Memcached は 3 ノード存在していました。</p> <p>Memcached は <code>m6g.large</code> を利用していたため、リザーブドノード料金で年間 3000 ドル以上のコスト削減が期待できました。</p> <h2 id="Memcached-と-Redis-の使い分け">Memcached と Redis の使い分け</h2> <p>駅メモ!では Memcached と Redis を以下のように使い分けていました。</p> <ul> <li>Memcached <ul> <li>セッションデータ</li> <li>揮発して良いデータ</li> </ul> </li> <li>Redis <ul> <li>ランキングデータ(sorted sets)</li> <li>揮発してはいけないデータ <ul> <li>ほとんどのデータは RDS で永続化しています</li> <li>ここではパフォーマンス面で揮発を許容できないことを指しています</li> </ul> </li> </ul> </li> </ul> <p>統合先として Redis を選択した理由はいくつかありますが、決め手は低負荷かつ高パフォーマンスなランキング処理の実現に Redis が得意とする sorted sets 型を利用していたためです。</p> <h2 id="セッションデータの移行">セッションデータの移行</h2> <p>ユーザに再ログインする手間をかけさせたくないため、Memcached のセッションデータだけは Redis に移行することとしました。 以下はセッションデータの移行の流れです。これによりメンテナンスなどを実施することなくセッションデータの移行を進められました。</p> <ol> <li>アクセス時にセッションデータを Memcached から Redis に移行するモジュールを作成 <ul> <li>(箇条書きの下にコードのサンプルを示します)</li> </ul> </li> <li>本番反映してからセッションの有効期限分の時間が経つまで様子を見る <ul> <li>セッションの有効期限分の期間を空けることでアクセスのあるユーザのセッションデータは 1 で作成したモジュールによって移行されます</li> <li>アクセスのないユーザのセッションデータは移行されませんが、どちらにせよセッションデータの有効期限が過ぎているのでユーザには影響ありません</li> </ul> </li> <li>Redis のセッションデータのみを参照するモジュールに差し替える <ul> <li>1 で作成したモジュールは Memcached も参照するので最終的には差し替える必要があります</li> </ul> </li> </ol> <p>以下は 1 で作成したモジュールのコードのサンプルです。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># セッションの登録</span> <span class="synComment"># 新規の登録は全てRedisに向ける</span> <span class="synStatement">sub </span><span class="synIdentifier">set_session </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$key</span>, <span class="synIdentifier">$value</span>) = <span class="synIdentifier">@_</span>; <span class="synComment"># 有効期限を設定しシリアライズしてRedisに保存</span> <span class="synIdentifier">$redis-&gt;set</span>(<span class="synIdentifier">$key</span>, <span class="synIdentifier">$value</span>, <span class="synConstant">'EX'</span>, <span class="synIdentifier">$session_expire</span>); } <span class="synComment"># セッションの取得</span> <span class="synComment"># Redisから取得できればそれを返し、そうでなければMemcachedから取得したものをRedisに登録して返す</span> <span class="synStatement">sub </span><span class="synIdentifier">get_session </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$key</span>) = <span class="synIdentifier">@_</span>; <span class="synStatement">my</span> <span class="synIdentifier">$session</span>; <span class="synIdentifier">$session</span> = <span class="synIdentifier">$redis-&gt;get</span>(<span class="synIdentifier">$key</span>); <span class="synStatement">if</span> (<span class="synStatement">defined</span> <span class="synIdentifier">$session</span>) { <span class="synStatement">return</span> <span class="synIdentifier">$session</span>; } <span class="synIdentifier">$session</span> = <span class="synIdentifier">$memcached-&gt;get</span>(<span class="synIdentifier">$key</span>); <span class="synStatement">if</span> (<span class="synStatement">defined</span> <span class="synIdentifier">$session</span>) { set_session(<span class="synIdentifier">$key</span>, <span class="synIdentifier">$session</span>); } <span class="synStatement">return</span> <span class="synIdentifier">$session</span>; } </pre> <h2 id="Memcached-を-Redis-に統合">Memcached を Redis に統合</h2> <p>Memcached を操作するコード全てを Redis に差し替えることを検討しましたが、差分が膨大になることでエンバグのリスク増え、動作確認およびレビュー範囲が広がってしまいます。そこで既存の Memcached を使うキャッシュモジュールと同じインターフェースを持つ Redis 実装のキャッシュモジュールを作成して差し替えることにしました。</p> <p>Memcached を使うキャッシュモジュールで呼ばれていた関数を調査し、必要最低限の機能を実装しました。以下は Redis 実装のキャッシュモジュールの関数の一覧です。</p> <ul> <li>set, set_multi, add, replace</li> <li>get, get_multi</li> <li>incr, decr</li> <li>delete, flush_all(Redis では flushdb に相当)</li> </ul> <p>これらの関数の実装では以下の苦労がありました。</p> <h3 id="Memcached-と-Redis-のモジュールの振る舞いの違い">Memcached と Redis のモジュールの振る舞いの違い</h3> <p>Memcached のキャッシュモジュールは <a href="https://metacpan.org/pod/Cache::Memcached::Fast::Safe">Cache::Memcached::Fast::Safe</a> で、Redis のキャッシュモジュールは <a href="https://metacpan.org/pod/Redis">Redis</a> をベースに作成しました。</p> <p>それぞれのモジュールで同じインターフェースを提供することを考えていましたが、一部のコマンドの振る舞いにクセがあり、同じインターフェースにできなかった部分もあります。 例えば Memcached は decr で 10 から 9 のように桁が減ったとき、値を取得し直すと<code>"9"</code>ではなく<code>"9 "</code>のように 2 文字返してきます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">use </span>Cache::Memcached::Fast::Safe; <span class="synComment"># ローカルのMemcachedに接続するインスタンスを作成</span> <span class="synStatement">my</span> <span class="synIdentifier">$memcached</span> = Cache::Memcached::Fast::Safe-&gt;new({ <span class="synConstant">servers</span> =&gt; [ { <span class="synConstant">address</span> =&gt; <span class="synConstant">&quot;localhost:11211&quot;</span>} ]}); <span class="synIdentifier">$memcached-&gt;set</span>(<span class="synConstant">&quot;key&quot;</span>, <span class="synConstant">10</span>); <span class="synIdentifier">$memcached-&gt;decr</span>(<span class="synConstant">&quot;key&quot;</span>, <span class="synConstant">1</span>); <span class="synStatement">warn</span> <span class="synStatement">sprintf</span>(<span class="synConstant">'&quot;%s&quot;'</span>, <span class="synIdentifier">$memcached-&gt;get</span>(<span class="synConstant">&quot;key&quot;</span>)); <span class="synComment"># =&gt; &quot;9 &quot;</span> </pre> <p>Perl では上記のような状態でも、数値として扱う分には問題にならないため、振る舞いの違いは許容できました。</p> <h3 id="Perl-オブジェクトを素直に-set-できない">Perl オブジェクトを素直に set できない</h3> <p>Memcached にはフラグ機能があり、データとは別にメタ情報を保存することができます。一方で Redis にはこのような仕組みはありません。<br/> 数値や文字列を単純に保存する場合には問題になりませんが、構造化されたデータを保存する場合には少し困ります。取り出したデータがただのバイナリデータなのか、構造化されたデータをシリアライズしたものなのかを判断できないためです。<br/> 駅メモ!では数値や文字列の扱いが多いものの、一部の機能で Perl オブジェクトを保存していました。</p> <p>上記を踏まえ、今回は以下の対応としました。</p> <ul> <li>set する値が Perl のオブジェクト(ref した結果が true)なら Storable#nfeeze を使ってシリアライズする</li> <li>get した値を Storable#thaw でデシリアライズしてみて、エラーになったら thaw する前の値を返す</li> </ul> <p>他の手法として、必ず一定のフォーマットのオブジェクトにしてからシリアライズしてセットするやりかたがあります。 この手法では Memcached のフラグ機能と同等の恩恵を得られますがデメリットもあります。ただの数値や文字列もシリアライズして保存すると、その値に対して incr などの直接の操作ができなくなり、redis-cli などで直接データを確認することも難しくなります。<br/> 上に書いた通り、駅メモ!では単純な数値や文字列を保存しているケースが多いため、この手法は採用しませんでした。</p> <h2 id="まとめとその後の展開">まとめとその後の展開</h2> <p>今回は駅メモ!における Memcached の廃止と Redis への統合についての手法を紹介しました。 費用面のコスト削減もさることながら、管理すべきミドルウェアが減ったことで運用面のコストも下がって一石二鳥だったと思います。</p> <p>その後の展開として「Memcached と Redis の使い分け」 で触れたランキング処理について、最もデータ容量の大きいランキングを RDS に処理させるように改善し、Redis のスペックも下げることができました。こちらもいずれ記事にしたいと考えていますのでお楽しみに!</p> <p>2023/01/29追記: MySQL でランキング処理を行うようにする仕組みの記事を公開しました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.mobilefactory.jp%2Fentry%2F2024%2F01%2F29%2F163000" title="コスト削減のため Redis の sorted sets で実装していたランキング処理を MySQL に移行しました - Mobile Factory Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.mobilefactory.jp/entry/2024/01/29/163000">tech.mobilefactory.jp</a></cite></p> yumlonne Flutter でカメラ映像と Widget を重ね合わせて劣化させずに撮影する hatenablog://entry/6801883189054615815 2023-10-30T16:30:00+09:00 2023-10-30T16:30:09+09:00 こんにちは!この記事では Flutter でカメラを扱うアプリを作成する際の工夫について、紹介します。 はじめに 弊社で開発されている駅メモ!おでかけカメラ(以下「おでかけカメラ」)は 2022 年 11 月にリニューアルし、UI の刷新や動作不良の解消、機能の拡充を行いました。 内部的には、これまでの Unity 製だったおでかけカメラ(以下「旧おでかけカメラ」)を一度全て捨て、新しく Flutter で作り直すということをしています。 社内には Flutter に関する知見がほとんどなかったため、おでかけカメラのリニューアルは技術のキャッチアップから始まり、試行錯誤を重ねた開発となりました… <p>こんにちは!この記事では Flutter でカメラを扱うアプリを作成する際の工夫について、紹介します。</p> <h1 id="はじめに">はじめに</h1> <p>弊社で開発されている駅メモ!おでかけカメラ(以下「おでかけカメラ」)は 2022 年 11 月にリニューアルし、UI の刷新や動作不良の解消、機能の拡充を行いました。 内部的には、これまでの Unity 製だったおでかけカメラ(以下「旧おでかけカメラ」)を一度全て捨て、新しく Flutter で作り直すということをしています。</p> <p>社内には Flutter に関する知見がほとんどなかったため、おでかけカメラのリニューアルは技術のキャッチアップから始まり、試行錯誤を重ねた開発となりました。</p> <p>今回はその試行錯誤の中から、カメラの映像内にフレームやスタンプのような装飾を Widget として配置し、撮影した際の、写真と Widget を重ね合わせる工夫について説明します。</p> <h1 id="プレビューをそのまま撮影結果とすることの問題点">プレビューをそのまま撮影結果とすることの問題点</h1> <p>カメラ映像と Widget を重ね合わせて撮影する最も簡単な方法として、画面のスクリーンショットを撮影し、プレビュー領域以外の部分を削除することが考えられます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20231030/20231030163003.png" width="800" height="552" loading="lazy" title="" class="hatena-fotolife" style="width:600px" itemprop="image"></span></p> <p>この方法であれば、プレビューで表示されていたものが間違いなくそのまま撮影結果として得られますし、実装も単純になるのですが、大きな欠点があります。 それは、撮影画質がディスプレイ解像度に依存しているため、多くの場合カメラの性能を活かしきれず、残念画質な写真にしかならないと言うことです。</p> <h1 id="撮影後の画像サイズに合わせて-Widget-を再構築し画像化する">撮影後の画像サイズに合わせて Widget を再構築し、画像化する</h1> <p>上記の問題点を解消し、カメラの性能を最大限活かしつつプレビュー通りの撮影結果を得るために、おでかけカメラでは写真のサイズや縦横比と合うように一緒に撮影する Widget を画像化し、写真と合成するアプローチを採用しました。</p> <p>具体的には画面に描画していないオフスクリーンな Widget Tree を構築できる <a href="https://api.flutter.dev/flutter/widgets/BuildOwner-class.html">BuildOwner</a> というクラスを使用しています。</p> <p>以下は撮影した写真のサイズに合わせた Widget を作成し、画像にしたものを取得するコードです。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synPreProc">import</span> <span class="synConstant">'dart:ui'</span> <span class="synStatement">as</span> ui; <span class="synPreProc">import</span> <span class="synConstant">'package:image/image.dart'</span> <span class="synStatement">as</span> img; <span class="synType">Future</span><span class="synStatement">&lt;</span>img.<span class="synType">Image</span><span class="synStatement">&gt;</span> <span class="synIdentifier">getOverlayImage</span>(<span class="synType">Size</span> imageSize) <span class="synStatement">async</span> { <span class="synComment">// 描画、再描画を効率的に行えるようにする Class</span> <span class="synComment">// ただし、ここでは Widget を画像化する機能を利用するために使っている</span> <span class="synType">final</span> repaintBoundary = <span class="synType">RenderRepaintBoundary</span>(); <span class="synComment">// 描画する場所</span> <span class="synComment">// 写真のサイズで Widget が描画されるようにパラメータを渡している</span> <span class="synType">final</span> renderView = <span class="synType">RenderView</span>( window<span class="synStatement">:</span> ui.window, child<span class="synStatement">:</span> <span class="synType">RenderPositionedBox</span>(alignment<span class="synStatement">:</span> <span class="synType">Alignment</span>.center, child<span class="synStatement">:</span> repaintBoundary), configuration<span class="synStatement">:</span> <span class="synType">ViewConfiguration</span>( size<span class="synStatement">:</span> imageSize, devicePixelRatio<span class="synStatement">:</span> <span class="synConstant">1.0</span>, ), ); <span class="synComment">// Widget Tree の描画を制御する Class</span> <span class="synType">final</span> pipelineOwner = <span class="synType">PipelineOwner</span>(); pipelineOwner.rootNode = renderView; renderView.<span class="synIdentifier">prepareInitialFrame</span>(); <span class="synComment">// Widget Tree の構築、再構築を制御する Class</span> <span class="synType">final</span> buildOwner = <span class="synType">BuildOwner</span>(focusManager<span class="synStatement">:</span> <span class="synType">FocusManager</span>()); <span class="synComment">// 描画するもの</span> <span class="synComment">// CameraOverlay() を画像化したものが最終的に欲しいもの</span> <span class="synType">final</span> element = <span class="synType">RenderObjectToWidgetAdapter</span><span class="synStatement">&lt;</span><span class="synType">RenderBox</span><span class="synStatement">&gt;</span>( container<span class="synStatement">:</span> repaintBoundary, child<span class="synStatement">:</span> <span class="synType">CameraOverlay</span>(), ).<span class="synIdentifier">attachToRenderTree</span>(buildOwner); <span class="synComment">// BuildOwner に構築する Widget Tree のスコープを指定</span> buildOwner.<span class="synIdentifier">buildScope</span>(element); buildOwner.<span class="synIdentifier">finalizeTree</span>(); <span class="synComment">// PipelineOwner で描画</span> pipelineOwner.<span class="synIdentifier">flushLayout</span>(); pipelineOwner.<span class="synIdentifier">flushCompositingBits</span>(); pipelineOwner.<span class="synIdentifier">flushPaint</span>(); <span class="synComment">// 画像に変換して返却する</span> <span class="synType">final</span> ui.<span class="synType">Image</span> widgetImage = <span class="synStatement">await</span> repaintBoundary.<span class="synIdentifier">toImage</span>(); <span class="synType">final</span> <span class="synType">ByteData</span> byteData = <span class="synStatement">await</span> widgetImage.<span class="synIdentifier">toByteData</span>(format<span class="synStatement">:</span> ui.<span class="synType">ImageByteFormat</span>.png); <span class="synStatement">return</span> img.<span class="synIdentifier">decodeImage</span>(byteData.buffer.<span class="synIdentifier">asUint8List</span>()); } </pre> <p>簡単に説明すると、 <a href="https://api.flutter.dev/flutter/widgets/BuildOwner-class.html">BuildOwner</a> で構築した Widget Tree を <a href="https://api.flutter.dev/flutter/rendering/PipelineOwner-class.html">PipelineOwner</a> で <a href="https://api.flutter.dev/flutter/rendering/RenderView-class.html">RenderView</a> にレンダリングし、 <a href="https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary-class.html">RenderRepaintBoundary</a> の機能を使って画像として書き出しています。</p> <p>あとは以下の通り、画像化した Widget を撮影した写真と合成してあげれば、出来上がりです。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">final</span> img.<span class="synType">Image</span> cameraImage = <span class="synIdentifier">takePicture</span>(); <span class="synType">final</span> <span class="synType">Size</span> imageSize = <span class="synType">Size</span>(cameraImage.width.<span class="synType">double</span>(), cameraImage.height.<span class="synType">double</span>()); <span class="synType">final</span> img.<span class="synType">Image</span> overlayImage = <span class="synStatement">await</span> <span class="synIdentifier">getOverlayImage</span>(imageSize); <span class="synType">final</span> img.<span class="synType">Image</span> compositeImage = img.<span class="synIdentifier">drawImage</span>(cameraImage, overlayImage); </pre> <h1 id="まとめ">まとめ</h1> <p>Flutter でカメラ映像と、その上に重ねて表示した Widget を合わせて撮影する際に、品質を落とさずプレビュー通りの結果を得るための工夫について紹介しました。</p> <p>やりたいことは単純なのに意外と一工夫が必要ということで、詰まりやすいところなのかなと思いました。 カメラアプリを作る際にでも参考になれば幸いです。</p> yokoi0803 駅メモ!開発チームにおける Vue.js のマイグレーションプロセス hatenablog://entry/820878482973521876 2023-10-12T16:00:00+09:00 2023-10-12T16:00:01+09:00 こんにちは、駅メモ!でフロントエンドを良い感じにしたかったチームの id:yunagi_n です。 今回は、駅メモ!にて使用している Vue.js を 2 系から 3 系へあげて行くに当たって、採用した手法とマイグレーションプロセスについて紹介します。 今回、マイグレーションするに当たって、以下の要件がありました: 機能開発を止めてはいけない 駅メモ!では 6 月と 10 月に周年リリースがあり、それの開発を止めるわけにはいきませんでした もちろん、その間にあったイベントなどについても、開発は継続し続けています 多くのメンバーは割けない 基本はわたしが中心に、追加で 1 人〜2 人に手伝っても… <p>こんにちは、駅メモ!でフロントエンドを良い感じにしたかったチームの <a href="http://blog.hatena.ne.jp/yunagi_n/">id:yunagi_n</a> です。<br/> 今回は、駅メモ!にて使用している Vue.js を 2 系から 3 系へあげて行くに当たって、採用した手法とマイグレーションプロセスについて紹介します。</p> <p>今回、マイグレーションするに当たって、以下の要件がありました:</p> <ul> <li>機能開発を止めてはいけない <ul> <li>駅メモ!では 6 月と 10 月に周年リリースがあり、それの開発を止めるわけにはいきませんでした</li> <li>もちろん、その間にあったイベントなどについても、開発は継続し続けています</li> </ul> </li> <li>多くのメンバーは割けない <ul> <li>基本はわたしが中心に、追加で 1 人〜2 人に手伝ってもらうことはありました</li> </ul> </li> </ul> <p>また、参考のため、駅メモ!のフロントエンドの規模感を紹介しておくと:</p> <ul> <li>Vue コンポーネント数は 1500 コンポーネント <ul> <li><code>fd --type file --extension vue | wc -l</code> にて算出 <a href="#f-7325d8ca" name="fn-7325d8ca" title="fd はファイルシステムのエントリーを検索するツールです。">*1</a></li> </ul> </li> <li>フロントエンドのコード全体は 4700 ファイル、16 万行 <ul> <li><code>tokei .</code> にて算出 <a href="#f-c082e720" name="fn-c082e720" title="tokei は指定ディレクトリー内コードの統計データを出してくれるツールです。">*2</a></li> </ul> </li> </ul> <p>といった感じでした。<br/> 規模感で言えば超大規模、またフロントエンドにおいてはテストなども一切存在していなかったため、かなり厳しい作業となることが予想されました。<br/> ただ、駅メモ!では、歴史的な理由から外部の依存パッケージが少なく、サードパーティーパッケージの更新による影響は避けられました。<br/> しかしながら、パッケージ数による影響は無いですが、 Babel や Webpack などの依存関係は<strong>導入当時以降アップデートされておらず</strong> (導入当時は、つまりサービス開始日より前です)、そのままでは 2023 年に開発されたパッケージの一部はバンドル出来ない状態でした。</p> <p>また、今回は Vue.js 公式から提供されている <a href="https://v3-migration.vuejs.org/migration-build.html">Migration Build</a> は使用していません。<br/> これは、超大規模となった場合、一度入れてしまったライブラリは抜くことが困難であることや、かえって効率が低下する可能性があったことからです。</p> <p>これらの状態から、 Vue 3 環境へとマイグレーションするため、以下のような手法を採用しました。</p> <ol> <li>新しく Vue.js 3.x で起動可能なエントリーポイントを別パッケージとして作成</li> <li>新しいエントリーポイントが起動でき次第、各種ロジックについて適切にパッケージとして切り出す (monorepo) <ul> <li>各パッケージは <code>main</code> と <code>exports</code> フィールドを用いてビルド成果物を出し分ける <ul> <li><code>main</code> には古いエントリーポイントを対象としたコードを (Webpack が古すぎて <code>exports</code> を認識できないため)</li> <li><code>exports</code> には新しいエントリーポイント、もしくは新しいパッケージを対象としたコードを</li> </ul> </li> </ul> </li> <li>徐々に現在のエントリーポイントに属する部分から新しいパッケージへと分離する <ul> <li>純粋なロジックは単純な JavaScript パッケージとして</li> <li>Vue に関わるものはコンポーネントライブラリとして</li> </ul> </li> <li>最終的には現在のエントリーポイントは廃止し、削除する</li> </ol> <p>といった形です。<br/> それぞれ、なぜこのような手法を取ったのか詳しく補足していきます。</p> <h3 id="新しく-Vuejs-3x-で起動可能なエントリーポイントを別パッケージとして作成">新しく Vue.js 3.x で起動可能なエントリーポイントを別パッケージとして作成</h3> <p>これは、以下の理由からになります</p> <ol> <li>現在のエントリーポイントにあたる Babel や Webpack では、まずアップグレードプロセスが必要になる <ul> <li>ただ、アップグレードについては過去業務委託の方に調査をお願いしたが、現実的な時間では不可能という結論となった</li> </ul> </li> <li>フロントエンドの開発体験が著しく悪い (初期開発ビルドまで数分、リロードまでも数十秒) という話があり、マイグレーションするにあたって障壁となる <ul> <li>時間的制約が厳しい中で、1 イテレーションに対して数十秒かかるのは進行スピードに大きな影響を与える</li> </ul> </li> </ol> <p>これらの問題を解決するため、既存のエントリーポイントに追加や置き換えをするのでは無く、新たにパッケージを切り出すことで、上記問題を解決することが可能でした (少々力尽くですが......)。<br/> また、このタイミングで今までは超巨大な 1 パッケージだったのを monorepo として切り出すため、以下のような整備も行いました。</p> <ul> <li><a href="https://turbo.build/repo">Turborepo</a> の導入</li> <li><a href="https://yarnpkg.com/features/workspaces">Yarn Workspaces</a> の導入</li> </ul> <p>monorepo としては Yarn Workspaces を入れるのは良く聞きますが、 Turborepo を入れたのはなかなか当時としては珍しい構成だった記憶があります。<br/> Turborepo を導入したきっかけとしては、依存グラフを用いて必要なパッケージだけでコマンドが実行できる点、キャッシュ機能により変化がないパッケージについてはビルドがされない点などです。</p> <h3 id="新しいエントリーポイントが起動でき次第各種ロジックについて適切にパッケージとして切り出す">新しいエントリーポイントが起動でき次第、各種ロジックについて適切にパッケージとして切り出す</h3> <p>こちらは、以下の理由があります。</p> <ol> <li>上記に繋がるが、 Babel が古すぎて ES2015 の一部文法のみが使用可能であり、生産性が低い</li> <li>現状のエントリーポイントにあたる部分では、循環参照などが当たり前に起きており、バンドル時に明らかに不要なファイルなども含まれていた</li> </ol> <p>これについては、パッケージを分けたことで、単純に新しい文法とスピード感あるイテレーションをやりたい、の他にも以下の利点がありました。</p> <ul> <li>Turborepo を採用したので、パッケージを適切に分けることで、ビルドキャッシュがうまく使える</li> <li>パッケージマネージャーレベルで循環参照を防げる</li> <li>完全な形で TypeScript の導入が出来る</li> <li>妥協しない形で ESLint や Stylelint 、 Prettier の導入が出来る</li> </ul> <p>とくに、前者は今フロントエンドのビルドにおいて、プロダクションビルドを行った際 10 分程度かかっていた時間が大幅に削減できます。<br/> そして、ここで分けたパッケージについては GitHub Package Registry に Publish しており、そちらを利用することであらかじめビルドしたパッケージも使えます。 また、いままで駅メモ!のフロントエンドでは、今年中頃まで ESLint も Stylelint もまともに運用されていませんでした (もちろん Prettier もありません)。 それらについて、さすがに無いのはコードクオリティ面で問題があるので導入はしたのですが、かなり妥協した設定です。<br/> しかしながら、パッケージとして切り出した部分については、厳密な形で TypeScript や ESLint, Prettier ,必要であれば Stylelint も導入できます。<br/> これによって、副作用的に新しく書かれた部分についてはコードクオリティ面での向上も図れました。</p> <h3 id="徐々に現在のエントリーポイントに属する部分から新しいパッケージへと分離する">徐々に現在のエントリーポイントに属する部分から新しいパッケージへと分離する</h3> <p>これは開発が常に続いているといった前提によるものです。<br/> 例えば、いまわたしがこの記事を書いている時点でもフロントエンドを含む新規開発が行われており、それらを新しく Vue3 で書いて・書き直して欲しい、というのは納期や教育コストなどを考えた場合現実的ではありません。</p> <p>そのため、現在のエントリーポイントについてはそのままで、 Vue3 で書かれたパッケージを <code>vue-demi</code> や社内で新たに作成したマイグレーション用パッケージを用いて Vue 2/3 両対応としてビルド可能にし、それを上記パッケージ経由で使用することで、極力同じ使用感で Vue3 パッケージを使用できるようにしています。</p> <p>例えば、以下のコードは書き換え前のコードです。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">button</span><span class="synIdentifier"> v-se-player&gt;</span>...<span class="synIdentifier">&lt;/</span><span class="synStatement">button</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synSpecial"> </span><span class="synStatement">import</span><span class="synSpecial"> sePlayer from </span><span class="synConstant">'example/directives/v-se-player'</span><span class="synSpecial">; </span><span class="synComment">// Vue2</span> <span class="synSpecial"> </span><span class="synStatement">export</span><span class="synSpecial"> </span><span class="synIdentifier">{</span> <span class="synSpecial"> directives: </span><span class="synIdentifier">{</span><span class="synSpecial"> sePlayer </span><span class="synIdentifier">}</span> <span class="synSpecial"> </span><span class="synIdentifier">}</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre> <p>これらは、次のように書き換えるだけで、 Vue3 にも対応したコードに出来ます。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">button</span><span class="synIdentifier"> v-se-player&gt;</span>...<span class="synIdentifier">&lt;/</span><span class="synStatement">button</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">template</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synSpecial"> </span><span class="synStatement">import</span><span class="synSpecial"> </span><span class="synIdentifier">{</span><span class="synSpecial"> vSePlayer </span><span class="synIdentifier">}</span><span class="synSpecial"> from </span><span class="synConstant">'@example-scope/directives'</span><span class="synSpecial">; </span><span class="synComment">// Vue3</span> <span class="synSpecial"> </span><span class="synStatement">export</span><span class="synSpecial"> </span><span class="synIdentifier">{</span> <span class="synSpecial"> directives: </span><span class="synIdentifier">{</span><span class="synSpecial"> sePlayer: vSePlayer </span><span class="synIdentifier">}</span> <span class="synSpecial"> </span><span class="synIdentifier">}</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre> <p>インポート先を変えるだけですね。簡単です。<br/> このような形で、既存モジュールをパッケージとして切り出し、かつ同じ形式で使えるようにすることで、学習コストがほぼ無い状態のままで移行ができます。 また、新規開発部分については、既存のモジュールを使いつつインポート先を変えるだけで Vue3 にも対応できるので、今後のコストが下げられます。</p> <h3 id="まとめ">まとめ</h3> <p>ということで、現在対応している駅メモ!における Vue.js のマイグレーションについて採用した手法とそのプロセスについての紹介でした。<br/> わたしはこの仕事を最後に退職してしまうので、続きは残った人にお願いする形ですが、また次回の <a href="http://blog.hatena.ne.jp/yunagi_n/">id:yunagi_n</a> の記事をお楽しみください。</p> <div class="footnote"> <p class="footnote"><a href="#fn-7325d8ca" name="f-7325d8ca" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/sharkdp/fd">fd</a> はファイルシステムのエントリーを検索するツールです。</span></p> <p class="footnote"><a href="#fn-c082e720" name="f-c082e720" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/XAMPPRocky/tokei">tokei</a> は指定ディレクトリー内コードの統計データを出してくれるツールです。</span></p> </div> yunagi_n 社内で「朝Rustもくもく会」を開催しました hatenablog://entry/820878482972736374 2023-10-04T16:00:00+09:00 2023-10-04T16:00:04+09:00 こんにちは!エンジニアの id:mkan0141 です! モバイルファクトリーでは「シェアナレ」という 1 日の業務時間のうち 1 時間であれば自習・勉強に使って OK という制度があります。 今回はその制度を利用して 8 月に「朝Rustもくもく会」というものを開催したので紹介します。 朝 Rust もくもく会とは 8 月の平日毎朝 08:30~09:30 に Rust に関することをもくもくと勉強・作業する会です。 目的は「Rust を触ったことがない・少し触ったけど続かなかった人に Rust をちゃんと勉強する機会を作る」と「早起きして健康になる」です。参加するだけで Rust の知識と… <p>こんにちは!エンジニアの <a href="http://blog.hatena.ne.jp/mkan0141/">id:mkan0141</a> です!</p> <p>モバイルファクトリーでは「シェアナレ」という 1 日の業務時間のうち 1 時間であれば自習・勉強に使って OK という制度があります。 今回はその制度を利用して 8 月に「朝Rustもくもく会」というものを開催したので紹介します。</p> <h2 id="朝-Rust-もくもく会とは">朝 Rust もくもく会とは</h2> <p>8 月の平日毎朝 08:30~09:30 に Rust に関することをもくもくと勉強・作業する会です。</p> <p>目的は「Rust を触ったことがない・少し触ったけど続かなかった人に Rust をちゃんと勉強する機会を作る」と「早起きして健康になる」です。参加するだけで Rust の知識と健康が手に入ります。素敵ですね。</p> <p>各自やることに関しては特に縛りはなく、Rust であればなんでも OK という方針にしました。一部ですが参加者が作業していた内容を以下にまとめます。</p> <ul> <li>読書 <ul> <li><a href="https://doc.rust-jp.rs/book-ja/">The Rust Programming Language 日本語版</a></li> <li><a href="https://tourofrust.com/">Tour of Rust</a></li> <li><a href="https://www.shuwasystem.co.jp/book/9784798061702.html">実践 Rust プログラミング入門</a></li> <li><a href="https://danielkeep.github.io/tlborm/book/README.html">The Little Book of Rust Macros</a></li> </ul> </li> <li>作業 <ul> <li>Discord Bot の開発</li> <li>自作ツールのアップデート</li> </ul> </li> </ul> <h2 id="なぜ朝にするの">なぜ朝にするの?</h2> <p>もちろん健康になりたいからというのはありますが、真面目な理由としては静かで集中しやすい環境だと思ったからです。</p> <p>時間帯にもよりますが、午後はメンションが多かったり、優先度の高い対応が起きてしまったりすることが多い印象です。そのため、勉強会中に少し気が逸れたり、参加することができなかったりします。もちろん業務優先なので仕方がないのですが、せっかくやるなら集中してやりたい!というので、比較的これらのことが起こりづらい早朝を選んで開催してみました。</p> <h2 id="進め方">進め方</h2> <p>今回、初めてもくもく会を主催したので他のもくもく会を参考に以下のように決めました。 主催者と参加者に負担がかからないようなゆるい会になるのが目標でした。</p> <ol> <li>各自やることを宣言して作業(50 分間)</li> <li>各自の知見の docbase に記入・共有(10 分間)</li> </ol> <h2 id="感想">感想</h2> <p>初もくもく会主催だったのもあり、途中で参加者 0 人にならないか少し不安になりながら開催したのですが、ほぼ参加者が絶えることなく開催できてほっとしています。</p> <p>早朝開催に関しては、やはり早い時間のもくもく会は予想通り静かで集中しやすい環境になっていたので個人的にはかなりアリだと思いました。ただ、振り返ってみると参加メンバーの大半は普段から早めに出勤している人だったので、実は夕方開催の方が参加しやすく参加者も多くなっていたかもしれないです。</p> <p>また、もくもく会の進行に関しては、知見の共有の時間を設けたのは良かったと思いました。 各々の視点で理解した内容や Rust の躓いた話が聞けて、自分の理解が合っているかや躓いたポイントについて他の人が補足したりと Rust の理解がさらに深まったかなと思います。</p> <h2 id="終わりに">終わりに</h2> <p>朝 Rust もくもく会について紹介しました。 朝にする勉強会は健康になれますし学びも得られて個人的には満足度が高かったです。 ぜひ、みなさんも朝もくもく会を企画してみてはいかがでしょうか!</p> mkan0141 PerlパッケージからC#クラスの雛形を作ってみる hatenablog://entry/820878482971622371 2023-10-02T17:00:00+09:00 2023-10-02T17:00:19+09:00 駅メモ!開発基盤チームの id:xztaityozx です! 皆さんは Perl を書いていますか?モバイルファクトリーが長く提供しているサービスなどでは、バックエンドが Perl で書かれています。 しかしながら、自分は普段インフラ領域をやらせてもらっているということもあり、Perl で新機能開発をする!といった機会がそんなにありません。 せっかく Perl だらけの環境にいるのに、あんまり Perl に触れられないのはもったいないな〜と思い、今年のゴールデンウィークは PPI を使ったメタプログラミングで遊んでいました。 metacpan.org で、ちょっと遊んでいたら Perl のパッ… <p>駅メモ!開発基盤チームの <a href="http://blog.hatena.ne.jp/xztaityozx/">id:xztaityozx</a> です!</p> <p>皆さんは Perl を書いていますか?モバイルファクトリーが長く提供しているサービスなどでは、バックエンドが Perl で書かれています。<br/> しかしながら、自分は普段インフラ領域をやらせてもらっているということもあり、Perl で新機能開発をする!といった機会がそんなにありません。<br/> せっかく Perl だらけの環境にいるのに、あんまり Perl に触れられないのはもったいないな〜と思い、今年のゴールデンウィークは PPI を使ったメタプログラミングで遊んでいました。</p> <p><cite class="hatena-citation"><a href="https://metacpan.org/pod/PPI">metacpan.org</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmetacpan.org%2Fpod%2FPPI" title="PPI - Parse, Analyze and Manipulate Perl (without perl) - metacpan.org" 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></p> <p>で、ちょっと遊んでいたら Perl のパッケージ情報を使って C#のクラスを吐き出すプログラムができたので記事にしてみました。自由研究発表という感じです。</p> <p>変換先に C#を選んだのは C#が大好きだからです。</p> <h2 id="Perl-パッケージを-Cクラスにする例">Perl パッケージを C#クラスにする例</h2> <p>研究をしていたリポジトリは以下のものです。リポジトリ名気に入ってます。ガッっと書いたのでめちゃめちゃ雑です。</p> <p><cite class="hatena-citation"><a href="https://github.com/xztaityozx/katatsumuri">github.com</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fxztaityozx%2Fkatatsumuri" title="GitHub - xztaityozx/katatsumuri" 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></p> <p>README にも書いてあることですが、以下のような Perl パッケージがあるとき</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">package</span><span class="synType"> My::Namespace::B</span>; <span class="synStatement">use </span>strictures <span class="synConstant">2</span>; <span class="synStatement">use </span>Function::Parameters; <span class="synStatement">use </span>Function::Return; <span class="synStatement">use </span>Types::Standard -types; <span class="synStatement">use </span>Data::Validator; <span class="synStatement">use </span>Mouse; has <span class="synConstant">name</span> =&gt; (<span class="synConstant">is</span> =&gt; <span class="synConstant">'ro'</span>, <span class="synConstant">isa</span> =&gt; Str, <span class="synConstant">default</span> =&gt; <span class="synConstant">'this is name'</span>); has <span class="synConstant">age</span> =&gt; (<span class="synConstant">is</span> =&gt; <span class="synConstant">'ro'</span>, <span class="synConstant">isa</span> =&gt; Int, <span class="synConstant">required</span> =&gt; <span class="synConstant">1</span>); has <span class="synConstant">union</span> =&gt; (<span class="synConstant">is</span> =&gt; <span class="synConstant">'ro'</span>, <span class="synConstant">isa</span> =&gt; Str|Int, <span class="synConstant">required</span> =&gt; <span class="synConstant">1</span>); has <span class="synConstant">dict</span> =&gt; (<span class="synConstant">is</span> =&gt; <span class="synConstant">'ro'</span>, <span class="synConstant">isa</span> =&gt; Dict[<span class="synConstant">name</span> =&gt; Str, <span class="synConstant">age</span> =&gt; Int], <span class="synConstant">required</span> =&gt; <span class="synConstant">1</span>); <span class="synStatement">no </span>Mouse; __PACKAGE__<span class="synIdentifier">-&gt;meta-&gt;make_immutable</span>; <span class="synStatement">sub </span><span class="synIdentifier">a </span>{ <span class="synStatement">return</span> <span class="synConstant">10</span>; } fun b() :Return(Int) { <span class="synStatement">return</span> <span class="synConstant">10</span>; }; method c() :Return(Int) { <span class="synStatement">return</span> <span class="synConstant">10</span>; }; method d(Str <span class="synIdentifier">$str</span>, <span class="synIdentifier">$x</span>, Int <span class="synIdentifier">$y</span> //= <span class="synConstant">1</span>) { <span class="synStatement">return</span> <span class="synConstant">10</span>; }; <span class="synStatement">sub </span><span class="synIdentifier">e </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$rule</span> = Data::Validator-&gt;new( <span class="synConstant">str</span> =&gt; { <span class="synConstant">isa</span> =&gt; Str }, <span class="synConstant">x</span> =&gt; { <span class="synConstant">isa</span> =&gt; Any }, <span class="synConstant">y</span> =&gt; { <span class="synConstant">isa</span> =&gt; Int, <span class="synConstant">default</span> =&gt; <span class="synConstant">1</span> }, ); <span class="synIdentifier">$rule-&gt;validate</span>(<span class="synIdentifier">@_</span>); <span class="synStatement">return</span> <span class="synConstant">10</span>; } <span class="synStatement">sub </span><span class="synIdentifier">f </span><span class="synPreProc">:Return(Str, Int) </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$self</span>, <span class="synIdentifier">$str</span>, <span class="synIdentifier">$x</span>, <span class="synIdentifier">$y</span>) = <span class="synIdentifier">@_</span>; <span class="synStatement">return</span> [<span class="synIdentifier">$str</span>, <span class="synIdentifier">$x</span>+<span class="synIdentifier">$y</span>]; } <span class="synStatement">sub </span><span class="synIdentifier">g </span>{ <span class="synStatement">return</span>; } <span class="synStatement">sub </span><span class="synIdentifier">h </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$a</span> = <span class="synStatement">shift</span>; <span class="synStatement">return</span> <span class="synConstant">1</span>; } <span class="synConstant">1</span>; </pre> <p>以下のような C#コードが生成されます。基礎部分は NJsonSchema を使って JSON Schema から生成し、メソッド部分はシグネチャだけ移植したようなコードになります。そこそこ長くなるので一部省略しております。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">//----------------------</span> <span class="synComment">// &lt;auto-generated&gt;</span> <span class="synComment">// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v9.0.0.0) (http://NJsonSchema.org)</span> <span class="synComment">// &lt;/auto-generated&gt;</span> <span class="synComment">//----------------------</span> <span class="synType">namespace</span> My.Namespace { #pragma warning disable <span class="synComment">// Disable all warnings</span> [System.CodeDom.Compiler.GeneratedCode(<span class="synConstant">&quot;NJsonSchema&quot;</span>, <span class="synConstant">&quot;10.9.0.0 (Newtonsoft.Json v9.0.0.0)&quot;</span>)] <span class="synType">public</span> <span class="synStatement">partial</span> <span class="synType">class</span> <span class="synType">B</span> { <span class="synComment">/// </span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">/// original Perl type: Int</span> <span class="synComment">/// </span><span class="synIdentifier">&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> [System.Text.Json.Serialization.JsonPropertyName(<span class="synConstant">&quot;age&quot;</span>)] [System.Text.Json.Serialization.JsonIgnore(Condition <span class="synStatement">=</span> System.Text.Json.Serialization.JsonIgnoreCondition.Never)] <span class="synType">public</span> <span class="synType">int</span> Age { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synComment">/// </span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">/// original Perl type: Dict[age</span>=<span class="synType">&amp;</span><span class="synStatement">gt</span><span class="synType">;</span><span class="synComment">Int,name</span>=<span class="synType">&amp;</span><span class="synStatement">gt</span><span class="synType">;</span><span class="synComment">Str]</span> <span class="synComment">/// </span><span class="synIdentifier">&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> [System.Text.Json.Serialization.JsonPropertyName(<span class="synConstant">&quot;dict&quot;</span>)] [System.Text.Json.Serialization.JsonIgnore(Condition <span class="synStatement">=</span> System.Text.Json.Serialization.JsonIgnoreCondition.Never)] <span class="synType">public</span> Dict Dict { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synStatement">new</span> Dict(); <span class="synComment">/// </span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">/// original Perl type: Str</span> <span class="synComment">/// </span><span class="synIdentifier">&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> [System.Text.Json.Serialization.JsonPropertyName(<span class="synConstant">&quot;name&quot;</span>)] [System.Text.Json.Serialization.JsonIgnore(Condition <span class="synStatement">=</span> System.Text.Json.Serialization.JsonIgnoreCondition.Never)] <span class="synType">public</span> <span class="synType">string</span> Name { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synConstant">&quot;this is name&quot;</span>; <span class="synComment">/// </span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">/// original Perl type: Str|Int</span> <span class="synComment">/// </span><span class="synIdentifier">&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> [System.Text.Json.Serialization.JsonPropertyName(<span class="synConstant">&quot;union&quot;</span>)] [System.Text.Json.Serialization.JsonIgnore(Condition <span class="synStatement">=</span> System.Text.Json.Serialization.JsonIgnoreCondition.Never)] <span class="synType">public</span> Union Union { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synType">private</span> System.Collections.Generic.IDictionary&lt;<span class="synType">string</span>, <span class="synType">object</span>&gt; _additionalProperties; [System.Text.Json.Serialization.JsonExtensionData] <span class="synType">public</span> System.Collections.Generic.IDictionary&lt;<span class="synType">string</span>, <span class="synType">object</span>&gt; AdditionalProperties { <span class="synComment">// ... 省略</span> } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">object</span> a() { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;str&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Str</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;x&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Any</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;y&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Int</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">object</span> e(<span class="synType">string</span> str, <span class="synType">object</span> x, <span class="synType">int</span> y <span class="synStatement">=</span> <span class="synConstant">1</span>) { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Str, Int</span><span class="synIdentifier">&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">object</span> f() { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">void</span> g() { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;a&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Any</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">object</span> h(<span class="synType">object</span> a) { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Int</span><span class="synIdentifier">&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">int</span> b() { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Int</span><span class="synIdentifier">&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">int</span> c() { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">summary</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;str&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Str</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;x&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Any</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">param</span><span class="synIdentifier"> </span><span class="synType">name</span><span class="synIdentifier"> </span>=<span class="synIdentifier"> </span><span class="synConstant">&quot;y&quot;</span><span class="synIdentifier">&gt;</span><span class="synComment">original Perl type: Int</span><span class="synIdentifier">&lt;/</span><span class="synStatement">param</span><span class="synIdentifier">&gt;</span> <span class="synComment">///</span><span class="synIdentifier">&lt;</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">returns</span><span class="synIdentifier">&gt;</span> <span class="synType">public</span> <span class="synType">object</span> d(<span class="synType">string</span> str, <span class="synType">object</span> x, <span class="synType">int</span> y) { <span class="synStatement">throw</span> <span class="synStatement">new</span> NotImplementedException(); } } [System.CodeDom.Compiler.GeneratedCode(<span class="synConstant">&quot;NJsonSchema&quot;</span>, <span class="synConstant">&quot;10.9.0.0 (Newtonsoft.Json v9.0.0.0)&quot;</span>)] <span class="synType">public</span> <span class="synStatement">partial</span> <span class="synType">class</span> <span class="synType">Dict</span> { <span class="synComment">// ... 省略</span> } [System.CodeDom.Compiler.GeneratedCode(<span class="synConstant">&quot;NJsonSchema&quot;</span>, <span class="synConstant">&quot;10.9.0.0 (Newtonsoft.Json v9.0.0.0)&quot;</span>)] <span class="synType">public</span> <span class="synStatement">partial</span> <span class="synType">class</span> <span class="synType">Union</span> { <span class="synComment">// ... 省略</span> } } </pre> <h2 id="やっていること">やっていること</h2> <p>このリポジトリでやっているのはざっくりいうと以下の 2 つのことだけです。</p> <ol> <li>PPI とかを使って Perl パッケージ情報を取り出して JSON に書き出す</li> <li>C#で JSON を読んで C#の AST を構築し、できたものをファイルに書き出す</li> </ol> <p>同時に 2 つの AST を読むことになったので結構混乱しました。どちらの言語もむずいです。</p> <h3 id="1-PPI-とかを使って-Perl-パッケージ情報を取り出して-JSON-に書き出す">1. PPI とかを使って Perl パッケージ情報を取り出して JSON に書き出す</h3> <p>今回の研究で取り出したいと思ったのは以下の 5 つです。</p> <ol> <li>パッケージ名</li> <li>名前空間</li> <li>親パッケージ</li> <li>Mouse のプロパティ</li> <li>サブルーチンやメソッドのシグネチャ情報</li> </ol> <p>1,2 は PPI で簡単に取り出せます。具体的には以下のように書けます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">my</span> <span class="synIdentifier">$package_nodes</span> = <span class="synIdentifier">$document-&gt;find</span>(<span class="synConstant">'PPI::Statement::Package'</span>); <span class="synStatement">foreach</span> <span class="synStatement">my</span> <span class="synIdentifier">$package_node</span> (<span class="synIdentifier">@{$package_nodes}</span>) { <span class="synStatement">my</span> <span class="synIdentifier">@namespace</span> = <span class="synStatement">split</span>(<span class="synStatement">/</span><span class="synConstant">::</span><span class="synStatement">/x</span>, <span class="synIdentifier">$package_node-&gt;namespace</span>); <span class="synStatement">my</span> <span class="synIdentifier">$name</span> = <span class="synStatement">pop</span> <span class="synIdentifier">@namespace</span>; } </pre> <p>3 の親パッケージは <code>@ISA</code> を見るだけ、4 のプロパティは PPI で宣言を探しつつ、Mouse のメタ情報から情報を取り出せばよいですね。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># 親パッケージを取り出す</span> <span class="synStatement">no strict</span> <span class="synConstant">'refs'</span>; <span class="synStatement">my</span> <span class="synIdentifier">@superclasses</span> = <span class="synIdentifier">@{</span> <span class="synIdentifier">$class_name</span> . <span class="synConstant">'::ISA'</span> <span class="synIdentifier">}</span>; <span class="synStatement">use strict</span> <span class="synConstant">'refs'</span>; <span class="synComment"># プロパティの宣言を探して、プロパティ名を取り出す。</span> <span class="synStatement">my</span> <span class="synIdentifier">$has_statements</span> = <span class="synIdentifier">$ppi_document-&gt;find</span>( <span class="synStatement">sub </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$root</span>, <span class="synIdentifier">$node</span>) = <span class="synIdentifier">@_</span>; <span class="synStatement">if</span> (<span class="synIdentifier">$node-&gt;isa</span>(<span class="synConstant">'PPI::Token::Word'</span>) &amp;&amp; <span class="synIdentifier">$node-&gt;content</span> <span class="synStatement">eq</span> <span class="synConstant">'has'</span>) { <span class="synStatement">return</span> <span class="synConstant">1</span>; } <span class="synStatement">return</span> <span class="synConstant">0</span>; } ); <span class="synComment"># メタ情報から名前が一致するプロパティの情報を取り出す</span> <span class="synComment"># こうしておくと継承してたりMouseがはやしたりしたやつを省ける(別の方法はないのでしょうか…?)</span> <span class="synStatement">my</span> <span class="synIdentifier">$meta</span> = Mouse::Util::get_metaclass_by_name(<span class="synIdentifier">$package_statement-&gt;namespace</span>); <span class="synStatement">my</span> <span class="synIdentifier">@properties</span>; <span class="synStatement">foreach</span> <span class="synStatement">my</span> <span class="synIdentifier">$has_statement</span> (<span class="synIdentifier">@{$has_statements}</span>) { <span class="synStatement">my</span> <span class="synIdentifier">$property_name</span> = <span class="synIdentifier">$has_statement-&gt;snext_sibling-&gt;string</span>; <span class="synStatement">my</span> <span class="synIdentifier">$attr</span> = <span class="synIdentifier">$meta-&gt;get_attribute</span>(<span class="synIdentifier">$property_name</span>); <span class="synStatement">push</span> <span class="synIdentifier">@properties</span>, <span class="synIdentifier">$attr</span>; } </pre> <p>ここまでは順調ですが、問題は 5 のサブルーチン・メソッドのシグネチャ情報ですね…。</p> <h3 id="FunctionParameters-のメタ情報からシグネチャ情報を取り出してみる">Function::Parameters のメタ情報からシグネチャ情報を取り出してみる</h3> <p>Perl にはサブルーチンやメソッドのシグネチャをサポートするようなモジュールが沢山あります。たとえば <a href="https://metacpan.org/pod/Type::Params">Type::Params</a> や <a href="https://metacpan.org/pod/Function::Parameters">Function::Parameters</a> などですね。 こういうモジュールは大体の場合メタ情報が存在するため、そこから欲しい情報を取り出すことができます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># Function::Parametersの例</span> <span class="synStatement">use </span>strictures <span class="synConstant">2</span>; <span class="synStatement">use </span>Function::Parameters; <span class="synStatement">use </span>Types::Standard -types; method hoge(<span class="synIdentifier">$class</span>: Int <span class="synIdentifier">$i</span>) { <span class="synStatement">return</span> <span class="synIdentifier">$i</span>; } <span class="synComment"># Function::Parameters::Info が返される。引数のリストなどが分かる</span> <span class="synStatement">my</span> <span class="synIdentifier">$info</span> = Function::Parameters::info(\<span class="synIdentifier">&amp;hoge</span>); </pre> <p>つまり、PPI でサブルーチン・メソッドを見つけ次第、メタ情報からシグネチャ情報を取り出せば良さそうです。</p> <p>しかしながら、先程述べた通りこういうモジュールは沢山あるので全て対応するのは難しいです。そこでまずは Function::Parameters と <a href="https://metacpan.org/pod/Function::Return">Function::Return</a> だけ注目することにしました。Function::Return は最近駅メモ!のコードでも使われるようになったので、これを機に仲良くなっておこうと思って選びました。</p> <h4 id="普通のサブルーチンからもシグネチャ情報を取り出したい">普通のサブルーチンからもシグネチャ情報を取り出したい</h4> <p>Function::Parameters や Function::Return を使って書かれているサブルーチンはいいのですが、以下のようなよくあるサブルーチンはどうしましょう。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">sub </span><span class="synIdentifier">hoge_sub </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$hoge_arg</span> = <span class="synStatement">shift</span>; <span class="synStatement">return</span> <span class="synIdentifier">$hoge_arg</span>; } </pre> <p>最初、こういうのについては諦めようと思っていたんですが、PPI で遊んでいるうちに「なんとかなりそうだな」と思いました。 値の受け取りは、大体の場合以下のうちのどれかのパターンになるのでは?と考えたからです。(もちろん網羅できてるとは思っていません)</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">my</span> <span class="synIdentifier">$arg</span> = <span class="synStatement">shift</span>; <span class="synStatement">my</span> (<span class="synIdentifier">$arg1</span>, <span class="synIdentifier">$arg2</span>) = <span class="synIdentifier">@_</span>; <span class="synStatement">my</span> (<span class="synIdentifier">$arg1</span>, <span class="synIdentifier">$arg2</span>, <span class="synIdentifier">$arg3</span>) = (<span class="synStatement">shift</span>, <span class="synStatement">shift</span>, <span class="synStatement">shift</span>); </pre> <p>これらの式自体は <code>PPI::Statement::Variable</code> になります。<code>children</code>メソッドを呼び出すと、そこにぶら下がっている子を見ることができます。<br/> 以下の例は <code>my $arg = shift</code> の <code>children</code> です。</p> <pre class="code" data-lang="" data-unlink>[0] my (PPI::Token::Word), [1] (PPI::Token::Whitespace), [2] $arg (PPI::Token::Symbol), [3] (PPI::Token::Whitespace), [4] = (PPI::Token::Operator), [5] (PPI::Token::Whitespace), [6] shift (PPI::Token::Word), [7] ; (PPI::Token::Structure)</pre> <p>なんかなんとかなりそうな気がしませんか?</p> <p>そうですね。右辺値の <code>PPI::Token::Word</code> が <code>shift</code> か <code>@_</code> な <code>PPI::Statement::Variable</code> から、左辺値の <code>PPI::Token::Symbol</code> を取り出せば引数名がわかりますね。<br/> 残念ながら型はわからないので全部 Any になってしまいますが、今回は引数名だけでも分かればヨシ!ということでこの方針を採用しました。</p> <h4 id="DataValidator-からも引数情報を取り出したい">Data::Validator からも引数情報を取り出したい</h4> <p><a href="https://metacpan.org/pod/Data::Validator">Data::Validator</a> は引数のバリデーションをしてくれるモジュールです。引数名、型、制約を指定しておくことで、実際に渡ってきた値を評価してくれるものです。そうですね。これも引数の情報なのです。</p> <p>メタ情報を取り出せるような仕組みがあればよかったのですが、自分が調べた限りでは見つけられませんでした。なので PPI でパースしました。機能の網羅はできてないかも…。</p> <h4 id="JSON-に書き出す">JSON に書き出す</h4> <p>ここまでで取り出した情報を JSON に書き出してみます。フォーマットを JSON Schema や ProtoBuf みたいなスキーマ定義に寄せられればよかったのですが、メソッド情報を格納するところがなくて独自になってしまいました。具体的には以下のようなフォーマットです。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">パッケージ名</span>&quot;, &quot;<span class="synStatement">namespace</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">名</span>&quot;,&quot;<span class="synConstant">前</span>&quot;,&quot;<span class="synConstant">空</span>&quot;,&quot;<span class="synConstant">間</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">schema</span>&quot;: <span class="synSpecial">{</span> パッケージを表す<span class="synError">JSON</span> <span class="synError">Schema</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">methods</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">arguments</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">引数の名前</span>&quot;, &quot;<span class="synStatement">required</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">type</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">description</span>&quot;: &quot;<span class="synConstant">元々の型がなんだったかの説明</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">stringとかnumberみたいな型の名前</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> ... <span class="synSpecial">}</span>, <span class="synSpecial">{</span> ... <span class="synSpecial">}</span>, ... <span class="synSpecial">]</span>, &quot;<span class="synStatement">declare_type</span>&quot;: &quot;<span class="synConstant">subとかfunとかサブルーチン定義のキーワード</span>&quot;, &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">関数名</span>&quot;, &quot;<span class="synStatement">returns</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">number</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> ... <span class="synSpecial">}</span>, <span class="synSpecial">{</span> ... <span class="synSpecial">}</span>, ... <span class="synSpecial">]</span> <span class="synSpecial">}</span> </pre> <p>メソッド情報が必要ないのであれば、<code>schema</code>メンバーだけ使えば良いという親切設計です(?)</p> <h2 id="2-Cで-JSON-を読んで-Cの-AST-を構築しできたものをファイルに書き出す">2. C#で JSON を読んで C#の AST を構築し、できたものをファイルに書き出す</h2> <p>こちらは <a href="https://github.com/RicoSuter/NJsonSchema">NJsonSchema</a> を使ってクラスの雛形を作り、 <a href="https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/get-started/syntax-analysis">Roslyn API</a> を使って雛形にメソッド定義を追加していくという感じです。<br/> JSON を読んで展開していくだけなので、PPI の時ほど難しいことはありません。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> csharpGeneratorSetting <span class="synStatement">=</span> <span class="synStatement">new</span> CSharpGeneratorSettings { JsonLibrary <span class="synStatement">=</span> CSharpJsonLibrary.SystemTextJson, Namespace <span class="synStatement">=</span> <span class="synType">string</span>.Join(<span class="synConstant">&quot;.&quot;</span>, package.Namespace), }; <span class="synType">var</span> schema <span class="synStatement">=</span> <span class="synStatement">await</span> JsonSchema.FromJsonAsync(<span class="synConstant">&quot;schemaプロパティの値&quot;</span>); <span class="synType">var</span> csharpGenerator <span class="synStatement">=</span> <span class="synStatement">new</span> CSharpGenerator(schema, csharpGeneratorSetting); <span class="synType">var</span> file <span class="synStatement">=</span> csharpGenerator.GenerateFile() <span class="synStatement">??</span> <span class="synStatement">throw</span> <span class="synStatement">new</span> FileNotFoundException(); <span class="synStatement">using</span> <span class="synType">var</span> stream <span class="synStatement">=</span> <span class="synStatement">new</span> StringReader(file); <span class="synType">var</span> syntaxTree <span class="synStatement">=</span> CSharpSyntaxTree.ParseText(<span class="synStatement">await</span> stream.ReadToEndAsync()); <span class="synType">var</span> root <span class="synStatement">=</span> <span class="synStatement">await</span> syntaxTree.GetRootAsync(); <span class="synType">var</span> targetClassDeclarationSyntax <span class="synStatement">=</span> root.DescendantNodes() .OfType&lt;ClassDeclarationSyntax&gt;() .FirstOrDefault(syntax <span class="synStatement">=&gt;</span> syntax.Identifier.ValueText <span class="synStatement">==</span> package.Name) <span class="synStatement">??</span> <span class="synStatement">throw</span> <span class="synStatement">new</span> NoNullAllowedException(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>package.Name<span class="synSpecial">}</span><span class="synConstant"> が見つかりませんでした&quot;</span>); <span class="synComment">// targetClassDeclarationSyntax が schema プロパティから生成したクラス</span> <span class="synComment">// ここに methods プロパティの情報を使ってメソッド定義を追加していく</span> <span class="synComment">// 全部書くと長いので今回は省略…。リポジトリを見てください。</span> <span class="synType">var</span> newClassDeclarationSyntax <span class="synStatement">=</span> AddMethodDeclarationSyntax(); <span class="synType">var</span> newRoot <span class="synStatement">=</span> root.ReplaceNode(targetClassDeclarationSyntax, newClassDeclarationSyntax); <span class="synType">var</span> result <span class="synStatement">=</span> syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); </pre> <h2 id="まとめ">まとめ</h2> <p>今回は自由研究として、PPI を使った Perl パッケージ情報の解析をやってみました。解析結果を使って Perl パッケージを C#のクラスに変換しました。<br/> 結果としてそれっぽいクラスを生成できました。いまのところ今回作った生成機能をどこかで使う予定はないですが、解析結果を使えば LSP を更に便利にできるのでは?と考えています。</p> <p>メタプログラミングはやはり楽しいですね。また、まとまった時間があれば研究してみたいなと思いました。</p> <p>以上です。</p> xztaityozx Perl で App Store Server Notification V2 の検証をする hatenablog://entry/820878482964955375 2023-09-14T16:30:00+09:00 2023-09-14T16:30:02+09:00 こんにちは、エンジニアの id:kaoru-k_0106 です。 駅奪取のサブスク機能である「駅奪取er定期券」は、App Storeのサーバ通知の実装の際に App Store Server Notification V2 を用いました。 他の言語での Server Notification V2 の実装例は見つかりますが、Perl のものはありませんでした。 そこで、今回は Perl での検証部分の実装方法について触れようと思います。 App Store Server Notification V2 について V1 のときは、通常の App 内課金と同じように、サーバ通知で送られてきたレシ… <p>こんにちは、エンジニアの <a href="http://blog.hatena.ne.jp/kaoru-k_0106/">id:kaoru-k_0106</a> です。</p> <p>駅奪取のサブスク機能である「<a href="https://ekidash.com/info/detail?info_uuid=VkgAdST27RGOxqSeftqgtw">駅奪取er定期券</a>」は、App Storeのサーバ通知の実装の際に App Store Server Notification V2 を用いました。</p> <p>他の言語での Server Notification V2 の実装例は見つかりますが、Perl のものはありませんでした。</p> <p>そこで、今回は Perl での検証部分の実装方法について触れようと思います。</p> <h2 id="App-Store-Server-Notification-V2-について">App Store Server Notification V2 について</h2> <p>V1 のときは、通常の App 内課金と同じように、サーバ通知で送られてきたレシートを、App Store サーバの verifyReceipt エンドポイントに送信して検証する必要がありました。</p> <p>参考: <a href="https://developer.apple.com/jp/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store/">App Storeを使用してレシートを検証する - 日本語ドキュメント - Apple Developer</a></p> <p>ですが、V2 では署名されたレシートが送られてくるようになったため、App Store サーバに問い合わせる必要がなくなり、アプリケーションサーバで検証処理が完結します。</p> <p>また、通知タイプが追加されたりと、サブスクリプションの状態判定が V1 よりも簡単にできるようになっています。</p> <p>2023 年 6 月に V1 のサーバ通知は Deprecated になったので、新規で採用することはないと思いますが、V2 によって実装がしやすくなったと思います。</p> <p>詳しくは Apple Developer Documentation を参照してください。</p> <p>参考: <a href="https://developer.apple.com/documentation/appstoreservernotifications/">App Store Server Notifications | Apple Developer Documentation</a></p> <h2 id="Server-Notification-V2-の-JWS-を-Perl-で検証する">Server Notification V2 の JWS を Perl で検証する</h2> <p>V2 のサーバ通知は、JWS (JSON Web Signature) で送られてきます。</p> <p>この JWS の署名を検証することで、レシートの検証が完了します。</p> <p>それでは、サーバ通知を受け取ってから、JWS の署名検証が完了するまでの手順を順に追っていきます。</p> <h3 id="1-JWS-のヘッダから証明書を取り出す">1. JWS のヘッダから証明書を取り出す</h3> <p>JWS を扱うモジュールは CPAN でいくつか見つかりますが、一番更新が新しい <a href="https://metacpan.org/pod/Crypt::JWT">Crypt::JWT</a> を使うことにしました。</p> <p>ペイロードの署名検証に必要な証明書は、ヘッダの x5c フィールドに含まれていますが、このモジュールは x5c フィールドに対応していません。</p> <p>そのため、証明書を手動で取り出す必要がありますが、JWS のフォーマットはシンプルなので簡単にできます。</p> <p>JWS は、以下のように「ヘッダ」「ペイロード」「ヘッダとペイロードの署名」が<code>.</code>(ピリオド)で繋がれた形になっています。</p> <pre class="code" data-lang="" data-unlink>${header}.${payload}.${signature}</pre> <p>これを <code>split</code> すればヘッダを取り出せます。</p> <p>ただし、JWS の各パートは通常の Base64 ではなく、URL Safe な Base64 (base64url) でエンコードされているので、その点は注意が必要です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># 証明書が欲しいのでdecode_jwtする前にヘッダを取得</span> <span class="synStatement">my</span> (<span class="synIdentifier">$header</span>,,) = <span class="synStatement">map</span> <span class="synStatement">{</span> decode_base64url(<span class="synIdentifier">$_</span>) <span class="synStatement">}</span> <span class="synStatement">split</span>(<span class="synStatement">/</span><span class="synSpecial">\.</span><span class="synStatement">/</span>, <span class="synIdentifier">$token</span>); </pre> <p>ヘッダを base64url でデコードすると JSON になっており、そのうちの x5c フィールドに以下の 3 つの証明書が含まれています。</p> <ol> <li>サーバ証明書: 署名に使ったサーバ証明書</li> <li>中間証明書: サーバ証明書を署名した公開鍵を含む証明書</li> <li>ルート証明書: 中間証明書を署名した公開鍵を含む証明書、自己証明書</li> </ol> <p>参考: <a href="https://developer.apple.com/documentation/appstoreservernotifications/jwsdecodedheader">JWSDecodedHeader | Apple Developer Documentation</a></p> <p>証明書は、通常の Base64 でエンコードされた DER 形式で格納されています。</p> <p>次の証明書チェーンの検証で PEM 形式の証明書が必要になるので、ここで変換しておきます。</p> <p>PEM 形式は DER 形式の証明書を Base64 でエンコードしたものにヘッダとフッタをつけたものなので、簡単に変換ができます。</p> <p>ただ、<a href="https://datatracker.ietf.org/doc/html/rfc7468">RFC 7468</a> に、最終行以外はちょうど 64 文字にする必要があると書かれていたので、仕様に準拠させるため改行を入れるようにしました。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># ヘッダのx5cから証明書を取り出してPEMにする}</span> <span class="synStatement">my</span> <span class="synIdentifier">@cert_chain</span> = <span class="synStatement">map</span> <span class="synStatement">{</span> _base64encoded_der_to_pem(<span class="synIdentifier">$_</span>) <span class="synStatement">}</span> <span class="synIdentifier">@{</span> decode_json(<span class="synIdentifier">$header</span>)<span class="synIdentifier">-&gt;{</span><span class="synConstant">x5c</span><span class="synIdentifier">}</span> <span class="synIdentifier">}</span>; </pre> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">sub </span><span class="synIdentifier">_base64encoded_der_to_pem </span>{ <span class="synComment"># certはbase64エンコードされたDER</span> <span class="synStatement">my</span> <span class="synIdentifier">$cert</span> = <span class="synStatement">shift</span>; <span class="synComment"># RFC 7468 の仕様に準拠させるため、64文字ごとに改行を入れる</span> <span class="synIdentifier">$cert</span> =~ <span class="synStatement">s/</span><span class="synSpecial">(.{64})</span><span class="synStatement">/</span><span class="synIdentifier">$1</span><span class="synSpecial">\n</span><span class="synStatement">/g</span>; <span class="synComment"># ヘッダとフッタをつければPEMになる</span> <span class="synStatement">return</span> <span class="synConstant">&quot;-----BEGIN CERTIFICATE-----</span><span class="synSpecial">\n</span><span class="synIdentifier">$cert</span><span class="synSpecial">\n</span><span class="synConstant">-----END CERTIFICATE-----</span><span class="synSpecial">\n\n</span><span class="synConstant">&quot;</span> } </pre> <h3 id="2-証明書チェーンの検証">2. 証明書チェーンの検証</h3> <p>次に、証明書チェーンが正しいものか検証します。</p> <p>証明書チェーンの検証には <a href="https://metacpan.org/pod/Crypt::OpenSSL::CA">Crypt::OpenSSL::CA</a> を使いました。</p> <p>また、検証には Apple のルート証明書が必要であるため <a href="https://www.apple.com/certificateauthority/">Apple PKI</a> から「Apple Root CA - G3 Root」をダウンロードして、サーバ内に配置しました。</p> <p>X.509 証明書についての詳しい解説は割愛しますが、ルート証明書は自己証明書なので、ローカルに保存したルート証明書を用いることで検証できます。</p> <p>x5c フィールドから取り出した証明書チェーンとサーバ内に配置したローカルのルート証明書を用いて以下の流れで検証を行います。</p> <ol> <li>中間証明書でサーバ証明書を検証</li> <li>チェーン内のルート証明書で中間証明書を検証</li> <li>ローカルのルート証明書でチェーン内のルート証明書を検証</li> </ol> <p>中間証明書も Apple PKI で公開されているため、中間証明書の検証までで完了とすることもできますが、期限がルート証明書より短く、管理コストがかかるため、ルート証明書で検証するのがいいと思います。</p> <p>検証に失敗した場合は例外が発生するので、その場合は処理を中断させます。</p> <p>コードは以下のようになります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># ローカルの証明書を読み込んでPEMに変換する</span> <span class="synStatement">my</span> <span class="synIdentifier">$root_cert</span> = Crypt::OpenSSL::X509-&gt;new_from_file(<span class="synConstant">'/path/to/AppleRootCA-G3.cer'</span>, Crypt::OpenSSL::X509::FORMAT_ASN1)-&gt;as_string(Crypt::OpenSSL::X509::FORMAT_PEM); <span class="synStatement">my</span> <span class="synIdentifier">@certs</span> = <span class="synStatement">map</span> <span class="synStatement">{</span> Crypt::OpenSSL::CA::X509-&gt;parse(<span class="synIdentifier">$_</span>); <span class="synStatement">}</span> (<span class="synIdentifier">@cert_chain</span>, <span class="synIdentifier">$root_cert</span>); <span class="synStatement">for</span> <span class="synStatement">my</span> <span class="synIdentifier">$i</span> ( <span class="synConstant">0</span> .. (<span class="synIdentifier">$#certs</span> - <span class="synConstant">1</span>) ) { (<span class="synIdentifier">$certs[$i]</span>)-&gt;verify((<span class="synIdentifier">$certs[$i</span> + <span class="synConstant">1</span><span class="synIdentifier">]-&gt;get_public_key</span>)); } </pre> <h3 id="3-JWS-のデコード">3. JWS のデコード</h3> <p>証明書が正しいことを検証できたので、 Crypt::JWT の decode_jwt で署名を検証しつつデコードして完了です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synComment"># 証明書チェーンの先頭の証明書で署名検証</span> <span class="synStatement">my</span> <span class="synIdentifier">$decoded_payload</span> = decode_jwt(<span class="synConstant">token</span> =&gt; <span class="synIdentifier">$token</span>, <span class="synConstant">key</span> =&gt; \<span class="synIdentifier">$cert_chain[</span><span class="synConstant">0</span><span class="synIdentifier">]</span> ); </pre> <h2 id="おわりに">おわりに</h2> <p>Ruby や Go など他の言語では、OpenSSL 周りが標準機能として提供されていますが、Perl では外部モジュールを使う必要があり、少し手間がかかりました。 とはいえ、ひととおりは必要なモジュールが揃っているので、問題なく実装ができます。</p> <p>また、実装に当たっては、他の言語での実装がとても参考になりました。ありがとうございました。</p> <h2 id="参考">参考</h2> <ul> <li><a href="https://mixi-developers.mixi.co.jp/verify-app-store-server-notification-version-2-22f6cb88add1">App Store Server Notifications Version 2(StoreKit 2)の JWS を検証する | by Taiga ASANO | MIXI DEVELOPERS</a></li> <li><a href="https://medium.com/eureka-engineering/published-oss-for-app-store-server-notifications-v2-ca8737dd5a90">App Store Server Notifications V2をGoで検証するOSSを作った | by ぺりー | Eureka Engineering | Medium</a></li> <li><a href="https://qiita.com/mogmet/items/c6f8f4485e4c354a170c">初心者でもわかるiOSサブスク課金のサーバ側の実装!App Store Server Notifications Version 2(StoreKit 2)のJWS検証と判定方法を解説! - Qiita</a></li> </ul> kaoru-k_0106 TypeORMのData Mapperパターンにおけるリレーションの型安全性を担保する hatenablog://entry/820878482964985338 2023-09-06T16:00:00+09:00 2023-09-06T16:00:11+09:00 こんにちは!BC チームでエンジニアをしている id:d-kimuson です。 今回は外部リレーションに関して型安全性の乏しい TypeORM の Data Mapper パターンを独自のユーティリティ型を使ってちょっとマシにする方法を紹介します。 前提: TypeORM の外部リレーションについて TypeORM では ManyToMany 等のデコレータを使ってスキーマに Foreign Key を書くことができます。 // 公式ドキュメントのサンプルです @Entity() export class Category { @PrimaryGeneratedColumn() id: nu… <p>こんにちは!BC チームでエンジニアをしている <a href="http://blog.hatena.ne.jp/d-kimuson/">id:d-kimuson</a> です。</p> <p>今回は外部リレーションに関して型安全性の乏しい TypeORM の Data Mapper パターンを独自のユーティリティ型を使ってちょっとマシにする方法を紹介します。</p> <h2 id="前提-TypeORM-の外部リレーションについて">前提: TypeORM の外部リレーションについて</h2> <p>TypeORM では ManyToMany 等のデコレータを使ってスキーマに Foreign Key を書くことができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 公式ドキュメントのサンプルです</span> <span class="synSpecial">@Entity</span><span class="synStatement">()</span> <span class="synStatement">export</span> <span class="synStatement">class</span> Category <span class="synIdentifier">{</span> <span class="synSpecial">@PrimaryGeneratedColumn</span><span class="synStatement">()</span> id: <span class="synType">number</span> <span class="synSpecial">@Column</span><span class="synStatement">()</span> name: <span class="synType">string</span> <span class="synSpecial">@ManyToMany</span><span class="synStatement">((type)</span> <span class="synStatement">=&gt;</span> Question<span class="synStatement">,</span> <span class="synStatement">(</span>question<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> question.categories<span class="synStatement">)</span> questions: Question<span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> <span class="synSpecial">@Entity</span><span class="synStatement">()</span> <span class="synStatement">export</span> <span class="synStatement">class</span> Question <span class="synIdentifier">{</span> <span class="synSpecial">@PrimaryGeneratedColumn</span><span class="synStatement">()</span> id: <span class="synType">number</span> <span class="synSpecial">@Column</span><span class="synStatement">()</span> title: <span class="synType">string</span> <span class="synSpecial">@Column</span><span class="synStatement">()</span> text: <span class="synType">string</span> <span class="synSpecial">@ManyToMany</span><span class="synStatement">((type)</span> <span class="synStatement">=&gt;</span> Category<span class="synStatement">,</span> <span class="synStatement">(</span>category<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> category.questions<span class="synStatement">)</span> <span class="synSpecial">@JoinTable</span><span class="synStatement">()</span> categories: Category<span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> </pre> <p>そして、実際にデータをクエリビルダや<a href="https://orkhan.gitbook.io/typeorm/docs/repository-api">リポジトリ</a>のメソッドから取ってくるに当たって、外部リレーションを一緒に取ってくるには複数のやり方がサポートされています。</p> <p>① クエリレベルでリレーションを選択する</p> <p>最もオーソドックスなやり方でクエリを叩くときにリレーションを選択します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">declare</span> <span class="synType">const</span> repository: Repository<span class="synStatement">&lt;</span>Question<span class="synStatement">&gt;</span> <span class="synComment">// リポジトリメソッド</span> <span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository.findOneOrFail<span class="synStatement">(</span><span class="synIdentifier">{</span> relations: <span class="synIdentifier">[</span><span class="synConstant">&quot;categories&quot;</span><span class="synIdentifier">]</span><span class="synStatement">,</span> where: <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// relation に指定した categories は自動的に Join されて取得されます</span> <span class="synComment">// クエリビルダ</span> <span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .innerJoinAndSelect<span class="synStatement">(</span><span class="synConstant">&quot;question.categories&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;categories&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> <span class="synComment">// joinAndSelect に指定した categories は自動的に Join されて取得されます</span> </pre> <p><code>innerJoinAndSelect</code> や <code>relation</code> によって明示することでリレーションのあるレコードを拾ってきてくれます。</p> <p>② スキーマレベルで <code>eager: true</code> を設定し、選択しなくても勝手に Join させる</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">@Entity</span><span class="synStatement">()</span> <span class="synStatement">export</span> <span class="synStatement">class</span> Question <span class="synIdentifier">{</span> <span class="synComment">// ...</span> <span class="synSpecial">@ManyToMany</span><span class="synStatement">((type)</span> <span class="synStatement">=&gt;</span> Category<span class="synStatement">,</span> <span class="synStatement">(</span>category<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> category.questions<span class="synStatement">,</span> <span class="synIdentifier">{</span> eager: <span class="synConstant">true</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synSpecial">@JoinTable</span><span class="synStatement">()</span> categories: Category<span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> </pre> <p><code>eager: true</code> を指定しておくと relations に指定せずとも自動的に Join されて取得されます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// リポジトリメソッド</span> <span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository.findOneOrFail<span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synComment">// relations: ['categories'], // なくても自動的に JOIN される</span> where: <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// クエリビルダ</span> <span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> <span class="synComment">// .innerJoinAndSelect('question.categories', 'categories') // なくても自動的に JOIN される</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> </pre> <p>明示せずとも Join して取ってこれるので便利ですが、必要ないケースでも必ずリレーションを取ってきてしまうという欠点があります。</p> <p>③ スキーマレベルで lazy relation を設定し、参照時にクエリを発行する</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">@Entity</span><span class="synStatement">()</span> <span class="synStatement">export</span> <span class="synStatement">class</span> Question <span class="synIdentifier">{</span> <span class="synComment">// ...</span> <span class="synSpecial">@ManyToMany</span><span class="synStatement">((type)</span> <span class="synStatement">=&gt;</span> Category<span class="synStatement">,</span> <span class="synStatement">(</span>category<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> category.questions<span class="synStatement">)</span> <span class="synSpecial">@JoinTable</span><span class="synStatement">()</span> categories: <span class="synSpecial">Promise</span><span class="synStatement">&lt;</span>Category<span class="synIdentifier">[]</span><span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> </pre> <p>スキーマで Promise でリレーションを宣言すると、lazy relation となります。 参照したタイミングでクエリが走り、await してあげることでリレーション先のデータを取得できます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">declare</span> <span class="synType">const</span> question: Question <span class="synStatement">await</span> question.categories <span class="synComment">// 参照したタイミングでクエリが発行されて取得できる</span> </pre> <p>外部リレーションを取得する方法としては、以上の 3 種類が存在します。</p> <p>弊チームでは</p> <ul> <li>② Eager Relation: 必要ないユースケースでもリレーションを取ってくることになってしまうため基本使わない</li> <li>③ Lazy Relation: 弊チームでは TypeORM をリポジトリパターンとして利用しているので、参照時にクエリが発行される Lazy Relation のアプローチは取りたくない</li> </ul> <p>ことが理由で基本的には ① のアプローチのみを利用しています。 この記事は ① のアプローチを使用している(eager relation や lazy relation を利用しない)ことが前提となります。</p> <h2 id="問題-都度リレーション指定だと型安全性がない">問題: 都度リレーション指定だと型安全性がない</h2> <p>② や ③ のアプローチで外部リレーションを取ってきている場合や、Active Record パターンを使っている場合は都度フェッチしたり必ず Join されていたりするので問題にならないのですが、Data Mapper で ① の都度 Join の利用だと、外部リレーションに関して型安全性の問題があります。</p> <p>これは Join するかどうかがクエリビルダやリポジトリメソッドのオプションの指定に左右されますが、返されるデータの型はスキーマクラスの型で固定になってしまうからです。</p> <p>例えば</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .innerJoinAndSelect<span class="synStatement">(</span><span class="synConstant">&quot;question.categories&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;categories&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> question.categories <span class="synComment">// Categories[] 型になっていて、実際にアクセスもできる</span> </pre> <p>のように使うリレーションが Join 済みであれば問題ありませんが</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> question.categories <span class="synComment">// Categories[] 型になってるのに、アクセスすると undefined を受け取ってしまう</span> </pre> <p>このように Join がされていない場合には型が存在するので取得済みだと思ったデータにアクセスすると実際には undefined を受け取ってしまうことになります。</p> <p>これだと例えば question を引数に取るサービスメソッドを用意したときに Join なしで取得していても、Join が前提になるメソッドへ渡せてしまうことになります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> question <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> <span class="synComment">// categories は拾ってきていない</span> <span class="synType">const</span> someMethod <span class="synStatement">=</span> <span class="synStatement">(</span>question: Question<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// categories を使ってゴニョゴニョする</span> <span class="synType">const</span> x <span class="synStatement">=</span> question.categories.filter<span class="synStatement">(</span>...<span class="synStatement">)</span> <span class="synComment">// Uncaught TypeError: Cannot read properties of undefined (reading 'filter')</span> <span class="synIdentifier">}</span> someMethod<span class="synStatement">(</span>question<span class="synStatement">)</span> <span class="synComment">// 渡せてしまう...</span> </pre> <p>この問題があるため以前は <code>question: Question // categories の Join が前提</code> みたいなコメントを使って意図を伝えるようにしており、辛い状態でした。</p> <h2 id="ユーティリティ型で解決する">ユーティリティ型で解決する</h2> <p>この問題をちゃんと型チェックで気付けるようにする回避策として独自の型ユーテリティを用意しています。 本当はライブラリ側で良い感じに吸収してくれて型が付くと嬉しいのですが、現状ではできていないので独自のユーティリティ型を用意してデータフェッチ時に型を書くことで対応しています。</p> <p>アプローチとしては</p> <ul> <li>スキーマクラス型からリレーションについて型安全な型に変換するユーティリティ型(<code>StrictEntity</code>)を用意し、データフェッチや制約として関数の引数で使うときに StrictEntity を通す</li> <li>StrictEntity はスキーマクラスからリレーションのキーを除外する <ul> <li>リレーションのキーは <code>string</code>, <code>number</code>, <code>bigint</code>, <code>boolean</code>, <code>Date</code> 以外が値にあるもの</li> </ul> </li> <li>Join されている対象は Generics で明示的に指定する</li> </ul> <p>のような形で実現しています。</p> <p>実装は以下のようになってます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> TypeOrmPrimitive <span class="synStatement">=</span> <span class="synType">string</span> | <span class="synType">number</span> | bigint | <span class="synType">boolean</span> | <span class="synSpecial">Date</span> <span class="synStatement">export</span> <span class="synStatement">type</span> PlainObject<span class="synStatement">&lt;</span><span class="synType">Entity</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> Exclude<span class="synStatement">&lt;keyof</span> <span class="synType">Entity</span><span class="synStatement">,</span> MethodKeys<span class="synStatement">&lt;</span><span class="synType">Entity</span><span class="synStatement">&gt;&gt;</span><span class="synIdentifier">]</span>: <span class="synType">Entity</span><span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synComment">/**</span> <span class="synComment"> * @desc TypeOrm の Entity からリレーションを持たないキーを取り出す Utility</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synStatement">type</span> PrimitiveKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">keyof</span> <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> <span class="synStatement">keyof</span> PlainObject<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">as</span> NonNullable<span class="synStatement">&lt;</span>T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span><span class="synStatement">&gt;</span> <span class="synStatement">extends</span> TypeOrmPrimitive ? K : <span class="synType">never</span><span class="synIdentifier">]</span>: T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synComment">/**</span> <span class="synComment"> * @desc Class のメソッドのキーを抜き出す Utility</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synStatement">type</span> MethodKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">keyof</span> <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> <span class="synStatement">keyof</span> T <span class="synStatement">as</span> T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synStatement">extends</span> <span class="synSpecial">Function</span> ? K : <span class="synType">never</span><span class="synIdentifier">]</span>: T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synIdentifier">}</span> <span class="synComment">/**</span> <span class="synComment"> * @desc TypeOrm の Entity から別テーブルへのリレーションのキーを抜き出す Utility</span> <span class="synComment"> */</span> <span class="synStatement">type</span> RelationKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=</span> Exclude<span class="synStatement">&lt;keyof</span> T<span class="synStatement">,</span> PrimitiveKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> | MethodKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;&gt;</span> <span class="synStatement">type</span> OptionalKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>P <span class="synStatement">in</span> <span class="synStatement">keyof</span> T<span class="synIdentifier">]</span>-?: <span class="synIdentifier">{}</span> <span class="synStatement">extends</span> Pick<span class="synStatement">&lt;</span>T<span class="synStatement">,</span> P<span class="synStatement">&gt;</span> ? P : <span class="synType">never</span> <span class="synIdentifier">}[</span><span class="synStatement">keyof</span> T<span class="synIdentifier">]</span> <span class="synStatement">export</span> <span class="synStatement">type</span> StrictEntity<span class="synStatement">&lt;</span> T<span class="synStatement">,</span> JoinedRelationKeys <span class="synStatement">extends</span> RelationKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synType">never</span><span class="synStatement">,</span> RelationsOptionalKeys <span class="synStatement">extends</span> <span class="synStatement">keyof</span> T <span class="synStatement">=</span> Extract<span class="synStatement">&lt;</span> JoinedRelationKeys<span class="synStatement">,</span> OptionalKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">&gt;,</span> RelationsRequiredKeys <span class="synStatement">extends</span> <span class="synStatement">keyof</span> T <span class="synStatement">=</span> Exclude<span class="synStatement">&lt;</span> JoinedRelationKeys<span class="synStatement">,</span> RelationsOptionalKeys <span class="synStatement">&gt;</span> <span class="synStatement">&gt;</span> <span class="synStatement">=</span> Pick<span class="synStatement">&lt;</span>T<span class="synStatement">,</span> PrimitiveKeys<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;&gt;</span> &amp; <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> RelationsOptionalKeys<span class="synIdentifier">]</span>?: T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synStatement">extends</span> <span class="synType">undefined</span> | infer I ? I <span class="synStatement">extends</span> <span class="synSpecial">Array</span><span class="synStatement">&lt;</span>infer Item<span class="synStatement">&gt;</span> ? ReadonlyArray<span class="synStatement">&lt;</span>StrictEntity<span class="synStatement">&lt;</span>Item<span class="synStatement">&gt;&gt;</span> : StrictEntity<span class="synStatement">&lt;</span>I<span class="synStatement">&gt;</span> : <span class="synType">never</span> <span class="synIdentifier">}</span> &amp; <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> RelationsRequiredKeys<span class="synIdentifier">]</span>: T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span> <span class="synStatement">extends</span> <span class="synSpecial">Array</span><span class="synStatement">&lt;</span>infer Item<span class="synStatement">&gt;</span> ? ReadonlyArray<span class="synStatement">&lt;</span>StrictEntity<span class="synStatement">&lt;</span>Item<span class="synStatement">&gt;&gt;</span> : StrictEntity<span class="synStatement">&lt;</span>T<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span><span class="synStatement">&gt;</span> <span class="synIdentifier">}</span> </pre> <p>読んでもらうより実際に例を見せるのが良いと思うので、まずはシンプルな例を提示します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> question: StrictEntity<span class="synStatement">&lt;</span>Question<span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> </pre> <p>このように変数の型を明示的に <code>StrictEntity&lt;Question&gt;</code> で型付けをします。ここで <code>StrictEntity&lt;Question&gt;</code> は <code>Question</code> 型の部分型(より制約が強い型)なので型注釈のみで変数に入れられます。</p> <p><code>StrictEntity&lt;Question&gt;</code> は外部リレーションのプロパティを取り除くので、実態としては</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synIdentifier">{</span> id: <span class="synType">number</span> title: <span class="synType">string</span> text: <span class="synType">string</span> <span class="synComment">// categories: Category[] // 除外される</span> <span class="synIdentifier">}</span> </pre> <p>のような型として型付けされます。</p> <p>また、リレーションが存在するときは</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> question: StrictEntity<span class="synStatement">&lt;</span>Question<span class="synStatement">,</span> <span class="synConstant">&quot;categories&quot;</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">await</span> repository .createQueryBuilder<span class="synStatement">(</span><span class="synConstant">&quot;question&quot;</span><span class="synStatement">)</span> .innerJoinAndSelect<span class="synStatement">(</span><span class="synConstant">&quot;question.categories&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;categories&quot;</span><span class="synStatement">)</span> .where<span class="synStatement">(</span><span class="synConstant">&quot;question.id = :id&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> id: <span class="synConstant">1</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> .getOneOrFail<span class="synStatement">()</span> </pre> <p>のように型付けをします。</p> <p>型引数で <code>categories</code> を指定すると、今度は</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">interface</span> <span class="synIdentifier">{</span> id: <span class="synType">number</span> title: <span class="synType">string</span> text: <span class="synType">string</span> categories: StrictEntity<span class="synStatement">&lt;</span>Category<span class="synStatement">&gt;</span><span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> </pre> <p>こういう型に型付けされます。</p> <p><code>StrictEntity&lt;Question, "categories"&gt;</code> で型付けされているときに <code>innerJoinAndSelect</code> が呼ばれていることを保証することはできませんが、型安全でない範囲をこの部分だけに閉じることができるようになりました。これであれば目視での確認もしやすいですし、<code>categories</code> のリレーションのみ存在するという暗黙的な情報を型で表現できるようになりました。</p> <p>データフェッチした変数がこれで型安全にできたので、サービスメソッドの引数を同じユーティリティを使った型安全にしていきます。問題点のところで紹介したメソッドの型付けを単に <code>Question</code> から必要なリレーションに限定した <code>StrictEntity&lt;Question, "categories"&gt;</code> へ変更します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> someMethod <span class="synStatement">=</span> <span class="synStatement">(</span>question: StrictEntity<span class="synStatement">&lt;</span>Question<span class="synStatement">,</span> <span class="synConstant">&quot;categories&quot;</span><span class="synStatement">&gt;)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// categories を使ってゴニョゴニョする</span> <span class="synType">const</span> x <span class="synStatement">=</span> question.categories.filter<span class="synStatement">(</span>...<span class="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>これにより</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">declare</span> <span class="synType">const</span> question: StrictEntity<span class="synStatement">&lt;</span>Question<span class="synStatement">&gt;</span> someMethod<span class="synStatement">(</span>question<span class="synStatement">)</span> <span class="synComment">// categories リレーションが必要なためちゃんと型エラーになる</span> </pre> <p>Join されている情報が型で表現できるようになったため、必要なリレーションが足りない場合には型エラーが出てくれるようになりました。</p> <p>これで、「Join せずに拾ってきたデータを誤って Join 前提の引数に渡してしまう」ようなミスを型レベルで防げるようになり、暗黙的にどのリレーションが Join されているかを意識する必要性がなくなりました。</p> <h2 id="まとめ">まとめ</h2> <p>TypeORM における Eager / Lazy Relation を使わないときの型安全性のなさを型ユーテリティを使って回避する方法を紹介しました。</p> <p>このやり方でもデータフェッチ時の型付けと Join の有無の整合性は開発者で担保する必要があり完全な解決策にはなりませんが、比較的手軽につらさを軽減できると思います。TypeORM を使っていてリレーションの型安全性にお困りの方はぜひお試しください!</p> d-kimuson GitHub ActionsのSelf-hosted Runnerで複数設定のRunnerを使う hatenablog://entry/820878482963346884 2023-09-01T11:00:00+09:00 2023-09-01T11:00:09+09:00 駅メモ!開発基盤チームの id:xztaityozx です!今回は CI/CD のお話です。 現在、駅メモ!チームでは Jenkins を使った CI/CD が構築されています。今回ここに GitHub Actions を加えることとなりました。チームでは段階的に GitHub Actions に移行していく計画です。 GitHub Actions を採用した理由としては、技術スタックの変化による需要の増加と Jenkins で抱えていた問題を解決するためという 2 点が主です。この記事では後者について書こうと思います。 現在の Jenkins 構成で困っていたこと 現在の Jenkins 構… <p>駅メモ!開発基盤チームの <a href="http://blog.hatena.ne.jp/xztaityozx/">id:xztaityozx</a> です!今回は CI/CD のお話です。</p> <p>現在、駅メモ!チームでは Jenkins を使った CI/CD が構築されています。今回ここに GitHub Actions を加えることとなりました。チームでは段階的に GitHub Actions に移行していく計画です。 GitHub Actions を採用した理由としては、技術スタックの変化による需要の増加と Jenkins で抱えていた問題を解決するためという 2 点が主です。この記事では後者について書こうと思います。</p> <h1 id="現在の-Jenkins-構成で困っていたこと">現在の Jenkins 構成で困っていたこと</h1> <p>現在の Jenkins 構成では、起動できるサーバのスペックを柔軟に切り替えたり、追加できるようにしたいというリクエストに答えづらい状態でした。仕組み上の制約などから、追加の作業はそこそこ時間のかかる作業となっていたからです。</p> <p>以前 CI でカバレッジ計測をしたいというリクエストがありましたが、提供まで 3 ヶ月の時間を要しました。<br/> 他のタスクに対応しつつだったので、かかりっきりというわけではなかったのですが、カバレッジ計測に合った別パイプラインの追加などが必要で、その作業すべてが手作業だったため、時間を取られてしまい提供が遅れてしまったのです。</p> <p>このように、新しいワークフローを追加する際に、気軽さと柔軟性の欠如が問題となっていました。似たような事例でも直列に繋いだり、git hook 使用して対応したりといった対策が必要だった状況です。</p> <p>もっと気軽に、自由に自動チェックやテストを追加したいですね…。</p> <h2 id="philips-labsterraform-aws-github-runnerの-Multi-runner-モジュールを使っていろんな設定の-Runner-を使えるようにする"><code>philips-labs/terraform-aws-github-runner</code>の Multi runner モジュールを使っていろんな設定の Runner を使えるようにする</h2> <p>この問題に対する解決策として、GitHub Actions と <a href="https://github.com/philips-labs/terraform-aws-github-runner">philips-labs/terraform-aws-github-runner</a> の <a href="https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/multi-runner">Multi runner</a>モジュールを活用する方法を紹介します。<code>philips-labs/terraform-aws-github-runner</code> はオートスケールする GitHub Actions の Self-Hosted Runner を AWS 上に構築してくれる Terraform モジュールです。<a href="http://blog.hatena.ne.jp/Eadaeda/">id:Eadaeda</a> が以前にもこのテックブログで紹介していますので、興味があれば参照してみてください。</p> <p><cite class="hatena-citation"><a href="https://tech.mobilefactory.jp/entry/2022/07/26/170000">tech.mobilefactory.jp</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.mobilefactory.jp%2Fentry%2F2022%2F07%2F26%2F170000" title="GitHub ActionsのワークフローをオートスケールするSelf-hosted runnerに移行した話 - Mobile Factory Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <p>さて、この<code>Multi runner</code>モジュールの README には以下のように書かれています。</p> <blockquote><blockquote><p>This module replaces the top-level module to make it easy to create with one deployment multiple type of runners.</p></blockquote> <p>This module creates many runners with a single GitHub app. The module utilizes the internal modules and deploys parts of the stack for each runner defined.</p></blockquote> <p>どうやらこのモジュール、1 つの GitHub App に様々な設定の Runner を複数個構築できるというもののようです。コミット履歴やプルリクエストを辿ってみると、実装のきっかけになった議論を見つけることができました。</p> <p><cite class="hatena-citation"><a href="https://github.com/philips-labs/terraform-aws-github-runner/discussions/1428">github.com</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fphilips-labs%2Fterraform-aws-github-runner%2Fdiscussions%2F1428" title="Queue Workflows for Specific Instance Types / Arch / OS · philips-labs/terraform-aws-github-runner · Discussion #1428" 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></p> <p>ざっくりとした内容は <code>runs-on</code> に与えた値で起動するインスタンスのインスタンスタイプや OS を制御できると良いよねということです。これの具体的な実現方法としては、単純に Runner の仕組みを複数個作るというもので、図にすると以下のような感じでしょうか。</p> <p><figure class="figure-image figure-image-fotolife" title="multi-runner"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20230901/20230901110007.png" width="745" height="508" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>multi-runner</figcaption></figure></p> <h2 id="Multi-runner-モジュールを使ってみる">Multi runner モジュールを使ってみる</h2> <p>早速試してみましょう。今回の場合は<code>t3.small</code>や<code>t3a.small</code>を起動できる<code>small</code>という Runner の設定がほしいというリクエストが来たとして、Multi runner モジュールで Runner を作ってみます!</p> <pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">module</span> <span class="synConstant">&quot;multi_runner&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synConstant">&quot;philips-labs/github-runner/aws//modules/multi-runner&quot;</span> <span class="synComment"># 実装当時のLatest</span> <span class="synIdentifier">version</span> = <span class="synConstant">&quot;3.6.0&quot;</span> <span class="synIdentifier">prefix</span> = <span class="synConstant">&quot;hoge-multi-runner&quot;</span> <span class="synIdentifier">aws_region</span> = data.aws_region.current.name <span class="synIdentifier">vpc_id</span> = data.aws_vpc.vpc.id <span class="synIdentifier">subnet_ids</span> = data.aws_subnets.subnets.ids <span class="synIdentifier">github_app</span> = <span class="synSpecial">{</span> <span class="synIdentifier">id</span> = <span class="synConstant">&quot;ここにAppID&quot;</span> <span class="synIdentifier">key_base64</span> = <span class="synConstant">&quot;ここに秘密鍵&quot;</span> <span class="synIdentifier">webhook_secret</span> = random_id.random.hex <span class="synSpecial">}</span> <span class="synIdentifier">webhook_lambda_zip</span> = <span class="synConstant">&quot;./download-lambda/webhook.zip&quot;</span> <span class="synIdentifier">runners_lambda_zip</span> = <span class="synConstant">&quot;./download-lambda/runners.zip&quot;</span> <span class="synIdentifier">runner_binaries_syncer_lambda_zip</span> = <span class="synConstant">&quot;./download-lambda/runner-binaries-syncer.zip&quot;</span> <span class="synIdentifier">multi_runner_config</span> = <span class="synSpecial">{</span> <span class="synConstant">&quot;small&quot;</span> = <span class="synSpecial">{</span> <span class="synComment"># t3.smallかt3a.smallなRunner</span> <span class="synIdentifier">matcherConfig</span> = <span class="synSpecial">{</span> <span class="synComment"># このRunnerを呼び出すのに使う runs-on の値</span> <span class="synIdentifier">labelMatchers</span> = <span class="synSpecial">[[</span><span class="synConstant">&quot;self-hosted&quot;</span>, <span class="synConstant">&quot;linux&quot;</span>, <span class="synConstant">&quot;x64&quot;</span>, <span class="synConstant">&quot;ubuntu-20.04&quot;</span>, <span class="synConstant">&quot;small&quot;</span><span class="synSpecial">]]</span> <span class="synIdentifier">exactMatch</span> = <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synIdentifier">runner_extra_labels</span> = <span class="synConstant">&quot;ubuntu-20.04,small&quot;</span> <span class="synIdentifier">runner_name_prefix</span> = <span class="synConstant">&quot;linux-x64-small_&quot;</span> <span class="synComment"># エファメラルランナーの場合は0でよい</span> <span class="synComment"># https://github.com/philips-labs/terraform-aws-github-runner#ephemeral-runners</span> <span class="synIdentifier">delay_webhook_event</span> = <span class="synConstant">0</span> <span class="synIdentifier">runner_config</span> = <span class="synSpecial">{</span> <span class="synIdentifier">runner_os</span> = <span class="synConstant">&quot;linux&quot;</span> <span class="synIdentifier">runner_architecture</span> = <span class="synConstant">&quot;x64&quot;</span> <span class="synComment"># エファメラルランナー設定を有効にしてRunnerは1つのJobを実行したら終了するように</span> <span class="synIdentifier">enable_ephemeral_runners</span> = <span class="synConstant">true</span> <span class="synIdentifier">ami_filter</span> = <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;hoge-github-runner-*&quot;</span><span class="synSpecial">]</span> <span class="synIdentifier">state</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;available&quot;</span><span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synIdentifier">ami_owners</span> = <span class="synSpecial">[</span>data.aws_caller_identity.current.account_id<span class="synSpecial">]</span> <span class="synComment"># このRunner設定はt3.smallかt3a.smallを起動する!</span> <span class="synIdentifier">instance_types</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;t3.small&quot;</span>, <span class="synConstant">&quot;t3a.small&quot;</span><span class="synSpecial">]</span> <span class="synComment"># 起動速度を考えて事前ビルドしたAMIから上げることにしたので、Syncerでactions/runnerのバイナリをSyncする機能はOFFにした</span> <span class="synIdentifier">enable_runner_binaries_syncer</span> = <span class="synConstant">false</span> } } } } </pre> <p>あとは<code>terraform plan</code>の出力を読んで<code>terraform apply</code>、GitHub App 側の設定を行って終了です。更に Runner の設定を増やしたい場合は、<code>"small"</code>と同じように増やしていけば良いだけです!簡単!</p> <pre class="code lang-hcl" data-lang="hcl" data-unlink><span class="synType">module</span> <span class="synConstant">&quot;multi_runner&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synConstant">&quot;philips-labs/github-runner/aws//modules/multi-runner&quot;</span> <span class="synIdentifier">version</span> = <span class="synConstant">&quot;3.6.0&quot;</span> ... <span class="synIdentifier">multi_runner_config</span> = <span class="synSpecial">{</span> <span class="synConstant">&quot;small&quot;</span> = <span class="synSpecial">{</span> ... <span class="synSpecial">}</span> <span class="synConstant">&quot;medium&quot;</span> = <span class="synSpecial">{</span> ... <span class="synSpecial">}</span> <span class="synConstant">&quot;large&quot;</span> = <span class="synSpecial">{</span> ... <span class="synSpecial">}</span> <span class="synSpecial">}</span> ... } </pre> <h2 id="PerlCritic-を動かしてみる">Perl::Critic を動かしてみる</h2> <p>テストの一部として組み込まれている Perl::Critic による静的解析を GitHub Actions で動かしてみます。<code>small</code>ぐらいのスペックがあれば十分なので、最初の移行としてぴったりな題材です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># https://json.schemastore.org/github-workflow.json</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> Perl Critic <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>opened, synchronize, reopened<span class="synSpecial">]</span> <span class="synIdentifier">paths</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;**.pl&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;lib/**.pm&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;t/**.t&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;t/**.pm&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;.github/workflows/critic.yaml&quot;</span> <span class="synIdentifier">concurrency</span><span class="synSpecial">:</span> <span class="synIdentifier">group</span><span class="synSpecial">:</span> critic-${{github.ref}} <span class="synIdentifier">cancel-in-progress</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">critic</span><span class="synSpecial">:</span> <span class="synComment"> # 動かすRunnerの指定をする部分</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>self-hosted, linux, x64, ubuntu-20.04, small<span class="synSpecial">]</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get delta <span class="synIdentifier">id</span><span class="synSpecial">:</span> changed-files <span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo diffs=&quot;$(gh pr diff --name-only ${{ github.event.number }} | grep -Pe <span class="synConstant">'^(.*\.(pl|pm)|t/.*\.t)$'</span> | tr <span class="synConstant">'\n'</span> <span class="synConstant">' '</span>)&quot; &gt;&gt; $GITHUB_OUTPUT <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synComment"> # 認証に secrets.GITHUB_TOKEN が使えるのが本当に便利</span> <span class="synIdentifier">GH_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.GITHUB_TOKEN }} <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Run Check <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ steps.changed-files.outputs.diffs <span class="synType">!=</span> <span class="synConstant">''</span> }} <span class="synIdentifier">run</span><span class="synSpecial">:</span> | /usr/local/bin/perlcritic --profile config/dev/perlcriticrc \ ${{steps.changed-files.outputs.diffs}} </pre> <p>Perl ファイルの差分だけを検査するような簡単なワークフローです。ポイントは <code>runs-on</code>に Runner の<code>labelMatchers</code>設定の値が書かれているところです。</p> <p>注意点として、<code>runs-on</code>のラベルは部分一致するもののうちのどれかが選ばれるという GitHub の仕様があります。例えば以下のように複数の Runner の設定があるとき</p> <ul> <li><code>[self-hosted, linux, x64, ubuntu-20.04, small]</code></li> <li><code>[self-hosted, linux, x64, ubuntu-20.04, large]</code></li> <li><code>[self-hosted, linux, x64, ubuntu-20.04, xlarge]</code></li> </ul> <p><code>runs-on</code>に<code>[self-hosted, linux, x64, ubuntu-20.04]</code>と設定するだけでは<code>small</code>,<code>large</code>,<code>xlarge</code>のどれにもマッチするため、どの Runner がジョブを拾うかはわからないのです。このことは<code>Multi runner</code>モジュールの <a href="https://github.com/philips-labs/terraform-aws-github-runner/blob/cfbcc944fc183b481caaee323e7832ec1964eb54/modules/multi-runner/README.md#the-catch">README</a> にも書いてあるので、詳しくはそちらをお読みください。 すこし<code>runs-on</code>を間違えただけで意図しないコスト増加がありえるので、ワークフローのコードレビューでは必ず見ておきたい項目になりそうです。</p> <h2 id="まとめ">まとめ</h2> <ul> <li>Self-Hosted Runner な GitHub Actions を構築することになった</li> <li>起動できる Runner のスペックを柔軟に切り替えたり、追加できるようにしたかった</li> <li><code>philips-labs/terraform-aws-github-runner</code>の Multi runner できそうだったのでやってみた</li> <li>うまく動いたのでよかった</li> </ul> <p>今後、利用が活発になると見える問題点や、<a href="https://github.com/philips-labs/terraform-aws-github-runner">JIT Runner</a>も試してみたいなと思っているので、また知見が溜まったらこのブログで記事を書きたいと思います。以上です。</p> xztaityozx Perl5.38の変更点 hatenablog://entry/820878482962956870 2023-08-31T11:00:00+09:00 2023-08-31T11:00:21+09:00 こんにちは、エンジニアの id:mp0liiu です。 今年も7/2にPerlの最新安定バージョンである5.38がリリースされたので新機能や変更点についてまとめます。 5.38 はかなり変更点が多いですが、ニッチな機能に対する変更も多いので影響の大きそうな箇所だけ知りたい方は最初の方だけ読んで頂くといいと思います。 重要な変更点 class構文の追加 実験的機能としてですが、ついに Perl にclass構文が追加されました。 次のような構文になります。 use v5.38; use experimental 'class'; class Point; field $x :param = 0;… <p>こんにちは、エンジニアの <a href="http://blog.hatena.ne.jp/mp0liiu/">id:mp0liiu</a> です。</p> <p>今年も7/2にPerlの最新安定バージョンである5.38がリリースされたので新機能や変更点についてまとめます。<br/> 5.38 はかなり変更点が多いですが、ニッチな機能に対する変更も多いので影響の大きそうな箇所だけ知りたい方は最初の方だけ読んで頂くといいと思います。</p> <h1 id="重要な変更点">重要な変更点</h1> <h2 id="class構文の追加">class構文の追加</h2> <p>実験的機能としてですが、ついに Perl にclass構文が追加されました。</p> <p>次のような構文になります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">use </span>experimental <span class="synConstant">'class'</span>; class Point; field <span class="synIdentifier">$x</span> :param = <span class="synConstant">0</span>; field <span class="synIdentifier">$y</span> :param = <span class="synConstant">0</span>; method move(<span class="synIdentifier">$dx</span> = <span class="synConstant">0</span>, <span class="synIdentifier">$dy</span> = <span class="synConstant">0</span>) { <span class="synIdentifier">$x</span> = <span class="synIdentifier">$dx</span>; <span class="synIdentifier">$y</span> = <span class="synIdentifier">$dy</span>; } method <span class="synStatement">print</span> <span class="synStatement">{</span> <span class="synStatement">say</span> <span class="synConstant">&quot;x: </span><span class="synIdentifier">$x</span><span class="synConstant">, y: </span><span class="synIdentifier">$y</span><span class="synConstant">&quot;</span>; <span class="synStatement">}</span> <span class="synStatement">my</span> <span class="synIdentifier">$p</span> = Point-&gt;new(<span class="synConstant">x</span> =&gt; <span class="synConstant">3</span>, <span class="synConstant">y</span> =&gt; <span class="synConstant">5</span>); <span class="synIdentifier">$p-&gt;print</span>(); <span class="synComment"># x: 3, y: 5</span> <span class="synIdentifier">$p-&gt;move</span>(<span class="synConstant">2</span>, <span class="synConstant">4</span>); <span class="synIdentifier">$p-&gt;print</span>(); <span class="synComment"># x: 2, y: 4</span> </pre> <ul> <li>クラスは <code>class</code> 文で宣言します。使い方は <code>package</code> と同じ感じで、ブロックでスコープを作ったりバージョンを指定することもできます。 <code>class</code> 文を利用するとコンストラクタである <code>new</code> メソッドは自動的に生成されますが、自前で書くことはできないです(実行時にエラーになる)</li> <li>インスタンス変数は <code>field</code> 文で宣言します。スカラだけでなく配列、ハッシュの変数も宣言可能です。従来よく使われていたハッシュリファレンスを bless したクラスと違って、宣言されたインスタンス変数はパッケージ外部から直接参照することはできません</li> <li>メソッドは <code>method</code> 文で宣言します。 <code>method</code> 内では <code>field</code> で宣言されたインスタンス変数を参照することと、現在のオブジェクト自身を指す暗黙的変数 <code>$self</code> を参照することができます。他はサブルーチンと同じような仕様でシグネチャの定義や匿名メソッドを作ることも可能です。ちなみに <code>$self</code> からインスタンス変数を参照することはできません(Java の <code>this.x</code> のようにインスタンス変数を参照することは不可能)</li> </ul> <p>class, field には attribute を設定することが可能で、これにより上記の構文だけでは実現できない機能を実現しています。</p> <p>上記で既に使用していますが、<code>field</code> に <code>:param</code> attribute を設定するとその変数名の名前付き引数をコンストラクタに渡して初期化できるようになり、 <code>field</code> の初期値が定義されていない場合は名前付き引数が渡されないとエラーが発生するようになります。</p> <p>継承は <code>class</code> に <code>:isa</code> attribute を設定することでできます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">use </span>experimental <span class="synConstant">'class'</span>; class People { field <span class="synIdentifier">$name</span> :param; } class User :isa(People) { field <span class="synIdentifier">$id</span> :param; } </pre> <p>親クラスは1つしか指定することができません。また、親クラスのインスタンス変数は直接参照できません。</p> <p>従来のクラスと違う点をまとめると以下のようになります。</p> <ul> <li>継承できるクラスは1つだけ</li> <li>インスタンス変数はパッケージ外部から直接参照できない</li> <li>bless しているリファレンスがないので従来のオブジェクトとは異なる判定をされることがあるので注意 <ul> <li><code>Scalar::Util::reftype</code> や <code>builtin::reftype</code> では <code>OBJECT</code> という値が返ってきます</li> <li><code>Scalar::Util::blessed</code> や <code>bultin::blessed</code> は同じように使えます</li> </ul> </li> </ul> <p>現在実装されている機能は主に上記のものだけです。 Moose などであったRole機能がなかったり、コンストラクタに渡された引数を操作できなかったり、アクセサの自動生成ができないため、まだ本格的に class 構文をプロダクトで利用するのは難しそうです。</p> <p>一応今後の開発方針としては</p> <ul> <li>Role機能の実装</li> <li>コンストラクタが呼び出されたあとに呼び出される ADJUST ブロックでコンストラクタに渡された引数を受け取れるようにする</li> <li>アクセサを自動生成する attribute の実装</li> <li>メタプログラミングAPIの実装</li> <li>コアモジュールをclass構文に対応させる</li> </ul> <p>ということが決まっているようなので、次以降のバージョンに期待ですね。</p> <h2 id="モジュールの最後で-1-を書かなくて良くなった">モジュールの最後で 1; を書かなくて良くなった</h2> <p>モジュールの最後で真値を返さなくても良くなる機能 <code>module_true</code> が追加されました。<br/> これを使うことでモジュールファイルの最後で <code>1;</code> を書く必要がなくなります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synComment"># Hoge.pm</span> <span class="synStatement">package</span><span class="synType"> Hoge</span>; <span class="synStatement">use feature</span> <span class="synConstant">'module_true'</span>; <span class="synStatement">sub </span><span class="synIdentifier">do_something </span>{} <span class="synComment"># main.pl</span> <span class="synStatement">use </span>Hoge; <span class="synComment"># 1; を書いていなくてもロードに成功する</span> </pre> <p><code>module_true</code> は <code>use v5.38</code> することでも有効になるので、積極的に <code>use v5.38</code> していきましょう!</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synComment"># Hoge.pm</span> <span class="synStatement">package</span><span class="synType"> Hoge</span>; <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">sub </span><span class="synIdentifier">do_something </span>{} <span class="synComment"># main.pl</span> <span class="synStatement">use </span>Hoge; </pre> <p>設定ファイルなどで稀に任意の値をファイルの最後で返したいこともあると思いますが、そのような場合は <code>use v5.38</code> しないか <code>no feature 'module_true'</code> などとすると従来と同じように利用できます。</p> <h2 id="構文エラー発生後パースを続けないようになった">構文エラー発生後パースを続けないようになった</h2> <p>perl5.36以前では一度構文エラーが発生した後もパースを続けていましたが、perl5.38からは変数名や定数名を間違えたときのエラーを除いて一度構文エラーするとそこでパースが止まるようになりました。</p> <p>例えば次のような構文エラーになるコードをperl5.36で実行すると</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.36</span>; <span class="synStatement">my</span> <span class="synIdentifier">$hash</span> = +{ <span class="synConstant">a</span> =&gt; <span class="synConstant">10</span> ; <span class="synStatement">my</span> <span class="synIdentifier">$str</span> = <span class="synConstant">'aaaaa'</span>; <span class="synStatement">my</span> <span class="synIdentifier">$str2</span> <span class="synConstant">'bbbbb'</span>; </pre> <p>次のようなエラーが発生します。</p> <pre class="code" data-lang="" data-unlink> String found where operator expected at compile_error.pl line 7, near &#34;$str2 &#39;bbbbb&#39;&#34; (Missing operator before &#39;bbbbb&#39;?) syntax error at compile_error.pl line 5, near &#34;my &#34; Global symbol &#34;$str&#34; requires explicit package name (did you forget to declare &#34;my $str&#34;?) at compile_error.pl line 5. syntax error at compile_error.pl line 7, near &#34;$str2 &#39;bbbbb&#39;&#34; Missing right curly or square bracket at compile_error.pl line 7, at end of line Execution of compile_error.pl aborted due to compilation errors.</pre> <p>エラーが5行目と7行目で発生していて、無理にパースを続けているせいで <code>Global symbol "$str" requires explicit package name</code> など的外れなエラーメッセージもでてしまっていてわかりにくいです。</p> <p>このコードをperl5.38で実行すると次のように最初の構文エラーしか発生しないようになっていて、構文エラー発生後パースを続けないようになったことがわかります。</p> <pre class="code" data-lang="" data-unlink> syntax error at compile_error.pl line 5, near &#34;my &#34; Execution of compile_error.pl aborted due to compilation errors.</pre> <p>変数名や定数名などを間違えたときのエラーは変わらず複数あっても出続けるので、そのような修正は今までと同じようにできます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">my</span> <span class="synIdentifier">$str</span> = <span class="synConstant">'Hello'</span>; <span class="synStatement">say</span> <span class="synIdentifier">$srt</span>; <span class="synStatement">my</span> <span class="synIdentifier">$str2</span> = <span class="synConstant">'World'</span>; <span class="synStatement">say</span> <span class="synIdentifier">$srt2</span>; </pre> <pre class="code" data-lang="" data-unlink> Global symbol &#34;$srt&#34; requires explicit package name (did you forget to declare &#34;my $srt&#34;?) at compile_error_2.pl line 4. Global symbol &#34;$srt2&#34; requires explicit package name (did you forget to declare &#34;my $srt2&#34;?) at compile_error_2.pl line 7. Execution of compile_error_2.pl aborted due to compilation errors.</pre> <p>他の言語も基本的に複数箇所でコンパイルエラーが起きるコードを実行しても最初のエラーで止まるので、今までのperlが特殊だった感じがありますし、<br/> 途中でコンパイルエラーになるコードをパースし続けても変なパースをしてわかりにくいエラーメッセージが出たりセグフォになる可能性もあるので、いい変更だと思います。</p> <h2 id="廃止予定になった機能">廃止予定になった機能</h2> <p>次の機能が廃止予定になり、利用していると警告が発生するようになりました。</p> <h3 id="パッケージセパレータ-">パッケージセパレータ <code>'</code></h3> <p>パッケージの区切り文字は通常 <code>::</code> を使いますが、Perl4の頃は <code>'</code> を区切り文字に利用していてその流れでPerl5でもパッケージセパレータとして利用することができていました。<br/> これがPerl5.42で廃止されます。<br/> jcode.pl を利用しているCGIスクリプトなど、Perl4時代から運用しているコードがあれば将来かなり影響を受けることになりそうです。</p> <h3 id="switch構文スマートマッチング演算子">switch構文、スマートマッチング演算子</h3> <p>Perl5.10で追加され、Perl5.18で実験的機能となったswitch構文とスマートマッチング演算子が失敗した機能とされ、Perl5.42で廃止されることが決まりました。</p> <p>swtich構文は以下のような given, when などからなるswitch文のような構文です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use feature</span> <span class="synConstant">'switch'</span>; <span class="synStatement">given</span> (<span class="synIdentifier">$num</span>) { <span class="synStatement">when</span> (<span class="synIdentifier">$num</span> &lt; <span class="synConstant">50</span>) { <span class="synStatement">say</span> <span class="synConstant">'yes'</span> } <span class="synStatement">default</span> { <span class="synStatement">say</span> <span class="synConstant">'no'</span> } } </pre> <p>スマートマッチング演算子は以下のように項のデータ型に基づいて比較を行う演算子で、例えば配列やハッシュが一致するかを調べたりすることができました。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">my</span> <span class="synIdentifier">@ary</span> = (<span class="synConstant">0</span> .. <span class="synConstant">10</span>); <span class="synStatement">my</span> <span class="synIdentifier">@ary2</span> = (<span class="synConstant">0</span> .. <span class="synConstant">10</span>); <span class="synStatement">if</span> (<span class="synIdentifier">@ary</span> ~~ <span class="synIdentifier">@ary2</span>) { <span class="synStatement">say</span> <span class="synConstant">'Same array'</span>; } </pre> <h2 id="廃止予定機能使用時の警告にサブカテゴリが追加">廃止予定機能使用時の警告にサブカテゴリが追加</h2> <p>廃止予定の機能を使っている警告は <code>no warnings 'deprecated';</code> で抑制することができます。<br/> しかしこれだと廃止予定の機能を使用したの際の警告をすべて抑制してしまうので、<code>no warnings 'deprecated::${機能名}';</code> というように機能ごとに警告を抑制することができるようになりました。</p> <p>例えばこのようなコードだとスマートマッチング演算子以外の廃止予定機能を使用したときも警告がでなくなります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">no warnings</span> <span class="synConstant">'deprecated'</span>; <span class="synStatement">use </span><span class="synConstant">v5.10</span>; <span class="synStatement">if</span> ( [<span class="synConstant">1</span> .. <span class="synConstant">3</span>] ~~ [<span class="synConstant">1</span> .. <span class="synConstant">3</span>] ) { <span class="synStatement">print</span> <span class="synConstant">&quot;match&quot;</span>; } </pre> <p>スマートマッチング演算子利用のサブカテゴリの警告のみを抑制よるすることでスマートマッチング演算子だけ使用したときの警告がでなくなります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink><span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">no warnings</span> <span class="synConstant">'deprecated::smartmatch'</span>; <span class="synComment"># Downgrading a use VERSION declaration to below v5.11 is deprecated, and will become fatal in Perl 5.40</span> <span class="synStatement">use </span><span class="synConstant">v5.10</span>; <span class="synStatement">if</span> ( [<span class="synConstant">1</span> .. <span class="synConstant">3</span>] ~~ [<span class="synConstant">1</span> .. <span class="synConstant">3</span>] ) { <span class="synStatement">print</span> <span class="synConstant">&quot;match&quot;</span>; } </pre> <h1 id="細かな改善やニッチな変更点">細かな改善やニッチな変更点</h1> <h2 id="try-catch-構文defer構文の改善">try-catch 構文、defer構文の改善</h2> <ul> <li>finally, deferブロック内で goto 文が使えませんでしたが、finally, defer ブロック内にジャンプする場合は使えるようになりました</li> <li>finally, defer から離れる制御フロー(return, gotoなど)はコンパイル時にエラーになるようになりました</li> </ul> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">use </span>experimental <span class="synConstant">'try'</span>; try { <span class="synStatement">die</span> <span class="synStatement">if</span> <span class="synStatement">rand</span>(<span class="synConstant">1</span>) &gt; <span class="synConstant">0.5</span>; } catch (<span class="synIdentifier">$e</span>) { <span class="synStatement">say</span> <span class="synConstant">&quot;caught error: </span><span class="synIdentifier">$e</span><span class="synConstant">&quot;</span>; } finally { <span class="synStatement">say</span> <span class="synConstant">&quot;finally&quot;</span>; <span class="synStatement">return</span>; <span class="synComment"># Error: Can't &quot;return&quot; out of a &quot;finally&quot; block</span> }; </pre> <h2 id="HOOK-API-の導入">%{^HOOK} API の導入</h2> <p>perlのコア関数の中にはオーバーライドすることが難しい関数があるため、そのような関数の前後に呼ばれるコールバック関数を登録することのできるAPIが追加されました。<br/> 現状 require のみ対応しており、今後他のオーバーライドすることが難しい関数にもフック関数を登録できるようにする予定らしいです。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synIdentifier">${</span>^HOOK<span class="synIdentifier">}{</span><span class="synConstant">require__before</span><span class="synIdentifier">}</span> = <span class="synStatement">sub </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$filename</span>) = <span class="synStatement">shift</span>; <span class="synStatement">warn</span> <span class="synConstant">&quot;require before: </span><span class="synIdentifier">$filename</span><span class="synConstant">&quot;</span>; }; <span class="synIdentifier">${</span>^HOOK<span class="synIdentifier">}{</span><span class="synConstant">require__after</span><span class="synIdentifier">}</span> = <span class="synStatement">sub </span>{ <span class="synStatement">my</span> (<span class="synIdentifier">$filename</span>) = <span class="synStatement">shift</span>; <span class="synStatement">warn</span> <span class="synConstant">&quot;require after: </span><span class="synIdentifier">$filename</span><span class="synConstant">&quot;</span>; }; <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">require</span> List::Util; <span class="synStatement">say</span> <span class="synIdentifier">List</span>::Util::sum(<span class="synConstant">1</span> .. <span class="synConstant">10</span>); </pre> <pre class="code" data-lang="" data-unlink>require before: List/Util.pm at override_require_hook.pl line 3. require before: strict.pm at override_require_hook.pl line 3. ... require after: strict.pm at override_require_hook.pl line 8. require after: List/Util.pm at override_require_hook.pl line 8. 55</pre> <p>コア関数をオーバーライドしたい場合は CORE::GLOBAL をコンパイル時にオーバーライドするのですが、それがAPIを利用してできるようになった感じだと思います。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synPreProc">BEGIN </span>{ *CORE::GLOBAL::<span class="synStatement">require</span> = <span class="synStatement">sub </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$file</span> = <span class="synStatement">shift</span>; <span class="synStatement">warn</span> <span class="synConstant">&quot;require args: </span><span class="synIdentifier">$file</span><span class="synConstant">&quot;</span>; CORE::<span class="synStatement">require</span>(<span class="synIdentifier">$file</span>); }; } <span class="synStatement">use </span>List::Util <span class="synConstant">qw( sum )</span>; <span class="synStatement">say</span> sum <span class="synConstant">1</span> .. <span class="synConstant">10</span>; </pre> <p>perl5380deltaでは require をオーバーライドするとスタックの深さが変わってモジュールの関数をエクスポートする処理で意図していないpackageにインポートしてしまう問題があると書いてあり、<br/> 実際に再現しようと上記のようなコードを用意してみましたが関数をエクスポートする処理(List::Util::sum)はちゃんと成功して再現できませんでした。<br/> 詳しい方がいらっしゃればぜひ教えていただきたいです。</p> <h2 id="INCフックの改良">@INCフックの改良</h2> <p>Perl で use, require などでモジュールをロードするとき、配列 <code>@INC</code> に格納されているディレクトリのリストの中にモジュール名に対応するファイルパスがないか検索するといった処理が実行されます。<br/> @INC にはコードリファレンスやオブジェクトなどを入れることでモジュールのロード処理にフック処理を入れることができ、このフック処理を@INCフックと呼びます。</p> <p>@INCフックにはフックが自身が <code>@INC</code> を修正するとセグフォや例外が発生する不具合があり、それが発生しないように修正されました。<br/> また、次の機能が追加されました。</p> <h3 id="INCDIRメソッドの追加">INCDIRメソッドの追加</h3> <p>新しいフックメソッド <code>INCDIR</code> が追加されました。<br/> <code>INCDIR</code> メソッドが実装されたクラスのオブジェクトを <code>@INC</code> に追加してそのフックが実行されると、<code>INCDIR</code> メソッドが実行され返り値のリストが <code>@INC</code> に追加されます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">package</span><span class="synType"> INCHooker</span> { <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">sub </span><span class="synIdentifier">new</span><span class="synType">($</span><span class="synError">class, %args</span><span class="synType">) </span>{ <span class="synStatement">return</span> <span class="synStatement">bless</span> +{ <span class="synIdentifier">%args</span> }, <span class="synIdentifier">$class</span>; } <span class="synStatement">sub </span><span class="synIdentifier">INCDIR </span>{ <span class="synStatement">return</span> (<span class="synConstant">'/usr/local/lib/perl5'</span>, <span class="synConstant">'tmp'</span>); } } <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">my</span> <span class="synIdentifier">$hooker</span> = INCHooker-&gt;new; <span class="synStatement">push</span> <span class="synIdentifier">@INC</span>, <span class="synIdentifier">$hooker</span>; <span class="synComment"># エラーになるが、エラーメッセージを見ると</span> <span class="synComment"># Can't locate Ghost.pm in @INC (... INCHooker=HASH(0x55ecde25c5e8) /usr/local/lib/perl5 tmp)</span> <span class="synComment"># というように @INC に INCDIR の返り値が追加されたことがわかる</span> <span class="synStatement">require</span> Ghost </pre> <h3 id="INC-によるINCのイテレーション制御">$INC による@INCのイテレーション制御</h3> <p>@INCフックの中ではインデックスが <code>$INC</code> に格納されるようになり、 <code>$INC</code> を書き換えると次にチェックされる@INCの要素は <code>$INC</code> の値の次の要素(undefの場合は0)になります。</p> <p>例えば次のようなフックがある場合、requireで存在しないモジュールをロードしようとしたとき、@INCの要素を走査していって最後にフックが実行されるがフックの中で <code>$INC</code> がリセットされ、また先頭から@INCを走査する・・・といった処理を無限に繰り返すようになります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">push</span> <span class="synIdentifier">@INC</span>, <span class="synStatement">sub </span>{ <span class="synStatement">warn</span> <span class="synIdentifier">$INC</span>; <span class="synStatement">undef</span> <span class="synIdentifier">$INC</span>; }; <span class="synStatement">require</span> Ghost; </pre> <p>@INCフックは非常にトリッキーな機能なのでプロダクトの開発で利用することはないと思いますが、Carmel などのライブラリで利用されています。<br/> モジュールマネージャーなど特殊なモジュールロードをするようなコードを書いている人にとってはより効率的にモジュールをロードできるようになったりと、これらの機能強化は嬉しい変更になるのかもしれません。</p> <h2 id="サブルーチンシグネチャのデフォルト式の定義性論理和と論理和">サブルーチンシグネチャのデフォルト式の定義性論理和と論理和</h2> <p>サブルーチンシグネチャのデフォルト引数を <code>//=</code>, <code>||=</code> でも指定することが可能になりました。<br/> 前者は引数が未定義値なら、後者は偽値ならデフォルト値が使われます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">sub </span><span class="synIdentifier">func</span><span class="synType">($</span><span class="synError">str //= 'World'</span><span class="synType">) </span>{ <span class="synStatement">say</span> <span class="synConstant">&quot;Hello, </span><span class="synIdentifier">$str</span><span class="synConstant">&quot;</span>; } func(); <span class="synComment"># Hello, World</span> func(<span class="synStatement">undef</span>); <span class="synComment"># Hello, World</span> func(<span class="synConstant">'Anonymous'</span>); <span class="synComment"># Hello, Anonymous</span> </pre> <h2 id="正規表現のパターン中のコードの埋め込みで楽観的評価が可能に">正規表現のパターン中のコードの埋め込みで楽観的評価が可能に</h2> <p>正規表現のパターンの中では <code>(?{ code })</code> や <code>(??{ code })</code> といった拡張構文でコードを埋め込むことが可能ですが、これらを使うとそのパターン全体での様々な最適化が無効になってしまいます。</p> <p>そこで正規表現エンジンの最適化が無効化されない新しい拡張構文 <code>(*{ ... })</code> が追加されました。<br/> 拡張構文中のコードが呼び出される回数が増えたり減ったりするかもしれないので挙動が不安定になる恐れがありますが、実行される回数は正規表現エンジンが動作する回数と一致します。<br/> 例えば通常の使用では O(N) のパターンが、 <code>(?{ ... })</code> パターンを含むと O(N*N) になるところを <code>(*{ ... })</code> に切り替えるとパターンは O(N) のままになるケースがあります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">my</span> <span class="synIdentifier">$count1</span> = <span class="synConstant">0</span>; (<span class="synConstant">'a'</span> x <span class="synConstant">10</span>) =~ <span class="synStatement">/</span><span class="synSpecial">(.*)(?</span><span class="synConstant">{ </span><span class="synIdentifier">$count1</span><span class="synSpecial">++</span><span class="synConstant"> }</span><span class="synSpecial">)[bc]</span><span class="synStatement">/</span>; <span class="synStatement">say</span> <span class="synIdentifier">$count1</span>; <span class="synComment"># 66</span> <span class="synStatement">my</span> <span class="synIdentifier">$count2</span> = <span class="synConstant">0</span>; (<span class="synConstant">'a'</span> x <span class="synConstant">10</span>) =~ <span class="synStatement">/</span><span class="synSpecial">(.*)(*</span><span class="synConstant">{ </span><span class="synIdentifier">$count2</span><span class="synSpecial">++</span><span class="synConstant"> }</span><span class="synSpecial">)[bc]</span><span class="synStatement">/</span>; <span class="synStatement">say</span> <span class="synIdentifier">$count2</span>; <span class="synComment"># 11</span> </pre> <p>コードの評価結果が正規表現として扱われる <code>(??{ code })</code> に対応する <code>(**{ code })</code> はまだ実装されていないです。</p> <h2 id="新しい正規表現変数-LAST_SUCCESSFUL_PATTERN-の追加">新しい正規表現変数 ${^LAST_SUCCESSFUL_PATTERN} の追加</h2> <p>現在のスコープで最後にマッチングに成功したパターンを利用したい場合は空パターンにすることで参照できていましたが、変数 <code>${^LAST_SUCCESSFUL_PATTERN}</code> でも参照できるようになりました。</p> <p>例えば最後にマッチングに成功したパターンにマッチする箇所を置換したい場合は置換対象のパターンを空にしていたところ、</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">my</span> <span class="synIdentifier">$str</span> = <span class="synConstant">'foofoo'</span>; <span class="synStatement">if</span> (<span class="synIdentifier">$str</span> =~ <span class="synStatement">/</span><span class="synConstant">foo</span><span class="synStatement">/</span> || <span class="synIdentifier">$str</span> =~ <span class="synStatement">/</span><span class="synConstant">bar</span><span class="synStatement">/</span>) { <span class="synIdentifier">$str</span> =~ <span class="synStatement">s//</span><span class="synConstant">hoge</span><span class="synStatement">/</span>; } <span class="synStatement">say</span> <span class="synIdentifier">$str</span>; <span class="synComment"># hogefoo</span> </pre> <p><code>${^LAST_SUCCESSFUL_PATTERN}</code> を使うと次のように書き直すことができるようになり、コードが読みやすくなります。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">my</span> <span class="synIdentifier">$str</span> = <span class="synConstant">'foofoo'</span>; <span class="synStatement">if</span> (<span class="synIdentifier">$str</span> =~ <span class="synStatement">/</span><span class="synConstant">foo</span><span class="synStatement">/</span> || <span class="synIdentifier">$str</span> =~ <span class="synStatement">/</span><span class="synConstant">bar</span><span class="synStatement">/</span>) { <span class="synIdentifier">$str</span> =~ <span class="synStatement">s/</span><span class="synIdentifier">${</span>^LAST_SUCCESSFUL_PATTERN<span class="synIdentifier">}</span><span class="synStatement">/</span><span class="synConstant">hoge</span><span class="synStatement">/</span>; } <span class="synStatement">say</span> <span class="synIdentifier">$str</span>; <span class="synComment"># hogefoo</span> </pre> <h2 id="組み込み関数builtinの追加">組み込み関数(builtin)の追加</h2> <h3 id="export_lexically-関数の追加">export_lexically 関数の追加</h3> <p>現在コンパイル中のスコープにシンボルをエクスポートする関数です。<br/> これによって builtin のレキシカルインポートの機能が builtin 以外のモジュールでも利用できるようになりました。</p> <p>使い方は奇数番目の引数にシンボルの名前を指定して偶数番目の引数に対応するエクスポートしたい値のリファレンスを指定します。<br/> 値が変数の場合名前にシジルが必須で、変数の型と一致しなければなりません。値がサブルーチンの場合はなくてもよいです。</p> <p>この関数はコンパイル時に呼び出されなければなりません。<br/> 通常この関数はモジュールのimportメソッド内で使われ、use文によって呼び出されます。</p> <p>次のコードは関数 <code>do_something</code> と 変数 <code>$hoge</code> をレキシカルスコープにエクスポートするモジュールのコードと、それを use したコードです。</p> <p>Hoge.pm</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">package</span><span class="synType"> Hoge</span>; <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">use </span>builtin <span class="synConstant">qw( export_lexically )</span>; <span class="synStatement">no warnings</span> <span class="synConstant">'experimental::builtin'</span>; <span class="synStatement">sub </span><span class="synIdentifier">import </span>{ <span class="synStatement">my</span> <span class="synIdentifier">$class</span> = <span class="synStatement">shift</span>; export_lexically <span class="synConstant">do_something</span> =&gt; \<span class="synIdentifier">&amp;do_something</span>, <span class="synConstant">'$hoge'</span> =&gt; \<span class="synConstant">'hogehoge'</span>; } <span class="synStatement">sub </span><span class="synIdentifier">do_something </span>{ <span class="synStatement">say</span> <span class="synConstant">'Who am I?'</span>; } </pre> <p>main.pl</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; { <span class="synStatement">use </span>Hoge; do_something(); <span class="synComment"># Who am I?</span> <span class="synStatement">say</span> <span class="synIdentifier">$hoge</span>; <span class="synComment"># hogehoge</span> } do_something(); <span class="synComment"># Undefined subroutine &amp;main::do_something</span> </pre> <p>2行目からのスコープ内のみに <code>Hoge::do_something</code> をエクスポートしていて、スコープから外れると未定義になることがわかります。</p> <h3 id="is_tainted-関数の追加">is_tainted 関数の追加</h3> <p>perlには汚染モードと呼ばれる外部から入力されたデータを汚染されたデータとしてチェックするモードがありますが(詳細は<a href="https://perldoc.jp/docs/perl/5.38.0/perlrun.pod">perlrun</a>, <a href="https://perldoc.jp/docs/perl/5.38.0/perlsec.pod">perlsec</a>を参照)、それが有効になっているときに値が外部から入力されたデータかどうかをチェックする関数です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> <span class="synStatement">use </span><span class="synConstant">v5.38</span>; <span class="synStatement">use </span>builtin <span class="synConstant">qw( is_tainted )</span>; <span class="synStatement">no warnings</span> <span class="synConstant">'experimental::builtin'</span>; <span class="synStatement">my</span> <span class="synIdentifier">$line</span> = <span class="synIdentifier">&lt;STDIN&gt;</span>; <span class="synStatement">if</span> (<span class="synIdentifier">${</span>^TAINT<span class="synIdentifier">}</span>) { <span class="synStatement">say</span> is_tainted(<span class="synIdentifier">$line</span>); <span class="synComment"># 1</span> } </pre> <p>Scalar::Util の tainted を builtin に持ってきた形になります。</p> <h1 id="まとめ">まとめ</h1> <p>一昨年までのPerl開発チームでの体制変更がいい影響を及ぼし続けているのか、今年もかなり大きな変更がありました。<br/> 特にクラス構文の追加やモジュール末尾の <code>1;</code> が不要になったのは大きく、初心者が躓きやすいポイントや他言語出身の方が感じるとっつきづらさがどんどん減っていってると思います。<br/> 今後もPerlの進化が楽しみですね。</p> <p>昨年度から<a href="http://perldoc.jp/">perldoc.jp</a>でperldocの翻訳がかなり進んでおり、この記事では書けなかったこともあるので詳しいことが気になった方は<a href="https://perldoc.jp/docs/perl/5.38.0/perl5380delta.pod">ドキュメント</a>の方もぜひ読んで見てください。</p> mp0liiu NestJS Way より TS Way を意識したバックエンド設計事例と Tips hatenablog://entry/820878482951537751 2023-07-21T16:30:00+09:00 2023-07-21T16:30:13+09:00 こんにちは!BC チームでエンジニアをしている id:d-kimuson です。 最近、弊チームで構築した社内向け Web API のバックエンド設計をしたので事例として紹介しようと思います。 フレームワークとして NestJS を採用していますが、NestJS Way よりも TS Way を意識した設計をしており、このエントリの主題でもあるため、TS Backend の設計事例として読んでいただければと思います。 対象システムの概要 社内の他サービス向けの Web API で、他チームのサービスを経由してエンドユーザーに届く中間システム チーム内のサービスからもチーム外のサービスからも叩か… <p>こんにちは!BC チームでエンジニアをしている <a href="http://blog.hatena.ne.jp/d-kimuson/">id:d-kimuson</a> です。</p> <p>最近、弊チームで構築した社内向け Web API のバックエンド設計をしたので事例として紹介しようと思います。</p> <p>フレームワークとして NestJS を採用していますが、NestJS Way よりも TS Way を意識した設計をしており、このエントリの主題でもあるため、TS Backend の設計事例として読んでいただければと思います。</p> <h2 id="対象システムの概要">対象システムの概要</h2> <ul> <li>社内の他サービス向けの Web API で、他チームのサービスを経由してエンドユーザーに届く中間システム <ul> <li>チーム内のサービスからもチーム外のサービスからも叩かれる想定</li> </ul> </li> <li>チーム外からも叩かれるため、なんらかのスキーマを共有したいというモチベーションがある <ul> <li>→ 2023 年現在で標準的な OpenAPI Specification (以後 OAS と呼びます) を共有したい</li> </ul> </li> <li>その他の採用している技術や環境 <ul> <li>Node.js v18</li> <li>ORM として Prisma</li> </ul> </li> </ul> <h2 id="なぜ-NestJS-か">なぜ NestJS か?</h2> <p>システムの概要で触れたように、本システムでは OpenAPI のスキーマを吐き出したいというモチベーションがありました。</p> <p>TypeScript における Server/Client 間でのスキーマの共通化としては <a href="https://trpc.io/">tRPC</a> や <a href="https://github.com/frouriojs/frourio">frourio</a> 等も選択肢としてあります。</p> <p>しかし、今回のシステムでは、クライアント側は TypeScript とは限らないため、言語に依存しない標準的なスキーマとして書き出す必要があり、OAS を採用しました。</p> <p>OAS スキーマを用意するときは、実装とスキーマが一致することを保証するため</p> <ul> <li>コードファースト: 実装を書くと実装に沿ったスキーマが生成される</li> <li>スキーマファースト: (OpenAPI の)スキーマを書くと実装のボイラープレート(あるいはそのまま使える実装)が生成できる。また実装に対する制約の型定義等が生成され、スキーマを満たす実装しかかけない状態になる</li> </ul> <p>の 2 つのいずれかの選択肢を取るのが望ましいと考えています。</p> <p>今回は先にスキーマを用意したかったのでスキーマファーストのアプローチの方が望ましかったのですが、TS Backend においてスキーマファーストを適切に行うスタンダードな方法があまりなく、コードファーストなアプローチを採用しつつ、インタフェースだけ先にコーディングする形で「先にスキーマを用意したい」という要件を満たすことにしました。</p> <p>コードファーストのやりやすさとして、<a href="https://nestjs.com/">NestJS</a> はコントローラーの引数・戻り値の型がそのまま OAS が書き出され、体験がとても良いことに加えて</p> <ul> <li>チームでの採用事例があったこと</li> <li>(Express 等の薄いフレームワークと比べ) NestJS では包括的に Web 開発における機能をフレームワークとして提供してくれるため、Web 開発全般で必要な標準的な機能を再実装することなくドメインの API 開発に集中できること</li> </ul> <p>等の利点もあることから、フレームワークとして NestJS を採用しました。</p> <h2 id="基本的な方針">基本的な方針</h2> <h3 id="公式ドキュメントエコシステムの型チェックオプションとは距離を置く">公式ドキュメント・エコシステムの型チェックオプションとは距離を置く</h3> <p>NestJS 公式のジェネレータでボイラープレートを作成すると、TypeScript のデフォルトでは strict オプションによって off にされている</p> <ul> <li>any を許容するオプション</li> <li>null 安全性をなくするオプション</li> </ul> <p>が有効にされた状態の <code>tsconfig.json</code> が作成されます。</p> <p>公式ドキュメントでも</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> CreateCatDto <span class="synIdentifier">{</span> name: <span class="synType">string</span> age: <span class="synType">number</span> breed: <span class="synType">string</span> <span class="synIdentifier">}</span> </pre> <p>のようなコンストラクタでの初期化がされていない DTO(Data Transfer Object) を標準的に紹介しており、strictNullChecks が off であることが前提となっていることがわかります。</p> <p>可能な限りフレームワークの Way や公式ドキュメントの内容に沿っておくことはとても重要ですが、少なくとも NestJS 周辺の型チェック周りの行儀はそれほど良くないので、適切な距離感で付き合っていくことが大事だと思います。</p> <h3 id="関数型プログラミングを軸とする">関数型プログラミングを軸とする</h3> <p>NestJS はオブジェクト志向をベースとした他言語でも適用できるフレームワーク・設計を TS の世界に持ってきたようなフレームワークです。<a href="#f-b2b04f93" name="fn-b2b04f93" title="あくまで筆者の個人的な解釈です。">*1</a></p> <p>標準でコントローラー層やサービス層、組み込みの DI 解決に class を使ったサンプルを提示していたり、実際に DI 機能を提供している点から素直に実装をするとオブジェクト志向的な設計・実装になる引力が働きがちだと思っています。</p> <p>しかし、個人的に柔軟なデータ構造の取り回しがしやすい構造的部分型の型システムを持つ TypeScript においては、データ構造とふるまい(メソッド)をセットで定義するオブジェクト志向的なやり方よりも、型駆動でドメインのデータ構造を宣言し、ふるまいを関数として分離する関数型的なアプローチのほうが相性が良いと考えています。</p> <p>したがって、今回の Web API 開発においては関数型プログラミングのエッセンスを軸に設計を行いました。</p> <p>関数型プログラミングとは言っても、大事にしているのは</p> <ul> <li>いわゆる「Entity」に定義されるメソッドとデータ構造は、型と関数としてしっかり分離しよう<a href="#f-1902ff53" name="fn-1902ff53" title="ここで言うEntityはO/R Mapper的な文脈ではなく、クリーンアーキテクチャにおける「ドメインモデルと紐づくビジネスルールがカプセルバされてるもの」という意味です。">*2</a></li> <li>副作用を分離しよう</li> <li>関数はユニットテストが書きやすい小さい単位で作っていき、それらを組み合わせることでビジネスロジックを構成しよう</li> </ul> <p>という側面に重きを置いています。</p> <p>一方、TypeScript における関数型プログラミングとしては、<a href="https://gcanti.github.io/fp-ts/">fp-ts</a> 等のライブラリが提供する</p> <ul> <li>関数のカリー化 (<code>(arg1, arg2) =&gt; ret</code> を <code>(arg1) =&gt; (arg2) =&gt; ret</code> にする)</li> <li>関数合成・パイプ (<code>f(g(arg))</code> ではなく <code>pipe(arg, g, f)</code> 的な書き方)</li> </ul> <p>等の機能を使うこともできますが、こういった書き方・ライブラリとは距離を置いています。</p> <p>これは</p> <ul> <li>標準の TypeScript の書き心地とはかなりズレてしまうこと</li> <li>追加の学習コストが発生すること</li> <li>標準とのズレによるチームでのメンテナンス性の低下</li> </ul> <p>というデメリットがあるためです。</p> <p>関数型を軸としたバックエンド設計として、書籍 <a href="https://www.amazon.co.jp/dp/B07B44BPFB">Domain Modeling Made Functional</a> を参考にしています。知見が少ない関数型ベースの設計手法が語られていてとても参考になりました。</p> <hr /> <p>基本的なコンセプトは以上の 2 点です。</p> <p>ここからはより具体的なアーキテクチャや方針について紹介していきます。</p> <h2 id="型チェックを厳格にする">型チェックを厳格にする</h2> <p>「公式ドキュメント・エコシステムの型チェックオプションとは距離を置く」でも紹介したように、公式がデフォルトで提供する型チェックはかなり緩い状態になっています。</p> <p>今回は、<a href="https://github.com/tsconfig/bases">tsconfig/bases</a> の strictest オプションをベースにしつつ、NestJS で利用できるようにオプションを一部上書き指定しています。 以下は実際に利用している TS 設定の一部です。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">extends</span>&quot;: <span class="synSpecial">[</span>&quot;<span class="synConstant">@tsconfig/strictest</span>&quot;, &quot;<span class="synConstant">@tsconfig/node18</span>&quot;<span class="synSpecial">]</span>, &quot;<span class="synStatement">compilerOptions</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">module</span>&quot;: &quot;<span class="synConstant">commonjs</span>&quot;, &quot;<span class="synStatement">declaration</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">removeComments</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">emitDecoratorMetadata</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">experimentalDecorators</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">allowSyntheticDefaultImports</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">sourceMap</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">outDir</span>&quot;: &quot;<span class="synConstant">./dist</span>&quot;, &quot;<span class="synStatement">incremental</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">skipLibCheck</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">importsNotUsedAsValues</span>&quot;: &quot;<span class="synError">remove</span>&quot; <span class="synError">// decorator で使う・eslint でやるので</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>特に後から緩い型チェックを硬い型チェックへの移行していくのは大変になりますので、硬すぎるくらいのチェックを適用して運用に併せて緩くしていくくらいのスタンスをオススメします。</p> <p>また、この記事では特に断りがない限り以上の型チェックオプションが適用された TS 5.0 を利用していることを前提とします。</p> <h3 id="strictNullChecks-と-DTO-と-constructor">strictNullChecks と DTO と constructor</h3> <p>NestJS ではリクエストパラメタ/ボディ、レスポンスボディの型定義には DTO を使います。</p> <p>DTO とはデザインパターンの一種で、一般的にビジネスロジック(メソッド)が含まれないデータをいれる箱のことを指します。厳密な定義は置いておいて、NestJS の文脈においては</p> <ul> <li>コントローラーの引数(リクエストパラメタ・ボディ)、戻り値(レスポンスボディ)のインタフェースを宣言する class であり</li> <li>引数に関して class-validator のデコレータを書くと勝手にバリデーションをしてくれてれる class であり <a href="#f-9c896992" name="fn-9c896992" title="Pipe の設定は必要です。">*3</a></li> <li><code>@nestjs/swagger</code> によって、引数・戻り値をプロパティの型定義から OAS に書き出してくれる class</li> </ul> <p>といった意味合いを持っていて、インタフェース宣言に加えてバリデーションやOAS書き出しの責務を持つ特殊な class だと思ってもらえれば良いです。</p> <p>strictNullChecks オプションが有効な状態では公式ドキュメントに提示されている DTO のサンプル</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> CreateCatDto <span class="synIdentifier">{</span> name: <span class="synType">string</span> age: <span class="synType">number</span> breed: <span class="synType">string</span> <span class="synIdentifier">}</span> </pre> <p>を使うと、constructor でプロパティを初期化していないため型エラーが発生します。</p> <p>constructor をちゃんと書いてあげるのが理想的ですが、<a href="https://github.com/typestack/class-validator">class-validator</a> のデコレータと <a href="https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties">Parameter Properties</a> の併用ができないので</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> CreateCatDto <span class="synIdentifier">{</span> <span class="synSpecial">@IsString</span><span class="synStatement">()</span> name: <span class="synType">string</span> <span class="synSpecial">@IsNumber</span><span class="synStatement">()</span> age: <span class="synType">number</span> <span class="synSpecial">@IsString</span><span class="synStatement">()</span> breed: <span class="synType">string</span> <span class="synStatement">public</span> <span class="synStatement">constructor(</span> name: <span class="synType">string</span><span class="synStatement">,</span> age: <span class="synType">number</span><span class="synStatement">,</span> breed: <span class="synType">string</span> <span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>.name <span class="synStatement">=</span> name <span class="synIdentifier">this</span>.age <span class="synStatement">=</span> age <span class="synIdentifier">this</span>.breed <span class="synStatement">=</span> breed <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>のような非常に冗長な記述になってしまいます。</p> <p>弊チームでは <code>Non-Null Assertion Operator</code> を使って</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">class</span> CreateCatDto <span class="synIdentifier">{</span> name<span class="synConstant">!</span>: <span class="synType">string</span> age<span class="synConstant">!</span>: <span class="synType">number</span> breed<span class="synConstant">!</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> </pre> <p>のようにして型エラーを回避することにしています。</p> <p>また、チームでは Dto の使い方に関して、2 つの規約を敷いています。</p> <ul> <li>① Dto は abstract class で宣言すること</li> <li>② Dto を controller, dto 以外のファイルから参照しないこと</li> </ul> <p>① を敷いているのは <code>コントローラーの戻り値は Dto のクラスインスタンスではなくプレーンオブジェクトを返すように統一する</code> ことが目的です。</p> <p>TypeScript では、Dto 型のクラスの戻り値が指定されているときに、実際にはクラスインスタンスではなくプレーンオブジェクトを返しても型エラーにならないという挙動になります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> plainToClass <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;class-transformer&quot;</span> <span class="synStatement">class</span> SomeController <span class="synIdentifier">{</span> <span class="synComment">// class が private なフィールドを持たないときに継承関係ではなくプロパティの構造で型チェックされる仕様を利用してプレーンオブジェクトを返すパターン</span> createCatV1<span class="synStatement">()</span>: CreateCatDto <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> name: <span class="synConstant">&quot;nyash&quot;</span><span class="synStatement">,</span> age: <span class="synConstant">3</span><span class="synStatement">,</span> breed: <span class="synConstant">&quot;A&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synComment">// CreateCatDto のインスタンスを作成してインスタンスか返すが、プロパティは Object.assign で割り当てるパターン</span> createCatV2<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synSpecial">Object</span>.assign<span class="synStatement">(new</span> CreateCatDto<span class="synStatement">(),</span> <span class="synIdentifier">{</span> name: <span class="synConstant">&quot;nyash&quot;</span><span class="synStatement">,</span> age: <span class="synConstant">3</span><span class="synStatement">,</span> breed: <span class="synConstant">&quot;A&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synComment">// CreateCatDto のインスタンスを作成してインスタンスか返すが、プロパティは plainToClass で割り当てるパターン</span> createCatV3<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> plainToClass<span class="synStatement">(new</span> CreateCatDto<span class="synStatement">(),</span> <span class="synIdentifier">{</span> name: <span class="synConstant">&quot;nyash&quot;</span><span class="synStatement">,</span> age: <span class="synConstant">3</span><span class="synStatement">,</span> breed: <span class="synConstant">&quot;A&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>上記の <code>createCatV1</code> ではプレーンオブジェクトを返し、<code>createCatV2</code>, <code>createCatV3</code> ではクラスインスタンスを返していますがどちらも型チェックを通る正しいコードです。</p> <p>これらがいずれも許容される混在する状態では</p> <ul> <li>テストで <code>toBeInstanceOf</code> で判定できなかったり</li> <li>コーディングする上でインスタンスなのか、プレーンオブジェクトなのかを意識する必要があり、認知負荷が増える</li> </ul> <p>といった辛さが想定されるので、「プレーンオブジェクトを返すこと」に一貫性をもたせるために <code>① Dto は abstract class で宣言すること</code> の規約を敷いています。</p> <p><code>② Dto を controller, dto 以外のファイルから参照しないこと</code> に関しては、Dto はあくまで「OAS を書き出したり class-validator を使えたりするために、インタフェースが欲しいだけだけどやむなく class を使う必要がある」というだけなので、必然性のあるコントローラー層以外からは使うのはやめましょうね、というものです。</p> <p>DTO の class との向き合い方は今回の方針以外にもいくつか考えられる<a href="#f-f8c22434" name="fn-f8c22434" title="任意のprivateなプロパティを持つBaseEntityを継承させることで逆にクラスインスタンスに統一させるという選択肢も有ると思います。このやり方だとObject.assignやplainToClassが型安全性を損なうハッチになってしまうことが想定されるため弊チームでは避けています。しかしconstructorをちゃんと書くようにもすれば冗長性と引き換えに健全な形で運用できると思います。">*4</a>と思いますが、いずれにせよ型チェック上の抜け穴になりやすいのでコーディング規約やガイドライン等でスタンスを明確にしておくことが望ましいと思います。</p> <h2 id="ドメインモデルは-class-ではなく単一ファイル内に宣言する-type-と関数によって宣言される">ドメインモデルは class ではなく単一ファイル内に宣言する type と関数によって宣言される</h2> <p>よく class で表現される Entity は、一般的にデータと関連するメソッド群を持ちます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">class</span> UserEntity <span class="synIdentifier">{</span> <span class="synStatement">public</span> <span class="synStatement">constructor(</span> <span class="synStatement">public</span> id: <span class="synType">number</span><span class="synStatement">,</span> <span class="synStatement">public</span> firstName: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> lastName: <span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">public</span> birthDate: <span class="synSpecial">Date</span><span class="synStatement">,</span> <span class="synStatement">public</span> isAuthenticated: <span class="synType">boolean</span><span class="synStatement">,</span> <span class="synStatement">public</span> createdAt: <span class="synSpecial">Date</span> <span class="synStatement">)</span> <span class="synIdentifier">{}</span> <span class="synStatement">public</span> <span class="synStatement">get</span> fullName<span class="synStatement">()</span>: <span class="synType">string</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">this</span>.firstName + <span class="synConstant">&quot; &quot;</span> + <span class="synIdentifier">this</span>.lastName <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>以上のような Entity は型と関数に分離して以下のように宣言します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">/* Orm からマッピングされた型 */</span> <span class="synStatement">type</span> User <span class="synStatement">=</span> <span class="synIdentifier">{</span> id: <span class="synType">number</span> firstName: <span class="synType">string</span> lastName: <span class="synType">string</span> birthDate: <span class="synSpecial">Date</span> isAuthenticated: <span class="synType">boolean</span> createdAt: <span class="synSpecial">Date</span> <span class="synIdentifier">}</span> <span class="synComment">/**</span> <span class="synComment"> * すべてではありませんが、DB のレコードとドメインモデルが対応関係になることも多いと思います</span> <span class="synComment"> * そういうケースでは以下のような形を取っておくと便利です</span> <span class="synComment"> *</span> <span class="synComment"> * @example UserEntity&lt;{ posts: Post[] }&gt; -- リレーションを持つデータ構造のパターンだったり</span> <span class="synComment"> * @example UserEntity&lt;{ tag: 'Validated' }&gt; -- 同じデータ構造でバリデーション済み等のライフサイクルを表すタグを付加したり</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synStatement">type</span> UserEntity<span class="synStatement">&lt;</span> AdditionalFields <span class="synStatement">extends</span> Record<span class="synStatement">&lt;</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synType">unknown</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">{}</span> <span class="synStatement">&gt;</span> <span class="synStatement">=</span> User &amp; <span class="synIdentifier">{</span> age: <span class="synType">number</span> <span class="synIdentifier">}</span> &amp; Omit<span class="synStatement">&lt;</span>AdditionalFields<span class="synStatement">,</span> <span class="synStatement">keyof</span> User<span class="synStatement">&gt;;</span> <span class="synStatement">export</span> <span class="synType">const</span> buildUserEntity <span class="synStatement">=</span> <span class="synStatement">&lt;</span>T <span class="synStatement">extends</span> User<span class="synStatement">&gt;(</span>data: T<span class="synStatement">)</span>: UserEntity<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> ...data<span class="synStatement">,</span> age: age<span class="synStatement">(</span>data.birthDate<span class="synStatement">),</span> <span class="synIdentifier">}</span> satisfies UserEntity <span class="synStatement">as</span> UserEntity<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synComment">/**</span> <span class="synComment"> * @desc</span> <span class="synComment"> * - メソッドに該当する処理は `Pick&lt;EntityType, 依存するプロパティの一覧&gt;` を第一引数に取る関数</span> <span class="synComment"> * - こうすることで必要なプロパティが明示されるのと、部分型(例: `Omit&lt;UserEntity, 'age'&gt;`)に対しても最低限のプロパティが揃っていれば呼べるようになる</span> <span class="synComment"> */</span> <span class="synType">const</span> fullName <span class="synStatement">=</span> <span class="synStatement">(</span>user: Pick<span class="synStatement">&lt;</span>UserEntity<span class="synStatement">,</span> <span class="synConstant">'firstName'</span> | <span class="synConstant">'lastName'</span><span class="synStatement">&gt;)</span>: <span class="synType">string</span> <span class="synStatement">=&gt;</span> user.firstName + <span class="synConstant">' '</span> + user.lastName </pre> <p>型と関数が分離されているので、ロジックを別ファイルに持っていくこともできますが、ドメインモデルと同じファイル内にかかれているほうが凝集で、読みやすいので同ファイル内に置いています。</p> <p>データ型と関数が分離されていることで、class では冗長な記述が必要だった型を使った制約の表現がしやすくなります。</p> <p>例えば「バリデーション通過済みのデータでのみユーザー作成のロジックを実行できる」を以下のように型で表現・制約をつけることができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> ValidatedUserEntity <span class="synStatement">=</span> UserEntity<span class="synStatement">&lt;</span><span class="synIdentifier">{</span> tag: <span class="synConstant">&quot;VALIDATED&quot;</span> <span class="synIdentifier">}</span><span class="synStatement">&gt;</span> <span class="synComment">// service</span> <span class="synType">const</span> validateUser <span class="synStatement">=</span> <span class="synStatement">(</span>user: UserEntity<span class="synStatement">)</span>: ValidatedUserEntity <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// バリデーションを通過したデータにのみタグ 'VALIDATED' を付加する</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> tag: <span class="synConstant">&quot;VALIDATED&quot;</span><span class="synStatement">,</span> ...user<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synType">const</span> createUser <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>validatedUser: ValidatedUserEntity<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// ...</span> <span class="synIdentifier">}</span> <span class="synStatement">declare</span> <span class="synType">const</span> user: User <span class="synType">const</span> userEntity <span class="synStatement">=</span> buildUserEntity<span class="synStatement">(</span>user<span class="synStatement">)</span> <span class="synComment">// validateUser を通過していないデータでは呼び出せない</span> createUser<span class="synStatement">(</span>userEntity<span class="synStatement">)</span> <span class="synComment">// Argument of type 'UserEntity&lt;User&gt;' is not assignable to parameter of type 'ValidatedUserEntity'.</span> <span class="synType">const</span> validatedUser <span class="synStatement">=</span> validateUser<span class="synStatement">(</span>userEntity<span class="synStatement">)</span> createUser<span class="synStatement">(</span>validatedUser<span class="synStatement">)</span> <span class="synComment">// 型エラーなしで呼び出せる</span> </pre> <p>他にも例えばバリデーションを通過して認証済みに絞られているデータであれば <code>type AuthenticatedUser = UserEntity &amp; { isAuthenticated: true }</code> のように型を作って制約として利用することもできます。</p> <p>このように同じ <code>User</code> ドメインモデルでも一連の手続きのタイミング毎で取りうるデータ構造は代わります。 それぞれのステップを区別した・厳格な型で表現できると、暗黙的にではなく型チェックで呼び出しの制約等を制御することができます。 class ではこの辺の取り回しがし辛いですが、型と関数に分離してあげることで柔軟に・型駆動でビジネスロジックを記述していきやすくなります。</p> <p>メソッドに対応する関数も <code>Pick&lt;UserEntity, 'firstName' | 'lastName'&gt;</code> のように必要なプロパティだけ列挙することで、より制約の強い部分型(例えば<code>AuthenticatedUser</code>)でも最低限のプロパティだけ持っていれば呼び出すことができます。</p> <h2 id="副作用を分離する">副作用を分離する</h2> <p>関数型プログラミングで大事にされるエッセンスの 1 つとして副作用の分離があります。</p> <p>副作用とはすなわち「関数の戻り値を返す以外の効果」のことで、わかりやすいところで HTTP 通信・DB の Read/Write・ロギング等があります。</p> <p>副作用の特徴として</p> <ul> <li>外部状態に依存するため、処理が安定しづらい <ul> <li>例えば正しく実装された関数が依存する DB や HTTP 通信先の状態に影響されて壊れたりする可能性がある</li> </ul> </li> <li>外部の状態に依存するためテスタビリティが低い <ul> <li>事前に依存状態を用意する必要があり、テストケースが関数の入力 → 出力ではなく、事前の DB 状態 → 関数の出力(あるいは事後の DB 状態)になり、テストケースが読みづらい傾向にある</li> <li>上手く DB がセットアップされない・テストケース単位での DB 分離が壊れた等、対象のテストケースとは直接関係ないレイヤーの問題でテストがフレイキーになりやすい</li> </ul> </li> <li>(DB の副作用について) 副作用がボトルネックになり、テストの実行時間が長くなる <ul> <li>DB 接続がボトルネックになる <ul> <li>融和策としてインメモリ DB への差し替え等のアプローチもあるが、根本的な解決にはならず、また本番と異なる環境でテストを回すことになってしまう</li> </ul> </li> <li>依存する DB の状態がテストケースごとに分離されている必要があるため並列でのテスト実行がしづらくなる</li> </ul> </li> </ul> <p>というつらい側面があります。</p> <p>今回のアプリケーションでは全体のアーキテクチャはレイヤー構造を取っていて、レイヤーレベルで副作用を許容する層としない層を分離することで、つらい範囲を狭めています。</p> <p>ただし、副作用の分離と言ってますが純粋関数型の言語でない TypeScript では厳密にすべての副作用を分離することは難しくコストも見合わないので考えておらず、<strong>特に影響の大きい「外部との HTTP 通信」や 「DB 接続」を副作用と考えて</strong>分離しています。</p> <p>特に補足がない限り、この記事内で副作用と読んだときはこれらを指すものとします。<a href="#f-8bd34ed0" name="fn-8bd34ed0" title="一般的にはロギング等も副作用に分類されます。">*5</a></p> <h3 id="レイヤー構造と副作用">レイヤー構造と副作用</h3> <p>アプリケーションで採用しているレイヤー構造は以下の通りです</p> <table> <thead> <tr> <th style="text-align:center;"> 層 </th> <th style="text-align:center;"> 説明 </th> <th style="text-align:center;"> 副作用 </th> </tr> </thead> <tbody> <tr> <td style="text-align:center;"> domain-object </td> <td style="text-align:center;"> 上のセクションで説明したドメインモデル(型とモデルに閉じた関数)が入る層(関心がドメインモデル) </td> <td style="text-align:center;"> なし </td> </tr> <tr> <td style="text-align:center;"> サービス層 </td> <td style="text-align:center;"> 複数のドメインモデルに跨るビジネスロジックを実装する層(関心がシステム) </td> <td style="text-align:center;"> なし </td> </tr> <tr> <td style="text-align:center;"> リポジトリ層 </td> <td style="text-align:center;"> HTTP 通信や DB 依存があるときにデータフェッチを行う層 </td> <td style="text-align:center;"> あり </td> </tr> <tr> <td style="text-align:center;"> ユースケース層 </td> <td style="text-align:center;"> サービス層・リポジトリ層・domain-object のロジックを組み合わせて意味のあるユースケースを実装する層です層 </td> <td style="text-align:center;"> なし </td> </tr> <tr> <td style="text-align:center;"> コントローラー層 </td> <td style="text-align:center;"> HTTP リクエストを受けてレスポンスを組み建てる層です </td> <td style="text-align:center;"> あり </td> </tr> </tbody> </table> <p>依存関係は以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20230721/20230721163007.png" alt="&#x30EC;&#x30A4;&#x30E4;&#x30FC;&#x56F3;" width="800" height="459" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ユースケース層は厳密には副作用を持つ場合が多いですが、後述する Dependency Injection を使うことで、見かけ上副作用がない状態<a href="#f-6422c6be" name="fn-6422c6be" title="副作用は引数からのコールバック関数に含まれていて関数の責務範囲には副作用がない・テストにおいて副作用がないことを指します。">*6</a>にしています。</p> <h3 id="jestconfig-レベルでテストを-2-種類の系統に分離する">jest.config レベルでテストを 2 種類の系統に分離する</h3> <p>副作用を分離したい 1 つの大きな理由として「テスタビリティを高めたい」という点がありました。 そこで、層ごとに副作用を扱うルールが分離されていることから jest.config レベルで副作用を持つ層のテストを副作用を持たない層のテストを分離しています。</p> <p>以下は実際に使っている jest.config の抜粋です。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// jest.config.pure.ts</span> <span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> Config <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;jest&quot;</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> baseConfig <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;./jest.config.base&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> config <span class="synStatement">=</span> <span class="synIdentifier">{</span> ...baseConfig<span class="synStatement">,</span> testMatch: <span class="synIdentifier">[</span> <span class="synConstant">&quot;**/usecases/**/*.spec.ts&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;**/services/**/*.spec.ts&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;**/domain-object/**/*.spec.ts&quot;</span> <span class="synIdentifier">]</span> <span class="synIdentifier">}</span> satisfies Config<span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">default</span> config<span class="synStatement">;</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// jest.config.with-side-effects.ts</span> <span class="synStatement">import</span> <span class="synStatement">type</span> <span class="synIdentifier">{</span> Config <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;jest&quot;</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> baseConfig <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;./jest.config.base&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> config <span class="synStatement">=</span> <span class="synIdentifier">{</span> ...baseConfig<span class="synStatement">,</span> testMatch: <span class="synIdentifier">[</span> <span class="synConstant">&quot;**/*.controller.spec.ts&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;**/repositories/**/*.spec.ts&quot;</span> <span class="synIdentifier">]</span><span class="synStatement">,</span> setupFilesAfterEnv: <span class="synIdentifier">[</span> ...baseConfig.setupFilesAfterEnv<span class="synStatement">,</span> <span class="synComment">// DB 依存側はセットアップや DBリセット等の setup が必要になる</span> <span class="synConstant">&quot;./src/test-utils/setup/separate-db-per-worker.ts&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;./src/test-utils/setup/setup-prisma.ts&quot;</span><span class="synStatement">,</span> <span class="synConstant">&quot;./src/test-utils/setup/reset-table.ts&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">]</span> <span class="synIdentifier">}</span> satisfies Config<span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">default</span> config<span class="synStatement">;</span> </pre> <p>あとは</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">test:pure</span>&quot;: &quot;<span class="synConstant">jest --config ./jest.config.pure.ts</span>&quot;, &quot;<span class="synStatement">test:with-side-effects</span>&quot;: &quot;<span class="synConstant">jest --config ./jest.config.with-side-effects.ts</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>のような npm-scripts を用意しておくことで</p> <pre class="code bash" data-lang="bash" data-unlink>$ pnpm test:pure # DB 非依存のテストのみ実行される $ pnpm test:with-side-effects # DB 依存のテストのみ実行される</pre> <p>のような形で分離して手元でテストを実行できます。</p> <p><code>test:with-side-effects</code> 側は</p> <ul> <li>ファイルごとに jest の worker 単位で並列化されていますが、ファイル単位では個別のテストケースが直列に動くこと <ul> <li>参考: <a href="https://www.mizdra.net/entry/2022/11/24/153459">Prisma で本物の DBMS を使って自動テストを書く - mizdra's blog</a></li> </ul> </li> <li>テストケースのたびに DB のリセット処理が入ること</li> </ul> <p>の 2 点が理由で、仮に DB の副作用がなかったとしてもテストケースごとにオーバーヘッドが発生する構成になっています。</p> <p><code>test:pure</code> が <code>test:with-side-effects</code> から分離されていることで、<code>test:pure</code> 側に不要なオーバーヘッドがかからず、軽量なテストとして手元で実行しやすくなり、TDD 的な開発がしやすくなると考えています。</p> <p><code>test:pure</code> 側ではインフラストラクチャへの関心がないビジネスロジック側に関心が置かれているため、このレイヤーを軽量に・高速にテストできるととても体験が良いです。</p> <p>現時点での計測値としては、後者は 90 テストケースありフルテストで 82 秒程かかりますが、前者は 126 テストケースが 2.5 秒程で実行できます。</p> <h3 id="副作用のサンドイッチによる分離">副作用のサンドイッチによる分離</h3> <p>基本的にサービス層・domain-object はあまり意識せずとも副作用のない関数になっていきやすいですが、ユースケース層は DB に依存することが多いので結構厳しいです。</p> <p>副作用はできるだけ分離したいので</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20230721/20230721163010.png" alt="Read(API&#x547C;&#x3073;&#x51FA;&#x3057; =&amp;gt; DB Read =&amp;gt; Usecase(&#x526F;&#x4F5C;&#x7528;&#x304C;&#x306A;&#x3044;) =&amp;gt; &#x30EC;&#x30B9;&#x30DD;&#x30F3;&#x30B9;),Write(API&#x547C;&#x3073;&#x51FA;&#x3057; =&amp;gt; (DB Read) =&amp;gt; Usecase(&#x526F;&#x4F5C;&#x7528;&#x304C;&#x306A;&#x3044;) =&amp;gt; DB Write =&amp;gt; &#x30EC;&#x30B9;&#x30DD;&#x30F3;&#x30B9;)" width="800" height="381" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>のようにリクエストの前後に副作用を集約できるような形式を取れるのであればこれが理想的です。</p> <p>ただし、複雑なユースケースになってくると、DB から取得した値を元に他の値を DB から値を取得する必要がある場合や、処理の途中で副作用を挟まらずを得ないケースもあります。こういうケースではユースケース層から副作用を排除することは難しいため、Dependency Injection を使っていきます。</p> <h3 id="関数ベースの-Dependency-Injection">関数ベースの Dependency Injection</h3> <p>Dependency Injection と聞くとクラスベースの DI を思い浮かべる人が多いと思いますが、今回は高階関数を使った DI を使っていきます。</p> <p>Injectable な関数を宣言するためのユーテリティ関数を用意します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> ArgType<span class="synStatement">&lt;</span>T <span class="synStatement">extends</span> <span class="synSpecial">Function</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> T <span class="synStatement">extends</span> <span class="synStatement">(</span>...args: infer I<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">any</span> ? I : <span class="synType">never</span><span class="synStatement">;</span> <span class="synStatement">type</span> InjectableFn <span class="synStatement">=</span> <span class="synStatement">(</span>...args: <span class="synType">any</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span>...args: <span class="synType">any</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">any</span><span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synStatement">type</span> IInjectable<span class="synStatement">&lt;</span> Deps <span class="synStatement">extends</span> Record<span class="synStatement">&lt;</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">(</span>...args: <span class="synType">any</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">any</span><span class="synStatement">&gt;,</span> Args <span class="synStatement">extends</span> ReadonlyArray<span class="synStatement">&lt;</span><span class="synType">unknown</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">[]</span><span class="synStatement">,</span> Ret <span class="synStatement">=</span> <span class="synType">void</span> <span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synStatement">(</span>deps: Deps<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span>...args: Args<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> Ret<span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synType">const</span> defineInjectable <span class="synStatement">=</span> <span class="synStatement">&lt;</span> DepFns <span class="synStatement">extends</span> Record<span class="synStatement">&lt;</span><span class="synType">string</span><span class="synStatement">,</span> InjectableFn<span class="synStatement">&gt;,</span> Deps <span class="synStatement">extends</span> Record<span class="synStatement">&lt;</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synStatement">(</span>...args: <span class="synType">any</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">any</span><span class="synStatement">&gt;</span> <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">[</span>K <span class="synStatement">in</span> <span class="synStatement">keyof</span> DepFns<span class="synIdentifier">]</span>: ReturnType<span class="synStatement">&lt;</span>DepFns<span class="synIdentifier">[</span>K<span class="synIdentifier">]</span><span class="synStatement">&gt;;</span> <span class="synIdentifier">}</span> <span class="synStatement">&gt;(</span> _depFns: DepFns <span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">&lt;</span>Fn <span class="synStatement">extends</span> <span class="synStatement">(</span>deps: Deps<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span>...args: <span class="synType">any</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synType">any</span><span class="synStatement">&gt;(</span> fn: Fn <span class="synStatement">)</span>: Fn <span class="synStatement">=&gt;</span> fn satisfies IInjectable<span class="synStatement">&lt;</span>Deps<span class="synStatement">,</span> ArgType<span class="synStatement">&lt;</span>Fn<span class="synStatement">&gt;,</span> ReturnType<span class="synStatement">&lt;</span>Fn<span class="synStatement">&gt;&gt;;</span> </pre> <p>中身の説明は重要ではないので置いておきますが、高階関数を使って <code>(ORM) =&gt; (引数) =&gt; Promise&lt;取得した値&gt;</code> の形式で実装されたリポジトリ層の DI 解決をするユーティリティです。</p> <p>以上のユーテリティを使って、ユースケース層は以下のように書きます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// repository がこんな感じで定義されてるとして</span> <span class="synType">const</span> findUserById <span class="synStatement">=</span> <span class="synStatement">(</span>prisma: PrismaClient<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">async</span> <span class="synStatement">(</span>id: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> prisma.user.findUnique<span class="synStatement">(</span><span class="synIdentifier">{</span> where: <span class="synIdentifier">{</span> id<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// ユースケースはこう定義してあげます</span> <span class="synType">const</span> loginUsecase <span class="synStatement">=</span> defineInjectable<span class="synStatement">(</span><span class="synIdentifier">{</span> findUserById <span class="synComment">/* 依存するリポジトリ層, (prisma) =&gt; (arg) =&gt; Promise&lt;Data&gt; */</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)(</span> <span class="synStatement">(</span>deps <span class="synComment">/* DI が解決された (arg) =&gt; Promise&lt;Data&gt; を受け取る */</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">async</span> <span class="synStatement">(</span>id: <span class="synType">number</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> user <span class="synStatement">=</span> <span class="synStatement">await</span> deps.findUserById<span class="synStatement">(</span>id<span class="synStatement">)</span> <span class="synComment">// ...</span> <span class="synIdentifier">}</span> <span class="synStatement">)</span> </pre> <p>コントローラー層からは、リポジトリ層に prismaClient (ORM Client) を Inject してあげてそのまま呼び出します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> result1 <span class="synStatement">=</span> loginUsecase<span class="synStatement">(</span><span class="synIdentifier">{</span> findUserById: findUserById<span class="synStatement">(</span>prismaClient<span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)(</span><span class="synConstant">1</span><span class="synStatement">)</span> </pre> <p>テストからはリポジトリ層を実 DB 依存の元の関数からテスト用のものに差し替えてあげることで実 DB への依存をはがすことができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// テストでの呼び出し方</span> <span class="synType">const</span> result <span class="synStatement">=</span> loginUsecase<span class="synStatement">(</span><span class="synIdentifier">{</span> findUserById: <span class="synStatement">async</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> id: <span class="synConstant">1</span><span class="synStatement">,</span> firstName: <span class="synConstant">&quot;yamada&quot;</span><span class="synStatement">,</span> lastName: <span class="synConstant">&quot;taro&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)(</span><span class="synConstant">1</span><span class="synStatement">)</span> </pre> <p>高階関数での DI を使うことで副作用がない(取り除かれた)状態を作ることができました。 提示したサンプルコードは <a href="https://www.typescriptlang.org/play?#code/PTAEFcBcEsBtsgTwFBIA4FNQEEBOBzAFUUwB5DQMAPSDAOwBMBnUAMXDoGMYB7OgPlABeUBWq1GLABQA6OQEMCTAFyhodAGYZcoAJIBKYYPl0UoUAH49yc6roYAbtoDcyVCSy66AKwzd5AEawGKx0wqCyCkqqJoiGQoKRMor4KqCx8camrsjUaDy4kKDonl6+-kEYpDagACIYaCzi9MygAEp+BQykTJC46vgANBFyydHpppkTiPyDNXiplDQtLB3yDHywiHi48oikHADWdDwA7gLhANoAunPmHUUiDjzQDMiCIlIMDWn1jVNJFJpBZMKYPHJ5ApFTh8Xqgb4adQYMp+SCBYLCGrVczmP6hJrLSTtTq4bq9fp0IZ6Hyo9EhAR3HF-AkSVodGGknp9AbDQHjDJGaYfUAAbxqONAlwA0mowocMIgeBo6g18ddVA9wLg6MQyHi6Expdd+K4JQBfGr8KTi0AAfW+aHxqn1TBqmSxoSWrOkDt+PwBoyBMUmgtiVptGjoqlCbujYQSEbCTHkMCYiIwLF0KIqwVIzOGC11VVCs2JkC1Oo8pBLJrc304sEUWBKoAACv0mABbeQAYXg9Eeopt4CY2lUYolEsRjAAqnRoABHcAYVRSFLj0CnAAW2hXorUDDs4E7AW0oDN56m7Z4nego9IM9HuH4NotFuQIHhN-k6ncmFAj5niIE44q8R4ntoczvjCBpFNODCAbgABCiC6Aw4RSGgHbdqo7Z3t2fbQAOUzyEwiBcBEYGgHQx6nrg7rmFh+HyDII7aDI8Fzouy5SCB5jbru442uYryMuYZqMma+huJ+gBnDIAPwyAJ0MgCNDEpgD2DIAygyAGIMgBZ2oAlf6AOoMgBmDIAQgyAEoMgB+DIAmgzIDBcKwDw+DqIBnCkVgIgIki2ZopUvE1JxT4oWhoDAAAVKAgC58oAGtpWYA0QyAFcMgC7DIAHQyABMM8WACY6vJMV28gAikV64Ded5VLUKbyIIoXAFB+jWuYXw-CF4W1LooCADIMgDHkYAXjaAKoMgAxDIA+gwRAVgrXre95lWigiAEkMgDryoAigyAGvKsWgFVDE4qR5GcJRh7UbR2hTHxOJ2UUbE6CI8inD+RS+hx6gIYFqEMFIrzSZOIVgKMr7INJH5gDBfQ8LAwQ6IA5gyAHYMgA+KoAzgyAF+KBmAJ2myD1o2uBYCdoA5QR-Z0JAuHYb2OOQLZsJFGjTDgLAkAAIzhA5Tl0C5bl+eYAXaEFu1s8hT2YQThHETVUjU79n6AGMMimpRDMPw0jGPk5TkAAEx045zmjq5o4s6AXMczEZEUVIAJHVR1Nidr0C4L0ABy8idnuABEiC2+s8j22bjbW7bDtokVbs1FJgvC8gQA">こちら</a> で試すことができます。</p> <h2 id="例外戦略">例外戦略</h2> <p>TypeScript における例外は組み込みの throw がありますが</p> <ul> <li>関数のインタフェースからどんな例外を投げるかわからない</li> <li>エラーハンドリングが漏れる可能性がある</li> </ul> <p>というつらさがあり、代案として主に 2 種類の他のアプローチが提唱されています。</p> <ul> <li>組み込みの例外を throw ではなく return すること</li> <li>サードパーティライブラリ等を使った Result 型を使うこと <ul> <li>正常系・異常系をラップした構造</li> <li><code>result.isOk() ? result.value : result.err</code> のようなインタフェースで正常系にアクセスさせる型</li> </ul> </li> </ul> <p>これらのアプローチではいずれも異常系が戻り値として関数のインタフェースに現れるので、関数を見れば異常系がわかること・異常系のハンドリングが漏れない点で throw のつらいポイントが解消されています。</p> <h3 id="throw-は禁止すべきか">throw は禁止すべきか?</h3> <p>throw は型安全なエラーハンドリング手法ではないので、特にビジネスロジックの濃い層ではできるだけ型安全な手法を取りたいです。</p> <p>一方、throw に優れている点がないかというとそんなことはなくて <strong>「コードがシンプルになる」</strong> というとても大きなメリットがあります。</p> <p><code>return Error</code> や <code>Result</code> のみ(throw 禁止)でコードをちょっと書いてみるとわかるんですが、これはこれでとてもつらいです。React や Vue で props のバケツリレーつらいよね、みたいな話がありますが同種の辛さがあり、依存する関数から関数へひたすら <code>Error</code> (あるいは <code>Result</code>) をバケツリレーする必要が出てきます。</p> <p>その点 throw だと一番下で投げた後、一番上のコントローラー層あるいはミドルウェアなりで catch して異常レスポンスを返せば良いので、ビジネスロジックを書いてる層のコードがとてもすっきりします。</p> <p>このメリットは結構大きいと考えていて</p> <ul> <li>型安全なエラーハンドリングをしたいケース(呼び出し元でハンドリングを期待するケース)</li> <li>そうでないケース</li> </ul> <p>をしっかり定義して使い分けていく、というスタンスを取っています。</p> <h3 id="Result-型は使わず-return-Error-する">Result 型は使わず <code>return Error</code> する</h3> <p>Result 型は <code>return Error</code> と同じく</p> <ul> <li>インタフェースから想定される例外が読み取れる</li> <li>利用側にハンドリングを強制させられる</li> </ul> <p>というメリットを提供しますが、例外システムに標準でないライブラリを利用して強く依存することになるので、標準の機能だけで実現できる <code>return Error</code> 形式を採用しています。関数型のエッセンスは使うが、fp-ts を使わない理由と同様です。</p> <p><code>return Error</code> パターンを使う場合の注意点として、private なプロパティを持たない class は継承関係ではなくデータ構造で型チェックされてしまうため型チェック上の抜け穴が発生してしまう問題があります(<a href="https://www.typescriptlang.org/play?ssl=40&amp;ssc=1&amp;pln=1&amp;pc=1#code/MYewdgzgLgBApgJwSBBlAngWwEYgDYwC8MGO+AFAJQBQ1weAhhBDAEJNwCiSK8AHlDhgAJi27IEMAN4Bfag2zQEDYLHpMWqBgDM47CFx6S4AoaJjjeU6jBgAHBAEsAbg0EwEcBsPB50MAG1ECVJcPABdAC4YAFcROG1HMDhhajk6cGhYyB04ADEwIhhyYJRo-UMJSiIAPmkbWxhHbWLSySToBjBgOBAWissEautG0ZhQSHw4ADo8EABzcgByQFR9QAdTQHsGNg5BmEB1BkBpBkBTc0ArBkARBkA7BkBzBkBo9UAQt0BrBg3AUf1ALQZAZQZ3wCSGQCwEwGiGQAyDIBABkA+dqAbLSnj9AJEM53IyQA7tsDLtAKoMgBiGQBmDOdAPoMgAMGC6AdYZADcMgB+GQDPDIAqhkAawyADoZAOUMgHqGQATDCdrpQljQxo1PFAYggwABuBq2dKjNrCmAAeklMAAegB+CUTCBTWYLZb3J6AKnNAAxKgBUEk4bV4A8FPfV7DF7QB+DIAxBg5aVocQguQK5BGjTADEwcGiSzaSwANBLvcwGPMfTAloABhkA+wwkpZpGgZSCwZ26ApFEpGaJaXQDIzVQh1d22ZqtIxNFNdHp9Ei5fNVepc2zK1VzRarTZIyq8Q6nS63R4vD7ff7A00baGwhHd1GYnH44nk6n05msq7sznNnl8wUS0WNcWjaVyxWjVt4GbtjVD3UGo0miEbc2W2329InwCGDIBghkAyQyACwYbkACoZAEuGEkAUAd1jAFkGP5qDTfIwDdCVPW9X1-SDUYQ2dcNfVjeNE2oIA">参考</a>)。</p> <p>これを回避するために</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> errorSymbol <span class="synStatement">=</span> <span class="synSpecial">Symbol</span><span class="synStatement">()</span> <span class="synStatement">export</span> <span class="synStatement">abstract</span> <span class="synStatement">class</span> BaseError <span class="synStatement">extends</span> <span class="synSpecial">Error</span> <span class="synIdentifier">{</span> <span class="synStatement">private</span> <span class="synStatement">readonly</span> <span class="synIdentifier">[</span>errorSymbol<span class="synIdentifier">]</span>: <span class="synType">undefined</span> <span class="synIdentifier">}</span> </pre> <p>のような Base となるエラーを用意しておき、これを継承する形を取るのがオススメです。</p> <h3 id="throw-と-return-Error-の棲み分け">throw と return Error の棲み分け</h3> <p><code>throw</code> と <code>return</code> する例外の使わ分けについてですが、このプロジェクトでは例外を以下の 4 種類に分類しています。</p> <ul> <li><code>SubNormalError</code> <ul> <li>準正常系な例外で、例えば「リポジトリ層でレコードなかった」とか「Unique 制約にひっかかった」とか「バリデーション通過できなかった」等の <strong>仕様として想定される例外</strong> を指す</li> </ul> </li> <li><code>AbnormalError</code> <ul> <li>無条件に 500 を返して良いもの</li> <li>発生条件など想定されてはいるが、アプリケーションとして異常な状態で</li> <li>DB 接続が確認できない、依存する外部 API が動作していない等、<strong>発生条件は想定されているがアプリケーションとして異常な例外</strong> を指す</li> </ul> </li> <li><code>UnExpectedError</code> <ul> <li>その分岐を通ること自体が <strong>開発時点で想定されない例外</strong> を指す</li> <li>網羅チェックや、型システム上は通る分岐だけど実際にはありえない場所等</li> </ul> </li> <li><code>HttpException</code> <ul> <li>投げると指定のレスポンスを返せる例外</li> </ul> </li> </ul> <p>そして</p> <ul> <li>AbnormalError と UnExpectedError はハンドリングを強要させるメリットが少なく、例外のバケツリレーのデメリットだけ受ける形になってしまうので throw する</li> <li>SubNormalError については呼び出し元でハンドリングを強制させたいので return する</li> <li>HttpException は Usecase/Controller からのみインスタンス化及び throw できる</li> </ul> <p>という方針を取っています。 これで、型安全にハンドリングするメリットが薄い箇所のみ throw でコードベースをシンプルに保ちつつ、それ以外の場所で型安全に(ハンドリングを強要させた状態で)例外を扱うことができるようになりました。</p> <h2 id="その他-Tips">その他 Tips</h2> <p>基本的なアーキテクチャ設計のコンセプトは以上になりますが、その他便利な Tips をいくつか紹介します。</p> <h3 id="eslint-で参照ルールを設定する">eslint で参照ルールを設定する</h3> <p>コーディングルールを定めたらなるべく eslint で弾けるようにルールを設定しています。コードレビューで指摘が入るより効率が良いことと、強制力がより強いためです。</p> <p><a href="https://www.npmjs.com/package/eslint-plugin-import">eslint-import-plugin</a> の no-restricted-paths を使うとレイヤー構造の参照ルールを宣言できます。</p> <p>弊社では</p> <ul> <li>DTO を Controller/Dto ファイル以外で使わない</li> <li>Usecase から別の Usecase を呼ばない</li> <li>Service から Repository を呼ばない</li> </ul> <p>のルールを eslint で表現して設定しています。</p> <h4 id="eslintrccjs">.eslintrc.cjs</h4> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">{</span> rules: <span class="synIdentifier">{</span> <span class="synConstant">&quot;import/no-restricted-paths&quot;</span>: <span class="synIdentifier">[</span> <span class="synConstant">&quot;error&quot;</span>, <span class="synIdentifier">{</span> zones: <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> from: <span class="synConstant">&quot;./src/**/*.!(controller|dto).ts&quot;</span>, target: <span class="synConstant">&quot;**/*.dto.ts&quot;</span>, message: <span class="synConstant">&quot;Dto は Controller の引数・戻り値でのみ使用が許可されています&quot;</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> from: <span class="synConstant">&quot;./src/**/usecases/*.!(spec).ts&quot;</span>, target: <span class="synConstant">&quot;./src/modules/**/usecases/*.ts&quot;</span>, message: <span class="synConstant">&quot;Usecase 層から別の Usecase 層の参照は許可されていません&quot;</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">{</span> from: <span class="synConstant">&quot;./src/**/repositories/*.ts&quot;</span>, target: <span class="synConstant">&quot;**/features/**/services/**/*&quot;</span>, message: <span class="synConstant">&quot;Service 層から Repository 層への参照は許可されていません&quot;</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">]</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">]</span>, <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <h3 id="リテラル型とユニオン型はカスタム-ApiDecorator-を使う">リテラル型とユニオン型はカスタム ApiDecorator を使う</h3> <p>NestJS の OAS 書き出しの機能は便利ですが、制約がいくつかあります。</p> <ul> <li>① プロパティの型が type や interface だと書き出せない</li> <li>② プロパティの型がユニオン型だと書き出せない</li> <li>③ プロパティの型がリテラル型だと書き出せない</li> </ul> <p>① についてはプロパティも Dto 使いましょうね、で良いんですが後者についてはデコレータを使って型定義を明示して上げる必要があります。</p> <p>以下のようなユーテリティを用意しておくと便利です。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">/**</span> <span class="synComment"> * @example ApiProperty({</span> <span class="synComment"> * description: '説明',</span> <span class="synComment"> * ...enumSchema&lt;Gender&gt;(['male', 'female'])</span> <span class="synComment"> * })</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synType">const</span> enumSchema <span class="synStatement">=</span> <span class="synStatement">&lt;</span>T <span class="synStatement">extends</span> <span class="synType">string</span><span class="synStatement">&gt;(</span> enums: <span class="synStatement">readonly</span> T<span class="synIdentifier">[]</span><span class="synStatement">,</span> example?: T <span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> example: example ?? enums<span class="synIdentifier">[</span><span class="synConstant">0</span><span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synStatement">enum</span>: enums <span class="synStatement">as</span> T<span class="synIdentifier">[]</span><span class="synStatement">,</span> <span class="synIdentifier">}</span> satisfies SchemaObject<span class="synStatement">;</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synComment">/**</span> <span class="synComment"> * @example ApiProperty({</span> <span class="synComment"> * description: '説明',</span> <span class="synComment"> * ...unionSchema([SomeDto, Some2Dto], exampleData)</span> <span class="synComment"> * })</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synType">const</span> unionSchema <span class="synStatement">=</span> <span class="synStatement">&lt;</span> TargetDtoClass<span class="synStatement">,</span> <span class="synType">const</span> T <span class="synStatement">extends</span> ReadonlyArray<span class="synStatement">&lt;</span>AbstractClass<span class="synStatement">&lt;</span>TargetDtoClass<span class="synStatement">&gt;&gt;,</span> U <span class="synStatement">=</span> T<span class="synIdentifier">[</span><span class="synType">number</span><span class="synIdentifier">]</span> <span class="synStatement">&gt;(</span> unions: T<span class="synStatement">,</span> example: U <span class="synStatement">extends</span> <span class="synIdentifier">{</span> prototype: <span class="synType">unknown</span> <span class="synIdentifier">}</span> ? U<span class="synIdentifier">[</span><span class="synConstant">&quot;prototype&quot;</span><span class="synIdentifier">]</span> : <span class="synType">never</span> <span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> example<span class="synStatement">,</span> oneOf: unions.map<span class="synStatement">((</span>union<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> $ref: getSchemaPath<span class="synStatement">(</span>union<span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)),</span> <span class="synIdentifier">}</span> satisfies SchemaObject<span class="synStatement">;</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synComment">/**</span> <span class="synComment"> * @example ApiProperty({</span> <span class="synComment"> * description: '説明',</span> <span class="synComment"> * ...arraySchema(enumSchema(...args))</span> <span class="synComment"> * })</span> <span class="synComment"> */</span> <span class="synStatement">export</span> <span class="synType">const</span> arraySchema <span class="synStatement">=</span> <span class="synStatement">(</span>itemSchema: SchemaObject<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">&quot;array&quot;</span><span class="synStatement">,</span> items: itemSchema<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>ユニオン型やリテラル型(+これらを使った配列型)の場合には</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> Gender <span class="synStatement">=</span> <span class="synConstant">&quot;male&quot;</span> | <span class="synConstant">&quot;female&quot;</span> <span class="synStatement">class</span> SomeDto <span class="synIdentifier">{</span> <span class="synSpecial">@ApiDecorator</span><span class="synStatement">(</span><span class="synIdentifier">{</span> ...enumSchema<span class="synStatement">&lt;</span>Gender<span class="synStatement">&gt;(</span><span class="synConstant">&quot;male&quot;</span><span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> gender<span class="synConstant">!</span>: Gender <span class="synSpecial">@ApiDecorator</span><span class="synStatement">(</span><span class="synIdentifier">{</span> ...unionSchema<span class="synStatement">(</span><span class="synIdentifier">[</span>SomeDto<span class="synStatement">,</span> SomeDto2<span class="synIdentifier">]</span><span class="synStatement">,</span> exampleData<span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> someUnion<span class="synConstant">!</span>: SomeDto | SomeDto2 <span class="synSpecial">@ApiDecorator</span><span class="synStatement">(</span><span class="synIdentifier">{</span> ...arraySchema<span class="synStatement">(</span>enumSchema<span class="synStatement">&lt;</span>Gender<span class="synStatement">&gt;(</span><span class="synConstant">&quot;male&quot;</span><span class="synStatement">)),</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> genders<span class="synConstant">!</span>: Gender<span class="synIdentifier">[]</span> <span class="synIdentifier">}</span> </pre> <p>のように宣言することで適切な OAS を吐き出すことができます。</p> <h2 id="実装後の課題感">実装後の課題感</h2> <p>まだ実装を進めているステータスですが、現時点での所感や振り返りをして行きます。</p> <h3 id="Usecase-呼び出しが-Fat-になりがちでつらい">Usecase 呼び出しが Fat になりがちでつらい</h3> <p>主な副作用分離のパターンとして</p> <ol> <li>副作用のサンドイッチ</li> <li>副作用を DI するパターン</li> </ol> <p>を想定していましたが、副作用分離を意識しないときの書き方と近くて書きやすかったこともあり、1 より 2 のパターンでの実装が多くなりがちでした。</p> <p>2 のパターンでは依存する対象が多いと DI の呼び出し部分が Fat になりやすく</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink>someUsecase<span class="synStatement">(</span><span class="synIdentifier">{</span> repository1<span class="synStatement">,</span> repository2<span class="synStatement">,</span> repository3<span class="synStatement">,</span> <span class="synComment">// ...</span> <span class="synIdentifier">}</span><span class="synStatement">)(</span>arg<span class="synStatement">)</span> </pre> <p>のような書き方になります。</p> <p>冗長ではありますが、やむをえない部分ではあるので許容しています。ただし、DI が必要ないケースでまで DI していないかは注意していきたいなと思います。</p> <p>また、関数型DIのライブラリである <a href="https://github.com/frouriojs/velona">velona</a> を使うとプレーンな高階関数ではなく、必要な時だけ inject することができるインタフェースになっています。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// リポジトリのサンプルの抜粋です</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> basicFn <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./'</span> <span class="synType">const</span> injectedFn <span class="synStatement">=</span> basicFn.inject<span class="synStatement">(</span><span class="synIdentifier">{</span> add: <span class="synStatement">(</span>a<span class="synStatement">,</span> b<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> a * b <span class="synIdentifier">}</span><span class="synStatement">)</span> expect<span class="synStatement">(</span>injectedFn<span class="synStatement">(</span><span class="synConstant">2</span><span class="synStatement">,</span> <span class="synConstant">3</span><span class="synStatement">,</span> <span class="synConstant">4</span><span class="synStatement">))</span>.toBe<span class="synStatement">(</span><span class="synConstant">2</span> * <span class="synConstant">3</span> * <span class="synConstant">4</span><span class="synStatement">)</span> <span class="synComment">// pass</span> expect<span class="synStatement">(</span>basicFn<span class="synStatement">(</span><span class="synConstant">2</span><span class="synStatement">,</span> <span class="synConstant">3</span><span class="synStatement">,</span> <span class="synConstant">4</span><span class="synStatement">))</span>.toBe<span class="synStatement">((</span><span class="synConstant">2</span> + <span class="synConstant">3</span><span class="synStatement">)</span> * <span class="synConstant">4</span><span class="synStatement">)</span> <span class="synComment">// pass</span> </pre> <p>こういうアプローチであればテスト以外で依存する関数を用意しなくて良いので Usecase 層がFatにならないため、こういったアプローチを採用していれば良かったなと思っています。</p> <h3 id="外部-API-に対するモックがつらい">外部 API に対するモックがつらい</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20230721/20230721163007.png" alt="&#x30EC;&#x30A4;&#x30E4;&#x30FC;&#x56F3;" width="800" height="459" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>レイヤー構造の紹介に載せた図ですが、この図を見てわかるようにコントローラー層からリポジトリ層の利用には DI を利用していません。これはコントローラー層では、実際のデータベースを使ったテストをしたかったことが理由です。</p> <p>一方、こと外部 API に依存する処理に関しては実際にリクエストを送るのではなく、テストケースに応じた API レスポンスへ差し替える必要があります。DI を利用しない仕組みづくりをしてしまったので、差し替えるために Jest の mock が多用されており、辛い状態になってしまいました。</p> <p><a href="https://github.com/mswjs/msw">msw</a> 等を使ってネットワークのレイヤーをモックをするような方針を取るか、コントローラー層からも DI ができる仕組みを整備すべきだったなという後悔があります。</p> <h3 id="VSCode-で-Go-To-Definition-が使いにくい">VSCode で Go To Definition が使いにくい</h3> <p>関数での DI だとよくある話なのかもですが</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> usecase <span class="synStatement">=</span> defineInjectable<span class="synStatement">(</span><span class="synIdentifier">{</span> fetchUser<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)((</span>deps<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">async</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> deps.fetchUser <span class="synComment">// &lt;== こいつ</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <p>の「fetchUser」の実装へ定義ジャンプをしたくて「Go To Definition」しても DI 対象を宣言している 2 行目にジャンプしてしまい、実装を読みにいけません。</p> <p>依存性逆転されて実装ではなくインタフェースに依存しているので当然といえば当然ですが、実装開始して当初は少し困りました。</p> <p>対策としては、インタフェースにジャンプすれば良いので「Go To Type Definition」でジャンプしてあげると実装に飛ぶことができます。</p> <h3 id="型と単体テストによるフィードバックが高速で体験が良い">型と単体テストによるフィードバックが高速で体験が良い</h3> <p>テストを先に書いて実装を後から書いていく TDD の開発体験は、実際に Web API を呼び出して試すよりも実装があっているかのフィードバックが速い、という点でとても開発体験が良いです。</p> <p>型も基本的にはテストと構造が同じだと思っていて</p> <table> <thead> <tr> <th style="text-align:center;"> </th> <th style="text-align:center;"> チェック速度 </th> <th style="text-align:center;"> チェックできる範囲 </th> </tr> </thead> <tbody> <tr> <td style="text-align:center;"> 型チェック </td> <td style="text-align:center;"> とても速い </td> <td style="text-align:center;"> 型の範囲のチェック </td> </tr> <tr> <td style="text-align:center;"> DB 依存なしテスト </td> <td style="text-align:center;"> 速い </td> <td style="text-align:center;"> ロジックの検査 </td> </tr> <tr> <td style="text-align:center;"> DB 依存ありテスト </td> <td style="text-align:center;"> 遅い </td> <td style="text-align:center;"> DB の動き含めて包括的に </td> </tr> </tbody> </table> <p>のような特徴があります</p> <p>柔軟な型を用いて関数の引数・戻り値の型を正確に・先に用意しやすいので、先にインタフェースやテストを用意しつつ、実装しながら型チェックと DB 非依存のテストで FB をもらえるのでとても体験が良かったです。</p> <h2 id="まとめ">まとめ</h2> <p>NestJS における TS Backend 設計の一例と Tips を紹介しました。</p> <p>TypeScript の型システムを活かしながら、型駆動・関数型プログラミングのエッセンスを軸に</p> <ul> <li>型システム上抜け穴になりやすい NestJS のポイントを防ぐ方法</li> <li>副作用をレイヤー構造で分離してテスタビリティ・開発体験を高められること</li> <li>準正常系の例外を return し、異常系の例外を throw することで実装をシンプルに保ちつつ、例外を型安全に扱えること</li> </ul> <p>等を紹介しました。</p> <p>部分的にでも TS Backend 設計の参考になれば幸いです。</p> <h2 id="設計執筆の参考にした文献">設計・執筆の参考にした文献</h2> <ul> <li><a href="https://www.amazon.co.jp/dp/B07B44BPFB">Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# by Scott Wlaschin</a></li> <li><a href="https://speakerdeck.com/naoya/typescript-niyoru-graphql-batukuendokai-fa-75b3dab7-90a8-4169-a4dc-d1e7410b9dbd">TypeScript による GraphQL バックエンド開発 - Speaker Deck</a></li> <li><a href="https://dev.classmethod.jp/articles/error-handling-practice-of-typescript/">TypeScript の異常系表現のいい感じの落とし所 | DevelopersIO</a></li> <li><a href="https://docs.nestjs.com/">Documentation | NestJS - A progressive Node.js framework</a></li> <li><a href="https://mswjs.io/docs/">Introduction - Mock Service Worker Docs</a></li> <li><a href="https://eslint.org/docs/latest/rules/no-restricted-imports#disallow-specific-imports-no-restricted-imports">no-restricted-imports - ESLint - Pluggable JavaScript Linter</a></li> <li><a href="https://www.mizdra.net/entry/2022/11/24/153459#jest-worker-%E3%81%94%E3%81%A8%E3%81%AB-database-%E3%82%92%E5%88%86%E3%81%91%E3%82%8B">Prisma で本物の DBMS を使って自動テストを書く - mizdra's blog</a></li> </ul> <div class="footnote"> <p class="footnote"><a href="#fn-b2b04f93" name="f-b2b04f93" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">あくまで筆者の個人的な解釈です。</span></p> <p class="footnote"><a href="#fn-1902ff53" name="f-1902ff53" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">ここで言うEntityはO/R Mapper的な文脈ではなく、クリーンアーキテクチャにおける「ドメインモデルと紐づくビジネスルールがカプセルバされてるもの」という意味です。</span></p> <p class="footnote"><a href="#fn-9c896992" name="f-9c896992" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">Pipe の設定は必要です。</span></p> <p class="footnote"><a href="#fn-f8c22434" name="f-f8c22434" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">任意のprivateなプロパティを持つBaseEntityを継承させることで逆にクラスインスタンスに統一させるという選択肢も有ると思います。このやり方だとObject.assignやplainToClassが型安全性を損なうハッチになってしまうことが想定されるため弊チームでは避けています。しかしconstructorをちゃんと書くようにもすれば冗長性と引き換えに健全な形で運用できると思います。</span></p> <p class="footnote"><a href="#fn-8bd34ed0" name="f-8bd34ed0" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">一般的にはロギング等も副作用に分類されます。</span></p> <p class="footnote"><a href="#fn-6422c6be" name="f-6422c6be" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">副作用は引数からのコールバック関数に含まれていて関数の責務範囲には副作用がない・テストにおいて副作用がないことを指します。</span></p> </div> d-kimuson pnpm fetch で Docker キャッシュを活かす hatenablog://entry/4207575160649045058 2023-05-15T16:30:00+09:00 2023-05-15T16:30:01+09:00 pnpm には Docker でキャッシュを利用しやすくする fetch というコマンドが用意されています この記事では pnpm fetch を使ってキャッシュを利用しやすい Dockerfile を書いていく方法を紹介します Docker のマルチステージビルドとキャッシュ Docker にはマルチステージビルドという機能が存在し、単一の Docker イメージ下で実行するのではなく、ビルドやインストール, 本番実行等に分けて Docker イメージ を作成できます Dockerfile でマルチステージビルドを行う際の例を示します ARG NODE_VERSION=18.15.0 FRO… <p>pnpm には Docker でキャッシュを利用しやすくする fetch というコマンドが用意されています</p> <p>この記事では pnpm fetch を使ってキャッシュを利用しやすい Dockerfile を書いていく方法を紹介します</p> <h2 id="Docker-のマルチステージビルドとキャッシュ">Docker のマルチステージビルドとキャッシュ</h2> <p>Docker にはマルチステージビルドという機能が存在し、単一の Docker イメージ下で実行するのではなく、ビルドやインストール, 本番実行等に分けて Docker イメージ を作成できます</p> <p>Dockerfile でマルチステージビルドを行う際の例を示します</p> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">ARG </span>NODE_VERSION=18.15.0 <span class="synStatement">FROM </span>node:$NODE_VERSION-buster as builder <span class="synComment"># build 処理</span> <span class="synStatement">RUN </span>pnpm build <span class="synStatement">FROM </span>node:$NODE_VERSION-slim as runner <span class="synStatement">COPY </span>--from=builder /app/node_modules /app/node_modules <span class="synStatement">COPY </span>--from=builder /app/dist /app/dist <span class="synStatement">CMD </span>[<span class="synConstant">&quot;node&quot;</span>, <span class="synConstant">&quot;dist/index.js&quot;</span>] </pre> <p>使い方としてはこんな感じですね</p> <p>ステージを分ける目的としては、一般的に</p> <ul> <li>ビルドステップでのみ必要な依存関係(devDependency)やバンドルする場合は node_modules 全体等を本番で使うコンテナに持って行きたくない</li> <li>ビルドステップではフルイメージを利用したいが、runner ではできるだけ軽量にしたいため slim イメージを利用する</li> <li>ネットワーク転送が原因で時間のかかる処理が複数あるときに、並列で実行したい (COPY 元が前のステージになっていない、かつ <code>DOCKER_BUILDKIT=1</code> が設定されているとき並列で実行されます)</li> </ul> <p>等があります</p> <p>このステージ分けはキャッシュの単位としても機能していて、COPY 元のファイルがキャッシュキーになります</p> <p>つまり</p> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:$NODE_VERSION-buster as installer <span class="synStatement">COPY </span>package.json pnpm-lock.yaml ./ <span class="synStatement">RUN </span>pnpm i --frozen-lockfile <span class="synStatement">RUN </span>pnpm build <span class="synStatement">FROM </span>node:$NODE_VERSION-buster as builder <span class="synComment"># ...</span> </pre> <p>の場合は、package.json と pnpm-lock.yaml に変更が入らなければ installer ステップはキャッシュを利用してスキップできることになります</p> <p>ですので、pnpm に限らず npm や yarn 等の他のパッケージマネージャーを利用している場合でも package.json とロックファイルを installer としてステージを分離してあげるとキャッシュが適用されやすくなります</p> <h2 id="pnpm-fetch">pnpm fetch</h2> <p>pnpm fetch は pnpm-lock.yaml から依存関係の取得するコマンドです</p> <p>上の見出しで installer を分離するテクニックを紹介しましたが、pnpm fetch を使うことで、さらに package.json をキャッシュキーから取り除き、pnpm-lock.yaml のみをキャッシュキーとして依存関係をダウンロードできます</p> <p>設定ファイルとして package.json を利用するツールも(最近は減った気がしますが)ありますし、scripts も package.json に書かれます</p> <p>pnpm-lock.yaml のみから取得できると純粋に依存関係が変わらない限りキャッシュを利用し続けられる、ということになります</p> <h2 id="公式ドキュメントの-Dockerfile-が動かない">公式ドキュメントの Dockerfile が動かない</h2> <p>ここからが本記事の本題です</p> <p>上記の説明は <a href="https://pnpm.io/ja/cli/fetch">公式ドキュメントの pnpm fetch のページ</a> にサンプルの Dockerfile とセットで説明されています</p> <p>公式の推奨するサンプルは以下の通りです</p> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">FROM </span>node:14 <span class="synStatement">RUN </span>curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm <span class="synComment"># pnpm fetchはロックファイルのみが必要</span> <span class="synStatement">COPY </span>pnpm-lock.yaml ./ <span class="synComment"># パッケージにパッチを当てた場合は、pnpm fetchを実行する前にパッチを含める</span> <span class="synStatement">COPY </span>patches patches <span class="synStatement">RUN </span>pnpm fetch --prod ./ <span class="synStatement">RUN </span>pnpm install -r --offline --prod <span class="synStatement">EXPOSE </span>8080 <span class="synStatement">CMD </span>[ <span class="synConstant">&quot;node&quot;</span>, <span class="synConstant">&quot;server.js&quot;</span> ] </pre> <p>しかしながら、これをそのまま手元で動かすと上手く行きませんでした</p> <ul> <li><code>No projects found in "/app"</code> といわれてインストールされず、node_modules 以下には <code>.modules.yaml</code> と <code>.pnpm</code> のみ作成される</li> <li>おそらく package.json がないことで install 時にプロジェクトを認識できていないことが原因なので、空の package.json を生成してみる <ul> <li>→ 指定なしのときは pnpm-lock.yaml が空になる</li> </ul> </li> <li>frozen-lockfile でインストールしてみる <ul> <li>エラーになる</li> </ul> </li> </ul> <p>pnpm fetch 自体は意図通り動いていますが、おそらくバージョンアップ等で package.json がない状態での <code>pnpm install -r --offline --prod</code> が実行できなくなったものと思われます</p> <h2 id="fetcher-step-と-builder-step-に分離する">fetcher step と builder step に分離する</h2> <p>install が動かなかったので、回避策として fetch, install と build ではなく、fetch と install, build に分離しました</p> <p>node_modules 以下への展開は install で行われますが、fetch の時点でダウンロードは完了しているのでここまでキャッシュできれば install も十分高速に実行されることが期待できます</p> <p>分離後の Dockerfile は以下の通りです</p> <pre class="code lang-dockerfile" data-lang="dockerfile" data-unlink><span class="synStatement">ARG </span>NODE_VERSION=18.15.0 <span class="synStatement">FROM </span>node:$NODE_VERSION-buster as fetcher <span class="synStatement">WORKDIR </span>/app <span class="synStatement">COPY </span>pnpm-lock.yaml /app <span class="synStatement">RUN </span>corepack enable pnpm <span class="synStatement">RUN </span>corepack prepare pnpm@8.2.0 --activate <span class="synStatement">RUN </span>pnpm fetch --prod ./ <span class="synStatement">FROM </span>node:$NODE_VERSION-buster as builder <span class="synStatement">WORKDIR </span>/app <span class="synStatement">COPY </span>--from=fetcher /root/.local/share/pnpm/store/v3 /root/.local/share/pnpm/store/v3 <span class="synStatement">COPY </span>--from=fetcher /app/node_modules /app/node_modules <span class="synStatement">COPY </span>--from=fetcher /app/pnpm-lock.yaml /app/pnpm-lock.yaml <span class="synStatement">COPY </span>package.json /app <span class="synStatement">RUN </span>corepack enable pnpm <span class="synStatement">RUN </span>corepack prepare pnpm@8.2.0 --activate <span class="synStatement">RUN </span>pnpm i --offline --prod --frozen-lockfile <span class="synStatement">FROM </span>node:$NODE_VERSION-slim as runner <span class="synComment"># ...</span> </pre> <p>pnpm fetch すると仮想ストアにダウンロードされるので仮想ストアが格納されるパスもコピーしてきます</p> <p>パスは以下のようにして調べられます</p> <pre class="code bash" data-lang="bash" data-unlink>$ docker run node:18.15.0-buster bash -c &#34;corepack enable pnpm &amp;&amp; corepack prepare pnpm@8.2.0 --activate &amp;&amp; pnpm store path&#34; Preparing pnpm@8.2.0 for immediate activation... /root/.local/share/pnpm/store/v3</pre> <p>これで「依存関係が変わらない限り依存関係のダウンロードをキャッシュする」ことができるようになりました</p> <h2 id="まとめ">まとめ</h2> <p>このエントリでは pnpm fetch を使うことで、依存関係のダウンロードをキャッシュしやすい Dockerfile について紹介しました</p> <p>install 処理は <code>--offline</code> であっても package.json がないと動かないので fetch までで STAGE を分離し、node_modules と Virtual Store をコピーすることで対応しました</p> <p>pnpm をお使いの方はぜひお試しください</p> mobile-factory モバファクテックブログの記事管理を GitHub リポジトリに乗せてアドベントカレンダーを運用してみた hatenablog://entry/4207112889947323626 2022-12-25T00:00:00+09:00 2022-12-25T00:00:39+09:00 メリークリスマス 🎉 BC チームの id:d-kimuson です。アドベントカレンダーもとうとう最終日となりました! 今年のアドベントカレンダーでは、初日の記事は僕が執筆をしました この記事を書いていて、レビューをお願いしていたら以下のような投稿をもらいました 社内ではドキュメントサービスとして DocBase を使っているので、技術ブログの下書きを DocBase に書いていたのですが、Pull Request で行うレビューに比べてレビューがしづらいよね、というものです 課題があれば解決するのがエンジニアです かねてからローカルで書けたほうが執筆体験良いのに... と思っていたこともあ… <p>メリークリスマス 🎉</p> <p>BC チームの <a href="http://blog.hatena.ne.jp/d-kimuson/">id:d-kimuson</a> です。アドベントカレンダーもとうとう最終日となりました!</p> <p>今年のアドベントカレンダーでは、初日の記事は僕が執筆をしました</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.mobilefactory.jp%2Fentry%2F2022%2F12%2F01%2F000000" title="TS 4.9 satisfies operator を使って React Router のナビゲーションを型安全にしてみる - Mobile Factory Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe></p> <p>この記事を書いていて、レビューをお願いしていたら以下のような投稿をもらいました</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000025.png" width="474" height="116" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>社内ではドキュメントサービスとして DocBase を使っているので、技術ブログの下書きを DocBase に書いていたのですが、Pull Request で行うレビューに比べてレビューがしづらいよね、というものです</p> <p>課題があれば解決するのがエンジニアです</p> <p>かねてからローカルで書けたほうが執筆体験良いのに... と思っていたこともあり</p> <ul> <li>ローカルで記事を執筆できる</li> <li>GitHub 上でレビューができる</li> </ul> <p>ような仕組みを整えて、今年のアドベントカレンダーのお試し運用をしてみたので紹介します</p> <h2 id="ローカルで執筆できる環境の整備">ローカルで執筆できる環境の整備</h2> <p>エンジニアに限れば使い慣れたローカルの環境のほうが記事を書きやすいという人の方が多いでしょう</p> <p>また、モバイルファクトリーのテックブログは、はてなブログで運用していて元々 Markdown を使えることから、下書きはローカルで書くという選択肢も取ることができました</p> <p>各々のエディタのプレビュー拡張機能等で確認してもらっても良かったのですが、プレビューのスタイルもテックブログに近いほうが嬉しいよね、ということで、ローカルで動作するプレビュー用の開発サーバーも用意しました</p> <p>ブログで実際に利用しているカスタム CSS をあてることで、DocBase を使うよりも本番に近いプレビューをしながら記事を書くことができるようになりました 🎉</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000028.png" width="800" height="541" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>技術的には JAMStack 系の各種フレームワーク等の中でも、特にローカルでの執筆体験の良い <a href="https://vitepress.vuejs.org/">VitePress</a> を利用してローカルで使うプレビューサーバーを準備しました</p> <h2 id="執筆補助ツールを利用する">執筆補助ツールを利用する</h2> <p>また、ローカル環境では</p> <ul> <li><a href="https://prettier.io/">prettier</a> (フォーマッター)</li> <li><a href="https://GitHub.com/textlint/textlint">textlint</a> (日本語校正)</li> <li><a href="https://cspell.org/">cspell</a> (スペルミスのチェック)</li> </ul> <p>等の優れたツールを執筆の補助として利用できるという利点があるので、これらを活かせるような仕組みを作りました</p> <p>社内で利用者の多い VSCode の設定をリポジトリからまとめて配布することで</p> <ul> <li>onSave 時に textlint, prettier で自動修正可能な問題が修正される</li> <li>onType でスペルミスや日本語の問題点をそれぞれ cspell, textlint の拡張機能が教えてくれる</li> </ul> <p>ような執筆体験で記事をかけるようになりました</p> <p>また、今回は CI を整備する手間まで取れなかったので</p> <ul> <li><a href="https://GitHub.com/typicode/husky">husky</a></li> <li><a href="https://GitHub.com/okonet/lint-staged">lint-staged</a></li> </ul> <p>を使うことで、コミット時に prettier, textlint, cspell の制約を課す仕組みを追加しています</p> <p>これによって、VSCode を利用していない社員も、コミット時に校正ツール等の恩恵を受けることができるようになります</p> <h2 id="GitHub-上でレビューができる">GitHub 上でレビューができる</h2> <p>構築した環境のソースコード・記事ファイルは GitHub で管理されていて、Pull Request を作ることで記事のレビューを依頼できます</p> <p>コメントをいれる場所が明確ですし、ちょっとした内容であれば Suggested Change を使って提案できるのでレビューもしやすくなりました</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000031.png" width="800" height="472" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="自動デプロイはしない">自動デプロイはしない</h2> <p>GitHub で記事を管理する話になれば、必然的にマージのタイミングで CI/CD を使って記事を自動投稿したいという話にもなってきます</p> <p>セルフホストしている SSG ベースのブログとかでしたらこの辺りはやりやすいんですが、モバイルファクトリーのテックブログははてなブログで運用をしているためこの辺りの仕組みを作るのは結構大変になってしまいます</p> <p>今回はお試し運用であることと、仕組みを作ることが大変であることから「執筆・レビューのフローまで整備するが、実際に投稿(デプロイ)する作業は手作業で行う」という形での運用としています</p> <h2 id="使ってみた感想を聞いてみる">使ってみた感想を聞いてみる</h2> <p>元々お試しでやってみようという温度感だったので、実際に記事を書いてくれた人に感想を聞いてみました!</p> <h3 id="ローカル執筆とリポジトリでのレビューのフローを今後も利用したいですか">ローカル執筆とリポジトリでのレビューのフローを今後も利用したいですか?</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000034.png" width="588" height="265" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>約 9 割が今後も利用したいという回答でした</p> <h3 id="校正ツールである-textlint-や-cspell-等を導入していましたが執筆の補助として役に立ちましたか">校正ツールである textlint や cspell 等を導入していましたが、執筆の補助として役に立ちましたか?</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000037.png" width="732" height="282" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>校正ツールについても 7 割弱は好意的でした</p> <p>その他感想としては</p> <blockquote><p>GitHub 上のリポジトリという形で記事が管理されることによって、各々が好きなエディターを使うことが出来るようになり、記事を書くのが快適になりました。</p></blockquote> <p><span></span></p> <blockquote><p>初の技術ブログ執筆でしたが、使い慣れた GitHub 上レビューフローでとてもやりやすかったです。 基本的な日本語の誤りも自動で指摘してもらえて助かりました。</p></blockquote> <p>のようなポジティブな感想が多かったです</p> <p>レビュワー視点でも</p> <blockquote><p>下書き画面や DocBase だとコメント箇所の伝え方が難しいですが、GitHub を使うと行に対してコメントできるのがよかったです</p></blockquote> <p><span></span></p> <blockquote><p>コードレビューと同じ要領で記事のレビューができるので、使い勝手も分かっていてよかったです</p></blockquote> <p>のようなポジティブな感想をいただきました</p> <p>また、個人的には、今回のリポジトリに対して機能追加やバグ修正の PR を送ってくれる有志もいたため、普段開発業務で関わらない人のレビューをしたりすることもあったのが新鮮で良かったです</p> <p>一方、textlint に関しては設定しているルールが合わないこともあり、以下のような感想もありました<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221225/20221225000022.png" width="588" height="265" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <blockquote><p>textlint と cspell はスペルミスや構文ミスの発見などで何回か役に立ちました!しかし、自分の文章に対するこだわりと相なれない部分もあり、CI で拒絶するほどではないと思いました。以下は textlint で気になった点です。</p> <p>「思います」という文章は、技術ブログの導入や最後の感想の部分で登場することは自然です。「本格的に、厳格に、丁寧に、文章を書く必要があります」のようなリズミカルに「に」を重複させたいときも怒られてしまいます。「A や B、C、D などのように、〜〜」これも「、」を 4 回打つため怒られます。</p></blockquote> <p>この辺りは、人や記事によって語調も変わるので、場合によっては煩わしくなってしまうこともあり、ルール設定が難しいなと感じました</p> <p>textlint では部分的に textlint を無効にする <code>&lt;!-- textlint-disable --&gt;</code> コメントが用意されているので、ルールの整備は進めつつも、一旦はコメントを使ってもらう形で案内するしかないかなと思っています</p> <p>また、画像の追加についてもローカル環境だと手間になってしまうという声や、部署によってはローカルマシンで開発をしないため環境構築が手間だったという声もありました</p> <p>この辺りは今後の課題として解決していきたいですね</p> <h2 id="まとめ">まとめ</h2> <p>この記事では、はてなブログで運用しているモバファクテックブログの記事の管理を GitHub に載せて、ローカルで執筆する仕組みを作った話を紹介しました!</p> <p>たかが執筆・レビュー体験の話ではありますが、会社の名前でテックブログを書いてくれる人はとても貴重です。少しでも書きやすい環境を整備してみるのはいかがでしょうか!</p> <p>NextAction としては</p> <ul> <li>やはり執筆した記事を手動で転記するのは手間なので自動でデプロイするような仕組みを整えること</li> <li>運用していてこの辺も textlint で怒ってほしいよね、この辺は怒らなくても良いよね、というものが見えてきたのでルールを最適化していくこと</li> <li>執筆体験が良くなっただけでは意味が薄いので発信自体も増やしていきたい</li> </ul> <p>辺りをやっていきたいなと思っています</p> <p>以上となります! それでは良いお年を!</p> d-kimuson エンジニア以外の職種も勉強会を開こう hatenablog://entry/4207112889947490578 2022-12-24T09:00:00+09:00 2022-12-24T09:00:05+09:00 🎄モバイルファクトリー Advent Calendar 2022! 毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします! こんにちは。「駅メモ!」開発チームディレクターの id:Torch4083 です。 この記事では、エンジニア以外の職種による勉強会を増やすメリットについて改めて整理し、実際に開催する際に役立つ事例を添えてお伝えいたします。 まえおき:モバファクの社内制度 それって、エンジニアはなにがうれしいの どうやってるの 勉強会のケース 輪読会 LT会 ワークショップ プレゼン・講義形式(+ 質疑応答) 具体例 ゲームプランニング勉… <p>🎄モバイルファクトリー Advent Calendar 2022!</p> <p>毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします!</p> <hr /> <p>こんにちは。「駅メモ!」開発チームディレクターの <a href="http://blog.hatena.ne.jp/Torch4083/">id:Torch4083</a> です。</p> <p>この記事では、<strong>エンジニア以外の職種による勉強会を増やすメリット</strong>について改めて整理し、実際に開催する際に役立つ事例を添えてお伝えいたします。</p> <ul class="table-of-contents"> <li><a href="#まえおきモバファクの社内制度">まえおき:モバファクの社内制度</a></li> <li><a href="#それってエンジニアはなにがうれしいの">それって、エンジニアはなにがうれしいの</a></li> <li><a href="#どうやってるの">どうやってるの</a><ul> <li><a href="#勉強会のケース">勉強会のケース</a><ul> <li><a href="#輪読会">輪読会</a></li> <li><a href="#LT会">LT会</a></li> <li><a href="#ワークショップ">ワークショップ</a></li> <li><a href="#プレゼン講義形式-質疑応答">プレゼン・講義形式(+ 質疑応答)</a></li> </ul> </li> <li><a href="#具体例">具体例</a><ul> <li><a href="#ゲームプランニング勉強会">ゲームプランニング勉強会</a></li> <li><a href="#イシューからはじめよ輪読会">『イシューからはじめよ』輪読会</a></li> </ul> </li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="まえおきモバファクの社内制度">まえおき:モバファクの社内制度</h2> <p>弊社には「1日の業務時間のうち1時間は勉強に充ててOK!」という制度があります。(「シェアナレ」と呼ばれています)</p> <p>業務や個人で学んだことを積極的に社内へ共有することが推奨されており、職種・年次に関わらず制度を利用することができます。</p> <p>「勉強会といえばエンジニア」というイメージがつきがちですが、弊社は上記制度の後押しがあり、それ以外の職種が主体となっている勉強会も頻繁に開かれています。</p> <p>直近でエンジニア以外の職種により開かれたテーマは、以下のようなものがありました。</p> <ul> <li>自社ゲームを題材にした、ゲームプランニング入門</li> <li>自社プロダクトにおけるキャラクター制作</li> <li>人事・労務制度についての理解を深める</li> </ul> <h2 id="それってエンジニアはなにがうれしいの">それって、エンジニアはなにがうれしいの</h2> <p>エンジニア以外の職種に社内勉強会を開いてもらうメリットとしては、やはり<strong>開発以外の業務にも幅を広げることができる・意見できるようになる</strong>ことが大きいと思います。 例えば「もう少し上流から開発に関わりたいな……」と考えて自分で企画を持ち込んでみたけど反応が芳しくなかった、という体験をした方はいませんか?</p> <p>その場合、おそらく企画の良し悪しというよりは、企画を考える前提が理解できていなかった可能性が大きいです。(自社サービスのユーザーの傾向、具体的なKPIの推移を踏まえた予測、プロダクトのあるべき姿etc.)</p> <p>(※もちろん、施策立案者が間違っていることも多分にあります。そんなときはぜひ遠慮なく指摘してください!!)</p> <p>上記のケースの場合、「意思決定のためにはどんな材料が必要で、どこを見せれば説得できるのか?」は、おおよそディレクターなどの施策立案者の間で交わされるやりとりで完結してしまうため、普段の開発業務では分かりにくい部分かと思います。</p> <p>そのため「だったらそれを直接聞ける機会を作ろう!」というのが、エンジニア以外の職種に社内勉強会を開いてもらうメリットになります。</p> <p>つまるところ、<strong>他職種主体の勉強会は、エンジニア以外がどんな思考のプロセスで企画や施策を持ってくるのか(あるいは働いているのか)を理解するきっかけになる</strong>ということです。</p> <p>そういう訳で、技術的な話以外で勉強会をやってみませんか?という話が以下に続きます。</p> <h2 id="どうやってるの">どうやってるの</h2> <p>ここからは冒頭で触れた通り「社内勉強会のケースと進め方」を紹介し、エンジニア以外の職種が勉強会を開催しやすくなるようなヒントを提供します。</p> <p>「勉強会をやりたい気持ちはあっても、どういう風に進めたらよいか分からない!」</p> <p>「勉強会の開き方が分からないからそもそも依頼のイメージがつかない!」</p> <p>上記のような悩みに対する回答になれば幸いです。</p> <h3 id="勉強会のケース">勉強会のケース</h3> <h4 id="輪読会">輪読会</h4> <p>■ 概要</p> <p>最も手軽に進められるのは、本を用意するだけで開催できる輪読会形式です。 (もっとも、本の選定は悩みどころですが……。)</p> <p>輪読会は、チームや参加者間で共通言語を作るきっかけになります。</p> <p>この会の最終的な着地点は読んだ本の感想をシェアしたり意見を交換したりすることですが、基本的な進め方としては以下の2種類があります。</p> <ul> <li>参加者全員が同じタイミングで同じ本を読む</li> <li>参加者にそれぞれの担当を割り振り、要約してきてもらう</li> </ul> <p>前者はより活発な意見交換を促すことができるため、ビジネス書などの一般的な内容の理解を助けてくれます。後者は深い知識の習得・理解に向いているため、どちらかといえば専門書などを題材にする際に役立ちます。</p> <p>■ 開催にあたって</p> <p>輪読会を開催する際によくある困りごとは、以下が多いかと思われます。</p> <ul> <li>本を読む時間がない</li> <li>アウトプットの方法が分からない</li> </ul> <p>前者を解決するには、輪読会の時間に書籍を読む時間を予め設けておくのがおすすめです。章やページの区切りをその時間で読み切れるくらいに設定しておくことで、参加者のハードルを下げることができます。</p> <p>後者を解決するには、感想を記載するためのテンプレートを使用することが効果的です。</p> <p>例えば弊社では、以下のようなものを使用しています。</p> <pre class="code" data-lang="" data-unlink>1. 印象に残ったことを箇条書きで3つ取り上げる 2. その詳細を取り上げた項目の下に書き、強調したい部分にハイライトをあてる 3. 2.で取り上げた項目を「過去の自分はどうだったか?」という視点で深掘りする 4. 3.で深掘りしたものの中から、次に自分が実践したいものを選ぶ</pre> <h4 id="LT会">LT会</h4> <p>■ 概要</p> <p>LT会(= Lightning Talkの略)は、1人5分〜10分程度の発表を複数人で回す形式で行われます。個人の興味のあるトピックや近況報告など、大まかなテーマが最初から決まっていることが多いです。</p> <p>例えば弊社では、以下のようなテーマでLT会が行われました。</p> <ul> <li>今年1年間の業務を振り返りつつ紹介する</li> <li>管理系の職種は、普段どんな仕事をしているのか?</li> <li>今までの仕事における「しくじり」について</li> </ul> <p>こうした機会を設けることで普段の業務ではできないようなラフな質問がしやすくなり、施策や他職種の仕事への理解を深めることができます。</p> <p>ぜひ、コミュニケーションの一手段として活用してみてください!</p> <p>■ 開催にあたって</p> <p>LT会を開催する際によく起こるのが、登壇者が集まりにくく開催が難しくなることです。</p> <p>これを解決するには、もちろん社内でのこまめな告知も重要になるのですが、開催時期を考えることも重要になります。</p> <p>例えばLTのテーマを「第3クォーターで印象に残ったこと」「夏期休暇で触れたプロダクトと印象に残った点」などに絞り込むことで、参加者は無数にある話題からネタを絞り込んで考える必要がなくなります。(テーマを自由に設定するよりは最初から決まっていた方がやりやすい気がしますが、みなさんはいかがでしょうか?)</p> <p>ちなみに資料作成については、(エンジニア界隈では有名な?)「<a href="https://memo.sanographix.net/post/82160791768">大体いい感じになるテンプレート</a>」などを使用すると大体いい感じになるようです。</p> <h4 id="ワークショップ">ワークショップ</h4> <p>■ 概要</p> <p>ワークショップは、主催者があるテーマに沿っていくつか課題を用意し、参加者が取り組んだ成果についてフィードバックや議論を行う形式で行われます。個人の興味のあるトピックや近況報告など、大まかなテーマが最初から決まっていることが多いです。</p> <p>弊社で行われたワークショップの例を紹介します。</p> <ul> <li><a href="https://tech.mobilefactory.jp/entry/2020/12/13/000000">身の回りのものやサービスから、UXについて掘り下げを行う</a></li> <li>自社ゲームのイベントで配付するアイテムの種類と量を考えてみよう</li> </ul> <p>■ 開催にあたって</p> <p>ワークショップは、課題を用意する工数が膨らんでしまうのが悩みどころです。</p> <p>ひとつの解決法としては、<strong>「体験してほしいものは何かを考え、普段の業務を極限まで単純化する」</strong>ことが考えられます。</p> <p>例えば上で挙げた「自社ゲームのイベントで配付するアイテムの種類と量を考えてみよう」は、ディレクターが行っている業務の一部を切り出して課題化したものになります。</p> <p>普段はスプレッドシートにいちから作っていくものを穴埋め形式にしてみたり、必要なデータや資料を予めこちらで揃えておいたりするなど、体験してほしい事柄に結びつかないタスクはすべて削ぎ落としました。</p> <p>「あれもこれもこの機会に知ってもらおう!」というよりは、<strong>「他はいいからこれだけは知ってほしい!」というものを抽出する</strong>ことがワークショップ開催の第一歩です。</p> <h4 id="プレゼン講義形式-質疑応答">プレゼン・講義形式(+ 質疑応答)</h4> <p>■ 概要</p> <p>個人が作成したドキュメントやスライドを用いて発表を行い、参加者が質疑応答を行う一般的な形式です。</p> <p>■ 開催にあたって</p> <p>こうした形式の勉強会は発表者にかかる準備面での負担が大きいため、ワークショップ形式と同様に開催が難しくなりがちです。</p> <p>もし開催したい場合、以下のような方法を取ると工数を抑えられるのでおすすめです!</p> <ul> <li>まずはLT会から開催し、そこで発表したテーマを掘り下げる</li> <li>聞きたいテーマに対する質問を予めまとめておき、登壇者にその場で答えてもらう形式を取る</li> </ul> <h3 id="具体例">具体例</h3> <p>ここまでは勉強会の手法や、開催コストを下げる方法についてお話しました。 よりイメージがつきやすくなるように、以下で実際に開催された勉強会の例を紹介いたします。</p> <h4 id="ゲームプランニング勉強会">ゲームプランニング勉強会</h4> <p>■ 種別</p> <p>ワークショップ</p> <p>■ 概要</p> <p>自社のプロダクト「駅メモ!」を題材としてゲームデザインの初歩の初歩に触れることで、プロダクトやディレクターの業務・思想への理解を深めてもらうことが目的。簡単なワークから運営視点を身につけ、ゲームにおけるプロダクト分析の足がかりとする。</p> <p>■ 準備したもの</p> <ul> <li>実際の業務で使用するスプレッドシートを簡略化したもの</li> <li>会の進め方に関するドキュメント</li> </ul> <p>■ 会の流れ</p> <ul> <li>主催者(ディレクター)から、自社ゲームにおけるアイテム配付の思想について解説(5min.)</li> <li>直近開催したゲーム内イベントなどの事例に基づいて、アイテムの需要や優先順位について認識合わせを行う(5min.)</li> <li>スプレッドシートで空欄になっている箇所に、ユーザーに配付するアイテムの種別と量を入力する(15min.)</li> <li>実際のイベントの事例を見せながら、個別でフィードバック(5min.)</li> <li>その他質疑応答(Slackで随時受付)</li> </ul> <h4 id="イシューからはじめよ輪読会">『イシューからはじめよ』輪読会</h4> <p>■ 種別</p> <p>輪読会</p> <p>■ 概要</p> <p>書籍『イシューからはじめよ』を通して、普段の業務の進め方や施策検討の仕方について振り返りを行う。</p> <p>■ 準備したもの</p> <ul> <li>参加者が感想を記載するドキュメント</li> <li>本(及び書籍購入支援制度についての紹介)</li> </ul> <p>■ 会の流れ</p> <ul> <li>主催者が指定した対象のページを事前に読んでくる(開催前)</li> <li>テンプレートに沿って、ドキュメントに感想を記入(10min)</li> <li>それぞれの感想について掘り下げ・討論を行う(20min)</li> </ul> <p>■ 補足:使用したテンプレート</p> <p>この会では、以下のテンプレートに沿って感想の記入を行いました。</p> <pre class="code" data-lang="" data-unlink>ビフォー:「この本を読む前の私は〇〇でした」 気づき:「この本を読んで私は、〇〇について気づきました」 To Do:「今後、〇〇を実行していこうと思います」</pre> <h2 id="おわりに">おわりに</h2> <p>この記事では、以下を紹介しました。</p> <ul> <li>エンジニア職以外が主催する社内勉強会によって、エンジニアが得られるメリット</li> <li>社内勉強会の主なケース・開催時に気をつけること</li> <li>弊社における事例(サンプル)</li> </ul> <p>「良いモノ」を作るための一手段として、チームでの共通言語を増やし相互理解を深めるきっかけ(=社内勉強会)を作ってみてはいかがでしょうか?</p> <p>もし社内のメンバーに提案してみて「どう進めていいかわからないから勉強会が開けない!」と言われたら、この記事を紹介してみてください。</p> Torch4083 react-scroll で、 iOS の特定バージョン以降でも正しくアニメーションさせたい hatenablog://entry/4207112889947018995 2022-12-23T00:00:00+09:00 2022-12-23T00:00:32+09:00 こんにちは、 id:yunagi_n です。 本日の記事は React のお話です。 React で良い感じにスクロールしてくれるライブラリで、有名なものに react-scroll というものがあります。 これは、 JS からライブラリのメソッドを呼び出すことで、もしくは組み込みコンポーネントを使うことで、アニメーションさせながら自動的にその場所に行ってくれて便利なのですが、 iOS Safari でのみ発生するバグがあるので、そのワークアラウンドを紹介します。 バグの内容 以下のように、メソッド経由で ID 要素をつかってスクロールさせる場合、 iOS Safari の 15.4 以降でア… <p>こんにちは、 <a href="http://blog.hatena.ne.jp/yunagi_n/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/yunagi_n/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:yunagi_n</a> です。<br/> 本日の記事は React のお話です。</p> <p>React で良い感じにスクロールしてくれるライブラリで、有名なものに <a href="https://github.com/fisshy/react-scroll">react-scroll</a> というものがあります。</p> <p>これは、 JS からライブラリのメソッドを呼び出すことで、もしくは組み込みコンポーネントを使うことで、アニメーションさせながら自動的にその場所に行ってくれて便利なのですが、 iOS Safari でのみ発生するバグがあるので、そのワークアラウンドを紹介します。</p> <h2 id="バグの内容">バグの内容</h2> <p>以下のように、メソッド経由で ID 要素をつかってスクロールさせる場合、 iOS Safari の 15.4 以降でアニメーションされません (参照: <a href="https://github.com/fisshy/react-scroll/issues/502">fisshy/react-scroll#502</a>)。<br/> ただし、 Android や PC Chrome などでは正常に動作します。厄介ですね。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> React<span class="synStatement">,</span> <span class="synIdentifier">{</span> useCallback <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> scroller <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;react-scroll&quot;</span> <span class="synType">const</span> SomeComponent: React.FC <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> onClickScrollToItem <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> scroller.scrollTo<span class="synStatement">(</span><span class="synConstant">&quot;item&quot;</span><span class="synStatement">,</span> <span class="synConstant">true</span><span class="synStatement">)</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">[]</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synComment">// ... とても縦長な要素</span> <span class="synStatement">&lt;</span>div id<span class="synStatement">=</span><span class="synConstant">&quot;item&quot;</span><span class="synStatement">&gt;</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="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <h2 id="ワークアラウンド">ワークアラウンド</h2> <p>さすがに iOS 限定で動かない、というのも困るので、ワークアラウンドで回避しましょう。 といってもやり方は簡単で、単純に自前でスクロール位置を計算してあげれば良いのです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> React<span class="synStatement">,</span> <span class="synIdentifier">{</span> useCallback<span class="synStatement">,</span> useRef <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> animateScroll <span class="synStatement">as</span> scroller <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;react-scroll&quot;</span> <span class="synType">const</span> SomeComponent: React.FC <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> elem <span class="synStatement">=</span> useRef<span class="synStatement">&lt;</span>HTMLDivElement<span class="synStatement">&gt;(</span><span class="synType">null</span><span class="synStatement">)</span> <span class="synType">const</span> onClickScrollToItem <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">if</span> <span class="synStatement">(</span>elem.current<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> toY <span class="synStatement">=</span> elem.current.getBoundingClientRect<span class="synStatement">()</span>.<span class="synConstant">top</span> + <span class="synSpecial">window</span>.scrollY scroller.scrollTo<span class="synStatement">(</span>toY<span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">[]</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synComment">// ... とても縦長な要素</span> <span class="synStatement">&lt;</span>div ref<span class="synStatement">=</span><span class="synIdentifier">{</span>elem<span class="synIdentifier">}</span><span class="synStatement">&gt;</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="synStatement">)</span> <span class="synIdentifier">}</span> </pre> <p>これで、 iOS Safari 15.4 以降はもちろん、その他のプラットフォームでも正常に動作するようになりました。 お疲れ様でした。</p> yunagi_n 駅メモ!の地図をiOS16リリースに伴って負荷軽減した話 hatenablog://entry/4207112889946745938 2022-12-22T00:00:00+09:00 2022-12-22T00:00:23+09:00 はじめに id:wgg00sh です。 この記事では、2022年9月にリリースされた iOSの新バージョン 16.0 に向けて、駅メモ!の地図クライアントで行った対応について紹介します。 駅メモ!の地図について 昨年度のアドベントカレンダー で紹介していますが、駅メモ!のアプリ内地図は mapbox-gl-js を使用しています tech.mobilefactory.jp iOS16で発生していた問題 2022年9月頃、正式にリリースされる前のβ版iOS16で駅メモ!の動作を見ていたところ、意図しない表示になることがありました。 そして、特に地図を開いた直後にその問題が発生しやすいとわかったた… <h2 id="はじめに">はじめに</h2> <p><a href="http://blog.hatena.ne.jp/wgg00sh/">id:wgg00sh</a> です。 この記事では、2022年9月にリリースされた iOSの新バージョン 16.0 に向けて、駅メモ!の地図クライアントで行った対応について紹介します。</p> <h2 id="駅メモの地図について">駅メモ!の地図について</h2> <p><a href="https://tech.mobilefactory.jp/entry/2021/12/15/000000">昨年度のアドベントカレンダー</a> で紹介していますが、駅メモ!のアプリ内地図は <a href="https://docs.mapbox.com/jp/mapbox-gl-js/overview/">mapbox-gl-js</a> を使用しています <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.mobilefactory.jp%2Fentry%2F2021%2F12%2F15%2F000000" title="Mapbox GL JS で大量のデータを可視化する - Mobile Factory Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.mobilefactory.jp/entry/2021/12/15/000000">tech.mobilefactory.jp</a></cite></p> <h2 id="iOS16で発生していた問題">iOS16で発生していた問題</h2> <p>2022年9月頃、正式にリリースされる前のβ版iOS16で駅メモ!の動作を見ていたところ、意図しない表示になることがありました。 そして、特に地図を開いた直後にその問題が発生しやすいとわかったため、自分は地図の負荷を減らす方向で問題の軽減を進めました。</p> <h2 id="mapbox-gl-jsの-Safari-における問題">mapbox-gl-jsの Safari における問題</h2> <p><a href="https://github.com/mapbox/mapbox-gl-js/issues/8437">こちらの issue</a> に問題の詳細が書かれています。 mapbox-gl には、 <code>map.remove()</code> という終了用のクリーンアップを行うAPIがありますが、この処理に問題があり Safari では正常にクリーンアップされずメモリリークが発生してしまいます。 ここでは Safari と書いていますが、 iOSにおいてはサードパーティ製のブラウザもSafariと同一の WebKit を使用しているので、 例えばWebブラウザで動作するアワメモ!を iOS Chrome でプレイしていてもこの問題は避けられません。 また、駅メモ!では mapbox-gl-js の v1.13.0 を使用していましたが、当時バージョンの v2.10.0 でもこの問題は解消されていません。</p> <h2 id="iOS16対応以前-202209">iOS16対応以前 (~2022/09)</h2> <p>このSafariの問題に対する解決策が見つかっていなかった当初は駅メモ!内で地図の開閉を繰り返すことで動作が極端に重くなったり、Webページがクラッシュして強制的にブラウザがリロードされてしまう事がありました。 そこで地図画面に以下の暫定的対処をしていました。</p> <p>地図を閉じた際にmapbox-glのインスタンスを破棄せず全て保持して、再度地図を開く場合にはそのインスタンスを再利用するというものです。</p> <p>これによって地図を複数回開くことによるメモリリークを防ぐことはできたものの、地図を使用していない間もメモリを食っておりパフォーマンス的にはあまり良い状態とは言えませんでした。 iOS16ではこの状態で駅メモ!をプレイし続けると、他の問題も合わせて表示の不具合が発生しやすいとわかったため、この問題を解消することになりました。</p> <h2 id="解決">解決</h2> <p>この問題に対して他にも苦戦していた方がいたのか、偶然同時期に <a href="https://github.com/mapbox/mapbox-gl-js/pull/12224">同じ問題に関するPR</a> が他の方から上げられました。 diff を見ると、webglcontextlost などの webgl周りのイベントを破棄できていなかったり、canvas の初期化ができていなくてGCが機能していなかった様に見えます。 このPRと同等の内容を、 駅メモで使用しているmapbox-gl-js v1.13 に適用して解決を図ります。</p> <h2 id="駅メモで動作を確認">駅メモ!で動作を確認</h2> <p>修正を適用した mapbox-gl を使用して、実際に駅メモ!上での動作を確認してみます。</p> <table> <thead> <tr> <th> 修正前 </th> <th> 修正後 </th> </tr> </thead> <tbody> <tr> <td> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221222/20221222000019.png" width="800" height="651" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </td> <td><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221222/20221222000015.png" width="800" height="651" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </td> </tr> </tbody> </table> <p>Safariのインスペクタを開いた状態で地図の開閉を繰り返し行ったところ、修正後は地図画面を閉じると使われなくなった Canvas が削除されるようになりました。 また、開閉を繰り返すことでブラウザが強制リロードされる問題も発生しなくなることが確認できました。</p> <h2 id="終わりに">終わりに</h2> <p>iOS16がリリースされるにあたって、駅メモ!で行った対応は地図だけでなく他にもたくさんありましたが、今回は自分が担当した地図の問題解消について紹介しました。 とはいえ偶然同時期に同じ問題を解消された方が居てそれを利用させていただいた形でしたので、このPRが上がっていなければiOS16 がリリースされるまでに解消するのは困難でした。</p> <p>また、今回導入した実装は現状 iOS16 で駅メモ!を実行した場合にのみ動作するようになっています。 iOS15以前では引き続きmapbox-gl のインスタンスを保持して、地図を開き直す場合には再利用する形になっています。</p> wgg00sh 溜まっていく一方な技術的負債をどうにかしたい話 hatenablog://entry/4207112889946732290 2022-12-21T00:00:00+09:00 2022-12-21T00:00:03+09:00 駅メモ!開発チームエンジニアの id:yokoi0803 です。 駅メモ!チームで運用している「駅メモ! - ステーションメモリーズ!-」は今年で 8 周年を迎えました。 スマートフォン向けゲームとしては長く続くサービスとなりましたが、長期運用に伴ってそのコードベースは大きく、複雑になり、保守性の面での課題が段々と無視できなくなってきています。 しかし課題だと認識されているにも関わらず、その改善、つまりリファクタリングを行う機会は少なく、結果としてコードの複雑さは増す一方になっています。 この記事では、上記の問題はなぜ起きているのか、現状を再認識し、問題を解決するために考えたことを記述します。… <p>駅メモ!開発チームエンジニアの <a href="http://blog.hatena.ne.jp/yokoi0803/">id:yokoi0803</a> です。</p> <p>駅メモ!チームで運用している「駅メモ! - ステーションメモリーズ!-」は今年で 8 周年を迎えました。</p> <p>スマートフォン向けゲームとしては長く続くサービスとなりましたが、長期運用に伴ってそのコードベースは大きく、複雑になり、保守性の面での課題が段々と無視できなくなってきています。</p> <p>しかし課題だと認識されているにも関わらず、その改善、つまりリファクタリングを行う機会は少なく、結果としてコードの複雑さは増す一方になっています。</p> <p>この記事では、上記の問題はなぜ起きているのか、現状を再認識し、問題を解決するために考えたことを記述します。 直接、技術に関わる話はありませんが、読んでみて、自分のチームやプロダクトはどうなっているのか、思い返すきっかけになれば幸いです。</p> <h4 id="開発チームの構成">開発チームの構成</h4> <p>後の話を理解しやすくするため、まず開発チームの構成について軽く説明します。</p> <p>開発チームの主な役割は駅メモ!内のコンテンツであるでんこやイベントの開発、その他機能の追加や修正です。 技術職(エンジニア)と非技術職(ディレクター、マネージャー)で構成され、チームで行うタスクはほとんど非技術職が主体となって決定しています。 エンジニアはそれらタスクのうち、主にコーディングが関わる部分を受け持っています。</p> <h2 id="現状の整理">現状の整理</h2> <p>記事の冒頭で述べた通り、当プロダクトのコードは保守性の面で課題を抱えています。 今回は保守性に影響を与える要素として「技術的負債」に焦点を当て、現状を整理しました。</p> <h4 id="技術的負債の発生">技術的負債の発生</h4> <p>開発チームの業務の忙しさには年間を通して波があります。 繁忙期にはエンジニアもほぼフル稼働することになり、開発における余剰リソースがない状態になります。 そのような状態でコードの複雑性と納期が相まって、どうしても「その場しのぎ」な実装にせざるを得ないことがあります。</p> <p>また、当時最善だと思った実装だとしても、機能追加などでコードが変化していくにつれて最善ではなくなっていたというケースもあります。</p> <h4 id="技術的負債の解消">技術的負債の解消</h4> <p>繁忙期がある一方で閑散期もあり、このタイミングでは業務の改善をチームで積極的に取り組んでいます。 ただし、先述したようにチームのタスクは非技術職が主体となって決定するため、どうしても非技術職の視点から見える範囲の作業効率化などが優先されてしまいます。 エンジニアとしても、技術的負債があるからと言って直ちに問題があるわけではないため、その解消をすることの優先度を下げてしまっています。</p> <p>ここまでのように現状をまとめると、<strong>技術的負債は溜まっていく一方になっている</strong>ことが明らかになりました。</p> <h2 id="問題はどこか">問題はどこか</h2> <p>技術的負債が生まれることはビジネス上、避けられないものです。その前提で考えると、生まれた負債の解消、つまりリファクタリングをする機会を作っていない事が問題でしょう。 ではなぜ機会がないのかというと、<strong>技術的負債が溜まっていることを、非技術職を含むチーム全体が課題として認識していない</strong>からだと考えました。</p> <h2 id="チームとしての課題にするには">チームとしての課題にするには</h2> <p>コードベースの詳細な事情はエンジニアしか知り得ませんし、非技術職に対してそのまま説明しても成果が目に見えるような課題と優先度比較をできず、チームとしての課題に上げることは難しいです。ですから、お互いにとって共通の概念であるプロダクトを基準にしてみます。</p> <p>このままだとどのようなリスクがあるか、リファクタリングの意義は何か、考えてみました。</p> <h4 id="このまま技術的負債が溜まるとどうなるか">このまま技術的負債が溜まるとどうなるか</h4> <ul> <li>複雑さ故に実装速度が下がり、バグの混入率は上がる</li> <li>実装中、負債にぶつかる可能性が高くなり、その解消をしなければ作業が進められない場合は想定外の工数がかかる</li> <li>上記 2 点を考慮すると、作業見積もりは不正確、あるいはマージンを取らざるを得なくなる</li> </ul> <h4 id="プロダクトが抱えるリスク">プロダクトが抱えるリスク</h4> <ul> <li>実装速度が下がったり、見積もりが不正確になるため、ユーザに届けられる価値が減少する</li> <li>不具合の発生率が増加する</li> </ul> <p>リファクタリングの意義については、「プロダクトが抱えるリスク」の解消になります。</p> <p>これで一応、技術的負債の解消をチーム内の他の課題と同列に扱う根拠は得られました。</p> <h2 id="問題の解決に向けて">問題の解決に向けて</h2> <p>技術的負債が溜まっていることは課題であり、リファクタリングの意義についても明らかになりましたが、まだチーム内の成果が目に見えるものと優先度比較をすることは難しいです。 何がどのくらい良くなるのか、定量的に評価ができないからです。</p> <p>とはいえ、やる意味があることは明らかですし、やってみることで効果を計測できるようになるという考えのもと、弊チームでは定期的にリファクタリングを集中的に行う時間を設けることを考えています。</p> <p>また、リファクタリングの定量的な評価については別軸で動きがありまして、つい先日の記事にもなっていますので、興味があればそちらも是非読んでみてください。</p> <p><a href="https://tech.mobilefactory.jp/entry/2022/12/19/000000">Perl コードの「複雑さ」を計測する</a></p> <h2 id="まとめ">まとめ</h2> <ul> <li>弊チームでは現状、技術的負債を解消する機会が存在せず、溜まっていく一方になっている</li> <li>なぜなら、技術的負債の解消が、チーム内の他の課題と同じステージに立てていないから <ul> <li>非技術職では事情を知り得ないので、エンジニアが問題を認識し、それを提起しなければ状況は変わらない</li> </ul> </li> <li>技術的負債が溜まることでプロダクトがリスクを抱えることになる、という職種関係ない概念で認識を合わせるべき</li> <li>とりあえず、リファクタリングの時間を取ってみよう</li> </ul> yokoi0803 2022年のVSCodeのPerl開発環境 hatenablog://entry/4207112889946405182 2022-12-20T00:00:00+09:00 2022-12-20T11:35:44+09:00 こんにちは、エンジニアの id:mp0liiu です。 自分が所属しているチームでは現在もPerl製のプロダクトを運用しており、VSCode で Perl のコードを書いたり触ったりする機会が多いです。 Perl は開発環境が貧弱で他の言語と比べるとあまり開発体験はよくありませんが、それでも少しずつ便利な拡張機能が充実していってるので、この記事では自分が利用している便利な VSCode の Perl 向け拡張機能を紹介します。 Perl Navigator marketplace.visualstudio.com 今年話題になった Languager Server を利用した拡張機能です。 他… <p>こんにちは、エンジニアの <a href="http://blog.hatena.ne.jp/mp0liiu/">id:mp0liiu</a> です。</p> <p>自分が所属しているチームでは現在もPerl製のプロダクトを運用しており、VSCode で Perl のコードを書いたり触ったりする機会が多いです。<br/> Perl は開発環境が貧弱で他の言語と比べるとあまり開発体験はよくありませんが、それでも少しずつ便利な拡張機能が充実していってるので、この記事では自分が利用している便利な VSCode の Perl 向け拡張機能を紹介します。</p> <h1 id="Perl-Navigator">Perl Navigator</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dbscan.perlnavigator" title="Perl Navigator - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=bscan.perlnavigator">marketplace.visualstudio.com</a></cite></p> <p>今年話題になった Languager Server を利用した拡張機能です。</p> <p>他にも Perl の Languager Server を利用した拡張機能はいくつか種類がありますが、以前から存在する拡張機能と比べると自動補完やコードジャンプがちゃんとできたり、<a href="https://metacpan.org/pod/Perl::Critic">Perl::Critic</a>、<a href="https://metacpan.org/pod/Perl::Tidy">Perl::Tidy</a>、<a href="https://metacpan.org/dist/App-perlimports/view/script/perlimports">perlimports</a> をまとめて扱ってくれる点が優れています。</p> <p>環境にもよりますが、インストールするだけで基本的な機能は動作してくれます。<br/> Perl::Critic と perlimports はバンドルされていないので別途インストールする必要があります。</p> <p>使える機能はだいたい次のような感じです</p> <ul> <li>文法チェック</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/mobile-factory/20221220010107" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010107.png" width="800" height="155" loading="lazy" title="" class="hatena-fotolife" style="width:800px" itemprop="image"></a></span></p> <ul> <li>Perl::Critic によるコードの静的検査</li> </ul> <p>サブルーチンの返り値を return で返しているかどうかをチェックする Subroutines::RequireFinalReturn を有効にしてチェックしたときの様子です</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/mobile-factory/20221220010006" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010006.png" width="800" height="256" loading="lazy" title="" class="hatena-fotolife" style="width:700px" itemprop="image"></a></span></p> <ul> <li>自動補完</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/mobile-factory/20221220010009" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010009.png" width="800" height="166" loading="lazy" title="" class="hatena-fotolife" style="width:600px" itemprop="image"></a></span></p> <ul> <li>コードジャンプ</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010012.gif" width="360" height="360" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>Perl::Tidy によるコードフォーマット</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010019.gif" width="750" height="350" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>perlimports による use 周りのコードのクリーンアップ</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010033.gif" width="750" height="350" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>Object::Pad, Moose など DSL や keyword プラグイン系の構文のシンタックスハイライトへの対応</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/mobile-factory/20221220010048" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010048.png" width="548" height="508" loading="lazy" title="" class="hatena-fotolife" style="width:350px" itemprop="image"></a></span></p> <h1 id="Perlthe96vscode-perl">Perl(the96.vscode-perl)</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dthe96.vscode-perl" title="Perl - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=the96.vscode-perl">marketplace.visualstudio.com</a></cite></p> <p>ctags によるコードジャンプおよび自動補完と、Perl::Tidy によるコードフォーマットができる拡張機能です。<br/> 利用するには事前に ctags をインストールしておく必要があります。</p> <p>機能的には Perl Navigator と重複しているのですが、 ctags を利用してコードジャンプや自動補完をしているので精度が悪くなるかわりに動的にモジュールをロードしている場合や型を推測しにくい変数からもコードジャンプや補完が効くといったメリットがあるため、 Perl Navigator と併用しつつ邪魔と感じたら無効化したりしています。</p> <p>引数をポップアップで表示してくれるという点も少し嬉しいです。<br/> (名前付き引数や引数のバリデーターには対応していませんが・・・)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/mobile-factory/20221220010051" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010051.png" width="800" height="208" loading="lazy" title="" class="hatena-fotolife" style="width:500px" itemprop="image"></a></span></p> <h1 id="Perl-insert-package">Perl insert package</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dutgwkk.perl-insert-package" title="Perl insert package - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=utgwkk.perl-insert-package">marketplace.visualstudio.com</a></cite></p> <p>開いているファイル名と対応したパッケージ名を入力してくれるプラグインです。<br/> コマンドパレットから実行するか、自動補完もオプションで有効にできます。</p> <p>巨大なプロジェクトだと名前空間が深くなっていちいちファイル名と対応したパッケージ名を手動で入力するのは大変だし typo すると気づきにくくて大変なので重宝しています。</p> <h1 id="Perl-Rename-Symbol">Perl Rename Symbol</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dutgwkk.perl-rename-symbol" title="Perl Rename Symbol - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=utgwkk.perl-rename-symbol">marketplace.visualstudio.com</a></cite></p> <p><a href="https://metacpan.org/pod/App::PRT">App::PRT</a> や <a href="https://metacpan.org/pod/App::EditorTools">App::EditorTools</a> を利用して変数、メソッド名、パッケージ名など識別子を正確に rename してくれる拡張機能です。<br/> リファクタリングをするときなどに重宝しています。</p> <p>別途 App::PRT と App::EditorTools のインストールが必要です。</p> <h1 id="perl-auto-use">perl-auto-use</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Dtjmtmmnk.perl-auto-use" title="perl-auto-use - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=tjmtmmnk.perl-auto-use">marketplace.visualstudio.com</a></cite></p> <p>まだ use していないモジュールがある場合はファイルの先頭の方で <code>use $module</code> を挿入してくれる拡張機能です。<br/> コマンドパレットから使います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010053.gif" width="750" height="350" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>外部モジュールの関数を使っていてまだ use していない、といった場合も場合も <code>use $module qw( $function );</code> というようにモジュールを use しつつ利用している関数だけインポートしてくれますが、<br/> 同名の関数をもつモジュールが複数あったりすると期待したモジュールが挿入されるとは限らないので注意してください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221220/20221220010110.gif" width="750" height="350" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="Better-Perl-Syntax">Better Perl Syntax</h1> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3Djeff-hykin.better-perl-syntax" title="Better Perl Syntax - Visual Studio Marketplace" 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://marketplace.visualstudio.com/items?itemName=jeff-hykin.better-perl-syntax">marketplace.visualstudio.com</a></cite></p> <p>デフォルトの Perl の シンタックスハイライトから更に以下の字句に異なった色付けをしてくれるようになり、コードが見やすくなります。</p> <ul> <li>数値</li> <li>演算子</li> <li>関数呼び出し</li> <li>正規表現の文字クラス</li> <li><code>^</code> が先頭につく特殊変数、 <code>$^V</code> など</li> <li>関数ブラケット</li> </ul> <p>デフォルトのカラーテーマはこれらの字句の色付けに対応していないので、Material Theme などのカラーテーマと併用する必要があります。<br/> また、Perl Navigator で有効になる DSL や keyword プラグイン系の構文のシンタックスハイライトにはシンタックスハイライトが効かなくなるので、それらのモジュール使っているときは無効にしています。</p> <p>シンタックスハイライトにはいろいろ好みがあると思いますが、見やすくなると思うので一度試してみてはいかがでしょうか。</p> <h1 id="まとめ">まとめ</h1> <p>以上、自分が利用している VSCode の Perl 向け拡張機能を紹介させていただきました。<br/> ちゃんと型をもっている言語と比べるとどうしても劣ってしまいますが、これらの拡張機能を揃えるだけでもかなり開発体験はよくなるのでぜひ試してみてはいかがでしょうか!</p> mp0liiu Perlコードの「複雑さ」を計測する hatenablog://entry/4207112889945351755 2022-12-19T00:00:00+09:00 2022-12-19T00:00:25+09:00 駅メモ!チームでエンジニアをしている id:stakHash です。 弊社の主力プロダクトの 1 つである駅メモ!は、今年で 8 周年を迎えました 🎉 スマートフォンゲームとしては息の長いサービスですが、現在でも日々様々な新機能の開発が進んでいます。 今後も今以上の速度でユーザの皆様に価値提供をしていくためには、分かりやすく変更しやすいコードベースを維持・改善していくことが必要です。 しかし、「分かりやすさ」「複雑さ」という主観的でぼんやりとした感覚値は、長いライブサービスでは、人員の入れ替わりもあって判断が困難になっていました。 そこで、「複雑さ」 を定量的に計測する方法を探ってみました。 … <p>駅メモ!チームでエンジニアをしている <a href="http://blog.hatena.ne.jp/stakHash/">id:stakHash</a> です。</p> <p>弊社の主力プロダクトの 1 つである駅メモ!は、今年で 8 周年を迎えました 🎉</p> <p>スマートフォンゲームとしては息の長いサービスですが、現在でも日々様々な新機能の開発が進んでいます。</p> <p>今後も今以上の速度でユーザの皆様に価値提供をしていくためには、分かりやすく変更しやすいコードベースを維持・改善していくことが必要です。 しかし、「分かりやすさ」「複雑さ」という主観的でぼんやりとした感覚値は、長いライブサービスでは、人員の入れ替わりもあって判断が困難になっていました。</p> <p>そこで、<strong>「複雑さ」</strong> を定量的に計測する方法を探ってみました。</p> <h2 id="複雑さとは">「複雑さ」とは</h2> <p>今回は、Microsoft 社が主に Visual Studio 内で利用している<a href="https://learn.microsoft.com/ja-jp/visualstudio/code-quality/code-metrics-maintainability-index-range-and-meaning?view=vs-2022">保守容易性指数 (Maintainability Index)</a>を扱ってみます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink>MAX( <span class="synConstant">0</span>, (<span class="synConstant">171</span> - <span class="synConstant">5.2</span> * ln( Halstead ボリューム ) - <span class="synConstant">0.23</span> * ( 循環的複雑度 ) - <span class="synConstant">16.2</span> * ln( コード行数 )) * <span class="synConstant">100</span> / <span class="synConstant">171</span> ) </pre> <p>式の通り、これは 0~100 の範囲の値になります。低いほどそのコードが複雑で、保守しにくいことを表します。 Visual Studio では、次のように警告する範囲を決定しているようです。</p> <table> <thead> <tr> <th> 保守容易性指数 </th> <th> 警告色 </th> </tr> </thead> <tbody> <tr> <td> 0~9 </td> <td> 赤 </td> </tr> <tr> <td> 10~19 </td> <td> 黄 </td> </tr> <tr> <td> 20~100 </td> <td> 緑 </td> </tr> </tbody> </table> <p>20 が 1 つの基準値となるでしょうか。</p> <p>さて、2 つの別のメトリクスが出てきましたので、簡単に説明します。</p> <h3 id="Halstead-ボリューム">Halstead ボリューム</h3> <p>1 つ目は <strong>Halstead ボリューム (Halstead Volume)</strong> で、1977 年に Halstead 博士が導入した Halstead complexity measures という一連のメトリクスのうちの 1 つです。 詳しい説明は<a href="https://en.wikipedia.org/wiki/Halstead_complexity_measures">Wikipedia</a>に任せて、式を見ていきます。</p> <pre class="code lang-perl" data-lang="perl" data-unlink>Volume = (オペレータの数 + オペランドの数) * log2(オペレータの種類 + オペランドの種類) </pre> <p>式を見れば分かる通り、コード中の<strong>語彙の複雑さ</strong>に注目していることが分かりますね。</p> <p>Perl においては、 <a href="https://metacpan.org/pod/Perl::Metrics::Halstead">Perl::Metrics::Halstead</a>というパッケージが存在します (駅メモ!のサーバサイドは Perl で実装されています)。</p> <h3 id="循環的複雑度">循環的複雑度</h3> <p>2 つ目は <strong>循環的複雑度 (Cyclomatic Complexity)</strong> です。</p> <p>同じく詳しい説明は<a href="https://ja.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E7%9A%84%E8%A4%87%E9%9B%91%E5%BA%A6">Wikipedia</a>先生にお任せしますが、 線形的に独立した経路の数 <strong>= 構造的な複雑さ</strong> を表します。</p> <p>Perl においては <a href="https://metacpan.org/pod/Perl::Metrics::Simple">Perl::Metrics::Simple</a> で計測できます。</p> <h2 id="計測してみる">計測してみる</h2> <p>本題です。 今回計測したいのは、「複雑さ」という非常に抽象度の高い指標でした。</p> <p>上記で紹介した「保守容易性指数」は「語彙的な複雑さ」と「構造的な複雑さ」の両面を考慮しており、「複雑さ」の計測に適した指標の 1 つであると考えられます。</p> <p>これを計測する Perl モジュールが見つからなかったので、上述した 2 つのモジュールを参考に <a href="https://github.com/stakHash/p5-perl-metrics-maintainability">Perl::Metrics::Maintainability</a> を実装しました。</p> <p>このリポジトリ自体を計測してみると、全て 20 以上をマークしていました。 極端に複雑化していない事が確認できます。</p> <pre class="code txt" data-lang="txt" data-unlink>MI LoC cc volume path -------------------------------------------------------------------------------- 39.67 48 14 1287.07 ./lib/Perl/Metrics/Maintainability/Result.pm 39.89 47 11 1460.16 ./bin/perlmi 39.95 49 14 1100.45 ./lib/Perl/Metrics/Maintainability/File/Result.pm 40.56 47 5 1526.19 ./lib/Perl/Metrics/Maintainability/File.pm 46.19 33 5 720.46 ./lib/Perl/Metrics/Maintainability.pm</pre> <p>では、駅メモ!の実装を計測してみると、全体の約 2% に当たるファイルが 10 を下回っていました。 改善のし甲斐がありそうですね! (ファイルパスは機密保持の観点から削除しています)</p> <pre class="code txt" data-lang="txt" data-unlink>MI LoC cc volume path -------------------------------------------------------------------------------- 0.00 489 140 19917.78 0.00 437 208 23967.10 0.00 693 198 33429.84 0.00 216 199 15096.23 0.00 699 58 42585.91 0.00 1259 208 51969.83 0.00 653 116 31531.67 0.00 538 87 24446.56 0.00 866 109 43274.66 0.00 603 104 30685.96 0.00 417 156 22806.22 ...</pre> <h2 id="まとめ">まとめ</h2> <p>今回の計測により、今まで個々人が漠然と「ここは複雑そうだな」と思っていたものが、数値化・順位づけされて見えるようになりました。</p> <p>実際にどこを改善していくかは、コードを精査する必要がありますが、コードベースの「複雑さ」を抑えていくための目安としては有効に使えそうです。</p> stakHash git submodule update 忘れを防止したい hatenablog://entry/4207112889945335819 2022-12-18T00:00:00+09:00 2022-12-18T00:00:25+09:00 駅メモ!チームエンジニアの id:yumlonne です。 この記事ではスーパープロジェクト(サブモジュールが登録されている親プロジェクト)側で git checkout や git pull を実行したときに、自動で git submodule update 相当の処理を実行してくれる便利な設定を紹介します。 git submodule についてはドキュメントを参照してください。 記事中の各種動作は git version 2.38.1 で確認しています。 背景 私は最近サブモジュールが存在するプロジェクトを触り始めました。 しかし、git 操作をするときにサブモジュールが存在することを意識… <p>駅メモ!チームエンジニアの <a href="http://blog.hatena.ne.jp/yumlonne/">id:yumlonne</a> です。</p> <p>この記事ではスーパープロジェクト(サブモジュールが登録されている親プロジェクト)側で git checkout や git pull を実行したときに、自動で git submodule update 相当の処理を実行してくれる便利な設定を紹介します。</p> <p>git submodule については<a href="https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB">ドキュメント</a>を参照してください。</p> <p>記事中の各種動作は git version 2.38.1 で確認しています。</p> <h1 id="背景">背景</h1> <p>私は最近サブモジュールが存在するプロジェクトを触り始めました。 しかし、git 操作をするときにサブモジュールが存在することを意識していないと、サブモジュールの参照を意図せず書き換えてしまうことがありました。</p> <pre class="code" data-lang="" data-unlink>$ cd MyProject $ git checkout topic-A # サブモジュールをtopic-Aブランチが持っているコミットに向ける $ git submodule update $ git checkout topic-B # ここで git submodule update を忘れると、サブモジュールはtopic-Aの状態から更新されない! $ git add . # 気づかずにコミットすると、topic-Bのサブモジュールの向き先がtopic-Aと同じになってしまう $ git commit -m &#34;hoge&#34;</pre> <p>もちろんコミット前の確認やコードレビューがあるので気がつくことはできますが、pull や checkout をするたびに submodule update を打つのも面倒です。 そこで git config を調べたところ、<code>submodule.recurse</code>というフラグで実現できそうということが分かりました。 このフラグは様々な git コマンドの<code>--recurse-submodules</code>オプションを制御するため、他のよく使いそうな git コマンドに与える影響も調べてみました。</p> <h1 id="各コマンドへの影響">各コマンドへの影響</h1> <p>以下の各コマンドは<code>git config --global submodule.recurse true</code>を設定した上で検証しています。</p> <p>詳細な影響は <code>man git config</code>や<code>man git ${command}</code>で--recurse-submodules オプションの説明を参照してください。</p> <h2 id="switch">switch</h2> <p>ブランチを切り替えたときにサブモジュールも自動で追従します。</p> <pre class="code" data-lang="" data-unlink>$ git submodule status 5b8930e2a251ead82076cf17cab13b95f0ec392d SubProject (5b8930e) $ git switch topic-A Switched to branch &#39;topic-A&#39; $ git submodule status 89c87486bd15a4ebc84a7166a46977806ecfaced SubProject (heads/main-1-g89c8748)</pre> <h2 id="restore">restore</h2> <p>サブモジュールのファイルも復元されるようになります。</p> <pre class="code" data-lang="" data-unlink>$ ls README.md SubProject $ ls SubProject/ README.md $ echo &#34;hoge&#34; &gt;&gt; README.md $ echo &#34;fuga&#34; &gt;&gt; SubProject/README.md $ git status On branch main Changes not staged for commit: (use &#34;git add &lt;file&gt;...&#34; to update what will be committed) (use &#34;git restore &lt;file&gt;...&#34; to discard changes in working directory) (commit or discard the untracked or modified content in submodules) modified: README.md modified: SubProject (modified content) $ git restore . $ git status On branch main nothing to commit, working tree clean</pre> <h2 id="checkout">checkout</h2> <p>checkout でブランチを切り替えた場合は switch 相当、ファイルを復元した場合は restore 相当の動作になります。</p> <h2 id="pull">pull</h2> <p>サブモジュールの新しいコミットも取得し、必要があれば switch と同様に自動で追従してくれます。</p> <pre class="code" data-lang="" data-unlink>$ git submodule status 61b0cf596df0b2c68617be4a31f0feeca270d3f1 SubProject (61b0cf5) $ git pull origin main From github.com:yumlonne/MyProject * branch main -&gt; FETCH_HEAD Fetching submodule SubProject Updating ae9b6ab..edcc372 Fast-forward SubProject | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated detached HEAD. Submodule path &#39;SubProject&#39;: rebased into &#39;5b8930e2a251ead82076cf17cab13b95f0ec392d&#39; $ git submodule status 5b8930e2a251ead82076cf17cab13b95f0ec392d SubProject (5b8930e)</pre> <p><code>submodule.recurse true</code> の設定により、 pull のたびにサブモジュールのフェッチが実行されます。 以下のように fetch の config として<code>on-demand</code>を指定することで、変更されたサブモジュールのみフェッチされるようになります。</p> <p><code>git config --global fetch.recurseSubmodules on-demand</code></p> <h2 id="push">push</h2> <p>スーパープロジェクトで push を実行したとき、サブモジュール側のコミットも一緒に push してくれます。</p> <pre class="code" data-lang="" data-unlink>$ cd SubProject/ $ echo &#34;hoge&#34; &gt;&gt; README.md $ git add . $ git commit -m &#34;update README.md&#34; $ cd ../ $ git add . $ git push origin main Pushing submodule &#39;SubProject&#39; Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 2 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 322 bytes | 322.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:yumlonne/SubProject.git 47cd1b1..be119ef main -&gt; main Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Delta compression using up to 2 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (2/2), 237 bytes | 237.00 KiB/s, done. Total 2 (delta 1), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:yumlonne/MyProject.git 43ddc9d..9030778 main -&gt; main</pre> <p>他にも、config で<code>push.recurseSubmodules</code>を設定するか、push 時にオプションを渡すことで挙動をカスタマイズできます。(長くなるので省略します)</p> <h2 id="grep">grep</h2> <p>サブモジュールのファイルも grep できるようになります。</p> <pre class="code" data-lang="" data-unlink>$ git grep -n hoge SubProject/README.md:2:hoge</pre> <h1 id="まとめ">まとめ</h1> <p><code>submodule.recurse true</code>を設定することで、色々なコマンドがサブモジュールを意識して動いてくれるようになりました。</p> <p>ここでは紹介していないコマンドやオプションもあるので、使う際はお手元の git のバージョンに対応したドキュメントを読むことをおすすめします。</p> yumlonne エンタメ企業で勤める社員のエンタメの楽しみ方 hatenablog://entry/4207112889944748550 2022-12-17T00:00:00+09:00 2022-12-17T00:00:37+09:00 🎄モバイルファクトリー Advent Calendar 2022!毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします! こんにちは。モバファクでマネージャーをしているゆっぴぃです。 タイトルにもある通り、エンタメ企業の社員である私が、どのようにエンタメを楽しんでいるのかをブログ記事として書いてみました。 これを書こうと思った背景 社内でチームメンバーからこんな質問を受けました。 「ゆっぴぃさんって、いつこんなにアニメを見たりゲームをやったりしてるんですか?」 メンバーがこの質問をした背景には、普段の業務における会話の中で、私がアニメの話だっ… <p>🎄モバイルファクトリー Advent Calendar 2022!毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします!</p> <hr /> <p>こんにちは。モバファクでマネージャーをしている<a href="https://yuppy-k.hatenablog.com/">ゆっぴぃ</a>です。</p> <p>タイトルにもある通り、エンタメ企業の社員である私が、どのようにエンタメを楽しんでいるのかをブログ記事として書いてみました。</p> <h2 id="これを書こうと思った背景">これを書こうと思った背景</h2> <p>社内でチームメンバーからこんな質問を受けました。</p> <p>「ゆっぴぃさんって、いつこんなにアニメを見たりゲームをやったりしてるんですか?」</p> <p>メンバーがこの質問をした背景には、普段の業務における会話の中で、私がアニメの話だったりゲームの話をする印象があるからかな(?)と思います。 ただ、その私に対する印象はインプットしている量が多いからではなく、私のアニメ等に関する発言(アウトプット)頻度が多いことが影響しているのではないかなと思っています。 他のメンバーのほうが、たくさんのゲームやアニメを知っているなと私自身は感じています。</p> <p>ちなみに、「いつこんなにアニメ見たりゲームやったりしてるんですか?」の回答は平凡になるので割愛させてください。</p> <h2 id="私が実践するエンタメの楽しみ方">私が実践するエンタメの楽しみ方</h2> <h3 id="楽しいのアウトプット">「楽しい」のアウトプット</h3> <p>とある別のエンタメ企業の方がこんなツイートをしていらっしゃいました。 「エンタメ企業で働くものとして、「自身の思う楽しい」が多くの人に伝わるように言語化することが大事である。」と。</p> <p>私自身、これは働いている上で実感することが多々あります。 チームメンバーを巻き込み、みんなが「楽しい」と思えるものを作るには、企画者自身が「それがどうして楽しいのか」を話せないといけません。 企画者以外でも「我々自身が作っているものはどうすればさらに良くなるのか」を考えて行動することが、よりよいモノを作るうえでは必要です。</p> <p>そのため、日常的にエンタメの面白さを言語化することが、よいモノづくりをするうえで必要な訓練であると考えます。 ゲームやアニメはもちろん、遊園地やアウトドアアクティビティなどなど、「楽しい」経験をしたら言語化すること。 これが私が考え実践している、エンタメ企業で働く人のエンタメの楽しみ方です。</p> <h2 id="実際にどうやっているのか">実際にどうやっているのか</h2> <h3 id="エンタメに触れる時に大切にしていること">エンタメに触れる時に大切にしていること</h3> <p>これまで「アウトプットは大事だ!!」と語ってしまいましたが、私自身、一番大事にしていることは「全力で楽しむこと」です。</p> <p>個人的には「勉強の一環だ!」と思ってエンタメに触れると(個人的には)心から楽しむことができないです。 アウトプットをすることなど忘れて、目の前のエンタメに全力で向き合っています。</p> <p>アウトプットのことは楽しんだ後に考えましょう。</p> <p>そもそも楽しまないと、そのあとのアウトプットも苦労しますよね……?</p> <h3 id="アウトプット時に大切にしていること">アウトプット時に大切にしていること</h3> <p>アウトプットの方法は、あまり真面目に考えないのが吉だと思います。</p> <p>まず一番簡単なのは、友達など身近な人と話すこと。 映画などに一緒に行った場合、そのあとにカフェやファミレスで感想を話しあうなんてことを経験したことがある人は多いと思います。 これもアウトプットの一つの形です。私もよくやります。</p> <p>それ以外に私がやっているのは、いわゆるレビューサイト的なところに書くことです。</p> <p>世の中には便利なサービスがたくさんあって、アニメの感想を書いたり、飲食店の感想を書いたり、エンタメの種類に合わせて自分の記録を残すことができるようになっています。 意外にも、そういうサイトに感想を投稿するとほんの少しばかりですが、知らない人から反応をいただけることもあったりするものです。</p> <p>私は初めてそういうサイトに投稿したときに反応があるとは予想していなかったので、嬉しい・楽しい気持ちが湧いてきました。</p> <p>アウトプットの手段は様々です。アウトプット自体も「楽しい」と思えるような方法を地道に見つけるのが個人的にはおすすめです。そのほうが継続できるからです。</p> <h2 id="その他のPOINT">その他のPOINT</h2> <h3 id="楽しいをさらに深堀るために">「楽しい」をさらに深堀るために</h3> <p>「楽しい」を言語化していく上で、どのように深堀りをしていくのかを意識したほうが学びは大きいと思います。</p> <p>深堀りの方法について簡単に書くと以下の通りです。<br> <span style="font-size: 80%">※ちなみにSNSで拾った知識だったりしますので、エンタメ業界の公式なお話ではありません</span></p> <ul> <li><p>横に広げていく深堀り:「□□は他の○○と、こういうところが共通していて面白い」</p></li> <li><p>縦に広がていく深堀り:「××が面白いと感じる理由は、▲▲なところにある。そもそも▲だとなぜ面白いかというと~」</p></li> </ul> <p>私はどちらかというと横に広げる掘り方が得意なタイプです。だからこそ、心持ちとしては、いろんなエンタメに触れようとしています。</p> <p>他のチームメンバーと話していると、縦に深堀りをするのが上手だと感じる人ももちろんいます。そういう方は一つのエンタメに対しての情熱が人一倍あり、そのエンタメについて話をしている様子を見るだけで聞き手もワクワクしてしまうものです。</p> <h2 id="最後に">最後に</h2> <p>いかがでしたでしょうか。エンタメを作ることを仕事にしている社会人のエンタメの楽しみ方でした。 ただ「楽しい」だけで済まさずに、言語化していく姿勢はすごく大事です。</p> <p>私自身もまだまだ「楽しい」の言語化が得意とは言えないですし、エンタメは日々変化し進化していくので継続して取り組んでいきます。</p> <p>また、私の所属するチームでは、「楽しい」を共有をする会議があります。厳密には「楽しい」に限定した話ではなく、身の回りにあったことを話す会議です。 その会議には、毎週新しくプレイしたゲームを話してくれる人がいたり、おいしい食べ物の写真を載せてくれるメンバーもいます。</p> <p>このように「楽しい」の言語化をするタイミングも設けて、チームみんなで良いモノづくりができるよう励んでいます。</p> <p>ぜひ皆さんも「楽しい」のアウトプットを意識してみてくださいね!それでは!</p> yuppy_k 業務で登場したDBロック待ちの3つの改善方法 hatenablog://entry/4207112889944998872 2022-12-16T00:00:00+09:00 2022-12-16T00:00:12+09:00 こんにちは。駅奪取チームエンジニアのid:dorapon2000です。 私達のチームでは、4月〜7月にプロダクトの負荷対策に注力しました。その結果、通信量の削減やDB負荷の低減、それに伴うインフラコストの削減などに繋がりました。負荷対策の方法は手探りながら多くのことをしたのですが、その中で今回は不要なDBのロック待ちを改善した部分に注目して、どのような方法でロック待ちを改善したかについてサンプルコードを交えてお話していきます。 ロック待ちの改善にバリエーションがあることについて持ち帰っていただけると幸いです。 環境 Amazon Aurora MySQL version 2 (MySQL 5.… <p>こんにちは。駅奪取チームエンジニアの<a href="http://blog.hatena.ne.jp/dorapon2000/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/dorapon2000/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:dorapon2000</a>です。</p> <p>私達のチームでは、4月〜7月にプロダクトの負荷対策に注力しました。その結果、通信量の削減やDB負荷の低減、それに伴うインフラコストの削減などに繋がりました。負荷対策の方法は手探りながら多くのことをしたのですが、その中で今回は不要なDBのロック待ちを改善した部分に注目して、どのような方法でロック待ちを改善したかについてサンプルコードを交えてお話していきます。</p> <p>ロック待ちの改善にバリエーションがあることについて持ち帰っていただけると幸いです。</p> <h1 id="環境">環境</h1> <ul> <li>Amazon Aurora MySQL version 2 (MySQL 5.7)</li> <li>データベースエンジンはInnoDB</li> <li>トランザクション分離レベルはREPEATABLE READ</li> </ul> <h1 id="ロックとロック待ち">ロックとロック待ち</h1> <p>詳細は他の記事にお譲りします。ここでは簡単な説明をば。</p> <p>ロックをわかりやすく言うと、他の人に自分の作業領域への割り込み作業をさせない仕組みです。 もう少し具体的に言うと、DB内の指定範囲を別のトランザクションからの参照・更新を一時的に不可にさせる仕組みです。指定範囲はテーブル全体だったり、1行だったり、複数行だったりします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221216/20221216000006.png" width="800" height="212" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>特定のレコードをロックすると、他のトランザクションはロックが解除されるまで待たなくてはいけません。これをロック待ちと言います。</p> <h1 id="私のチームで起きていたロック待ちの問題">私のチームで起きていたロック待ちの問題</h1> <p>ロック待ちが瞬間的に連鎖的に発生し、そのうち大半のトランザクションがロックを獲得する前にタイムアウトによってエラーになっていました。 大量のトランザクションがタイムアウトまで負荷をかけ続ける状態です。</p> <p>負荷がかかる様子はAWS RDSのPerformance Insightsで確認できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221216/20221216000009.png" width="800" height="200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="ロック待ちを見つける">ロック待ちを見つける</h1> <p>RDSのPerformance Insightsでは負荷の原因となった発行クエリは見られても、ソースコード内の場所までは特定できません。</p> <p>そのため、私達は以下の方法でロック待ちが多発している箇所を特定しました。</p> <ol> <li>発行クエリからソースコードの該当箇所を予想する</li> <li>負荷がかかりそうな処理のソースコードをコードリーディングする</li> <li>クエリの発行箇所のファイル名と行数をコメントとして発行クエリに付随させる仕組みを自作する</li> </ol> <p>特に3つ目の方法により、Performance Insights 上でクエリの呼び出し元が明確になりました。</p> <h1 id="修正の方針">修正の方針</h1> <p>問題のロック待ちが多発する箇所を特定したら、次は修正です。</p> <ul> <li>本当にそのロックは必要か</li> <li>条件で絞り込み、ロックを取る回数を減らせないか</li> <li>ロックを取る前に早期returnができないか</li> <li>そもそも、その処理は必要か</li> </ul> <h1 id="修正例-利用されないuserロック">修正例① 利用されないuserロック</h1> <p>最も素朴な修正例です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> db-&gt;txn_do(<span class="synStatement">sub </span>{ <span class="synComment"># BEGIN</span> <span class="synIdentifier">$user-&gt;lock</span> <span class="synComment"># SELECT * FROM user WHERE id = :user_id FOR UPDATE;</span> <span class="synStatement">return</span> <span class="synStatement">if</span> <span class="synConstant">9</span>割Trueになる条件; 処理A 処理B <span class="synComment"># 実はuserレコードをロックしている</span> 処理C <span class="synComment"># userレコードのロックが必要</span> }); <span class="synComment"># COMMIT</span> </pre> <p>以下のようにすることで無駄なロックを削除できました。</p> <pre class="code lang-diff" data-lang="diff" data-unlink> db-&gt;txn_do(sub { <span class="synSpecial">- $user-&gt;lock</span> <span class="synSpecial">-</span> return if 9割Trueになる条件; 処理A 処理B 処理C }); </pre> <h2 id="解説">解説</h2> <p>利用されないロックは削除すればいいです。しかし、言うは易く行なうは難し。 実際に利用されていないことを証明するために、userロック以降のすべての処理を目で追いかけました。</p> <p>その結果、処理Bで実は重複してuserロックをしていることがわかり、それ以前でuserロックを利用する処理がないことがわかりました。 トランザクション先頭のuserロックを削除することで、10割userロックしていたコードは1割しかロックしないコードになりました。</p> <h1 id="修正例-早期return">修正例② 早期return</h1> <p>美しくないですが簡単で大きな効果があった修正例です。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> db-&gt;txn_do(<span class="synStatement">sub </span>{ <span class="synComment"># BEGIN</span> <span class="synIdentifier">$user-&gt;lock</span> <span class="synComment"># SELECT * FROM user WHERE id = :user_id FOR UPDATE;</span> <span class="synStatement">return</span> <span class="synStatement">if</span> <span class="synIdentifier">$user-&gt;has_active_license</span>; <span class="synComment"># ほぼここで早期returnする</span> ライセンスが切れている場合の処理 }); <span class="synComment"># COMMIT</span> </pre> <p>以下のようにすることでロックの回数を減らす事ができました。</p> <pre class="code lang-diff" data-lang="diff" data-unlink> db-&gt;txn_do(sub { <span class="synIdentifier">+ return if $user-&gt;has_active_license;</span> $user-&gt;lock return if $user-&gt;has_active_license; ライセンスが切れている場合の処理 }); </pre> <h2 id="解説-1">解説</h2> <h3 id="修正前の問題点">修正前の問題点</h3> <p>has_active_licenseは、ライセンスの有効期限が切れていればFalseを、切れていなければTrueを返すメソッドです。ほとんどの場合で切れていないためTrueを返し、ソースコード上は早期returnします。 修正前のコードの問題点は、ほとんどの場合で早期returnして何もしないにも関わらず、必ずuserロックが取られてしまうことです。</p> <h3 id="ifの前にロックを取る理由">ifの前にロックを取る理由</h3> <p>has_active_licenseの前でロックを取る理由は、最新のユーザ情報を取得したいからです。MySQLでは、SELECT FOR UPDATEをすることで、トランザクション中に別トランザクションでレコード更新が発生しても、更新後の値を読み取ることができます。つまり最新のレコード情報を取得できます。Locking Readと呼ぶようです。詳しくは以下の記事が詳しいです。</p> <p><a href="http://nippondanji.blogspot.com/2013/12/innodbrepeatable-readlocking-read.html?m=1">漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点</a></p> <h3 id="早期returnの重ねがけ">早期returnの重ねがけ</h3> <p>さて、本題である早期returnを重ねて書くことの効果について説明します。第一にコードが冗長以外の副作用がないことは明らかです。第二にロックの回数を減らす事ができます。説明すると言ってもこれだけですが、非常に嬉しいわけです。すべてのコードを目で追いかける必要がありません。</p> <p>Locking Readをしない1回目の早期returnはユーザ情報が古く、ライセンスが切れているのに切れていないと判定される可能性があります。それをLocking Readする2個目の早期returnで拾ってあげます。その逆であるパターンはロジック上存在しないため考慮しません。</p> <h1 id="修正例-キャッシュを使う">修正例③ キャッシュを使う</h1> <p>まずは修正前のコードから。</p> <pre class="code lang-perl" data-lang="perl" data-unlink> db_master-&gt;txn_do( <span class="synStatement">sub </span>{ <span class="synComment"># BEGIN</span> <span class="synIdentifier">$user-&gt;lock</span> <span class="synComment"># SELECT * FROM user WHERE id = :user_id FOR UPDATE;</span> ログイン処理A <span class="synStatement">if</span> <span class="synConstant">1</span>日<span class="synConstant">1</span>回だけ ログイン処理B <span class="synStatement">if</span> イベント参加後に<span class="synConstant">1</span>日<span class="synConstant">1</span>回だけ ログイン処理C <span class="synStatement">if</span> API経由のアクセスを含めて<span class="synConstant">1</span>日<span class="synConstant">1</span>回だけ ログイン処理D <span class="synStatement">if</span> など }); <span class="synComment"># COMMIT</span> </pre> <p>ログイン情報をキャッシュに保存して、キャッシュがないときに限りトランザクションの処理を実行するようにしました。</p> <pre class="code lang-diff" data-lang="diff" data-unlink><span class="synIdentifier">+ my $cache_key = $class-&gt;generate_key($user-&gt;id、日付、イベント参加してるかのフラグ、API経由かどうかのフラグ);</span> <span class="synIdentifier">+ my $is_already_logged_in = cache-&gt;get($cache_key);</span> <span class="synIdentifier">+ return if $is_already_logged_in;</span> db_master-&gt;txn_do( sub { $user-&gt;lock ログイン処理A if 1日1回だけ ログイン処理B if イベント参加後に1日1回だけ ログイン処理C if API経由のアクセスを含めて1日1回だけ ログイン処理D if など }); <span class="synIdentifier">+ cache-&gt;set($cache_key =&gt; 1);</span> </pre> <h2 id="解説-2">解説</h2> <h3 id="修正前の問題点-1">修正前の問題点</h3> <p>ほとんどの場合で処理が何もされないにも関わらず、userロックを取っていることです。</p> <h3 id="userロックを取る理由">userロックを取る理由</h3> <p>前述と同様に最新の情報がほしいためでもありますし、ログイン処理の中で並列に実行されると不具合・不整合が起きる箇所が多くあるためでもあります。</p> <h3 id="キャッシュ利用によるロック回避">キャッシュ利用によるロック回避</h3> <p>修正例③の解決方法は修正例②と思想は同じです。ロックを取る前に条件に合致しないときだけ早期returnして、合致したときは処理を実行するようにしています。 今回はその条件をキャッシュから取得できるにしています。</p> <p>ポイントはキャッシュに使うキー(鍵)です。ログイン処理A/B/C/Dのいずれかを実行する必要があるとき、キャッシュキーが存在していなければいいわけです。例で示します。</p> <ul> <li>その日1回もアクセスしたことがない <ul> <li>キャッシュキーはないため、早期returnされない</li> <li>ログイン処理ABCDが実行される</li> <li><code>login_1_20221216_false_false</code> のような値をキーとしてキャッシュが作成される</li> </ul> </li> <li>その日2回目のアクセス <ul> <li>キャッシュキーは <code>login_1_20221216_false_false</code> ですでに存在し、早期returnされる</li> <li>ログイン処理は実行されない</li> </ul> </li> <li>その日3回目のアクセス時にイベントにも参加していた <ul> <li>キャッシュキーは <code>login_1_20221216_true_false</code> で存在せず、早期returnされない</li> <li>ログイン処理Bのみ実行される</li> <li><code>login_1_20221216_true_false</code> のような値をキーとしてキャッシュが作成される</li> </ul> </li> </ul> <p>キャッシュを使う方法は、ログイン処理全体をリファクタリングせずにロックを削減できる点が嬉しいです。</p> <h1 id="さいごに">さいごに</h1> <p>並列実行を回避するためにuserレコードをロックしたくなりますが、ロックを剥がす労力は地道で相当だということが大きな学びでした。より影響が小さいレコードでロックできないか、そもそもロックを回避できないか。軽い気持ちでuserロックをすると将来痛い目に合うかもしれません。</p> dorapon2000 Android位置情報ライブラリでインターフェースによるテスタビリティ向上を確かめる hatenablog://entry/4207112889944439921 2022-12-15T02:00:00+09:00 2022-12-16T18:03:40+09:00 エンジニアのid:toricorです。今年の初めまではサーバサイド(Perl)のタスクを中心に仕事をしていましたが、その後Android & iOS開発を担当するようになりもうすぐ1年になります。 今日はAndroidの位置情報ライブラリを題材に、インターフェースを活用してテスト用に位置情報のデータソースを差し替えやすくするAndroidのテスト例を紹介します。 play-services-locationの21系ではFusedLocationProviderClientがクラスからインターフェースに変わった 位置情報取得の中心を担うライブラリplay-services-locationの最新… <p>エンジニアの<a href="http://blog.hatena.ne.jp/toricor/">id:toricor</a>です。今年の初めまではサーバサイド(Perl)のタスクを中心に仕事をしていましたが、その後Android &amp; iOS開発を担当するようになりもうすぐ1年になります。</p> <p>今日はAndroidの位置情報ライブラリを題材に、インターフェースを活用してテスト用に位置情報のデータソースを差し替えやすくするAndroidのテスト例を紹介します。</p> <h1 id="play-services-locationの21系ではFusedLocationProviderClientがクラスからインターフェースに変わった"><code>play-services-location</code>の21系ではFusedLocationProviderClientがクラスからインターフェースに変わった</h1> <p>位置情報取得の中心を担うライブラリ<code>play-services-location</code>の最新のリリースのうち、今回はFusedLocationProviderClientの変更に焦点をあてます。</p> <p>アプリケーション開発者はFusedLocationProviderClientを介して位置情報を利用します。FusedLocationProviderClientは、Android端末がGPSやWifiなどから取得した位置情報について、まとめて管理して適切な位置情報を返してくれます。</p> <p>さて、2022年10~11月リリースの<code>play-services-location</code>の21系のリリースノートによるとFusedLocationProviderClientがクラスからインターフェースになったとのことです。</p> <p><a href="https://developers.google.com/android/guides/releases#november_03_2022">21.0.1のリリースノート</a></p> <p><a href="https://developers.google.com/android/guides/releases#october_13_2022">21.0.0のリリースノート</a></p> <blockquote><p>FusedLocationProviderClient, ActivityRecognitionClient, GeofencingClient and SettingsClient are now interfaces instead of classes, which helps enforce correct usage and improves testability. <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdevelopers.google.com%2Fandroid%2Freference%2Fcom%2Fgoogle%2Fandroid%2Fgms%2Flocation%2FFusedLocationProviderClient" title="FusedLocationProviderClient  |  Google Play services  |  Google Developers" 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://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderClient">developers.google.com</a></cite></p></blockquote> <p>インターフェースになったことで <code>improves testability</code> テスタビリティ(テスト容易性)が向上したということです。 <a href="https://developer.android.com/training/testing/fundamentals/test-doubles#example-fake">Androidのテストではインターフェースが共通のテスト用の偽の実装と差し替えるパターンが推奨されています</a>が、実際にテストが容易になったのかをテストを書き実感したいと思います。</p> <h1 id="現在地を取得するgetCurrentLocationメソッドが正しい位置オブジェクトを返すかテストしたい">現在地を取得するgetCurrentLocationメソッドが正しい位置オブジェクトを返すかテストしたい</h1> <p>単純な現在地取得実装を用意しました</p> <ul> <li>GeoLocationRepository内でFusedLocationProviderClientの現在地取得(getCurrentLocation)メソッドを呼び出します</li> <li>GeoLocationRepositoryのコンストラクタはFusedLocationProviderClientを受け取り差し替え可能にします</li> <li>getCurrentLocationが成功すればaddOnSuccessListener、失敗すればaddOnFailureListenerで追加されたリスナーが呼ばれます</li> </ul> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synComment">// 一部省略</span> <span class="synType">class</span> GeoLocationRepository(<span class="synType">private</span> <span class="synType">val</span> locationProvider: FusedLocationProviderClient) { <span class="synType">private</span> <span class="synType">val</span> currentLocationRequest = CurrentLocationRequest.Builder().apply { setPriority(Priority.PRIORITY_HIGH_ACCURACY) setDurationMillis(<span class="synConstant">1000L</span>) setMaxUpdateAgeMillis(<span class="synConstant">30000L</span>) }.build() <span class="synComment">// テスト対象のメソッド</span> <span class="synComment">// 取得したLocationの各値を、自前で用意したLocationPayloadに詰め替える</span> <span class="synIdentifier">@SuppressLint</span>(<span class="synConstant">&quot;MissingPermission&quot;</span>) <span class="synType">suspend</span> <span class="synType">fun</span> getCurrentLocation(): LocationPayload { <span class="synType">val</span> def = CompletableDeferred&lt;LocationPayload&gt;() <span class="synType">val</span> cancellationTokenSource = CancellationTokenSource() <span class="synType">val</span> locationTask: Task&lt;Location&gt; = locationProvider.getCurrentLocation( currentLocationRequest, cancellationTokenSource.token ) locationTask.addOnSuccessListener { location: Location? <span class="synType">-&gt;</span> def.complete( <span class="synStatement">if</span> (location <span class="synStatement">==</span> <span class="synConstant">null</span>) { getEmptyLocationPayload() } <span class="synStatement">else</span> { buildLocationPayload(location) } ) } locationTask.addOnFailureListener { Log.d(<span class="synConstant">&quot;GeoLocationRepository&quot;</span>, <span class="synConstant">&quot;FailureListener @@@@@@&quot;</span>) def.complete(getEmptyLocationPayload()) } <span class="synStatement">return</span> <span class="synStatement">try</span> { def.await() } <span class="synStatement">finally</span> { cancellationTokenSource.cancel() } } </pre> <h2 id="本物のFusedLocationProviderClientを使うテストはセットアップと結果の制御が難しい">本物のFusedLocationProviderClientを使うテストはセットアップと結果の制御が難しい</h2> <p>まず本物のFusedLocationProviderClientクラスをそのまま使うテストを考えます。</p> <p>FusedLocationProviderClientにはmockモードがあり、任意の位置情報を返すようにセットすることができます。 getCurrentLocationを呼び出したときにセットしておいた位置情報が取れたかどうかを確かめるテストを書けます。</p> <p>しかし事前準備は少々手間がかかります。</p> <ul> <li><p>debug/AndroidManifest.xmlに<code>&lt;uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" tools:ignore="ProtectedPermissions" /&gt;</code>を与えます</p></li> <li><p>「設定」->「開発者向けオプション」-> 「仮の現在地情報アプリを選択(Select mock location app)」から対象アプリを指定しておきます</p></li> </ul> <p>(または<a href="https://developer.android.com/jetpack/androidx/releases/test-uiautomator?hl=ja">Uiautomator</a>を利用し<a href="https://android.suzu-sd.com/2020/09/android_geolocation_henkou_testrule/"><code>adb shell appops</code>を使う方法</a>があります )</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synComment">// setMockMode=trueの場合のテスト例</span> <span class="synType">class</span> GeoLocationRepositoryTest { <span class="synType">private</span> <span class="synType">lateinit</span> <span class="synType">var</span> client: FusedLocationProviderClient <span class="synType">private</span> <span class="synType">val</span> location = Location(<span class="synConstant">&quot;mock&quot;</span>).apply { latitude = <span class="synConstant">35.6812362</span> longitude = <span class="synConstant">139.7671248</span> speed = <span class="synConstant">42.0F</span> accuracy = <span class="synConstant">0.68f</span> time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } <span class="synIdentifier">@Before</span> <span class="synType">fun</span> setUp() { <span class="synType">val</span> context = InstrumentationRegistry.getInstrumentation().targetContext client = LocationServices.getFusedLocationProviderClient(context) client.setMockMode(<span class="synConstant">true</span>).addOnFailureListener { <span class="synStatement">throw</span> it } } <span class="synIdentifier">@After</span> <span class="synType">fun</span> tearDown() { client.setMockMode(<span class="synConstant">false</span>).addOnFailureListener { <span class="synStatement">throw</span> it } } <span class="synIdentifier">@Test</span> <span class="synType">fun</span> latitudeIsCorrect() { client.setMockLocation(location).addOnFailureListener { <span class="synStatement">throw</span> it } runTest { <span class="synType">val</span> acquiredLocation = GeoLocationRepository(client).getCurrentLocation() assertEquals(<span class="synConstant">35.6812</span>, acquiredLocation.latitude, <span class="synConstant">0.001</span>) } } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221215/20221215020004.png" alt="" width="800" height="188" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>本物のFusedLocationProviderClientが提供するsetMockModeをtrueにすることで、getCurrentLocationが成功した場合の本物のレスポンスに近しいテストが可能です</li> <li>getCurrentLocationを意図的に失敗させ任意の例外を発生させるようなテストはできません</li> </ul> <h2 id="FakeのFusedLocationProviderClientを使う場合はセットアップと返り値の改変が容易になる">FakeのFusedLocationProviderClientを使う場合はセットアップと返り値の改変が容易になる</h2> <p><code>play-services-location</code>の21系ではFusedLocationProviderClientがインターフェースとなりました。 この結果、本物のFusedLocationProviderClientの代わりに、FusedLocationProviderClientインターフェースを実装する偽のFakeFusedLocationProviderClientをGeoLocationRepositoryに渡すことができるようになりました。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink> <span class="synComment">// 偽のFusedLocationProviderClient</span> <span class="synComment">// 一部省略</span> <span class="synType">class</span> FakeFusedLocationProviderClient : FusedLocationProviderClient { <span class="synComment">// テストケースごとに返り値を変化させるためのフラグ</span> <span class="synType">var</span> shouldFail = <span class="synConstant">false</span> <span class="synType">private</span> <span class="synType">val</span> location = Location(<span class="synConstant">&quot;mock&quot;</span>).apply { latitude = <span class="synConstant">35.6812362</span> longitude = <span class="synConstant">139.7671248</span> speed = <span class="synConstant">42.0F</span> accuracy = <span class="synConstant">0.68f</span> time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } <span class="synType">override</span> <span class="synType">fun</span> getCurrentLocation( p0: CurrentLocationRequest, p1: CancellationToken? ): Task&lt;Location&gt; { <span class="synComment">// https://developers.google.com/android/reference/com/google/android/gms/tasks/Tasks</span> <span class="synStatement">return</span> <span class="synStatement">if</span> (shouldFail) { Tasks.forException(<span class="synType">Exception</span>()) } <span class="synStatement">else</span> { Tasks.forResult(location) } } <span class="synComment">// インターフェースのメンバーを省略</span> <span class="synType">override</span> <span class="synType">fun</span> setMockMode(p0: <span class="synType">Boolean</span>): Task&lt;Void&gt; { TODO(<span class="synConstant">&quot;Not yet implemented&quot;</span>) } } <span class="synIdentifier">@OptIn</span>(ExperimentalCoroutinesApi<span class="synStatement">::</span><span class="synType">class</span>) <span class="synType">class</span> GeoLocationRepositoryWithFakeClientTest { <span class="synType">private</span> <span class="synType">lateinit</span> <span class="synType">var</span> fakeClient: FakeFusedLocationProviderClient <span class="synIdentifier">@Before</span> <span class="synType">fun</span> setupClient() { fakeClient = FakeFusedLocationProviderClient() } <span class="synIdentifier">@After</span> <span class="synType">fun</span> tearDown() { fakeClient.shouldFail = <span class="synConstant">false</span> } <span class="synIdentifier">@Test</span> <span class="synType">fun</span> latitudeIsCorrect() { runTest { <span class="synComment">// FakeのClientを渡す!</span> <span class="synType">val</span> acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals(<span class="synConstant">35.6812362</span>, acquiredLocation.latitude, <span class="synConstant">0.0001</span>) } } <span class="synIdentifier">@Test</span> <span class="synType">fun</span> zeroLatitudeIsAcquiredWhenFail() { runTest { fakeClient.shouldFail = <span class="synConstant">true</span> <span class="synType">val</span> acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals(<span class="synConstant">0.0</span>, acquiredLocation.latitude, <span class="synConstant">0.0001</span>) } } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221215/20221215020001.png" alt="" width="800" height="211" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここまでで、次の2点でテスタビリティ向上を確認できました。</p> <ul> <li>テスト用の偽のFusedLocationProviderClientに置き換えることで、getCurrentLocationの返り値を自由に変更できるようになりました <ul> <li>getCurrentLocation失敗時のFailureリスナーを呼び出しやすくなりました</li> </ul> </li> <li>ACCESS_MOCK_LOCATION権限付与は不要になりセットアップが簡単になりました</li> </ul> <h1 id="まとめ">まとめ</h1> <p><code>play-services-location</code>の21系を使うテストでは、本物ではなく偽のFusedLocationProviderClientを使うことで、テスト対象メソッドの結果の制御がしやすくなったりセットアップが単純になったりすることでテストが容易になりました。</p> <h1 id="参考">参考</h1> <ul> <li><a href="https://developers.google.com/android/guides/releases#october_13_2022">Release Notes &nbsp;|&nbsp; Google Play services &nbsp;|&nbsp; Google Developers</a></li> <li><a href="https://developer.android.com/training/testing/fundamentals/test-doubles?hl=ja#types">Use test doubles in Android &nbsp;|&nbsp; Android Developers</a></li> <li><a href="https://android.suzu-sd.com/2020/09/android_geolocation_henkou_testrule/">Android&#x7AEF;&#x672B;&#x306E;&#x5730;&#x7406;&#x7684;&#x4F4D;&#x7F6E;&#x3092;&#x5909;&#x66F4;&#x3059;&#x308B;JUnit&#x30C6;&#x30B9;&#x30C8;&#x30EB;&#x30FC;&#x30EB; | Y_SUZUKI&#39;s Android Log</a></li> </ul> toricor GCPでシンプルなCI/CDパイプラインを構築する hatenablog://entry/4207112889944747749 2022-12-14T02:00:00+09:00 2022-12-14T02:00:10+09:00 はじめに サービスをデプロイするときはビルドしてテストしてから行うという手順はよくあります。 その時に、Google Cloud Platform (GCP) 上で CI/CD パイプラインを構築し、コードの変更をトリガーにしてビルド・テスト・デプロイが手軽にできる手法を紹介します。 使用するツール GCP Cloud Build App Engine GitHub 作成するもの Vue.js のプロジェクトで GitHub 上の main ブランチに push/merge されたら自動でビルド・テスト・デプロイを行う環境を構築します。 Cloud Build とは? 公式ドキュメント サーバ… <h1 id="はじめに">はじめに</h1> <p>サービスをデプロイするときはビルドしてテストしてから行うという手順はよくあります。 その時に、Google Cloud Platform (GCP) 上で CI/CD パイプラインを構築し、コードの変更をトリガーにしてビルド・テスト・デプロイが手軽にできる手法を紹介します。</p> <h1 id="使用するツール">使用するツール</h1> <ul> <li>GCP <ul> <li>Cloud Build</li> <li>App Engine</li> </ul> </li> <li>GitHub</li> </ul> <h1 id="作成するもの">作成するもの</h1> <p>Vue.js のプロジェクトで GitHub 上の main ブランチに push/merge されたら自動でビルド・テスト・デプロイを行う環境を構築します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221214/20221214020004.png" width="168" height="507" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="Cloud-Build-とは">Cloud Build とは?</h2> <p><a href="https://cloud.google.com/build?hl=ja">公式ドキュメント</a></p> <blockquote><p>サーバーレス CI / CD プラットフォームでビルド、テスト、デプロイを行います。</p></blockquote> <p>構成を yaml ファイルで記述でき、実行するのはシェルスクリプトから独自で作成した Docker イメージなども活用できるので自由度の高いパイプラインを作成できます。</p> <p>また、実行トリガーも GitHub 連携、Webhook など様々な場面で組み込みやすいものが用意されています。</p> <h2 id="App-Engine-とは">App Engine とは?</h2> <p><a href="https://cloud.google.com/appengine?hl=ja">公式ドキュメント</a></p> <blockquote><p>モノリシックなサーバーサイドのレンダリングのウェブサイトを構築し、アジリティを維持します。App Engine は一般的な開発言語をサポートし、さまざまなデベロッパー ツールを提供しています。</p></blockquote> <p>こちらも構成を yaml ファイルで記述するだけで準備が整い、コンテンツの配信からサービスのスケーリングまでマネージメントしてくれます。</p> <hr /> <h1 id="実際に作成する">実際に作成する</h1> <p>上で紹介した 2 つのサービス + GitHub で実際に構築してみます。</p> <h2 id="Vue-Application-の作成">Vue Application の作成</h2> <p>まずは、デプロイするサービスのセットアップです。 詳細は省きますが、今回は <a href="https://vuejs.org/guide/quick-start.html#creating-a-vue-application">Vue.js Quick Start の Creating a Vue Application</a> にそって、プロジェクトを作成しています。</p> <p>基本的に例示されている設定と同じですが、Vitest の追加だけ Yes に変更し、ユニットテスト環境のセットアップを行っています。</p> <h2 id="App-Engine-の設定">App Engine の設定</h2> <p>あらかじめ、App Engine でデプロイするサービスを確認しておきます。 何も設定しない場合は <code>default</code> サービスにデプロイされますが、 <code>default</code> 以外が良い場合は <code>app.yaml</code> に設定が必要になります。</p> <h2 id="appyaml-の設定">app.yaml の設定</h2> <p>デプロイするものや形式に合わせて適宜調整をしてください。 今回作成するのは Vue.js のプロジェクトでビルド成果物が配信できれば良いので、 <code>runtime</code> には <code>nodejs16</code> (<code>php81</code> とかでも大丈夫です)を、<code>service</code> にはデプロイする App Engine のサービス名を記述します。</p> <p><code>runtime</code> と <code>service</code> が記述できたら、 <code>handler</code> の項目でファイルと配信 URL を紐付けます。 <a href="https://vuejs.org/guide/quick-start.html#creating-a-vue-application">Vue.js Quick Start の Creating a Vue Application</a> から作成していれば、ビルド成果物が <code>dist</code> 以下に生成されるので、成果物を配信できるように記述します。</p> <p>詳細な記述方法は<a href="https://cloud.google.com/appengine/docs/standard/nodejs/config/appref?hl=ja">こちら</a>をご覧ください。</p> <p>今回は以下のような yaml を記述しました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">service</span><span class="synSpecial">:</span> your-service-name <span class="synIdentifier">runtime</span><span class="synSpecial">:</span> nodejs16 <span class="synIdentifier">handlers</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">url</span><span class="synSpecial">:</span> / <span class="synIdentifier">static_files</span><span class="synSpecial">:</span> dist/index.html <span class="synIdentifier">upload</span><span class="synSpecial">:</span> dist/index.html <span class="synIdentifier">secure</span><span class="synSpecial">:</span> always <span class="synStatement">- </span><span class="synIdentifier">url</span><span class="synSpecial">:</span> /(.*) <span class="synIdentifier">static_files</span><span class="synSpecial">:</span> dist/\1 <span class="synIdentifier">upload</span><span class="synSpecial">:</span> dist/(.*) <span class="synIdentifier">secure</span><span class="synSpecial">:</span> always </pre> <p>App Engine で配信するものの構成を <code>app.yaml</code> としてリポジトリ内に置いておきます。</p> <h2 id="gcloudignore-の設定">.gcloudignore の設定</h2> <p>続いて、App Engine でデプロイしないフォルダ類を指定します。 こちらもプロジェクトに合わせて適宜調整をしてください。</p> <p>詳細な記述方法は<a href="https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore">こちら</a>をご覧ください。</p> <p>今回は以下のように記述して、ビルド成果物以外はデプロイしないようにしています。</p> <pre class="code" data-lang="" data-unlink>* !dist/**</pre> <p>これもリポジトリの一番上に <code>.gcloudignore</code> として置いておきます。</p> <h2 id="Cloud-Build-のトリガーを設定する">Cloud Build のトリガーを設定する</h2> <p>続いて、GitHub と Cloud Build の連携をします。 <a href="https://console.cloud.google.com/cloud-build/dashboard?hl=ja">Cloud Build のダッシュボード</a>から<code>トリガー</code>にすすみ、<code>トリガーを作成</code>を選択。連携したいソースを選択して認証、接続したいリポジトリを決めます。</p> <p>リポジトリ決定後はトリガーの作成に移り、どのブランチにどのトリガーでビルドを開始するかを決めておきます。</p> <p>今回は以下のようなトリガーを設定しました。</p> <ul> <li>名前: <code>TestTrigger</code></li> <li>リージョン: <code>グローバル(非リージョン)</code></li> <li>イベント: <code>ブランチにpushする</code></li> <li>リポジトリ: <code>test-repo</code></li> <li>ブランチ: <code>^main$</code></li> <li>構成 形式: <code>自動検出</code></li> </ul> <p>ブランチ名はサジェストが出ますが、正規表現で安全に指定しましょう。</p> <h2 id="cloudbuildyaml-の構成">cloudbuild.yaml の構成</h2> <p>最後に、Cloud Build で用いる yaml ファイルを作成します。 Vue.js のプロジェクトなので、npm でパッケージをインストールした後ビルド、テストを実行して App Engine にデプロイを行うコマンドまでを自動実行するように記述します。</p> <p>yaml は、行いたいことをステップごとに書いていきます。 各ステップでは、 <code>name</code> で Docker イメージを指定します。指定できるのは <a href="https://github.com/GoogleCloudPlatform/cloud-builders">公式にサポートされているイメージ</a> や、 <a href="https://cloud.google.com/container-registry?hl=ja">Container Registry</a> で管理されているイメージなどが指定できます。 <code>node</code> イメージは <code>entrypoint</code> が <code>yarn</code> と <code>npm</code> が設定できるようになっているので、必要に応じて使い分けましょう。今回は <code>npm</code> で設定しています。 あとは、コマンドラインで入力するような引数を <code>args</code> に渡してあげれば、ステップの記述は完了です。</p> <p>詳細な記述方法は<a href="https://cloud.google.com/build/docs/build-config-file-schema?hl=ja">こちら</a>をご覧ください。</p> <p>今回は以下のような記述になりました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> node <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> npm <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synConstant">&quot;install&quot;</span><span class="synSpecial">]</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> node <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> npm <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synConstant">&quot;run&quot;</span>, <span class="synConstant">&quot;build&quot;</span><span class="synSpecial">]</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> node <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> npm <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synConstant">&quot;run&quot;</span>, <span class="synConstant">&quot;test:unit&quot;</span>, <span class="synConstant">&quot;run&quot;</span><span class="synSpecial">]</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">&quot;gcr.io/cloud-builders/gcloud&quot;</span> <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synConstant">&quot;app&quot;</span>, <span class="synConstant">&quot;deploy&quot;</span>, <span class="synConstant">&quot;app.yaml&quot;</span>, <span class="synConstant">&quot;--project&quot;</span>, <span class="synConstant">&quot;projectname&quot;</span>, <span class="synConstant">&quot;--quiet&quot;</span><span class="synSpecial">]</span> </pre> <p>このような yaml を記述し、 <code>cloudbuild.yaml</code> としてリポジトリ内に置いておきます。</p> <h2 id="確認">確認</h2> <p>いよいよ main ブランチへ push ... の前に、ディレクトリ構造を確認しておきます。 ここまでのステップで以下のような構造になっていれば大丈夫です。</p> <p>(重要じゃないところは省いています)</p> <pre class="code" data-lang="" data-unlink>project root L src L (Vue Application のソース) L (dist) L (なくてもOK。 手元でビルドをするとここに成果物が出てると思います。) L .gcloudignore L app.yaml L cloudbuild.yaml L package.json L package-lock.json</pre> <h1 id="完成">完成!</h1> <p>ここまで設定をすれば、実際に GitHub で main ブランチへ push, merge を行うと、これらが自動で実行され、数分でデプロイまで完了しているのが確認できると思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221214/20221214020001.png" width="800" height="469" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mobile-factory/20221214/20221214020006.png" width="800" height="635" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>お疲れ様でした。</p> <h1 id="まとめ">まとめ</h1> <p>Cloud Build + App Engine を使って、CI/CD パイプラインを構築しました。 これで、コードの変更だけに集中できますね!</p> <p>みなさんも良き CI/CD ライフを~</p> Dozi0116 ShellCheckを使おう!の話 hatenablog://entry/4207112889943107146 2022-12-13T00:00:00+09:00 2022-12-13T00:00:27+09:00 駅メモ!チームエンジニアの id:Eadaeda です。 みなさんシェルスクリプト書いてますか?私は時々書いています。12/2 の記事ではシェルスクリプトのテストを書いてみませんかという話を書きました。 tech.mobilefactory.jp 今回はテストではなく、linter の話です。 シェルの文法はなかなか難しいです。例えばダブルクォートで括るかどうかなどです。 # スクリプト a.sh があるとして $ cat ./a.sh #!/bin/bash echo "[$1]" "[$2]" "[$3]" "[$4]" "[$5]" "[$6]" # 例:引数のコマンド置換をダブルクォー… <p>駅メモ!チームエンジニアの <a href="http://blog.hatena.ne.jp/Eadaeda/">id:Eadaeda</a> です。</p> <p>みなさんシェルスクリプト書いてますか?私は時々書いています。12/2 の記事ではシェルスクリプトのテストを書いてみませんかという話を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.mobilefactory.jp%2Fentry%2F2022%2F12%2F02%2F000000" title="シェルスクリプトのテストを書こう! - Mobile Factory Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.mobilefactory.jp/entry/2022/12/02/000000">tech.mobilefactory.jp</a></cite></p> <p>今回はテストではなく、linter の話です。</p> <hr /> <p>シェルの文法はなかなか難しいです。例えばダブルクォートで括るかどうかなどです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># スクリプト a.sh があるとして</span> $ <span class="synStatement">cat</span> ./a.sh <span class="synComment">#!/bin/bash</span> <span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$1</span><span class="synConstant">]</span><span class="synStatement">&quot;</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$2</span><span class="synConstant">]</span><span class="synStatement">&quot;</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$3</span><span class="synConstant">]</span><span class="synStatement">&quot;</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$4</span><span class="synConstant">]</span><span class="synStatement">&quot;</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$5</span><span class="synConstant">]</span><span class="synStatement">&quot;</span><span class="synConstant"> </span><span class="synStatement">&quot;</span><span class="synConstant">[</span><span class="synPreProc">$6</span><span class="synConstant">]</span><span class="synStatement">&quot;</span> <span class="synComment"># 例:引数のコマンド置換をダブルクォートで括るかどうかで動作が変わる</span> $ ./a.bash <span class="synPreProc">$(</span><span class="synSpecial">date</span><span class="synPreProc">)</span> <span class="synStatement">[</span>Wed<span class="synStatement">]</span> <span class="synStatement">[</span>Nov<span class="synStatement">]</span> <span class="synStatement">[</span><span class="synConstant">30</span><span class="synStatement">]</span> <span class="synStatement">[</span>17:06:59<span class="synStatement">]</span> <span class="synStatement">[</span>JST<span class="synStatement">]</span> <span class="synStatement">[</span><span class="synConstant">2022</span><span class="synStatement">]</span> $ ./a.bash <span class="synStatement">&quot;</span><span class="synPreProc">$(</span><span class="synSpecial">date</span><span class="synPreProc">)</span><span class="synStatement">&quot;</span> <span class="synStatement">[</span>Wed Nov <span class="synConstant">30</span> 17:07:10 JST <span class="synConstant">2022</span><span class="synStatement">]</span> <span class="synStatement">[]</span> <span class="synStatement">[]</span> <span class="synStatement">[]</span> <span class="synStatement">[]</span> <span class="synStatement">[]</span> </pre> <p>こういった慣れてないと陥りやすい罠はどんなものにもありますが、linter があれば先に気づくことができそうですね。</p> <h2 id="シェルスクリプトの-linter">シェルスクリプトの linter</h2> <p>今回は<a href="https://github.com/koalaman/shellcheck">ShellCheck</a>を linter として使っていきます。例えば先の <code>a.sh</code>を実行するだけのスクリプト<code>b.sh</code>を以下のように書いたとします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment">#!/bin/bash</span> ./a.sh <span class="synPreProc">$(</span><span class="synSpecial">date</span><span class="synPreProc">)</span> </pre> <p>これを<code>shellckeck</code>にかけると…。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ shellcheck b.sh In b.bash line 3: ./a.sh <span class="synPreProc">$(</span><span class="synSpecial">date</span><span class="synPreProc">)</span> ^-----^ SC2046 <span class="synPreProc">(</span><span class="synSpecial">warning</span><span class="synPreProc">)</span>: Quote this to prevent word splitting. For more information: https://www.shellcheck.net/wiki/SC2046 <span class="synSpecial">--</span> Quote this to prevent word splitt... </pre> <p>こんな感じで警告してくれます。かんたんな対処方法が書かれていますね。より詳しい内容が知りたい場合は、同時に出力されている URL にアクセスするか、<code>SCxxxx</code>で gg れば該当のページを探すことができます。</p> <p>自分はそこそこチェックをするようにしています。意図しない罠を回避したいのもモチベですが、「ええっ!こんな罠が!?」と勉強にもなるのでハッピーです。</p> <h2 id="まとめ">まとめ</h2> <p>今回は ShellCheck をかんたんに紹介しました。ぜひお手持ちのシェルスクリプトで試してみてくださいね。</p> Eadaeda