一休.com Developers Blog https://user-first.ikyu.co.jp/ 一休のエンジニア、デザイナー、ディレクターが情報を発信していきます Thu, 20 Feb 2025 10:19:06 +0900 http://blogs.law.harvard.edu/tech/rss Hatena::Blog 一休 Frontend Meetupでエンジニアとデザイナーがフロントエンド開発の知見について紹介しました https://user-first.ikyu.co.jp/entry/2025/02/20/101906 <p><a href="https://x.com/kymmt90">kymmt</a>です。</p> <p>先日2月10日に、一休のフロントエンド技術にフォーカスしたイベント「一休 Frontend Meetup」を開催しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fikyu.connpass.com%2Fevent%2F343110%2F" title="一休 Frontend Meetup (2025/02/10 19:00〜)" 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://ikyu.connpass.com/event/343110/">ikyu.connpass.com</a></cite></p> <p>一休 Frontend Meetupとしては2年半ぶりの開催となりました。</p> <p>このイベントでは一休開発チームのメンバーが登壇し、各サービスのフロントエンドについて工夫や知見を紹介しました。この記事ではイベントの様子を紹介します!</p> <p>当日のハッシュタグは<a href="https://x.com/hashtag/ikyu_dev?src=hashtag_click&amp;f=live">#ikyu_dev</a>でご覧になれます。</p> <h2 id="発表">発表</h2> <h3 id="一休com-のログイン体験を支える技術-Web-Components-x-Vuejs-活用事例と最適化について">『一休.com のログイン体験を支える技術 〜Web Components x Vue.js 活用事例と最適化について〜』</h3> <p>1つ目の発表は、認証基盤などの開発に携わる渥美さんによる『一休.com のログイン体験を支える技術』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/b583ef0608934f44881d9a48a8fab0c6" title="一休.com のログイン体験を支える技術 〜Web Components x Vue.js 活用事例と最適化について〜" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>一休の各サービスが利用している社内認証基盤では、ユーザーのログイン/SMS認証の際に表示するモーダルウインドウなどを提供するアセットを配布しています。この発表では、ユーザー体験も考慮してスムーズなログインできるモーダルウインドウをWeb ComponentsやVue.jsを用いて開発する方法について紹介しました。</p> <h3 id="Webパフォーマンス改善-宿泊予約サービスでの取り組み">『Webパフォーマンス改善 〜宿泊予約サービスでの取り組み〜』</h3> <p>2つ目の発表は、CTO室で全社的なフロントエンド改善に取り組む卯田さんによる『Webパフォーマンス改善 〜宿泊予約サービスでの取り組み〜』でした<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>。</p> <p>宿泊予約サービスの一休.comのWebパフォーマンス改善では、指標として主にCore Web Vitalsの値をトラッキングしています。改善の方針としては、特定の箇所をカリカリにチューニングするより、ユーザー体験重視で全体的に遅くならないよう気をつけています。</p> <p>発表では、実際に改善活動で指標を監視するために使っているLooker StudioやDatadogのダッシュボードをデモを交えつつ紹介しました。また、フロントエンドに関する知見を収集するための方法についても紹介しました。</p> <h3 id="一休の世界観を形にするガイドラインとデザインシステム">『一休の世界観を形にする、ガイドラインとデザインシステム』</h3> <p>3つ目の発表は、一休.comレストランのデザイナー高橋さんによる『一休の世界観を形にする、ガイドラインとデザインシステム』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/6054a4f3a2d7462684c6cc7678ca55b1" title="一休の世界観を形にする ガイドラインとデザインシステム" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>一休では、デザイナーを中心に「一休らしさ」を実現するための<a href="https://www.ikyu.co.jp/design_guideline">IKYU Design Guideline</a>を策定し、各サービスを横断してブランドイメージの一貫性を保つようにしています。</p> <p>ガイドライン策定による成果として、デザインシステムの運用と各プロダクトへの適用や、デザイナーとエンジニアの協働がやりやすくなったので、一休らしい世界観の提供に役立っているという話がありました。</p> <h3 id="飲食店予約台帳を支えるインタラクティブUI設計と実装">『飲食店予約台帳を支えるインタラクティブUI設計と実装』</h3> <p>最後の発表は、RESZAIKO台帳のエンジニア白井さんによる『飲食店予約台帳を支えるインタラクティブUI設計と実装』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/7c3e57c1043e426e973d3910d45df30d" title="飲食店予約台帳を支えるインタラクティブ UI 設計と実装" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p><a href="https://reszaiko.com/about">RESZAIKO</a>は一休が飲食店向けに提供している予約管理のSaaSです。RESZAIKOが提供するサービスの1つとして、今回発表のテーマになった予約台帳サービスがあります。</p> <p>発表では、iPadのようなタブレットで操作しやすい予約台帳サービスのインタラクティブUIを設計する方法について、</p> <ul> <li>UIをインタラクティブにするための基本的な方法</li> <li>Canvasの使いどころ</li> <li>コンポーネントとしてUIのレイヤーを実装することで責務を整理する手法</li> </ul> <p>などを中心に解説しました。</p> <h2 id="おわりに">おわりに</h2> <p>「一休 Frontend Meetup」での一休のフロントエンド技術領域に関する発表の様子を紹介しました。一休では、宿泊予約やレストラン予約の領域でユーザーファーストなサービスを作りたいというフロントエンドエンジニアを募集しています!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F0001067" title="ソフトウェアエンジニア(フロントエンド)(正社員) | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/0001067">hrmos.co</a></cite></p> <p>当日は40人ほどの方に来場いただきました。ご来場いただいたみなさま、ありがとうございました!</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 資料非公開です<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> Thu, 20 Feb 2025 10:19:06 +0900 hatenablog://entry/6802418398329625884 Jotai を使った Dependency 管理とテスト技法 https://user-first.ikyu.co.jp/entry/2024/12/23/191134 <p>この記事は<a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a>の23日目の記事です。</p> <p>一休レストランのフロントエンドアーキテクトを担当してる恩田(<a href="https://x.com/takashi_onda">@takashi_onda</a>)です。</p> <h1 id="はじめに">はじめに</h1> <p>先日の <a href="https://jsconf.jp/2024/talk/takashi-onda/">JSConf JP 2024</a> で「React への依存を最小にするフロントエンドの設計」という内容で登壇しました。</p> <p><iframe id="talk_frame_1282994" class="speakerdeck-iframe" src="//speakerdeck.com/player/001a570b52274791b464dd5191a5e4d8" width="710" height="400" style="aspect-ratio:710/400; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/takonda/minimize-framework-dependency-in-frontend">speakerdeck.com</a></cite></p> <p>発表では駆け足になってしまった、React への依存をしていない Vanilla JS 部分をどのように構成しているのかを、Dependency 管理とテストの文脈でご紹介したいと思います。</p> <p>Dependency とは Dependency Injection の Dependency です。 タイトルも「Jotai を使った DI とテスト技法」とした方が伝わりやすいとは思います。 ですが、厳密には injection していないので、あえて Dependency という表現に留めています。</p> <p>以下 Dependency や依存関係という言葉を使っているときは Dependency Injection の Dependency のことだとご認識ください。</p> <h1 id="アーキテクチャ">アーキテクチャ</h1> <p>まずは、前提となるアーキテクチャの概観から説明します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20241223/20241223143615.png" alt="atom graph" width="707" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ステート管理には <a href="https://jotai.org/">Jotai</a> を利用しており、primitive atom にはステートマシンの state だけを持つ、<a href="https://user-first.ikyu.co.jp/entry/2023/12/22/190342">ステートマシンを中心に据えた設計</a><sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>を採っています。</p> <p>derived atom はステートマシンから導出しています。 図にあるように <a href="https://jotai.org/docs/extensions/query">jotai-tanstack-query</a> の queryOptions もステートマシンの derived atom です。 これにより、状態が遷移する度に必要に応じて fetch が走り、最新のデータが表示されます。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">const</span> isReservable$ = atom((<span class="synPreProc">get</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">/* snip */</span> <span class="synIdentifier">}</span>) <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useIsReservable</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> useAtomValue(isReservable$) <span class="synIdentifier">}</span> </pre> <p>React コンポーネントは末端の derived atom を見ているだけなので、ロジックとは疎結合を保っています。</p> <p>余談ですが、atom の命名として、かつての RxJS に倣い suffix として <code>$</code> を利用しています。 以降のコード片でも同じ命名としているので <code>$</code> は atom と思っていただければ。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">const</span> transition$ = atom(<span class="synConstant">null</span>, <span class="synStatement">async</span> (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> event</span>:<span class="synPreProc"> </span><span class="synIdentifier">CalendarEvent</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> current = get(calendarState$) <span class="synIdentifier">const</span> next = <span class="synStatement">await</span> transition(current, event) <span class="synStatement">if</span> (!isEqual(state, next)) <span class="synIdentifier">{</span> set(carendarState$, next) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>) <span class="synIdentifier">const</span> selectDate$ = atom(<span class="synConstant">null</span>, (<span class="synPreProc">_get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> date</span>:<span class="synPreProc"> </span><span class="synType">string</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> set(transition$, calendarEvent(<span class="synConstant">'selectDate'</span>, <span class="synIdentifier">{</span> <span class="synStatement">date</span>: toDate(date) <span class="synIdentifier">}</span>)) <span class="synIdentifier">}</span>) <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useSelectDate</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> useSetAtom(selectDate$) <span class="synIdentifier">}</span> </pre> <p>状態遷移は transition 関数を writable derived atom としていて、すべての変更・副作用は状態遷移を経由して実現しています。</p> <p>Flux アーキテクチャではあるものの、React コンポーネントからはフックで得られた関数を呼ぶだけの独立した作りであり、表示側同様にロジックの構造とは疎結合になるように留意しています。</p> <h1 id="Dependency-の管理">Dependency の管理</h1> <p>上述のアーキテクチャでは状態遷移を起点に、データの取得・更新など、外部とのやりとりが発生します。</p> <p>テストが多くを占めますが、利用場面によって、その振る舞いを切り替えたいときがあります。</p> <p>ここでは、 Jotai を Dependency の格納庫である Service Locator として活用する手法についてご紹介します。</p> <h2 id="Jotai-で-function-を管理する">Jotai で function を管理する</h2> <p>まずは軽く Jotai の TIPS 的なお話から。</p> <p>Jotai では primitive atom, derived atom いずれも atom 関数で作成します。 その実装では typeof で第一引数が function かどうかを判定して、オーバーロードを行っています。</p> <p>すなわち、そのままでは function を atom の値として扱えません。 derived atom とみなされてしまうためです。</p> <p>そこで、以下のようなユーティリティを作成しました。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">functionAtom</span>&lt;<span class="synIdentifier">F</span> <span class="synStatement">extends</span> <span class="synIdentifier">Function</span>&gt;(<span class="synPreProc">fn</span>:<span class="synPreProc"> </span><span class="synIdentifier">F</span>): <span class="synIdentifier">WritableAtom</span>&lt;<span class="synIdentifier">F</span>, <span class="synIdentifier">[F]</span>, <span class="synType">void</span>&gt; <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> wrapper$ = atom(<span class="synIdentifier">{</span> <span class="synStatement">fn </span><span class="synIdentifier">}</span>) <span class="synStatement">return</span> atom&lt;<span class="synIdentifier">F</span>, <span class="synIdentifier">[F]</span>, <span class="synType">void</span>&gt;( (<span class="synPreProc">get</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> get(wrapper$).fn, (<span class="synPreProc">_get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> fn</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> set(wrapper$, <span class="synIdentifier">{</span> <span class="synStatement">fn </span><span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> ) <span class="synIdentifier">}</span> </pre> <p>テスト時に function を test double に切り替える程度であれば、functionAtom ユーティリティだけで対応できます。 具体的には GraphQL クエリを実行する関数を管理しています。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synSpecial">export</span> <span class="synIdentifier">const</span> callGraphql$ = functionAtom(callGraphql) </pre> <p>テストコードでは以下のように test double で置き換えています。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">describe</span>(<span class="synConstant">'queryRestaurants$'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">test</span>(<span class="synConstant">'pageCount$'</span>, <span class="synStatement">async</span> ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// arrange</span> <span class="synIdentifier">const</span> store = createStore() store.<span class="synStatement">set</span>(callGraphql$, vi.fn().mockResolvedValue(<span class="synComment">/* snip */</span>)) <span class="synComment">// act</span> <span class="synIdentifier">const</span> page = <span class="synStatement">await</span> store.<span class="synStatement">get</span>(pageCount$) <span class="synComment">// drived from queryRestaurants$</span> <span class="synComment">// assert</span> <span class="synIdentifier">expect</span>(page).toEqual(<span class="synConstant">7</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span>) </pre> <h2 id="Jotai-Scope-で-Dependency-を切り替える">Jotai Scope で Dependency を切り替える</h2> <p>次は、もう少し複雑なケースです。</p> <p>コンポーネントの振る舞いを利用箇所によって切り替えたい、という場面を考えます。 カレンダーやモーダルダイアログで見られるような、複数の操作を持つ複雑なコンポーネントを想定してください。</p> <p>React で素直に書くならコールバックを渡し、コンポーネント root で Context に保持して、コンポーネントの各所で使う形になるでしょう。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">type </span><span class="synIdentifier">Dependency </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">onToggle</span>: (<span class="synSpecial">facet</span>:<span class="synSpecial"> </span><span class="synIdentifier">Facet</span>) <span class="synIdentifier">=&gt;</span> <span class="synType">boolean</span> <span class="synIdentifier">onCommit</span>: (<span class="synSpecial">criteria</span>:<span class="synSpecial"> </span><span class="synIdentifier">SearchCriteria</span>) <span class="synIdentifier">=&gt;</span> <span class="synType">void</span> <span class="synIdentifier">}</span> <span class="synIdentifier">const</span> Context = createContext&lt;<span class="synIdentifier">Dependency</span>&gt;(defaultDependency) <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">Component</span>(<span class="synPreProc">dependency</span>:<span class="synPreProc"> </span><span class="synIdentifier">Dependency</span>) <span class="synIdentifier">{</span> <span class="synStatement">return</span> ( <span class="synComment">&lt;</span><span class="synIdentifier">Context </span><span class="synType">value</span><span class="synStatement">=</span><span class="synSpecial">{</span>dependency<span class="synSpecial">}</span><span class="synComment">&gt;</span> <span class="synComment">&lt;</span><span class="synIdentifier">ComponentBody </span><span class="synComment">/&gt;</span> <span class="synComment">&lt;/</span><span class="synIdentifier">Context</span><span class="synComment">&gt;</span> ) <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useOnToggle</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> use(Context).onToggle <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useOnCommit</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> use(Context).onCommit <span class="synIdentifier">}</span> </pre> <p>さて、そもそもの動機に戻ると、React に依存したコードを最小限にしたい、という背景がありました。 ロジック部分は Vanilla JS だけで完結させるのが理想的です。</p> <p>言い換えれば、Jotai だけで Dependency を切り替える仕組みを作りたい、ということです。 そこで <a href="https://jotai.org/docs/guides/atoms-in-atom">atoms in atom</a> と <a href="https://jotai.org/docs/extensions/scope">jotai-scope</a> を利用することにしました。</p> <p>コードを見ていただくのが早いと思います。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">type </span><span class="synIdentifier">Dependency </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">toggle$</span>: <span class="synIdentifier">WritableAtom</span>&lt;<span class="synType">null</span>, <span class="synIdentifier">[Facet]</span>, <span class="synType">boolean</span>&gt; <span class="synIdentifier">commit$</span>: <span class="synIdentifier">WritableAtom</span>&lt;<span class="synType">null</span>, <span class="synIdentifier">[SearchCriteria]</span>, <span class="synType">void</span>&gt; <span class="synIdentifier">}</span> <span class="synIdentifier">const</span> dependencyA: <span class="synIdentifier">Dependency</span> = <span class="synIdentifier">{</span> <span class="synStatement">toggle$</span>: atom(<span class="synConstant">null</span>, (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> facet</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synConstant">true</span>), <span class="synStatement">commit$</span>: atom(<span class="synConstant">null</span>, (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> criteria</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{}</span>), <span class="synIdentifier">}</span> <span class="synIdentifier">const</span> dependencyB: <span class="synIdentifier">Dependency</span> = <span class="synIdentifier">{</span> <span class="synStatement">toggle$</span>: atom(<span class="synConstant">null</span>, (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> facet</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synConstant">false</span>), <span class="synStatement">commit$</span>: atom(<span class="synConstant">null</span>, (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span><span class="synStatement">,</span><span class="synPreProc"> criteria</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{}</span>), <span class="synIdentifier">}</span> <span class="synStatement">type </span><span class="synIdentifier">Mode </span><span class="synStatement">=</span> <span class="synConstant">'A'</span> <span class="synStatement">|</span> <span class="synConstant">'B'</span> <span class="synIdentifier">const</span> mode$ = atom&lt;<span class="synIdentifier">Mode</span>&gt;(<span class="synConstant">'A'</span>) <span class="synComment">// atom を返す atom</span> <span class="synIdentifier">const</span> dependency$ = atom((<span class="synPreProc">get</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">switch</span> (get(mode$)) <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">'A'</span>: <span class="synStatement">return</span> dependencyA <span class="synStatement">case</span> <span class="synConstant">'B'</span>: <span class="synStatement">return</span> dependencyB <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>) <span class="synStatement">if</span> (import.meta.vitest) <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> <span class="synIdentifier">{</span> <span class="synPreProc">describe</span>, <span class="synPreProc">test</span>, <span class="synPreProc">expect</span> <span class="synIdentifier">}</span> = import.meta.vitest <span class="synIdentifier">describe</span>(<span class="synConstant">'dependency$'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">test</span>(<span class="synConstant">'mode A toggle'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// arrange</span> <span class="synIdentifier">const</span> store = createStore() store.<span class="synStatement">set</span>(mode$, <span class="synConstant">'A'</span>) <span class="synComment">// act</span> <span class="synIdentifier">const</span> <span class="synIdentifier">{</span> <span class="synPreProc">toggle$</span> <span class="synIdentifier">}</span> = store.<span class="synStatement">get</span>(dependency$) <span class="synIdentifier">const</span> result = store.<span class="synStatement">set</span>(toggle$, facetFixture()) <span class="synComment">// assert</span> <span class="synIdentifier">expect</span>(result).toBe(<span class="synConstant">true</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">test</span>(<span class="synConstant">'mode B'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// snip</span> <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> </pre> <p>Jotai だけで Dependency の切り替えが完結しました。</p> <p>あとは React とのグルーコードです。</p> <p>ここで Jotai Scope が登場します。 React コンポーネントでは、振る舞いを切り替える区分値を指定するだけになりました。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useToggle</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> useSetAtom(useAtomValue(dependency$).toggle$) <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useCommit</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> useSetAtom(useAtomValue(dependency$).commit$) <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">ModeProvider</span>(<span class="synPreProc">{ mode</span><span class="synStatement">,</span><span class="synPreProc"> children }</span>:<span class="synPreProc"> </span><span class="synIdentifier">PropsWithChildren</span>&lt;<span class="synIdentifier">{</span> <span class="synIdentifier">mode</span>: <span class="synIdentifier">Mode</span> <span class="synIdentifier">}</span>&gt;) <span class="synIdentifier">{</span> <span class="synStatement">return</span> ( <span class="synComment">&lt;</span><span class="synIdentifier">ScopeProvider </span><span class="synType">atoms</span><span class="synStatement">=</span><span class="synSpecial">{</span><span class="synIdentifier">[</span>mode$<span class="synIdentifier">]</span><span class="synSpecial">}</span><span class="synComment">&gt;</span> <span class="synComment">&lt;</span><span class="synIdentifier">Init </span><span class="synType">mode</span><span class="synStatement">=</span><span class="synSpecial">{</span>mode<span class="synSpecial">}</span><span class="synIdentifier"> </span><span class="synComment">/&gt;</span> <span class="synSpecial">{</span>children<span class="synSpecial">}</span> <span class="synComment">&lt;/</span><span class="synIdentifier">ScopeProvider</span><span class="synComment">&gt;</span> ) <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">Init</span>(<span class="synPreProc">{ mode }</span>:<span class="synPreProc"> </span><span class="synIdentifier">{</span> <span class="synIdentifier">mode</span>: <span class="synIdentifier">Mode</span> <span class="synIdentifier">}</span>) <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> setMode = useSetAtom(mode$) useEffect(()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> setMode(mode) <span class="synIdentifier">}</span>, <span class="synIdentifier">[</span>mode, setMode<span class="synIdentifier">]</span>) <span class="synStatement">return</span> <span class="synConstant">null</span> <span class="synIdentifier">}</span> </pre> <h1 id="テスト技法">テスト技法</h1> <p>一休レストランでは単体テストに <a href="https://testing-library.com/">Testing Library</a> を利用していません。</p> <p>React に依存するコードを最小化することで、Vanilla JS だけで単体テストやロジックレベルのシナリオテストを実現しています。</p> <h2 id="純粋関数で書く">純粋関数で書く</h2> <p>基本的な方針として、derived atom とその計算ロジックは峻別しています。 言い換えれば Jotai の API を利用している部分とロジックの本体となる関数を分離するようにしています。</p> <p>値を取得する derived atom の例です。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">const</span> c$ = atom((<span class="synPreProc">get</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> a = get(a$) <span class="synIdentifier">const</span> b = get(b$) <span class="synStatement">return</span> calc(a, b) <span class="synIdentifier">}</span>) <span class="synStatement">function</span> <span class="synIdentifier">calc</span>(<span class="synPreProc">a</span>:<span class="synPreProc"> </span><span class="synType">number</span><span class="synStatement">,</span><span class="synPreProc"> b</span>:<span class="synPreProc"> </span><span class="synType">number</span>) <span class="synIdentifier">{</span> <span class="synStatement">return</span> a + b <span class="synIdentifier">}</span> <span class="synStatement">if</span> (import.meta.vitest) <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> <span class="synIdentifier">{</span> <span class="synPreProc">describe</span>, <span class="synPreProc">test</span>, <span class="synPreProc">expect</span> <span class="synIdentifier">}</span> = import.meta.vitest <span class="synIdentifier">describe</span>(<span class="synConstant">'calc'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">test</span>(<span class="synConstant">'1 + 2 = 3'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">expect</span>(calc(<span class="synConstant">1</span>, <span class="synConstant">2</span>)).toEqual(<span class="synConstant">3</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> </pre> <p>テストコードには Jotai への依存はなく、ただの純粋関数のテストになります。</p> <p>writable derived atom も同様です。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">const</span> update$ = atom(<span class="synConstant">null</span>, (<span class="synPreProc">get</span><span class="synStatement">,</span><span class="synPreProc"> set</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> a = get(a$) <span class="synIdentifier">const</span> b = get(b$) set(value$, (<span class="synPreProc">current</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> calcNextValue(current, a, b)) <span class="synIdentifier">}</span>) <span class="synStatement">function</span> <span class="synIdentifier">calcNextValue</span>(<span class="synPreProc">value</span>:<span class="synPreProc"> </span><span class="synIdentifier">Value</span><span class="synStatement">,</span><span class="synPreProc"> a</span>:<span class="synPreProc"> </span><span class="synIdentifier">A</span><span class="synStatement">,</span><span class="synPreProc"> b</span>:<span class="synPreProc"> </span><span class="synIdentifier">B</span>): <span class="synIdentifier">Value</span> <span class="synIdentifier">{</span> <span class="synComment">/* snip */</span> <span class="synIdentifier">}</span> </pre> <p>更新処理の中で次の値の計算を純粋関数として分けておけば、引数を与えて返り値を確認するだけの、もっともシンプルな形のテストとして書けるようになります。</p> <p>実際のコードでは、上述したように、ロジックの中核にステートマシンを据えているので、ステートマシンにイベントを送って次状態を確認するテストがそのほとんどを占めています。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">describe</span>(<span class="synConstant">'calendar state machine'</span>, ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">test</span>(<span class="synConstant">'日付を変更すると、選択されている時間帯にもっとも近い予約可能な時間を設定する'</span>, <span class="synStatement">async</span> ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> fetchTimes = vi.fn().mockResolvedValue(<span class="synIdentifier">{</span> <span class="synStatement">restaurant</span>: <span class="synIdentifier">{</span> <span class="synStatement">reservableTimes</span>: <span class="synIdentifier">[</span><span class="synConstant">'11:30'</span>, <span class="synConstant">'13:00'</span>, <span class="synConstant">'18:30'</span>, <span class="synConstant">'20:30'</span>, <span class="synConstant">'21:00'</span><span class="synIdentifier">]</span>, <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>) <span class="synIdentifier">const</span> <span class="synIdentifier">{</span> <span class="synPreProc">transition</span> <span class="synIdentifier">}</span> = createStateMachine(fetchCalendar, fetchTimes) <span class="synIdentifier">const</span> current = createCurrent() <span class="synIdentifier">const</span> result = <span class="synStatement">await</span> transition( current, calendarEvent(<span class="synConstant">'selectVisitDate'</span>, <span class="synIdentifier">{</span> <span class="synStatement">visitDate</span>: asDate(<span class="synConstant">'2024-10-26'</span>) <span class="synIdentifier">}</span>) ) <span class="synIdentifier">expect</span>(result.value).toEqual(<span class="synConstant">'READY'</span>) <span class="synIdentifier">expect</span>(result.<span class="synStatement">context</span>.visitTime).toEqual(<span class="synIdentifier">{</span> ...current.<span class="synStatement">context</span>, <span class="synStatement">visitDate</span>: <span class="synConstant">'2024-10-26'</span>, <span class="synStatement">selectedVisitDate</span>: <span class="synConstant">'2024-10-26'</span>, <span class="synStatement">visitTime</span>: <span class="synConstant">'18:30'</span>, <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span>) </pre> <h2 id="シナリオテスト">シナリオテスト</h2> <p>最後に、ロジックレベルのシナリオテストについてご紹介します。</p> <p>今まで見てきたように、画面上での操作は、ロジックレベルで見ると、ステートマシンの一連の状態遷移になります。 言い換えれば、ユーザーの操作に対応する状態遷移と、ステートマシンから派生する derived atom の値がどうなっているかを確認することで、ロジックレベルのシナリオテストが実現できます。</p> <p>長くなるので一部だけ抜粋しますが、以下のような形でテストを書いています。</p> <p>Jotai には依存していますが、一連のユーザー操作とそのときどんな値が得られるべきかのシナリオが Vanilla JS だけでテストできるのがポイントです。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synIdentifier">test</span>(<span class="synConstant">'人数・日時・時間未指定で、日付だけ選択して予約入力へ'</span>, <span class="synStatement">async</span> ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> store = createStore() store.<span class="synStatement">set</span>(calendarQueryFn$, <span class="synStatement">async</span> ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> reservableCalendar) store.<span class="synStatement">set</span>(timesQueryFn$, <span class="synStatement">async</span> ()<span class="synPreProc"> </span><span class="synType">=&gt;</span> reservableTimes) store.<span class="synStatement">set</span>(now$, <span class="synConstant">'2023-10-25T00:00:00.000+09:00'</span> as DateTime) <span class="synComment">// 初期表示</span> <span class="synStatement">await</span> store.<span class="synStatement">set</span>(transition$, calendarInitEvent()) <span class="synIdentifier">expect</span>(store.<span class="synStatement">get</span>(visitDate$)).toEqual(<span class="synConstant">'2023-10-26'</span>) <span class="synIdentifier">expect</span>(store.<span class="synStatement">get</span>(visitTime$)).toEqual(<span class="synConstant">'19:00'</span>) <span class="synComment">// 日付を選んだとき</span> <span class="synStatement">await</span> store.<span class="synStatement">set</span>(selectDate$, toDate(<span class="synConstant">'2023-11-04'</span>)) <span class="synIdentifier">expect</span>(store.<span class="synStatement">get</span>(visitDate$)).toEqual(<span class="synConstant">'2023-11-04'</span>) <span class="synIdentifier">expect</span>(store.<span class="synStatement">get</span>(visitTime$)).toEqual(<span class="synConstant">'18:30'</span>) <span class="synComment">// ...</span> <span class="synIdentifier">}</span>) </pre> <h1 id="おわりに">おわりに</h1> <p>ここまで読んでいただきありがとうございました。</p> <p>本記事がフロントエンド設計を検討する際の一助となれば幸いです。</p> <hr /> <p>一休では、本記事でお伝えしたような課題をともに解決するエンジニアを募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>まずはカジュアル面談からお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 記事では XState を紹介していますが、現在は独自のステートマシン実装への置き換えを進めています。軽量サブセットである <a href="https://www.npmjs.com/package/@xstate/fsm">@xstate/fsm</a> がバージョン 5 から提供されなくなったこと、型定義や非同期処理の機能不足が理由です。<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> Mon, 23 Dec 2024 19:11:34 +0900 hatenablog://entry/6802418398313821231 Cloud WorkflowsとCloud Tasksを使って日次のバッチ処理を作る https://user-first.ikyu.co.jp/entry/2024/12/19/154826 <ul class="table-of-contents"> <li><a href="#宿泊システムのバッチ処理について背景課題">宿泊システムのバッチ処理について(背景・課題)</a></li> <li><a href="#新たに必要になったバッチ処理をどうやって作るか">新たに必要になったバッチ処理をどうやって作るか</a></li> <li><a href="#Cloud-Workflows--Cloud-Tasks-を使ったバッチ処理">Cloud Workflows + Cloud Tasks を使ったバッチ処理</a><ul> <li><a href="#処理フロー">処理フロー</a></li> <li><a href="#Cloud-Workflows">Cloud Workflows</a><ul> <li><a href="#Workflowsから外部APIを呼び出す">Workflowsから外部APIを呼び出す</a></li> <li><a href="#APIのレスポンスをもとにCloud-Tasksにエンキューする">APIのレスポンスをもとにCloud Tasksにエンキューする</a></li> </ul> </li> <li><a href="#Cloud-Tasks">Cloud Tasks</a></li> <li><a href="#Web-API">Web API</a></li> </ul> </li> <li><a href="#リリース後の運用">リリース後の運用</a><ul> <li><a href="#Cloud-Tasksのキュー設定の調整">Cloud Tasksのキュー設定の調整</a></li> <li><a href="#異常終了時の検知を強化">異常終了時の検知を強化</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <p>宿泊プロダクト開発部の田中(<a href="http://blog.hatena.ne.jp/kentana20/">id:kentana20</a>)です。</p> <p>このエントリーは<a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a>の19日目の記事です。</p> <p>今回は一休.com宿泊のとあるプロジェクトで必要になった <strong>「ホテル・旅館の商品データを日次で更新する」</strong> という処理を</p> <ul> <li>Cloud Scheduler</li> <li>Cloud Workflows</li> <li>Cloud Tasks</li> </ul> <p>とWeb APIで構築、運用している事例をご紹介します。</p> <h1 id="宿泊システムのバッチ処理について背景課題">宿泊システムのバッチ処理について(背景・課題)</h1> <p>一休.com 宿泊には、業務に必要なデータ作成や更新を行うバッチ処理が多く存在します。たとえば</p> <ul> <li>投稿されたクチコミ評点を集計してホテル、旅館のスコアを更新する</li> <li>前月分までの宿泊予約データをもとにユーザーにポイントを付与する</li> </ul> <p>などです。 これらのバッチ処理は宿泊システムの中でも古い部類に入る技術スタック(ASP.NET(C#/VB))で作られており</p> <ul> <li>スピーディに開発できない</li> <li>バッチ処理の開発に慣れているメンバーが限られている</li> </ul> <p>といった課題がありました。</p> <h1 id="新たに必要になったバッチ処理をどうやって作るか">新たに必要になったバッチ処理をどうやって作るか</h1> <p>今年の春頃に実施したプロジェクトで「ホテル・旅館の売れ筋商品(プラン)を日次で洗替する」という処理を新たに作る必要が出てきました。</p> <p>ざっくりとした要件は以下のような内容です。</p> <ul> <li>一休.comに掲載している一部のホテル・旅館を処理対象とする</li> <li>処理対象のホテル・旅館に対して、直近XX日間の予約を集計して売れ筋商品(プラン)を抽出する</li> <li>対象の売れ筋商品(プラン)に対してフラグを立てる</li> <li>処理対象のホテル・旅館は増えたり、減ったりする</li> <li>売れ筋商品の洗替は日次で行う</li> </ul> <p>前述の背景・課題があったため「新しい開発基盤を作ってバッチ処理をスピーディに開発できるようにする」ことを考えてCTOに壁打ちをしたところ「新しい開発基盤を作る前に、そもそもこれはバッチで作るのがベストなのか?」というフィードバックをもらいました。具体的には</p> <ul> <li>一休.com宿泊では、歴史的経緯<a href="#f-a799b59e" id="fn-a799b59e" name="fn-a799b59e" title="非同期ジョブキューの仕組みがない時代に作られたバッチが多く残っています">*1</a>から、オンライン処理できないものをほとんどバッチで作っている</li> <li>現在では、そもそもバッチでまとめて処理せずに、非同期化・分散処理をする選択肢もある</li> <li>バッチで作るのが本当にベストなのか、ほかの選択肢も含めて検討したほうがよい</li> </ul> <p>といった内容でした。このフィードバック内容を踏まえて</p> <ol> <li>(もともとの案)新たにバッチ開発の基盤を作る</li> <li>マネージドなクラウドサービスを組み合わせて作る</li> </ol> <p>を検討し</p> <ul> <li>今回実施したい作業はシンプルな処理の組み合わせで実現可能であること</li> <li>並列、分散処理を考えやすい要件であること(ホテル・旅館単位で処理しても問題ない)</li> </ul> <p>といった理由から、最終的に2を選択しました。</p> <h1 id="Cloud-Workflows--Cloud-Tasks-を使ったバッチ処理">Cloud Workflows + Cloud Tasks を使ったバッチ処理</h1> <p>クラウドサービスについて、一休では、AWSとGoogle Cloudを併用しています。 新しく作るサービスではGoogle Cloudを使うケースが増えている一方で、一休.com 宿泊ではまだ事例が少なかったこともあり、今回はGoogle Cloudを使うことにしました。</p> <h2 id="処理フロー">処理フロー</h2> <ul> <li>Cloud Scheduler</li> <li>Cloud Workflows</li> <li>Cloud Tasks</li> </ul> <p>の3サービスと、シンプルなWeb APIを組み合わせた設計にしており、以下のような流れで動いています。</p> <p><figure class="figure-image figure-image-fotolife" title="処理フロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20241219/20241219143337.png" width="1200" height="481" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>処理フロー</figcaption></figure></p> <h2 id="Cloud-Workflows">Cloud Workflows</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fworkflows%2Fdocs%2Foverview%3Fhl%3Dja" title="ワークフローの概要  |  Workflows  |  Google Cloud" 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://cloud.google.com/workflows/docs/overview?hl=ja">cloud.google.com</a></cite></p> <p>Cloud Workflowsは、マネージドなジョブオーケストレーションサービスです。ワークフローに定義された処理順(ステップ)に従って</p> <ul> <li>Google Cloudのサービスを実行する</li> <li>任意のHTTPエンドポイントにリクエストする</li> </ul> <p>などを実行することができます。 <a href="https://cloud.google.com/workflows/docs/overview?hl=ja#use-cases">公式ドキュメント</a>にも日次のバッチジョブの例が載っており、バッチ処理がユースケースの1つであることがわかります。</p> <p>ワークフローで実行したい内容(ステップ)をYAML形式で記述します。 以下は、今回作ったワークフローのイメージです。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">main</span><span class="synSpecial">:</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">init</span><span class="synSpecial">:</span> <span class="synIdentifier">assign</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">queueName</span><span class="synSpecial">:</span> <span class="synConstant">&quot;cloud-tasks-queue-name&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">getTargetHotels</span><span class="synSpecial">:</span> <span class="synIdentifier">call</span><span class="synSpecial">:</span> http.get <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synIdentifier">url</span><span class="synSpecial">:</span> <span class="synConstant">&quot;https://api.example.com/hotels&quot;</span> <span class="synIdentifier">auth</span><span class="synSpecial">:</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> OIDC <span class="synIdentifier">query</span><span class="synSpecial">:</span> <span class="synIdentifier">target</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">result</span><span class="synSpecial">:</span> hotelData <span class="synStatement">- </span><span class="synIdentifier">createCloudTasks</span><span class="synSpecial">:</span> <span class="synIdentifier">palallel</span><span class="synSpecial">:</span> <span class="synIdentifier">for</span><span class="synSpecial">:</span> <span class="synIdentifier">in</span><span class="synSpecial">:</span> ${hotelData.body.hotels} <span class="synIdentifier">value</span><span class="synSpecial">:</span> hotel <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">createTask</span><span class="synSpecial">:</span> <span class="synIdentifier">call</span><span class="synSpecial">:</span> googleapis.cloudtasks.v2.projects.locations.queues.tasks.create <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synIdentifier">parent</span><span class="synSpecial">:</span> <span class="synConstant">&quot;projects/${sys.get_env('GOOGLE_CLOUD_PROJECT_ID')}/locations/${sys.get_env('LOCATION')}/queues/${queueName}&quot;</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synIdentifier">task</span><span class="synSpecial">:</span> <span class="synIdentifier">httpRequest</span><span class="synSpecial">:</span> <span class="synIdentifier">httpMethod</span><span class="synSpecial">:</span> <span class="synConstant">&quot;PUT&quot;</span> <span class="synIdentifier">url</span><span class="synSpecial">:</span> <span class="synConstant">&quot;https://api.example.com/hotels/${hotel.id}/popular&quot;</span> <span class="synIdentifier">headers</span><span class="synSpecial">:</span> <span class="synIdentifier">Content-Type</span><span class="synSpecial">:</span> <span class="synConstant">&quot;application/json&quot;</span> <span class="synIdentifier">oidcToken</span><span class="synSpecial">:</span> <span class="synIdentifier">serviceAccountEmail</span><span class="synSpecial">:</span> ${&quot;application@&quot; + projectId + <span class="synConstant">&quot;.iam.gserviceaccount.com&quot;</span>} </pre> <h3 id="Workflowsから外部APIを呼び出す">Workflowsから外部APIを呼び出す</h3> <p><code>getTargetHotels</code> のステップで、Web APIへリクエストして対象のホテル・旅館を取得しています。 <code>auth</code> でOIDCを指定していますが、これによってWorkflowsからのAPIリクエストにAuthorizationヘッダを付与することができます。</p> <p><a href="https://cloud.google.com/workflows/docs/authenticate-from-workflow?hl=ja">&#x30EF;&#x30FC;&#x30AF;&#x30D5;&#x30ED;&#x30FC;&#x304B;&#x3089;&#x306E;&#x8A8D;&#x8A3C;&#x6E08;&#x307F;&#x30EA;&#x30AF;&#x30A8;&#x30B9;&#x30C8; &nbsp;|&nbsp; Workflows &nbsp;|&nbsp; Google Cloud</a></p> <p>呼び出されるAPIで、このヘッダを使ってIDTokenを検証することで、Workflowsからのリクエストであることを保証しています。<a href="#f-a3f6cee4" id="fn-a3f6cee4" name="fn-a3f6cee4" title="実際には、この保証だけでなくほかの方法も含めて安全に運用できるように設計しています">*2</a></p> <p>以下は、IDTokenの検証をするミドルウェアのサンプル実装(Go)です。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">import</span> ( <span class="synConstant">&quot;fmt&quot;</span> <span class="synConstant">&quot;net/http&quot;</span> <span class="synConstant">&quot;strings&quot;</span> <span class="synConstant">&quot;google.golang.org/api/idtoken&quot;</span> ) <span class="synComment">// IDトークンを検証するミドルウェア</span> <span class="synStatement">func</span> AuthMiddleware(next http.Handler) http.Handler { <span class="synStatement">return</span> http.HandlerFunc(<span class="synType">func</span>(w http.ResponseWriter, r *http.Request) { <span class="synComment">// Authorization ヘッダからBearerトークンを取得</span> authHeader := r.Header.Get(<span class="synConstant">&quot;Authorization&quot;</span>) <span class="synStatement">if</span> authHeader == <span class="synConstant">&quot;&quot;</span> { http.Error(w, <span class="synConstant">&quot;Authorization header is required&quot;</span>, http.StatusUnauthorized) <span class="synStatement">return</span> } token := strings.TrimPrefix(authHeader, <span class="synConstant">&quot;Bearer &quot;</span>) <span class="synComment">// IDトークンの検証</span> _, err := idtoken.Validate(r.Context(), token, <span class="synConstant">&quot;&quot;</span>) <span class="synStatement">if</span> err != <span class="synStatement">nil</span> { <span class="synComment">// トークンの検証に失敗した場合はエラーを返す</span> http.Error(w, <span class="synConstant">&quot;Invalid ID Token&quot;</span>, http.StatusUnauthorized) <span class="synStatement">return</span> } <span class="synComment">// トークンが有効であれば、次のハンドラーを呼び出す</span> next.ServeHTTP(w, r) }) } </pre> <h3 id="APIのレスポンスをもとにCloud-Tasksにエンキューする">APIのレスポンスをもとにCloud Tasksにエンキューする</h3> <p><code>createCloudTask</code> のステップで、Web APIで取得した <code>hotelData.body.hotels</code> に含まれるホテル・旅館ごとにCloud Tasksにエンキューしています。前述したように実行順序を考慮する必要がないため、<code>parallel</code> を使って並列処理しています。</p> <p>また、 <code>oidcToken</code> を指定することで、Cloud TasksがAPIリクエストを送る際にOIDCトークンを付与することができます。これによってWorkflowsからのAPIリクエストと同様に、API側でIDTokenを検証することができます。</p> <h2 id="Cloud-Tasks">Cloud Tasks</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Ftasks%2Fdocs%3Fhl%3Dja" title="Cloud Tasks のドキュメント  |  Cloud Tasks Documentation  |  Google Cloud" 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://cloud.google.com/tasks/docs?hl=ja">cloud.google.com</a></cite></p> <p>Cloud Tasksについては、昨年のAdvent CalendarでCTO室の徳武が詳細に解説していますので、ぜひご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fs_tokutake%2Farticles%2F22edc021f6277f" title="Google Cloud Tasksのあれこれ" 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://zenn.dev/s_tokutake/articles/22edc021f6277f">zenn.dev</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fs_tokutake%2Farticles%2F0d6a47989868dd" title="Google Cloud Task あれこれ(その2)" 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://zenn.dev/s_tokutake/articles/0d6a47989868dd">zenn.dev</a></cite></p> <h2 id="Web-API">Web API</h2> <p>Cloud Workflows/Cloud Tasksが呼ぶWeb APIは、以下の2つを用意しました。</p> <ol> <li>処理対象のホテル・旅館を取得するAPI(GET)</li> <li>指定されたホテル・旅館IDをもとに売れ筋商品を更新するAPI(PUT)</li> </ol> <p>どちらのAPIも、特定のユースケースに合わせたAPIという形ではなく、単一のリソースを取得/更新するというシンプルな仕様にして再利用可能な設計にしています。</p> <p>この設計にしたことによって、リリース後に「ホテル・旅館が管理システムから任意の操作をした際に、売れ筋商品を更新したい」というユースケースが出てきたときも、2のAPIを使って対応することができました。</p> <h1 id="リリース後の運用">リリース後の運用</h1> <p>このWorkflowsを使ったバッチ処理をリリースした後に、安定運用のためにいくつか変更したポイントがあるのでご紹介します。</p> <h2 id="Cloud-Tasksのキュー設定の調整">Cloud Tasksのキュー設定の調整</h2> <p>Cloud Tasksの設定が適切ではなく、Web APIへの秒間リクエスト数が多すぎてレスポンスが遅くなるという事象があったため</p> <ul> <li>最大ディスパッチ数</li> <li>最大同時ディスパッチ数</li> </ul> <p>などを調整しました。</p> <p><figure class="figure-image figure-image-fotolife" title="キュー設定変更のPull Request"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20241219/20241219142840.png" width="1022" height="727" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>キュー設定変更のPull Request</figcaption></figure></p> <h2 id="異常終了時の検知を強化">異常終了時の検知を強化</h2> <p>異常があった場合に、受動的に気付けるように</p> <ul> <li>Workflowsのエラー処理を調整する</li> <li>エラーログ(Cloud Logging)をSlackに通知する</li> </ul> <p>といった対応をしました。</p> <h1 id="まとめ">まとめ</h1> <p>Cloud Workflows + Cloud TasksとWeb APIを組み合わせたバッチ処理を実装した事例をご紹介しました。 個人的な所感としては、以下のようなメリットを感じています。</p> <ul> <li>Cloud Workflowsはある程度複雑な処理も定義できるため、バッチ処理で必要な手続きをアプリケーション内部に書かずにシンプルなWeb APIとの組み合わせでバッチ処理を作れる</li> <li>データの更新処理は特に、処理単位を小さくする & Cloud Tasksなどのキュー処理を使うと並列実行やエラー時のリトライをマネージドにできるので、運用が楽になる <ul> <li>実際に、キュー設定の調整をする前は初回エラー → キューのリトライによって成功する、といったケースがあり、運用上問題になることはなかったです</li> </ul> </li> </ul> <p>また、今回は採用しませんでしたが、一休社内ではCloud Run Jobsを使ったバッチ処理の基盤も整ってきており、冒頭にご紹介した課題に対して複数の解決方法ができつつあるので、既存のレガシーなバッチ処理も少しずつ刷新していきたいと考えています。</p> <h1 id="おわりに">おわりに</h1> <p>一休では、事業の成果をともに目指せる仲間を募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>まずはカジュアル面談からお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>明日は @yamazakik の「一休バーチャル背景を作ったはなし」です。お楽しみに!</p> <div class="footnote"> <p class="footnote"><a href="#fn-a799b59e" id="f-a799b59e" name="f-a799b59e" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">非同期ジョブキューの仕組みがない時代に作られたバッチが多く残っています</span></p> <p class="footnote"><a href="#fn-a3f6cee4" id="f-a3f6cee4" name="f-a3f6cee4" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">実際には、この保証だけでなくほかの方法も含めて安全に運用できるように設計しています</span></p> </div> Thu, 19 Dec 2024 15:48:26 +0900 hatenablog://entry/6802418398312693811 一休.com の情シス / コーポレートIT 変遷、6年を経てどう変わったのか https://user-first.ikyu.co.jp/entry/corprate-it-2024 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212175203.png" width="1200" height="652" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="はじめに">はじめに</h1> <p><a href="http://blog.hatena.ne.jp/rotom/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/rotom/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:rotom</a> です。社内情報システム部 兼 CISO室 所属で ITとセキュリティを何でもやります。</p> <p>このエントリは <strong>一休.com Advent Calendar 2024</strong> 16日目の記事です。昨日は <a href="http://blog.hatena.ne.jp/naoya/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/naoya/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:naoya</a> による <a href="https://user-first.ikyu.co.jp/entry/2024/12/13/152224">TypeScript の Discriminated Union と Haskell の代数的データ型</a> でした。その他の素敵なエントリも以下のリンクからご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2024%2Fikyu" title="一休.com - Qiita Advent Calendar 2024 - Qiita" 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://qiita.com/advent-calendar/2024/ikyu">qiita.com</a></cite></p> <p>2018年のアドベントカレンダーにて「一休における情シスの取り組み」を紹介させていただき、一定の反響をいただくことができました。 早いものであれからすでに6年が経過しました。6年も経つとコーポレートIT も変遷しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2Finfo-sys" title="一休における「情シス」の取り組み - 一休.com Developers 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://user-first.ikyu.co.jp/entry/info-sys">user-first.ikyu.co.jp</a></cite></p> <p>これまで特定の製品・サービスの事例などは断片的に紹介していましたが、6年ぶりに改めて全体像をお話したいと思います。</p> <p>なお、主に私が進めてきたコーポレートIT、セキュリティ分野に注力して紹介します。ネットワーク、インフラ分野でも非常に多くの変遷・改善がありますが、同僚の ryoma-debari のエントリや、HPE 社のプレスリリースなどもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fryoma-debari%2Fitems%2F7b9543d63bf8d551c66d" title="社内オフィスの無線安定化のためにやったこと - Qiita" 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://qiita.com/ryoma-debari/items/7b9543d63bf8d551c66d">qiita.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.arubanetworks.com%2Fja%2Fpress-release%2Fikyu%2F" title="Aruba、一休.comを展開する株式会社一休の全社ネットワーク環境を NaaS(Network-as-a-Service)で刷新" 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://www.arubanetworks.com/ja/press-release/ikyu/">www.arubanetworks.com</a></cite></p> <h1 id="取り組んできたこと">取り組んできたこと</h1> <h2 id="組織体制の変化">組織体制の変化</h2> <p><strong>before: システム本部<br> after: コーポレート本部</strong></p> <p><figure class="figure-image figure-image-fotolife" title="社内研修資料より"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212145510.png" width="1200" height="667" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内研修資料より</figcaption></figure></p> <p>一休の情報システム部門は前身となるインフラチームからの流れを汲んでエンジニア部門に所属していましたが、部署ごとバックオフィス部門に異動しました。</p> <p>情シスがエンジニアとバックオフィスどちらに所属すべきか、という議論に定説はなく各の組織文化に依る部分がありますが、一休においてはバックオフィス部門に所属することで、人事総務、財務経理、法務などとの連携が円滑になり、後述する本社オフィス移転などの大規模プロジェクトもスムーズに進めることができたと思います。</p> <p>一休はここ数年で新規事業が複数立ち上がり、ビジネスとしても大きく成長しており、ともなって従業員も増加していますが、情シスのチームは非常にコンパクトに運営できており、2024/12 時点で専任の社員は2名です。 ゼロタッチデプロイやプロビジョニング、ChatOps を始め、業務の自動化・改善が進み、ルーチンワークが占める割合が減ったためです。引き続き情シスの省力化に取り組みます。</p> <h2 id="オフィスファシリティの刷新">オフィスファシリティの刷新</h2> <p><strong>before: 赤坂<br> after: 紀尾井町</strong></p> <p><figure class="figure-image figure-image-fotolife" title="紀尾井町オフィス ラウンジ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20221205/20221205131840.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>紀尾井町オフィス ラウンジ</figcaption></figure></p> <p>長らく赤坂見附のトラディッショナルなビルに3フロア借りていましたが、2022年に当時のZホールディングス、現・LINEヤフーの本社が入居する東京ガーデンテラス紀尾井町 紀尾井タワーへ移転しました。 先日は<a href="https://btcon.jp/2024">情シスカンファレンス BTCONJP 2024</a> の会場にもなりました。</p> <p>移転のタイミングで多くのオンプレミス資産を廃棄し、昨今のインターネット企業らしいモダンなコーポレートIT へ刷新をしました。 <strong>固定電話や FAX を廃止した</strong> 話や、入退室管理などのファシリティ周りの話については、下記エントリに詳細を書きましたので合わせてご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2Foffice-relocation" title="本社を東京ガーデンテラス紀尾井町へ移転し、オフィスファシリティ・コーポレートIT を刷新した話 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/office-relocation">user-first.ikyu.co.jp</a></cite></p> <p>なお、本社移転ほどの規模ではありませんが、6年間で <strong>支社・営業所の立ち上げは6拠点、移転は8拠点</strong> で実施しており、ほぼ常にどこかの拠点へ飛び回っていました。地方拠点においてもオンプレミスで持つ資産は廃止を進め、本社同様に固定電話や FAX、有線 LAN を廃止した非常にコンパクトなインフラ構成になりました。</p> <h2 id="Slack-Enterprise-Grid-移行">Slack Enterprise Grid 移行</h2> <p><strong>before: Slack Business Plus<br> after: Slack Enterprise Grid </strong></p> <p><figure class="figure-image figure-image-fotolife" title="10年お世話になっております"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241206/20241206160455.png" width="471" height="161" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>10年お世話になっております</figcaption></figure></p> <p>一休は2014年より Slack を利用しています、もう11年目になります。そんな10年の節目(?)にプランを<strong>最上位である Enterprise Grid へアップグレード</strong>しました。</p> <p>2つあったワークスペースは1つの OrG の配下に統制され、監査ログ API やデータ損失防止(DLP:Data Loss Prevention)などのエンタープライズ組織向けのセキュリティ機能が利用可能になり、よりセキュアに利用できるようになりました。</p> <p>Slack はカジュアルにコミュニケーションがとれる便利なツールである反面、情報漏えいの発生源になるリスクもあります。適切に監査・統制することで、利便性と安全性を両立していきます。</p> <p><figure class="figure-image figure-image-fotolife" title="クレデンシャル情報を書き込むと自動的に検知・削除・警告をします"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241206/20241206162001.png" width="1200" height="122" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>クレデンシャル情報を書き込むと自動的に検知・削除・警告をします</figcaption></figure></p> <p>Enterprise Grid 向け機能のひとつである「情報バリア」については、2023年のアドベントカレンダーで解説しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2Fsack-info-barrier" title="Slack Enterprise Grid における情報バリアの設計 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/sack-info-barrier">user-first.ikyu.co.jp</a></cite></p> <h2 id="デバイス管理の刷新">デバイス管理の刷新</h2> <p><strong>before: オンプレミス IT資産管理ツール<br> after: Microsoft Intune / Jamf Pro </strong></p> <p><figure class="figure-image figure-image-fotolife" title="Mac の標準スペックは 2024/12 時点でM4 Max(RAM 64GB)、社内に Intel Mac は0 "><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212150627.png" width="1200" height="187" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Mac の標準スペックは 2024/12 時点でM4 Max(RAM 64GB)、社内に Intel Mac は0 </figcaption></figure></p> <p>以前は Windows と Mac それぞれの OS 向けの資産管理ツールをオンプレミスのサーバー上に載せており、オフィスのサーバールームで元気に稼働していました。 Windows Server の EOL のタイミングなどもあり、フルクラウド型のモバイルデバイス管理(MDM:Mobile Device Management)への移行を検討し、Windows は Microsoft Intune、Mac は Jamf Pro を選定しました。</p> <p>MDM 導入前は入社準備でデスクに PC、iPhone、iPad を数十台並べてひたすらセットアップする光景が風物詩でしたが、Windows は Windows Autopilot、Mac、iPhone、iPad は Apple Business Manager と連携した Automated Device Enrollment により<strong>ゼロタッチデプロイが可能になり、キッティングにかかる工数を大幅に削減</strong>できました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.microsoft.com%2Fja-jp%2Fsecurity%2Fbusiness%2Fmicrosoft-intune" title="Microsoft Intune - エンドポイント管理 | Microsoft Security" 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://www.microsoft.com/ja-jp/security/business/microsoft-intune">www.microsoft.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.jamf.com%2Fja%2Fproducts%2Fjamf-pro%2F" title="Jamf Pro|Appleモバイルデバイス管理|MDMソフト" 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://www.jamf.com/ja/products/jamf-pro/">www.jamf.com</a></cite></p> <p>iPhone / iPad については当時すでに別の MDM が導入されていたのですが、後にリプレイスを行い、現在は Mac と合わせて全て Jamf Pro で統合管理されています。これらの製品は MDM として広く知られているものなので、詳細な説明は割愛します。</p> <p>当時の一休はエンジニアも含めて Windows の割合が非常に高く、 Windows / Mac 比率 8:2 という状態からの Jamf Pro 導入でした。 マイノリティである Mac は冷遇されがちでほぼ野良管理、自己責任での利用という状態から、Jamf Pro により適切に管理・統制された状態まで進めることができました。</p> <p>Windows 混在環境における Jamf Pro 導入については、 Jamf Connect も含め導入事例、プレスリリースで広く紹介していただいています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.jamf.com%2Fja%2Fresources%2Fcase-studies%2Fikyu%2F" title="株式会社一休 - Jamf ProとJamf Connectで叶える”従業員ファースト”のMac管理" 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://www.jamf.com/ja/resources/case-studies/ikyu/">www.jamf.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.jamf.com%2Fja%2Fresources%2Fpress-releases%2Fikyu.com%2F" title="一休、Jamf Pro、Jamf Connectを活用してシステム環境のモダナイズを推進" 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://www.jamf.com/ja/resources/press-releases/ikyu.com/">www.jamf.com</a></cite></p> <h2 id="EDR--SIEM-導入">EDR / SIEM 導入</h2> <p><strong>before: オンプレミス アンチウイルスソフト<br> after: Microsoft Defender for Endpoint, Microsoft Sentinel </strong></p> <p>エンドポイントセキュリティもIT資産管理ツール同様、オンプレミスで稼働するアンチウイルスソフトを利用していました。</p> <p>サーバーの保守運用コストがかかるだけではなく、デバイスへの負荷が大きい、最新 OS への対応が遅い、パターンマッチングでの検知・検疫はできる一方で、侵入後のリアルタイム検知ができないなどの課題もあり、EDR(Endpoint Detection and Response)型のセキュリティ製品へのリプレイスを検討している中で、<strong>Microsoft Defneder for Endpoint(以下、 MDE)を導入</strong>しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.microsoft.com%2Fja-jp%2Fsecurity%2Fbusiness%2Fendpoint-security%2Fmicrosoft-defender-endpoint" title="Microsoft Defender for Endpoint | Microsoft Security" 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://www.microsoft.com/ja-jp/security/business/endpoint-security/microsoft-defender-endpoint">www.microsoft.com</a></cite></p> <p>Mac については Jamf Protect という製品もありますが、Windows / Mac / iOS / iPadOS などマルチ OS に対応している点からも、Apple デバイスも MDE で運用しています。</p> <p>同時期に SIEM(Security Information and Event Management)として<strong> Microsoft Sentinel を導入</strong>しており、MDE や Microsoft Defender for Identity などで検知したログは Microsoft Sentinel に集約され、インシデントは Slack に通知され、リアルタイムに検知・分析・対応ができる運用をしています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fazure.microsoft.com%2Fja-jp%2Fproducts%2Fmicrosoft-sentinel" title="Microsoft Sentinel – クラウドネイティブの SIEM ソリューション | Microsoft Azure" 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://azure.microsoft.com/ja-jp/products/microsoft-sentinel">azure.microsoft.com</a></cite></p> <h2 id="ライセンスアカウント管理の改善">ライセンス・アカウント管理の改善</h2> <p><strong>before: Google スプレッドシート<br> after: Snipe-IT, Torii</strong></p> <p><figure class="figure-image figure-image-fotolife" title="更新せずに放置していると here メンションがついて赤くなります"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241206/20241206170834.png" width="916" height="620" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>更新せずに放置していると here メンションがついて赤くなります</figcaption></figure></p> <p>Google スプレッドシートなどでがんばっていたIT資産・ライセンス管理については<strong> Snipe-IT というOSS の IT資産管理ツール(ITAM:IT Asset Management)を導入</strong>しました。 OSS なので自前でホスティングすれば費用はかからず、hosting packages を利用すればランニングコストを支払い SaaS のように利用することもできます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsnipeitapp.com%2F" title="Home - Snipe-IT Open Source IT Asset Management" 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://snipeitapp.com/">snipeitapp.com</a></cite></p> <p>Snipe-IT に登録された情報をもとに Slack に更新期日の近いライセンスを通知することで、うっかり失効してしまう、自動更新してしまい事後稟議になってしまう、といった事故を防いでいます。</p> <p>また、近年では SaaS 管理プラットフォーム(SMP:SaaS Management Platform)というジャンルの、いわゆる SaaS を管理する SaaS が登場しています。国産ではジョーシスなどが有名ですが、グローバル SaaS を非常に多く取り扱う一休では <strong>Gartner の Magic Quadrant でも高く評価されている Toriiを選定</strong> しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toriihq.com%2F" title="Torii | SaaS Management Built for IT" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.toriihq.com/">www.toriihq.com</a></cite></p> <p>こちらでコスト可視化や Microsoft Entra ID の SCIM(System for Cross-domain Identity Management)によるプロビジョニングに対応していない SaaS の棚卸しを実施していきます。まだ導入して日が浅いため、運用設計のノウハウが溜まってきたらどこかでアウトプットできればと思います。</p> <h2 id="ヘルプデスクの改善">ヘルプデスクの改善</h2> <p><strong>before: Google フォーム<br> after: Jira Service Management </strong></p> <p>6年前のエントリでは Google フォームでヘルプデスク対応を行っていると書きましたが、その後、Halp という製品を導入し、Halp が Atlassian に買収されたことで、<strong>Jira Service Management(以下、JSM)に統合</strong>されました。 Slack のプレミアムワークフローが無償化したことから移行も検討していますが、現時点ではまだ機能に不足を感じており、JSM での運用を続ける予定です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.atlassian.com%2Fja%2Fsoftware%2Fjira%2Fservice-management" title="Jira Service Management で IT サポートに革命を起こしましょう" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.atlassian.com/ja/software/jira/service-management">www.atlassian.com</a></cite></p> <p>従業員は Slack に普通に投稿するだけでチケットが自動起票され、クイックに対応可能です。出張や外出が多い営業社員もスマートフォンからスムーズに問い合わせができます。ヘルプデスクでよくある<strong> DM 問い合わせ問題も解決</strong>しています。</p> <p>ヘルプデスク改善のあらましについては、下記エントリをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2Fhalp-helpdesk" title="ヘルプデスクに Halp を導入して改善した話 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/halp-helpdesk">user-first.ikyu.co.jp</a></cite></p> <h2 id="Slack-打刻--勤怠打刻自動化">Slack 打刻 / 勤怠打刻自動化</h2> <p><strong>before: Web アプリ / モバイルアプリ<br> after: Slack / Akerun 連携</strong></p> <p><figure class="figure-image figure-image-fotolife" title="Slack から打刻できるのはとても便利"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212134718.png" width="1200" height="1174" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Slack から打刻できるのはとても便利</figcaption></figure></p> <p>一休では勤怠管理システムとしてチムスピ勤怠(TeamSpirit)を利用しています。勤怠打刻をする際は Web アプリから打刻するか、Salesforce のモバイルアプリを利用する必要がありました。 ブラウザを立ち上げて、アクセスパネルアプリケーションから TeamSpirit を開いて打刻をする、というのは少々手間であり、勤怠打刻漏れもよくおきていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.teamspirit.com%2Fja-jp%2Fnews%2Frelease%2F2022%2F10%2Fslack" title="「TeamSpirit」、「Slack」との連携機能の提供を開始 -出退勤打刻や通知の受け取りがSlackで完結し、ユーザーエクスペリエンスと生産性向上に貢献- - TeamSpirit Inc.(株式会社チームスピリット)" 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://corp.teamspirit.com/ja-jp/news/release/2022/10/slack">corp.teamspirit.com</a></cite></p> <p>TeamSpirit が Slack 連携機能を提供開始した際には早速設定を行い、Slack で打刻が完結するようになりました。</p> <p>その後、全社で利用していた入退室カードリーダーをオンプレミスのシステムから Akerun というクラウド型のカードリーダーへリプレイスを行いました。サムターンに設置するタイプの Akerun Pro のイメージが強いかもしれませんが、<strong>オフィスビルの電子錠の信号線と連携できる Akerun コントローラーという製品を選定</strong>しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fakerun.com%2F" title="Akerun|導入社数No.1の法人向け入退室管理スマートロック" 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://akerun.com/">akerun.com</a></cite></p> <p>これによりクラウドサービス上で統合管理ができるようになっただけではなく、API を提供していることから勤怠管理システムとの連動もできるようになりました。こちらも TeamSpirit との API 連携を行うことで、オフィスに出社している際は、<strong>オフィスへの初回入室時刻が出勤打刻、最終退室時刻が退勤打刻に自動連携</strong> されるようになりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.teamspirit.com%2Fja-jp%2Fnews%2Frelease%2F2019%2F10%2Fakerun%25E5%2585%25A5%25E9%2580%2580%25E5%25AE%25A4%25E7%25AE%25A1%25E7%2590%2586%25E3%2582%25B7%25E3%2582%25B9%25E3%2583%2586%25E3%2583%25A0%25E3%2581%25A8teamspirit%25E3%2581%258C%25E9%2580%25A3%25E6%2590%25BA" title="Akerun入退室管理システムとTeamSpiritが連携 - TeamSpirit Inc.(株式会社チームスピリット)" 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://corp.teamspirit.com/ja-jp/news/release/2019/10/akerun%E5%85%A5%E9%80%80%E5%AE%A4%E7%AE%A1%E7%90%86%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%81%A8teamspirit%E3%81%8C%E9%80%A3%E6%90%BA">corp.teamspirit.com</a></cite></p> <h2 id="パスワードマネージャー全社展開">パスワードマネージャー全社展開</h2> <p><strong>before: 1Password (高権限者のみ)<br> after: Keeper </strong></p> <p><figure class="figure-image figure-image-fotolife" title="Keeper のログは全て Slack App 経由でチャンネルへ自動通知"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212182943.png" width="1134" height="460" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Keeper のログは全て Slack App 経由でチャンネルへ自動通知</figcaption></figure></p> <p>パスワードマネージャーは以前から 1Password を利用していましたが、一部の特権を持つエンジニアのみで利用されていました。 一般の従業員は個別にパスワードを管理している状態であり一定のセキュリティリスクを感じており、パスワードマネージャー全社展開を検討していました。</p> <p>数百人規模に展開する際は ITリテラシーの高くないメンバーにも使っていただくことになりマスターパスワードを紛失してしまった際の懸念や、組織変更への対応の運用負荷に懸念がありました。</p> <p>そこで SAML による SSO、SCIM によるプロビジョニングに対応した Keeper へリプレイスを行い、全社展開を行いました。導入時の話は事例化もしていただいたので、詳細はこちらもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.zunda.co.jp%2Fblog%2Fikyu-casestudy" title="Keeper導入のきっかけは「SSO対応&使い勝手の良さ」一休の目指すユーザーファーストなセキュリティ - IT Admin Blog by ZUNDA" 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://www.zunda.co.jp/blog/ikyu-casestudy">www.zunda.co.jp</a></cite></p> <h2 id="PPAP-廃止">PPAP 廃止</h2> <p><strong>before: PPAP, ファイル共有ツール<br> after: mxHERO </strong></p> <p>一休はソフトバンクグループの会社でもあり、ソフトバンクグループは Emotet などのマルウェア対策のため、2022年にパスワード付き圧縮ファイル(いわゆる、PPAP:Password付きZIPファイルを送ります、Passwordを送ります、Angoka、Protocol)を廃止しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.softbank.jp%2Fcorp%2Fnews%2Finfo%2F2022%2F20220215_01%2F" title="当社におけるパスワード付き圧縮ファイルの利用廃止に関するお知らせ | 企業・IR | ソフトバンク" 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://www.softbank.jp/corp/news/info/2022/20220215_01/">www.softbank.jp</a></cite></p> <p>一休も従来のセキュリティポリシーでは社外へ機密性の高いファイルを送付する際は PPAP で送信するルールでした。またメディア事業など外部と大容量のファイルをやりとりするチームへは個別にファイル共有ツールのアカウントを払い出す運用を行っていました。 このセキュリティポリシーの改定と、代替となる手段の整備を進めました。</p> <p>PPAP 代替ツールについても多くの製品がありますが、一休では<strong><a href="https://www.meti.go.jp/meti_lib/report/2020FY/000143.pdf">経済産業省</a> などの官公庁やエンタープライズ企業でも実績のある mxHERO を導入</strong>しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.mxhero.com%2Fjapan" title="mxHERO in Japan | mxhero" 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://www.mxhero.com/japan">www.mxhero.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloudnative.co.jp%2Fproduct%2FmxHero" title="mxHERO 販売代理店 | 株式会社クラウドネイティブ" 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://cloudnative.co.jp/product/mxHero">cloudnative.co.jp</a></cite></p> <p>メールの添付ファイルを自動的にファイルストレージの安全な共有リンクに変換して送信することから、誤送信をしてしまった場合もファイルを消したり、アクセス権限を解除したりすることで、情報漏えいを防止することができます。これにより PPAP を代替できると考えました。 一休ではファイルストレージとして Google ドライブを利用しているため、mxHERO と Google ドライブを組み合わせて導入することを検討しました。</p> <p><figure class="figure-image figure-image-fotolife" title="Google ドライブは Box と比較すると制限が多い"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241216/20241216123839.png" width="1200" height="577" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Google ドライブは Box と比較すると制限が多い</figcaption></figure></p> <p>しかし、Google ドライブは Google アカウントが前提となっていることが多く、Box と比較すると制限事項が多くありました。特に共有リンクに有効期限が付与できないと、共有が不要になったファイルも、設定変更を忘れると URL を知っていれば永久的にアクセスできてしまう可能性があり、解決する必要のある課題でした。 Box の導入も検討しましたが、既存のファイル共有ツールを比較するとランニングコストが大幅に上がってしまうことから断念しました。</p> <p><figure class="figure-image figure-image-fotolife" title="GAS の実装で実質的に共有 URL に有効期限を設定"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241216/20241216125133.png" width="1200" height="548" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>GAS の実装で実質的に共有 URL に有効期限を設定</figcaption></figure></p> <p>そこで、<strong>GAS(Google App Script)によるスクリプトで対象の共有ドライブ内のフォルダを、送信日時タイムスタンプから1週間経過したら自動削除する</strong> 、という実装を行い、実質的に共有リンクに1週間の有効期限を設定することにしました。</p> <p>これにより PPAP を廃止してセキュリティ上のリスクを低下できるだけではなく、従業員はただメールにファイルを添付するだけでよくなったためユーザビリティも向上し、また、ファイル共有サービスの解約によりアカウント管理などに伴う情シスの管理工数も削減することができました。</p> <p>注意点としては 25MB を超える大容量ファイルは mxHERO のルーティングより Gmail 側の Google ドライブ URL への自動変換が実施されてしまうため、mxHERO 経由で送信することができません。そのため、大容量ファイルについては手動で共有リンクを発行する運用をしています。 こちらも GAS により有効期限を設定していますが、手動で発生している作業も将来的にはより自動化を進めたいと考えています。</p> <h2 id="ファイルサーバー移行廃止">ファイルサーバー移行・廃止</h2> <p><strong>before: オンプレミス Windows Server<br> after: Google ドライブ ( Google Workspace Enterprise Plus )</strong></p> <p>一休には複数のオンプレミスのファイルサーバーが存在しておりましたが、AWS EC2 上への移行を経て、<strong>2023年にGoogle ドライブへの移行が完了し、完全に廃止</strong> しました。</p> <p>さらっと書きましたが、長年運用していたファイルサーバーにはブラックボックス化したマクロの組まれた Excel が潜んでいたり、情シスでもアクセスしてはいけない機微な情報を保管したフォルダがあったりと一筋縄で行くものではなく、全社を巻き込んでの数年がかりのプロジェクトでした。 ファイルサーバーの運用を行っている情シスの皆さんなら、この大変さを察していただけるのではないでしょうか・・・</p> <p>なお、ファイルサーバーは複合機からスキャンしたファイルの置き場にもなっていましたが、オンプレミスのプリンタサーバー廃止と合わせてクラウドプリントに移行しており、スキャンしたファイルの置き場も Google ドライブに移行しました。</p> <h2 id="SASE-導入の見送り">SASE 導入の見送り</h2> <p><strong>before: VPN <br> after: 未定</strong></p> <p>一休のネットワーク構成は現時点ではいわゆる境界型セキュリティであり、社外から社内リソースへ接続する際にはリモート VPN で接続を行います。 「脱・VPN」に向けて以前より SASE(Secure Access Service Edge)の導入を検討しており、今年はいくつかの製品を PoC(Proof of Concept / 概念実証)まで実施しました。</p> <p>大きな工数をかけて検証を行ってきましたが、<strong>特定の通信に対するパフォーマンス低下、開発環境への影響が PoC 期間中に解消せず見込みも立たなかったことから、残念ながら導入に至ることはできませんでした</strong>。</p> <p>導入は見送りにはなりましたが、PoC を通じて貴重なノウハウを得ることができました。 脱・VPN やゼロトラストネットワークの実現に、SASE 導入は必須ではなく、あくまで1つの手段であると考えています。デバイストラストなど別のアプローチからも、ユーザビリティを両立したセキュリティを目指していく予定です。</p> <h1 id="まとめ">まとめ</h1> <h2 id="オンプレからクラウド--SaaS-中心のモダンな-IT-へ">オンプレからクラウド / SaaS 中心のモダンな IT へ</h2> <p><figure class="figure-image figure-image-fotolife" title="解体されるサーバールームとラック"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rotom/20241212/20241212144242.png" width="1200" height="955" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>解体されるサーバールームとラック</figcaption></figure></p> <p>細かなプロジェクトを上げるとキリがありませんが、ここ数年の取り組みをまとめると、オンプレミスからクラウドへの転換期であったと思います。 それ故に創業当初からオンプレミスの資産がなく、フルクラウドでコーポレートIT を構築している IT企業から見ると目新しさはなく感じると思います。</p> <p>一休も外から見るとモダンなIT企業に見えるかもしれませんが、1998年に創業し間もなく四半世紀を迎える会社です。多くの資産を抱えた組織であり、クラウドへの移行やゼロトラストネットワークの実現は一朝一夕で実現できるものでありません。 クラウドサービス / SaaS も導入することは目的ではなく、その後の運用設計が重要となってきます。引き続きモダンなコーポレートIT環境を目指して最適化に向けて取り組んでいきます。</p> <h2 id="色々やったこれからどうするか">色々やった。これからどうするか</h2> <p>これまでは導入事例の取材や、ブログ、勉強会やカンファレンスで発表で外部へアウトプットできる、わかりやすい実績がありました。一方で、クラウド / SaaS も導入・移行フェーズが終わり運用に乗った今、今後はそういった機会も少なくなり、直近は地道な改善活動が多くなってくると思います。(これをチーム内では筋トレタスクと呼んでいます)</p> <p>目下の課題が解消に向かいつつある中、いかに課題を見つけ出し、ボトムアップでチーム、組織、ビジネスの課題をテクロノジーで解決していくか、を考え筋トレのように日々改善を進めていきます。 <strong>直近は現状の VPN の代替となる手段の検証と実装、セキュリティアラートの監視最適化、 DLP を活用した情報漏えい対策の強化、中長期的にはパスキーを活用した社内パスワードレス化</strong> に向けて取り組んでいく予定です。よい成果が得られた際はまたアウトプットをしていきます。</p> <h2 id="エンジニア採用中です-">エンジニア採用中です !</h2> <p>前述の通り、一休の情シスはコンパクトに運営しているため採用をしておらず、現時点で増員の予定もありません。 一方で、ソフトウェアエンジニア、SRE、データサイエンティスト、ディレクターなど多くの職種で積極的に採用をしております。</p> <p>ご興味のある方は以下から Job Description をご覧ください。カジュアル面談もやっています !</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Mon, 16 Dec 2024 19:06:30 +0900 hatenablog://entry/6802418398309466745 エンジニア 情報システム TypeScript の Discriminated Union と Haskell の代数的データ型 https://user-first.ikyu.co.jp/entry/2024/12/13/152224 <p>この記事は <a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a> の15日目の記事です。<br/> 予定より早く書き上げてしまったので、フライングですが公開してしまいます。</p> <p>TypeScript の Discriminated Union (判別可能な Union 型) を使うと、いわゆる「代数的データ型」のユースケースを模倣することができます。一休のような予約システム開発においては「ありえない状態を表現しない」方針で型を宣言するためによく利用されています。</p> <p>「あり得ない状態を表現しない」という型宣言の方針については以下の URL が参考になります。</p> <p><a href="https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/">Designing with types: Making illegal states unrepresentable | F# for fun and profit</a></p> <p>このユースケースで Discriminated Union を使う場合、それは文字どおり「型の判別」のために使われます。この場合、判別の手がかりとなる「ディスクリミネーター」はただの分岐のためのシンボル程度の役割にしか見えないでしょう。しかしこれは、本機能の部分的な見方でしかないと考えています。</p> <p>Haskell など、TypeScript のように模倣ではなく、型システムに代数的データ型がネイティブに組み込まれているプログラミング言語では、代数的データ型こそが新たなデータ型とデータ構造を宣言する手段です。代数的データ構造とパターンマッチを用いて、一般的なオブジェクトだけでなく、リストや木構造などのデータ型を構築・操作することができます。こちらのメンタルモデルから見ると、<strong>代数的データ型こそが、データの構築と分解を型安全かつ表現力豊かに扱う基盤を提供するものであり、型駆動開発を支える根幹である</strong>と捉えることができます。</p> <p>本記事では TypeScript の Discriminated Union による代数的データ型の模倣についてまずその基本を確認し、その後 Haskell の代数的データ型の文法をみていきます。後者をみて先のメンタルモデルを獲得したのちに前者を改めて眺めてみることにより、新たな視点で TypeScript の機能を捉えることを目指します。</p> <h2 id="TypeScript-の-Discriminated-Union-判別可能な-Union-型">TypeScript の Discriminated Union (判別可能な Union 型)</h2> <p>TypeScript の Discriminated Union (判別可能な Union 型) を使うと、他のプログラミング言語でいうところの代数的データ型のユースケースを模倣することができます。Discriminated Union はディスクリミネーター (もしくはタグ) と呼ばれる文字列リテラルにより Union で合併した型に含まれる型を判別できるところから「タグつき Union 型」と呼ばれることもあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftypescriptbook.jp%2Freference%2Fvalues-types-variables%2Fdiscriminated-union" title="判別可能なユニオン型 (discriminated union) | TypeScript入門『サバイバルTypeScript』" 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://typescriptbook.jp/reference/values-types-variables/discriminated-union">typescriptbook.jp</a></cite></p> <p>Discriminated Union をうまく使うと、アプリケーション開発において「存在しない状態」ができることを回避することが出来ます。存在する状態のみを型で宣言することで「存在しない状態ができていないこと」を型チェックにより保証することができます。書籍 <a href="https://pragprog.com/titles/swdddf/domain-modeling-made-functional/">Domain Modeling Made Functional</a> などでも語られている非常に有用な実装パターンであり、一休が扱う予約などの業務システム開発でも頻繁に利用しています。</p> <p>少しその様子を見てみます。</p> <p>典型例として、何かしらのシステムのユーザー (User) について考えます。ユーザーには会員登録済みの会員 (Member) と、会員登録はしていないゲスト会員 (Guest) の区分があるというのは、よくあるケースでしょう。会員はユーザーID、名前、メールアドレスなどの値をもつが、ゲストはそれらが確定していない。</p> <p>このとき ユーザーID が null なデータをゲストユーザーとして扱うという実装もあり得ますが、null チェックが必要になるし「ID が null なのがゲスト」という暗黙の仕様を持ち込むことになってしまいます。null に意味は与えたくありません。</p> <p>そこで以下のように、Member と Guest を定義します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type </span><span class="synIdentifier">User </span><span class="synStatement">=</span> <span class="synIdentifier">Member</span> <span class="synStatement">|</span> <span class="synIdentifier">Guest</span> <span class="synStatement">type </span><span class="synIdentifier">Member </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Member&quot;</span> <span class="synIdentifier">id</span>: <span class="synType">number</span> <span class="synIdentifier">name</span>: <span class="synType">string</span> <span class="synIdentifier">email</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> <span class="synStatement">type </span><span class="synIdentifier">Guest </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Guest&quot;</span> <span class="synIdentifier">}</span> </pre> <p>User 型のオブジェクトがあったとき、そのオブジェクトが Member 型なのか Guest 型なのかは <code>kind</code> プロパティの値によって判別できます。この <code>kind</code>プロパティが型の判別に使われるディスクリミネーター (あるいはタグ) です。</p> <p>例えば、Member か Guest かでプレゼンテーションを分けたいというときは以下のように switch 文により Union 型を分解し、それぞれの型ごとに処理を記述することができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">showUser</span>(<span class="synPreProc">user</span>:<span class="synPreProc"> </span><span class="synIdentifier">User</span>): <span class="synType">string</span> <span class="synIdentifier">{</span> <span class="synStatement">switch</span> (user.kind) <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Member&quot;</span>: <span class="synStatement">return</span> <span class="synConstant">`ID: </span><span class="synStatement">${</span>user.<span class="synStatement">id}</span><span class="synConstant">, Name: </span><span class="synStatement">${</span>user.<span class="synStatement">name}</span><span class="synConstant">, Email: </span><span class="synStatement">${</span>user.email<span class="synStatement">}</span><span class="synConstant">`</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Guest&quot;</span>: <span class="synStatement">return</span> <span class="synConstant">&quot;Guest&quot;</span> <span class="synStatement">default</span>: assertNever(user) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">assertNever</span>(<span class="synPreProc">_</span>:<span class="synPreProc"> </span><span class="synType">never</span>): <span class="synType">never</span> <span class="synIdentifier">{</span> <span class="synSpecial">throw</span> <span class="synIdentifier">new</span> <span class="synType">Error</span>(<span class="synConstant">&quot;Unexpected value. Should have been never.&quot;</span>) <span class="synIdentifier">}</span> </pre> <p><code>assertNever</code> は網羅性チェックのためのイディオムで、これを置くことでナローイングの結果 User 型に含まれるすべての型に対し処理を定義したかを、コンパイル時にチェックすることができます。</p> <p>以下の絵は実装途中の VSCode です。<code>Member</code> に対する処理は記述したが <code>Guest</code> に対する処理はまだ記述していない段階。コンパイラがエラーを出してくれています。</p> <p><figure class="figure-image figure-image-fotolife" title="網羅性チェックによるコンパイルエラー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/naoya/20241213/20241213151738.png" width="1200" height="237" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>網羅性チェックによるコンパイルエラー</figcaption></figure></p> <p>そして <code>kind</code> プロパティすなわちディスクリミネーターはリテラル型になっており、補完が効きます。</p> <p><figure class="figure-image figure-image-fotolife" title="ディスクリミネーターの補完が効く"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/naoya/20241213/20241213151825.png" width="1200" height="349" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ディスクリミネーターの補完が効く</figcaption></figure></p> <p>このように、Union により構造の異なる複数の型を合併しつつもディスクリミネーターによってそれを分解することができ、ナローイングによって型や網羅性チェックが効くことから、代数的データ型をエミューレトできていると言われます。ディスクリミネーターに基づいた switch 文での型の分解は、さながら「パターンマッチ」のように捉えられます。</p> <p>仮に Discriminated Union を使わず、ゲストユーザーを「ID が null」で表現したとすると以下のように定義することになります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type </span><span class="synIdentifier">User </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">id</span>: <span class="synType">number</span> <span class="synStatement">|</span> <span class="synType">null</span> <span class="synIdentifier">name</span>?: <span class="synType">string</span> <span class="synIdentifier">email</span>?: <span class="synType">string</span> <span class="synIdentifier">}</span> </pre> <p>この場合、たとえば ID が null にも関わらず name や email が null でない、という「ありえない状態」を表現できてしまいます。</p> <p>これは Record 型が AND (積) に基づいたデータ構造の宣言であり、3 つのプロパティがそれぞれ「ある・なし」の 2パターンを取り、その積で合計 8 パターンの状態を取れてしまうことに起因しています。8パターンの状態の中には、実際にはあり得ない状態が含まれます。「ある・ なし」の分岐は ID に関してだけでよいのに、ほかの 2 つのプロパティまでそれに巻き込まれてしまった結果です。</p> <p> Union 型は OR (和) に基づく合併なので「ID、名前、メールアドレスがある」 Member に、「プロパティがない」 Guest の状態を「足している」だけ。状態の積は取りません。よって合併しても状態が必要以上に増えません。</p> <p>Making illegal states unrepresentable (ありえない状態を表現しない) というのはこういうことです。</p> <h2 id="実際のユースケース--絵文字アイコンあるなしの表現">実際のユースケース ··· 絵文字アイコンあるなしの表現</h2> <p>もうひとつ、我々の実際のアプリケーションでの実例の中から、簡単なものを紹介します。</p> <p>我々の作ってる飲食店向け予約台帳システムには顧客管理の機能がありますが、顧客にタグ付けして分類することができます。タグは視認性向上のため絵文字が設定できるようになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="タグには絵文字が使える"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/naoya/20241213/20241213151915.png" width="1200" height="493" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>タグには絵文字が使える</figcaption></figure></p> <p>タグを新しく作るときは絵文字を設定することができます。絵文字は設定しても、しなくても OK という仕様になっています。</p> <p><figure class="figure-image figure-image-fotolife" title="絵文字は設定しても、しなくても OK"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/naoya/20241213/20241213151941.png" width="1200" height="1176" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>絵文字は設定しても、しなくても OK</figcaption></figure></p> <p>さて、このタグ用のアイコンである <code>TagIcon</code> のデータをどう管理するか、型を考えます。</p> <p>「アイコンがない」というのを null で表現しようとしがちですが、「アイコンなし」という状態はそれはそれで存在する状態と考えることもできます。これを <code>NoIcon</code> という型にしてみます。「ない」を「ある」とみなすことで、状態を定義することができました。</p> <p>結果、以下のように Union で表現することができるでしょう。こうして null に意味を持たせることを回避します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type </span><span class="synIdentifier">TagIcon </span><span class="synStatement">=</span> <span class="synIdentifier">EmojiIcon</span> <span class="synStatement">|</span> <span class="synIdentifier">NoIcon</span> <span class="synStatement">type </span><span class="synIdentifier">EmojiIcon </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Emoji&quot;</span> <span class="synIdentifier">symbol</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> <span class="synStatement">type </span><span class="synIdentifier">NoIcon </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;NoIcon&quot;</span> <span class="synIdentifier">}</span> </pre> <p>型を宣言したからには、この型の値を生成できるようにしましょう。コンストラクタ関数を定義します。このとき、型名と関数名を同じにする <a href="https://typescriptbook.jp/tips/companion-object">コンパニオンオブジェクトパターン</a> を使うと良いです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">EmojiIcon</span>(<span class="synPreProc">symbol</span>:<span class="synPreProc"> </span><span class="synType">string</span>): <span class="synIdentifier">EmojiIcon</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> <span class="synStatement">kind</span>: <span class="synConstant">&quot;Emoji&quot;</span>, <span class="synStatement">symbol </span><span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">NoIcon</span>(): <span class="synIdentifier">NoIcon</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> <span class="synStatement">kind</span>: <span class="synConstant">&quot;NoIcon&quot;</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>少し話しが脱線しますが、EmojiIcon の symbol の文字列が確かに絵文字かどうかをチェックすることで、値の完全性をより厳密にすることができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">EmojiIcon</span>(<span class="synPreProc">symbol</span>:<span class="synPreProc"> </span><span class="synType">string</span>): <span class="synIdentifier">Result</span>&lt;<span class="synIdentifier">EmojiIcon</span>, <span class="synIdentifier">ValidationError</span>&gt; <span class="synIdentifier">{</span> <span class="synStatement">return</span> symbol.<span class="synStatement">match</span>(<span class="synConstant">/\p{Emoji}/gu</span>) ? ok(<span class="synIdentifier">{</span> <span class="synStatement">kind</span>: <span class="synConstant">&quot;Emoji&quot;</span>, <span class="synStatement">symbol </span><span class="synIdentifier">}</span>) : err(<span class="synIdentifier">new</span> ValidationError(<span class="synConstant">'Emoji ではありません'</span>)) <span class="synIdentifier">}</span> </pre> <p>プロダクトの実装ではそうしていますが、例外をどう扱うかなど本稿とは関係のないトピックが出てきてしまうので以降省略します。</p> <p>もとい、これで型、つまりは値の構造の定義とその生成方法を定義できました。あとは先にみた User の例のように、アイコンが絵文字か・絵文字なしかで処理を切り分けたいときは <code>kind</code> プロパティでパターンマッチ的に分解すればよいです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">toHTMLIcon</span>(<span class="synPreProc">icon</span>:<span class="synPreProc"> </span><span class="synIdentifier">TagIcon</span>): <span class="synType">string</span> <span class="synIdentifier">{</span> <span class="synStatement">switch</span> (icon.kind) <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Emoji&quot;</span>: <span class="synStatement">return</span> icon.symbol <span class="synStatement">case</span> <span class="synConstant">&quot;NoIcon&quot;</span>: <span class="synStatement">return</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">default</span>: assertNever(icon) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">assertNever</span>(<span class="synPreProc">_</span>:<span class="synPreProc"> </span><span class="synType">never</span>): <span class="synType">never</span> <span class="synIdentifier">{</span> <span class="synSpecial">throw</span> <span class="synIdentifier">new</span> <span class="synType">Error</span>(<span class="synConstant">&quot;Unexpected value. Should have been never.&quot;</span>) <span class="synIdentifier">}</span> </pre> <p>追加の仕様で絵文字だけでなく、オリジナルのアップロード画像も扱いたいとしましょう。その場合は Union に新たに <code>ImageIcon</code> 型を追加すればよいでしょう。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type </span><span class="synIdentifier">TagIcon </span><span class="synStatement">=</span> <span class="synIdentifier">EmojiIcon</span> <span class="synStatement">|</span> <span class="synIdentifier">NoIcon</span> <span class="synStatement">|</span> <span class="synIdentifier">ImageIcon</span> <span class="synComment">// ImageIcon を新たに併合</span> <span class="synStatement">type </span><span class="synIdentifier">EmojiIcon </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Emoji&quot;</span> <span class="synIdentifier">symbol</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> <span class="synStatement">type </span><span class="synIdentifier">NoIcon </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;NoIcon&quot;</span> <span class="synIdentifier">}</span> <span class="synComment">// これを追加</span> <span class="synStatement">type </span><span class="synIdentifier">ImageIcon </span><span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Image&quot;</span> <span class="synIdentifier">url</span>: <span class="synType">string</span> <span class="synIdentifier">name</span>: <span class="synType">string</span> <span class="synIdentifier">}</span> </pre> <p><code>ImageIcon</code> 型を Union に追加すると、パターンマッチしている分岐で網羅性チェックが働き、期待通り、コンパイルが通らなくなります。型に応じた処理を追加します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">toHTMLIcon</span>(<span class="synPreProc">icon</span>:<span class="synPreProc"> </span><span class="synIdentifier">TagIcon</span>): <span class="synType">string</span> <span class="synIdentifier">{</span> <span class="synStatement">switch</span> (icon.kind) <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Emoji&quot;</span>: <span class="synStatement">return</span> icon.symbol <span class="synStatement">case</span> <span class="synConstant">&quot;NoIcon&quot;</span>: <span class="synStatement">return</span> <span class="synConstant">&quot;&quot;</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Image&quot;</span>: <span class="synComment">// これを追加しないとコンパイルエラー</span> <span class="synStatement">return</span> <span class="synConstant">`&lt;img src=&quot;</span><span class="synStatement">${</span>icon.<span class="synStatement">url}</span><span class="synConstant">&quot; alt=&quot;</span><span class="synStatement">${</span>icon.<span class="synStatement">name}</span><span class="synConstant">&quot; /&gt;`</span> <span class="synStatement">default</span>: assertNever(icon) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>実際に作った型を値として使う場合は、以下のような使い方になります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synIdentifier">const</span> icon1 = EmojiIcon(<span class="synConstant">&quot;🍣&quot;</span>) <span class="synIdentifier">const</span> icon2 = NoIcon() <span class="synIdentifier">const</span> icon3 = ImageIcon(<span class="synConstant">&quot;https://example.com/image.png&quot;</span>, <span class="synConstant">&quot;Example Image&quot;</span>) <span class="synType">console</span>.<span class="synStatement">log</span>(toHTMLIcon(icon1)) <span class="synComment">// 🍣</span> <span class="synType">console</span>.<span class="synStatement">log</span>(toHTMLIcon(icon2)) <span class="synComment">//</span> <span class="synType">console</span>.<span class="synStatement">log</span>(toHTMLIcon(icon3)) <span class="synComment">// &lt;img src=&quot;https://example.com/image.png&quot; alt=&quot;Example Image&quot; /&gt;</span> </pre> <p>Discriminated Union により型を構造化し、コンパニオンオブジェクトパターンで生成を実装し、switch 文によるナローイングでパターンマッチ的に分解を実装しました。null を使わず NoIcon という状態を導入したおかげで見通しよく、静的検査を有向に活用しながら実装できました。</p> <h2 id="ディスクリミネーターはただの判別用のシンボル">ディスクリミネーターは、ただの判別用のシンボル?</h2> <p>ここまででも十分、Discriminated Union の有用性が確認できますが、仕組みとしてはオブジェクトのプロパティに <code>kind</code> など適当なプロパティ名でディスクリミネーターを忍ばせた程度にも見えます。</p> <p>TypeScript レイヤではナローイングによって型チェックが効くなど上手いこと機能していて座布団一枚! という感じ (?) もありますが、JavaScript のレイヤーでみるとただオブジェクトのプロパティの文字列で分岐しているだけのようにも思えて、そんなに本質的な事柄なのか? とも思えてしまいます。</p> <p>Discriminated Union が表現できるものは、この程度のものと思っておけばいいのでしょうか? いいえ、という話を続けてみていこうと思います。</p> <h2 id="Haskell-のデータ型宣言">Haskell のデータ型宣言</h2> <p>代数的データ型を「模倣できる」 TypeScript ではなく、代数的データ型を型システムにネイティブで搭載しているプログラミング言語、たとえば Haskell で同じ実装がどうなるのか、見てみましょう。</p> <p>以下のように実装できます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synPreProc">import</span> Text.Printf (printf) <span class="synType">data</span> TagIcon <span class="synStatement">=</span> NoIcon <span class="synStatement">|</span> EmojiIcon String <span class="synStatement">|</span> ImageIcon String String toHTMLIcon <span class="synStatement">::</span> TagIcon <span class="synStatement">-&gt;</span> String toHTMLIcon NoIcon <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span> toHTMLIcon (EmojiIcon symbol) <span class="synStatement">=</span> symbol toHTMLIcon (ImageIcon url name) <span class="synStatement">=</span> printf <span class="synConstant">&quot;&lt;img src=</span><span class="synSpecial">\&quot;</span><span class="synConstant">%s</span><span class="synSpecial">\&quot;</span><span class="synConstant"> alt=</span><span class="synSpecial">\&quot;</span><span class="synConstant">%s</span><span class="synSpecial">\&quot;</span><span class="synConstant"> &gt;&quot;</span> url name main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> icon1 <span class="synStatement">=</span> NoIcon icon2 <span class="synStatement">=</span> EmojiIcon <span class="synConstant">&quot;🍣&quot;</span> icon3 <span class="synStatement">=</span> ImageIcon <span class="synConstant">&quot;https://exmaple.com/image.png&quot;</span> <span class="synConstant">&quot;Example Image&quot;</span> putStrLn <span class="synStatement">$</span> toHTMLIcon icon1 putStrLn <span class="synStatement">$</span> toHTMLIcon icon2 putStrLn <span class="synStatement">$</span> toHTMLIcon icon3 </pre> <p>TypeScript での実装に比較すると分量がかなり短くなっています。とは言え、コードが短いかどうかはあまり重要ではありません。より詳細に見てみましょう。</p> <p>まず、TypeScript のケースとは異なりコンストラクタの明示的な実装がないことに気がつきます。</p> <p>そして <code>toHTMLIcon</code> 関数の引数でパターンマッチをしていますが、TypeScript のディスクリミネーターに相当するのは文字列リテラル的な値ではなく <code>NoIcon</code> <code>EmojiIcon</code> <code>ImageIcon</code> などのシンボルです。Haskell ではこれを「データコンストラクタ」と呼びます。データコンストラクタにより <code>TagIcon</code> 型の値を分解することができています。</p> <p><code>TagIcon</code> 型の宣言にもデータコンストラクタが使われています。データコンストラクタはデータ型の形状や構造を定義するものとしても使われます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synType">data</span> TagIcon <span class="synStatement">=</span> NoIcon <span class="synStatement">|</span> EmojiIcon String <span class="synStatement">|</span> ImageIcon String String </pre> <p>そして値を生成するときも、データコンストラクタが使われています。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink> <span class="synStatement">let</span> icon1 <span class="synStatement">=</span> NoIcon icon2 <span class="synStatement">=</span> EmojiIcon <span class="synConstant">&quot;🍣&quot;</span> icon3 <span class="synStatement">=</span> ImageIcon <span class="synConstant">&quot;https://exmaple.com/image.png&quot;</span> <span class="synConstant">&quot;Example Image&quot;</span> </pre> <p>このように Haskell ではデータコンストラクタが「タグ付き Union」におけるタグ相当ですが、データコンストラクタは型に基づいた値の分解、データ型の構築、値の生成と、データ型にまつわる操作を提供するものになっています。</p> <p>TypeScipt で Discriminated Union とコンパニオンオブジェクトパターン、switch 文 と複数の文法を組み合わせて模倣していた機能が、Haskell ではデータコンストラクタという仕組みによって、より密結合された、統一的なかたちで実現されています。これが Haskell における代数的データ型(Algebraic Data Types, ADT)の特徴です。</p> <p>そして <strong>Haskell では新しい型とデータ構造を定義する基本的な方法が、この data キーワードによる宣言です。</strong> ···ということは、このデータコンストラクタを中心とした代数的データ型の文法でより複雑なデータ構造とその型を宣言することができることを意味します。</p> <h2 id="代数的データ型でより構造的なデータ型を扱う">代数的データ型でより構造的なデータ型を扱う</h2> <p><a href="https://user-first.ikyu.co.jp/entry/2024/12/03/091133">永続データプログラミングと永続データ構造 - 一休.com Developers Blog</a> で紹介した、二分木 (による永続データ配列) の実装を見てみましょう。実装詳細には立ち入らず、雰囲気だけみてもらえばよいです。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synComment">-- データ型の宣言</span> <span class="synType">data</span> Tree a <span class="synStatement">=</span> Leaf a <span class="synStatement">|</span> Node (Tree a) (Tree a) <span class="synComment">-- 木を根から走査。パターンマッチと再帰で辿っていく</span> read <span class="synStatement">::</span> Int <span class="synStatement">-&gt;</span> Tree a <span class="synStatement">-&gt;</span> a read _ (Leaf x) <span class="synStatement">=</span> x read i (Node left right) <span class="synStatement">|</span> i <span class="synStatement">&lt;</span> size left <span class="synStatement">=</span> read i left <span class="synStatement">|</span> otherwise <span class="synStatement">=</span> read (i <span class="synStatement">-</span> size left) right write <span class="synStatement">::</span> Int <span class="synStatement">-&gt;</span> a <span class="synStatement">-&gt;</span> Tree a <span class="synStatement">-&gt;</span> Tree a write _ v (Leaf _) <span class="synStatement">=</span> Leaf v write i v (Node left right) <span class="synStatement">|</span> i <span class="synStatement">&lt;</span> size left <span class="synStatement">=</span> Node (write i v left) right <span class="synStatement">|</span> otherwise <span class="synStatement">=</span> Node left (write (i <span class="synStatement">-</span> size left) v right) size <span class="synStatement">::</span> Tree a <span class="synStatement">-&gt;</span> Int size (Leaf _) <span class="synStatement">=</span> <span class="synConstant">1</span> size (Node left right) <span class="synStatement">=</span> size left <span class="synStatement">+</span> size right fromList <span class="synStatement">::</span> [a] <span class="synStatement">-&gt;</span> Tree a fromList [] <span class="synStatement">=</span> error <span class="synConstant">&quot;Cannot build tree from empty list&quot;</span> fromList [x] <span class="synStatement">=</span> Leaf x fromList xs <span class="synStatement">=</span> <span class="synStatement">let</span> mid <span class="synStatement">=</span> length xs <span class="synStatement">`div`</span> <span class="synConstant">2</span> <span class="synStatement">in</span> Node (fromList (take mid xs)) (fromList (drop mid xs)) main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> arr <span class="synStatement">=</span> fromList [<span class="synConstant">1</span> <span class="synStatement">..</span> <span class="synConstant">8</span> <span class="synStatement">::</span> Int] print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr <span class="synComment">-- 3</span> <span class="synStatement">let</span> arr' <span class="synStatement">=</span> write <span class="synConstant">3</span> <span class="synConstant">42</span> arr print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr' <span class="synComment">-- 42</span> print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr <span class="synComment">-- 3</span> </pre> <p>重要なポイントとしては、コメントに書いたとおり (1) 完全二分木の木構造を data キーワードのみで宣言していること、(2) 木の中から目的のノードを探すにあたりパターンマッチで分解しながら走査していること、の 2 点が挙げられます。</p> <p>データ型の宣言を改めてみてみましょう。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synType">data</span> Tree a <span class="synStatement">=</span> Leaf a <span class="synStatement">|</span> Node (Tree a) (Tree a) </pre> <p>Tree 型が再帰的に宣言されているのがわかります。再帰データ型が宣言できるため、木のようなデータ構造を代数的データ型により構築することができます。</p> <p>さて、こうして木を実装する例をみると代数的データ型は、冒頭でみたような、ただの型を合併して判別する機能というものではなく、まさに「データの型と構造を構築するためのもの」だというのがわかります。</p> <p>同様にリスト構造の List 型を自前で実装してみましょう。リストの走査として先頭に要素を追加する <code>cons</code> 関数と、リストの値それぞれを写像する <code>mapList</code> 関数も実装してみます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synType">data</span> List a <span class="synStatement">=</span> Empty <span class="synStatement">|</span> Cons a (List a) <span class="synType">deriving</span> (Show) empty <span class="synStatement">::</span> List a empty <span class="synStatement">=</span> Empty cons <span class="synStatement">::</span> a <span class="synStatement">-&gt;</span> List a <span class="synStatement">-&gt;</span> List a cons <span class="synStatement">=</span> Cons mapList <span class="synStatement">::</span> (a <span class="synStatement">-&gt;</span> b) <span class="synStatement">-&gt;</span> List a <span class="synStatement">-&gt;</span> List b mapList _ Empty <span class="synStatement">=</span> Empty mapList f (Cons x xs) <span class="synStatement">=</span> Cons (f x) (mapList f xs) <span class="synComment">-- テスト出力</span> main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> xs <span class="synStatement">=</span> cons <span class="synConstant">1</span> (cons <span class="synConstant">2</span> (cons <span class="synConstant">3</span> empty)) print (mapList (<span class="synStatement">*</span> <span class="synConstant">2</span>) xs) <span class="synComment">-- Cons 2 (Cons 4 (Cons 6 Empty))</span> </pre> <p>先の二分木に同じく、data キーワードにより再帰データ型を定義してリストのデータ構造を構築しています。<code>mapList</code> 関数ではパターンマッチを用いてリストを走査し、リストが保持する値に写像関数を適用しています。データコンストラクタが、データ構造の構築とパターンマッチによる分解双方に利用されていることがわかります。</p> <p>このように Haskell のデータ型は「値がどのように構造化され、意味づけられるか」を定義する手段です。データコンストラクタはその手段を提供し、構築と分解という双方向の操作を統一的に扱えるようにします。</p> <p>この観点に立つと、データ型とデータコンストラクタの役割は次のように整理できそうです。</p> <ol> <li>データ型は、プログラム内の「概念モデル」を定義する</li> <li>データコンストラクタは、そのモデルの構築ルールを提供する</li> <li>パターンマッチによる分解は、そのモデルを解析し操作する方法を提供する</li> </ol> <h3 id="TypeScript-に同様のメンタルモデルを持ち込む">TypeScript に同様のメンタルモデルを持ち込む</h3> <p>Haskell のデータ型の宣言をここまで見てから、改めて TypeScript に戻ってきましょう。代数的データ型に対するメンタルモデルが大きく更新されているはずです。</p> <p>その視点で、改めて Discriminated Union よる代数的データ型の模倣を見てみましょう。「 <code>kind</code> プロパティは分岐目的のもの」ではなく Haskell 同様 「データ型を構築、分解する手段」として捉えることができるのではないでしょうか?</p> <p>さて、TypeScript の型システムも Haskell 同様、再帰データ型は宣言できます。先の Haskell で実装したリストを、TypeScript で、これまでみた Discriminated Union、コンパニオンオブジェクトパターン、switch 文によるパターンマッチのイディオムで、実装してみます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type </span><span class="synIdentifier">List</span>&lt;<span class="synIdentifier">T</span>&gt;<span class="synIdentifier"> </span><span class="synStatement">=</span> <span class="synIdentifier">Empty</span> <span class="synStatement">|</span> <span class="synIdentifier">Cons</span>&lt;<span class="synIdentifier">T</span>&gt; <span class="synStatement">interface</span> <span class="synIdentifier">Empty</span> <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Empty&quot;</span> <span class="synIdentifier">}</span> <span class="synStatement">interface</span> <span class="synIdentifier">Cons</span>&lt;<span class="synIdentifier">T</span>&gt; <span class="synIdentifier">{</span> <span class="synIdentifier">kind</span>: <span class="synConstant">&quot;Cons&quot;</span> <span class="synIdentifier">head</span>: <span class="synIdentifier">T</span> <span class="synIdentifier">tail</span>: <span class="synIdentifier">List</span>&lt;<span class="synIdentifier">T</span>&gt; <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">Empty</span>(): <span class="synIdentifier">Empty</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> <span class="synStatement">kind</span>: <span class="synConstant">&quot;Empty&quot;</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">Cons</span>&lt;<span class="synIdentifier">T</span>&gt;(<span class="synPreProc">head</span>:<span class="synPreProc"> </span><span class="synIdentifier">T</span><span class="synStatement">,</span><span class="synPreProc"> tail</span>:<span class="synPreProc"> </span><span class="synIdentifier">List</span>&lt;<span class="synIdentifier">T</span>&gt;): <span class="synIdentifier">Cons</span>&lt;<span class="synIdentifier">T</span>&gt; <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> <span class="synStatement">kind</span>: <span class="synConstant">&quot;Cons&quot;</span>, <span class="synStatement">head</span>, <span class="synStatement">tail </span><span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">type </span><span class="synIdentifier">map </span><span class="synStatement">=</span> &lt;<span class="synIdentifier">T</span>, <span class="synIdentifier">U</span>&gt;(<span class="synSpecial">f</span>:<span class="synSpecial"> </span>(<span class="synSpecial">a</span>:<span class="synSpecial"> </span><span class="synIdentifier">T</span>) <span class="synIdentifier">=&gt;</span><span class="synSpecial"> </span><span class="synIdentifier">U</span><span class="synStatement">,</span><span class="synSpecial"> xs</span>:<span class="synSpecial"> </span><span class="synIdentifier">List</span>&lt;<span class="synIdentifier">T</span>&gt;) <span class="synIdentifier">=&gt;</span> <span class="synIdentifier">List</span>&lt;<span class="synIdentifier">U</span>&gt; <span class="synIdentifier">const</span> map: <span class="synIdentifier">map</span> = (<span class="synPreProc">f</span><span class="synStatement">,</span><span class="synPreProc"> xs</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">switch</span> (xs.kind) <span class="synIdentifier">{</span> <span class="synStatement">case</span> <span class="synConstant">&quot;Empty&quot;</span>: <span class="synStatement">return</span> Empty() <span class="synStatement">case</span> <span class="synConstant">&quot;Cons&quot;</span>: <span class="synStatement">return</span> Cons(f(xs.<span class="synStatement">head</span>), map(f, xs.tail)) <span class="synStatement">default</span>: assertNever(xs) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">assertNever</span>(<span class="synPreProc">_</span>:<span class="synPreProc"> </span><span class="synType">never</span>): <span class="synType">never</span> <span class="synIdentifier">{</span> <span class="synSpecial">throw</span> <span class="synIdentifier">new</span> <span class="synType">Error</span>() <span class="synIdentifier">}</span> <span class="synIdentifier">const</span> xs: <span class="synIdentifier">List</span>&lt;<span class="synType">number</span>&gt; = Cons(<span class="synConstant">1</span>, Cons(<span class="synConstant">2</span>, Cons(<span class="synConstant">3</span>, Empty()))) <span class="synType">console</span>.<span class="synStatement">log</span>(map(<span class="synPreProc">i</span> <span class="synType">=&gt;</span> i * <span class="synConstant">2</span>, xs)) </pre> <p>以下が実行結果です。Discriminated Union で構造化されたリストと、各値が写像により倍化された結果が得られています。</p> <pre class="code tyepscript" data-lang="tyepscript" data-unlink>$ deno run -A list.ts { kind: &#34;Cons&#34;, head: 2, tail: { kind: &#34;Cons&#34;, head: 4, tail: { kind: &#34;Cons&#34;, head: 6, tail: { kind: &#34;Empty&#34; } } } }</pre> <p>TypeScript でも無理なく、再帰データ構造を実装できました。</p> <p>比較してみると TypeScript による代数的データ型は模倣だけあって、Haskell ほど簡潔に表現することはできません。一方で、それをどのようなメンタルモデルで捉えるかは、プログラミング言語の文法には左右されないでしょうから、Haskell のそれ同様に捉えてもよいでしょう。簡潔性は及ばないものの、機能的にはさほど遜色のない実装をすることができました。もちろん、より複雑なパターンマッチを要するものまで実現できるかどうかや、ランタイム性能の影響まで考慮すると Haskell 同等とまではいきませんが。</p> <p>目論見どおり、TypeScript の Discriminated Union に対する印象をアップデートすることができたでしょうか? できていることを願います 😀</p> <p>実務で Discriminated Union を用いて再帰データ構造を宣言する、という機会はあまりないとは思いますが、それがただの Union で併合された型を判別できるものと小さく捉えるのではなく、本稿でみた通りデータ型の構築と分解の観点で捉えておくと視点が拡がるでしょうし、より広範囲に適用していってよいものだという確証が得られるのではないかと思います。</p> <h2 id="余談">余談</h2> <p>TypeScript と Haskell を比較する記事を、過去に幾つか書きました。</p> <ul> <li><a href="https://user-first.ikyu.co.jp/entry/2023/12/10/134411">TypeScriptでどこまで「関数型プログラミング」するか ─ 「手続き Haskell」から考察する - 一休.com Developers Blog</a></li> <li><a href="https://user-first.ikyu.co.jp/entry/2024/12/03/091133">永続データプログラミングと永続データ構造 - 一休.com Developers Blog</a></li> </ul> <p>TypeScript の型システムは JavaScript の上に後付けされたものということもあり、非常にプラクティカルで便利である一方、個人的には、やや散らかっていてその全体像や各機能の本質を掴みにくいと感じています。Haskell など表現に妥協の少ないプログラミング言語と比較し、相対化することでより深い理解に繋がることは多いです。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">雑にまとめると Haskell やれば TypeScript 書くの上達する</p>&mdash; naoya (@naoya_ito) <a href="https://twitter.com/naoya_ito/status/1857809449844617351?ref_src=twsrc%5Etfw">2024年11月16日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>Enjoy !</p> Fri, 13 Dec 2024 15:22:24 +0900 hatenablog://entry/6802418398311285259 Design Doc でチームを跨いだ開発を円滑に行う https://user-first.ikyu.co.jp/entry/2024/12/09/143648 <p>この記事は <a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a> 7 日目の記事です。</p> <p>宿泊事業本部 ユーザー向け開発チームの原です。 一休.com と Yahoo!トラベルの主にフロントエンドの開発を担当しています。</p> <p>今回は、普段の開発でコードを書き始める前段階で Design Doc を作ることで、円滑な開発を進められるようになったというお話をします。</p> <h2 id="チーム構成について">チーム構成について</h2> <p>まず、前提を共有するために私達が普段どのような体制で開発しているかを説明します。<br/> 私が所属している宿泊事業本部 ユーザー向け開発チームは、一休.com と Yahoo!トラベルの主に toC のユーザー向けの機能開発をしています。ユーザー向け開発チームのメインのミッションはユーザー体験を向上させることであり、そういった施策の機能開発を素早くリリースできることを大事にしています。<br/> 一方、プロダクト開発においては機能開発だけではなく、プログラミング言語や依存ライブラリのアップデートや、アーキテクチャの見直しといったシステムの健全性を向上させる取り組みも重要です。<br/> 機能開発とシステム改善を同じチームが両立して行えることが理想的かもしれません。しかし、Nuxt でできたフロントエンドのアプリケーションに関しては、施策に関する機能開発はユーザー向け開発チームが、システム改善はフロントエンド改善チームという専任のチームが担当しています。<br/> これは、変化の激しいフロントエンド開発でベストプラクティスを追い求めるには施策開発とシステム改善をする責務を分けたほうが進めやすいという判断によるものです。<br/> 実際、フロントエンド改善チームの取り組みにより、</p> <ul> <li>Nuxt2 から 3 へのアップデート</li> <li>Options API から Composition API への書き換え</li> </ul> <p>といった Vue/Nuxt 界隈の進化に追従したり、</p> <ul> <li>GraphQL の client-preset の導入</li> <li>デザインシステムの推進</li> </ul> <p>なども機能開発を止めずに完了しています。こういった取り組みにより、かなり開発者体験がいい環境で日々機能開発ができています。</p> <p>少し古いエントリーですが、フロントエンド改善チームの取り組みは以下でご確認できます。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F04%2F18%2F081144" title="一休.com、Yahoo!トラベルのNuxtをNuxt3にアップグレードしました - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/04/18/081144">user-first.ikyu.co.jp</a></cite></p> <p>開発チームと改善チームが分かれている状態においては、うまくコミュニケーションを取らないと問題が生じます。<br/> お互いどんな取り組みをするのか共有しないと、</p> <ul> <li>開発チームの施策で触るコードと、改善チームのリファクタリングしたいコードがコンフリクトする</li> <li>改善チームが行ったリアーキテクトを開発チームがちゃんと理解しないとベストプラクティスではない実装をしてしまう</li> </ul> <p>といったことが起こり得ます。<br/> 特に「ベストプラクティスではない実装をしてしまう」というのは避けたい問題です。<br/> そのため、開発チームが実装した機能は小さな修正を除いては基本的に Pull Request (以下 PR) でレビューしてもらうことになっています。<br/> 実際レビューの際に、最適な実装にたどり着くまで時間がかかってしまったということが何度かありました。</p> <p>前置きが長くなりましたが、こうした別のチームにコードレビューを依頼するとき、円滑な開発を進めるために私が必要だと思っていることを紹介します。</p> <h2 id="コードレビューについて">コードレビューについて</h2> <p>私はレビュアーとしてコードをレビューするのは非常に労力のかかる仕事だと思っています。<br/> よく「実装が終わって PR を出したので、もう少しで完了します」みたいなことを言ってしまいがちですが、コードレビューは実装と同等か、場合によってはそれ以上の負担が発生しうる作業だと思っています。<br/> というのも、Approve されるとリリースできるという運用においては、レビュアーの仕事はコード書く人(レビュイー)と同等の責任が発生するためです。<br/> いきなり数百行、数千行規模の差分が発生する修正をレビューするときには</p> <ul> <li>その施策や修正の背景</li> <li>実現するための最適な設計になっているか</li> <li>その diff を取り込むことでどんな影響が起こり得るか</li> </ul> <p>などを考える必要がありますが、それらを一から考えるのは、コードを最初から書くのと同じくらいの負担がかかるものです。<br/> 上記のような考慮はコードを書く側(レビュイー)は当然考えたうえで実装しているはずなので、レビュイーからレビュアーにうまく伝えられると負担を軽減できます。<br/> どういった工夫でレビュアーの負担を軽減しようとしているかを紹介します。</p> <h2 id="いきなりコードを書かない">いきなりコードを書かない</h2> <p>先程も述べたような差分が数百行、数千行規模の PR をいきなりレビューしてもらうのは、PR の description やコメントをいくら丁寧に書いたとしても、レビュアーの負担は大きいです。<br/> そこで実装に入る前の段階で Design Doc を作成して、大筋の実装内容について合意を取るようにしています。<br/> Design Doc は以下のようなアウトラインで書いています。</p> <pre class="code" data-lang="" data-unlink>## このドキュメントの目的 ## やりたいこと // ここではビジネス的な視点でなぜこの施策をするのかを書きます ## 仕様 // ここでは上記のやりたいことを満たす機能要件を書きます ## 対応内容 // ここではシステム的な視点でどんな対応が必要なのかを書きます</pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20241209/20241209105314.png" alt="&#x3053;&#x306E;&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;&#x306E;&#x76EE;&#x7684;&#x3001;&#x3084;&#x308A;&#x305F;&#x3044;&#x3053;&#x3068;&#x304C;&#x8A18;&#x8F09;&#x3055;&#x308C;&#x305F; Design Doc &#x306E;&#x30B9;&#x30AF;&#x30B7;&#x30E7;" width="800" height="710" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Design Doc の目的は、実装者とレビュアーの間で大まかな実装の合意をとることです。</p> <p>新規ページ作成を例にすると</p> <ul> <li>URL をどう命名するか</li> <li>コンポーネントの階層と、各コンポーネントをどう命名するか</li> <li>サーバー(GraphQL)からデータをどのように取得するか</li> <li>機能要件を満たすロジックをどう実装するか <ul> <li>既存のロジックで使えるものは何か</li> </ul> </li> </ul> <p>などを Design Doc で決定します。<br/> 特に命名は先に決めておくと実装、レビューともに楽です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20241209/20241209111924.png" alt="&#x65E2;&#x5B58;&#x306E;&#x30ED;&#x30B8;&#x30C3;&#x30AF;&#x3092;&#x4F7F;&#x3048;&#x308B;&#x3068;&#x3044;&#x3046;&#x30A2;&#x30C9;&#x30D0;&#x30A4;&#x30B9;&#x304C;&#x3082;&#x3089;&#x3048;&#x308B;" width="375" height="484" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>(↑ 既存のロジックを使えるというアドバイスがもらえる)</p> <p>Design Doc で事前に実装方針の合意をとることで、「なぜこのような設計にしたのか」をレビュアーがレビュー時に考える必要がなくなります。 また、レビューする段階で大まかな実装イメージがついているので、レビューの負担が軽減されると考えています。</p> <h2 id="Pull-Request-を出す際に気をつけていること">Pull Request を出す際に気をつけていること</h2> <h3 id="Design-Doc-との乖離がある場合">Design Doc との乖離がある場合</h3> <p>Design Doc で実装方針の合意をとれたら、実装をして、完了したらレビューに出します。<br/> 当然、実装する中で Design Doc で決めた通りにいかなかったり、もっといい方法が見つかったりすることもあるでしょう。<br/> それを何も共有せずレビューに出してしまうとせっかく実装方針を決めた意義が薄れてしまいます。<br/> Design Doc 時の決定と大きく変わる場合は、レビューを出す前に Design Doc 自体を修正して、もう一度合意を取り直すようにしています。<br/> Pull Request の Description やコメントにその旨を書くだけで伝わるような些細な変更の場合は、レビュー段階でそれを伝えるようにします。</p> <h3 id="レビュアーの負担を最小限に">レビュアーの負担を最小限に</h3> <p>当然ですが、レビューを依頼する前に自分で見つけられる粗は見つけておくべきなので、自分がレビュアーのつもりでセルフレビューをします。<br/> 施策とは直接は関係ないリファクタリングなど、レビュアーが「これはなぜいま修正が必要なのか?」と疑問を持ちそうな箇所はコメントを残しておきます。</p> <ul> <li>動作確認方法</li> <li>影響する既存機能が元通り動いていることをどうテストしたのか</li> </ul> <p>といった情報も記載します。<br/> また、実装していてもっと良い書き方があるはずだが思いつかなかったような場合、どんなことを試してうまく行かなったということを残しておくとよいでしょう。</p> <h2 id="最後に">最後に</h2> <p>今回はチーム間を跨いだレビューで私が気をつけていることを紹介しました。<br/> 常にペアプロ・モブプロを行っていたり、チームの成熟度が高い場合は Design Doc を作成することの必要性は薄いかもしれません。<br/> ただ、実装タイミングでどんな意思決定がなされたのかという情報は、時間が経った後から見返す際、有用になります。<br/> また、</p> <ul> <li>レビューのコストは実装と同じくらいのコストになり得る</li> <li>レビュアーの負担はレビュイーの工夫次第で軽減できる</li> </ul> <p>というのはどこでも共通する話だと思います。</p> Mon, 09 Dec 2024 14:36:48 +0900 hatenablog://entry/6802418398309404084 永続データプログラミングと永続データ構造 https://user-first.ikyu.co.jp/entry/2024/12/03/091133 <p>この記事は <a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a> の3日目の記事です。</p> <p>昨今は我々一休のような予約システム開発においても、関数型プログラミング由来のプラクティスを取り入れる機会が増えています。</p> <p>例えば、値はイミュータブルである方が扱いやすい、関数は副作用のない純粋関数にする方がテスタビリティなども含め何かと都合がよい、そういう場面では積極的に不変な値を使い、関数が冪等になるよう意識的に実装します。ドメインロジックを純粋関数として記述できると、堅牢で責務分離もしやすく、テストやデバッグもしやすいシステムになっていきます。</p> <p>ところで「関数型プログラミングとはなんぞや」というのに明確な定義はないそうです。ですが突き詰めていくと、計算をなるべく「文」ではなく「式」で宣言することが一つの目標だということに気がつきます。</p> <p>文と式の違いは何でしょうか?</p> <p>for 文、代入文、if 文などの文は、基本的には値を返しません。値を返さないということは、文は直接結果を受け取るものではなく、命令になっていると言えます。文は計算機への命令です。</p> <p>一方の式は、必ず返値を伴いますから、その主な目的は返値を得る、つまり式を評価して計算の結果を得ることだと考えることができます。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink>customer.archive() </pre> <p>と、文によって暗黙的に customer オブジェクトの内部状態を変更するのではなく</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synIdentifier">const</span> archivedCustomer = archiveCustomer(customer) </pre> <p>と、引数で与えられた customer オブジェクトを直接変更することなしに、アーカイブ状態に変更されたコピーとしての archivedCustomer オブジェクトを返値として返す、これが式です。この関数は純粋関数として実装し、customer オブジェクトは不変、つまりイミュータブルなものとして扱うと良いでしょう。</p> <p>式によるイミュータブルなオブジェクトの更新は TypeScript なら</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synSpecial">export</span> <span class="synIdentifier">const</span> archiveCustomer = (<span class="synPreProc">customer</span>:<span class="synPreProc"> </span><span class="synIdentifier">Customer</span>):<span class="synPreProc"> </span><span class="synIdentifier">Customer</span><span class="synPreProc"> </span><span class="synType">=&gt;</span> (<span class="synIdentifier">{</span> ...customer, <span class="synStatement">archived</span>: <span class="synConstant">true</span> <span class="synIdentifier">}</span>) </pre> <p>と、スプレッド構文を使うことで customer オブジェクトのコピーを作りつつ、変更したいプロパティを新たな値に設定したものを返すように実装します。</p> <p>このように、引数で与えたオブジェクトは直接変更せず、状態を変更した別のオブジェクトを返すような関数の連なりによって計算を定義していくのが関数型プログラミングです。</p> <p>このあたりの考え方については、過去の発表スライドがありますので参考にしてください。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/ed40eb21f5be431395028ee3777ca727?slide=19" title="関数型プログラミングと型システムのメンタルモデル" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>実際、我々の一部プロダクトのバックエンドでは TypeScript による関数型スタイルでの開発を実践しています。以下はプロダクトのコードの一例で、Customer オブジェクトに新しいメールアドレスの値を追加するための <code>addEmail</code> 関数です。先の実装に同じく、スプレッド構文を使って元のオブジェクトを破壊せずに、メールアドレスが追加されたオブジェクトを返します。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synIdentifier">const</span> addEmail = (<span class="synPreProc">address</span>:<span class="synPreProc"> </span><span class="synIdentifier">EmailAddress</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> (<span class="synPreProc">customer</span>:<span class="synPreProc"> </span><span class="synIdentifier">Customer</span>):<span class="synPreProc"> </span><span class="synIdentifier">Customer</span><span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> newAddress: <span class="synIdentifier">CustomerEmail</span> = <span class="synIdentifier">{</span> <span class="synStatement">id</span>: generateCustomerEmailId(), <span class="synStatement">address</span>, <span class="synIdentifier">}</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> ...customer, <span class="synStatement">emails</span>: <span class="synIdentifier">[</span>...customer.emails, newAddress<span class="synIdentifier">]</span>, <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <p>ドメインオブジェクトの状態遷移はすべて、この式による状態遷移のモデルで実装しています。</p> <h2 id="永続データプログラミング">永続データプログラミング</h2> <p>さて、本記事のテーマは「永続データ」です。永続データとは何でしょうか?</p> <p>式を意識的に使い、かつ値をイミュータブルに扱うことを基本としてやっていくと、何気なく書いたプログラムの中に特徴的な様子が現れることになります。</p> <p>以下、リスト操作のプログラムを見てみましょう。リストの先頭や末尾に値を追加したり、適当な値を削除する TypeScript のプログラムです。リストをイミュータブルに扱うべく、値の追加や削除などデータ構造の変更にはスプレッド構文を使い、非破壊的にそれを行うようにします。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// as1: 元のリスト</span> <span class="synIdentifier">const</span> as1 = <span class="synIdentifier">[</span><span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span>, <span class="synConstant">4</span>, <span class="synConstant">5</span><span class="synIdentifier">]</span>; <span class="synComment">// as2: 新しいリスト (先頭に 100 を追加)</span> <span class="synIdentifier">const</span> as2 = <span class="synIdentifier">[</span><span class="synConstant">100</span>, ...as1<span class="synIdentifier">]</span>; <span class="synComment">// as3: 新しいリスト (末尾に 500 を追加)</span> <span class="synIdentifier">const</span> as3 = <span class="synIdentifier">[</span>...as2, <span class="synConstant">500</span><span class="synIdentifier">]</span>; <span class="synComment">// as4: 新しいリスト (値 3 を削除)</span> <span class="synIdentifier">const</span> as4 = as3.<span class="synStatement">filter</span>(<span class="synPreProc">x</span> <span class="synType">=&gt;</span> x !== <span class="synConstant">3</span>); <span class="synType">console</span>.<span class="synStatement">log</span>(<span class="synConstant">&quot;as1:&quot;</span>, as1); <span class="synComment">// [1, 2, 3, 4, 5]</span> <span class="synType">console</span>.<span class="synStatement">log</span>(<span class="synConstant">&quot;as2:&quot;</span>, as2); <span class="synComment">// [100, 1, 2, 3, 4, 5]</span> <span class="synType">console</span>.<span class="synStatement">log</span>(<span class="synConstant">&quot;as3:&quot;</span>, as3); <span class="synComment">// [100, 1, 2, 3, 4, 5, 500]</span> <span class="synType">console</span>.<span class="synStatement">log</span>(<span class="synConstant">&quot;as4:&quot;</span>, as4); <span class="synComment">// [100, 1, 2, 4, 5, 500]</span> </pre> <p>更新をしても元のリストは不変なので、<code>as1</code> を参照しても更新済みの結果は得られません。リスト操作の返り値を <code>as2</code> <code>as3</code> <code>as4</code> とその都度変数にキャプチャし、そのキャプチャした変数に対して次のリスト操作を行います。こうしてデータ構造は不変でありつつも一連の、連続したリスト操作を表現します。</p> <p>データ構造を不変にした結果、リストが更新される過程の状態すべてが残りました。リストを何度か更新したにも関わらず、変更前の状態を参照することができています。<code>as1</code> を参照すれば初期状態を、<code>as2</code> や <code>as3</code> で途中の状態を参照することができます。このように値の変更後もそれ以前の状態が残るさまを「永続データ」と呼びます。そして永続データを用いたプログラミングを「永続データプログラミング」と呼びます。</p> <p>値をイミュータブルに扱うと必然的にそれは永続データになるので、永続データプログラミングはそれ自体、何か特別なテクニックというわけではありません。一方で、値が永続データであることをはっきりさせたい文脈上では「永続データプログラミング」という言葉でプログラミングスタイルを表現すると、その意図が明確になることも多いでしょう。</p> <p>以下の山本和彦さんの記事では、関数型プログラミングすなわち「永続データプログラミング」であり、永続データを駆使して問題を解くことこそが関数型プログラミングだ、と述べられています。</p> <blockquote><p>筆者の関数プログラミングの定義、すなわちこの特集での定義は、「⁠永続データプログラミング」です。永続データとは、破壊できないデータ、つまり再代入できないデータのことです。そして、永続データを駆使して問題を解くのが永続データプログラミングです。</p> <p>また関数型言語とは、永続データプログラミングを奨励し支援している言語のことです。関数型言語では、再代入の機能がないか、再代入の使用は限定されています。筆者の定義はかなり厳しいほうだと言えます。</p> <p><a href="https://gihyo.jp/dev/feature/01/functional-prog/0001">第1章 関数プログラミングは難しくない!―初めて学ぶ人にも、挫折した人にもきちんとわかる | gihyo.jp</a></p></blockquote> <p>命令型プログラミングにおいては変更にあたり値を直接破壊的に変更します。変更前のデータ構造の状態を参照することはできません。リストの破壊的変更は、基本的に (式ではなく) 文によって行われるでしょう。文を主体としたプログラミング··· 命令型プログラミングでは、永続ではないデータ、つまり短命データを基本にしていると言えます。一方、式によってプログラムを構成する関数型プログラミングでは、関数の冪等性を確保すべくイミュータブルに値を扱うことになるので、永続データが基本になります。</p> <p>イミュータブルな値によるプログラミングをする際、そこにある値は不変であるだけでなく、同時に永続データなのだということを認識できると、プログラミングスタイルに対するよりよいメンタルモデルが構築できると思います。</p> <h2 id="Haskell-と永続データプログラミング">Haskell と永続データプログラミング</h2> <p>やや唐突ですが、イミュータブルといえば純粋関数型言語の Haskell です。先の TypeScript によるリスト操作のプログラムを、Haskell で実装してみます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink>main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> as1 <span class="synStatement">=</span> [<span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span>, <span class="synConstant">4</span>, <span class="synConstant">5</span>] as2 <span class="synStatement">=</span> <span class="synConstant">100</span> <span class="synStatement">:</span> as1 as3 <span class="synStatement">=</span> as2 <span class="synStatement">++</span> [<span class="synConstant">500</span>] as4 <span class="synStatement">=</span> delete <span class="synConstant">3</span> as3 print as1 <span class="synComment">-- [1,2,3,4,5] </span> print as2 <span class="synComment">-- [100,1,2,3,4,5]</span> print as3 <span class="synComment">-- [100,1,2,3,4,5,500]</span> print as4 <span class="synComment">-- [100,1,2,4,5,500]</span> </pre> <p>Haskell はリストはもちろん、基本的に値がそもそもがイミュータブルです。リスト操作の API はすべて非破壊的になるよう実装されているので、変更にあたり TypeScript のようにスプレッド構文でデータを明示的にコピーしたりする必要はありません。裏を返せば、変更は永続データ的に表現せざるを得ず、式によってプログラムを構成することが必須となります。結果、Haskell による実装は自然と永続データプログラミングになります。</p> <p>関数型プログラミングすなわち永続データプログラミングだ、というのは、この必然性から来ています。</p> <h2 id="永続データの特性を利用した問題解決">永続データの特性を利用した問題解決</h2> <p>永続データプログラミングは不変な値を使うことですから、それを実践することで記事冒頭で挙げたようなプログラムの堅牢性など様々なメリットを享受できるわけですが、「変更前の過去の状態を参照できる」という、値が不変であるというよりは、まさに「永続」データの特性が部分が活きるケースがあります。</p> <p>わかりやすい題材として、競技プログラミングの問題を例に挙げます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fatcoder.jp%2Fcontests%2Fabc273%2Ftasks%2Fabc273_e" title="E - Notebook" 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://atcoder.jp/contests/abc273/tasks/abc273_e">atcoder.jp</a></cite></p> <p>問題文を読むのが面倒な方のために、これがどんな問題か簡単に解説します。入力の指示に従ってリストを更新しつつ、任意のタイミングでそのリストの現在の状態を保存する。また任意のタイミングで復元できるようするという、データ構造の保存と復元を題材にした問題です。</p> <pre class="code" data-lang="" data-unlink>ADD 3 SAVE 1 ADD 4 SAVE 2 LOAD 1 DELETE DELETE LOAD 2 SAVE 1 LOAD 3 LOAD 1</pre> <p>こういうクエリが入力として与えられる。</p> <ul> <li>空のリストが最初にある</li> <li>クエリを上から順番に解釈して、<code>ADD 3</code> のときはリスト末尾に <img src="https://chart.apis.google.com/chart?cht=tx&chl=%203" alt=" 3"/> を追加する</li> <li><code>DELETE</code> なら末尾の値を削除</li> <li><code>SAVE 1</code> のときは、今使っているリストを ID 番号 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%201" alt=" 1"/> の領域に保存、<code>LOAD 1</code> なら ID 番号 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%201" alt=" 1"/> の領域からリストを復元する</li> <li>クエリのたび、その時点でのリストの末尾の要素を出力する</li> </ul> <p>という問題になっています。</p> <p>この問題を永続データなしで解こうとすると、リストを更新しても以前の状況に戻れるような木のデータ構造を自分で構築する必要がありなかなか面倒です。一方、永続データを前提にすると、何の苦労もなく解けてしまいます。</p> <p>以下は Haskell で実装した例です。やっていることは、クエリの内容に合わせてリストに値を追加・削除、保存と復元のときは辞書 (IntMap) に、その時点のリストを格納しているだけです。問題文の通りにシミュレーションしているだけ、とも言えます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink>main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> q <span class="synStatement">&lt;-</span> readLn <span class="synStatement">@</span>Int qs <span class="synStatement">&lt;-</span> map words <span class="synStatement">&lt;$&gt;</span> replicateM q getLine <span class="synStatement">let</span> qs' <span class="synStatement">=</span> [<span class="synStatement">if</span> null args <span class="synStatement">then</span> (command, <span class="synStatement">-</span><span class="synConstant">1</span>) <span class="synStatement">else</span> (command, stringToInt (head args)) <span class="synStatement">|</span> command <span class="synStatement">:</span> args <span class="synStatement">&lt;-</span> qs] <span class="synStatement">let</span> res <span class="synStatement">=</span> scanl' f ([], IM.empty) qs' <span class="synType">where</span> f (xs, s) query <span class="synStatement">=</span> <span class="synStatement">case</span> query <span class="synStatement">of</span> (<span class="synConstant">&quot;ADD&quot;</span>, x) <span class="synStatement">-&gt;</span> (x <span class="synStatement">:</span> xs, s) <span class="synComment">-- リストに値を追加</span> (<span class="synConstant">&quot;DELETE&quot;</span>, _) <span class="synStatement">-&gt;</span> (drop1 xs, s) <span class="synComment">-- リストから値を削除</span> (<span class="synConstant">&quot;SAVE&quot;</span>, y) <span class="synStatement">-&gt;</span> (xs, IM.insert y xs s) <span class="synComment">-- 辞書にこの時点のリストを保存</span> (<span class="synConstant">&quot;LOAD&quot;</span>, z) <span class="synStatement">-&gt;</span> (IM.findWithDefault [] z s, s) <span class="synComment">-- 辞書から保存したリストを復元</span> _ <span class="synStatement">-&gt;</span> error <span class="synConstant">&quot;!?&quot;</span> printList [headDef (<span class="synStatement">-</span><span class="synConstant">1</span>) xs <span class="synStatement">|</span> (xs, _) <span class="synStatement">&lt;-</span> tail res] <span class="synComment">-- 各クエリのタイミングでのリストの先頭要素を得て、出力</span> </pre> <p>Haskell のリストは永続データですから、値を変更しても変更以前の値が残ります。その値が暗黙的に他で書き換えられる事はありません。よって素直にリストを辞書に保存しておけばよいのです。一方、命令型プログラミングにおいてリストがミュータブルな場合は、ある時点の参照を辞書に保存したとしても、どこかで書き換えが発生すると、辞書に保存された参照の先のデータが書き換わるためうまくいきません。</p> <h2 id="永続データ構造">永続データ構造</h2> <p>さて、ここからが本題です。TypeScript でリストを永続データとして扱うにあたり、スプレッド構文によるコピーを使いました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// as2: 新しいリスト (先頭に 100 を追加)</span> <span class="synIdentifier">const</span> as2 = <span class="synIdentifier">[</span><span class="synConstant">100</span>, ...as1<span class="synIdentifier">]</span>; <span class="synComment">// as3: 新しいリスト (末尾に 500 を追加)</span> <span class="synIdentifier">const</span> as3 = <span class="synIdentifier">[</span>...as2, <span class="synConstant">500</span><span class="synIdentifier">]</span>; </pre> <p>すでにお気づきの方も多いと思いますが、値の更新にあたり、リスト全体のコピーが走ってしまっています。一つ値を追加、削除、更新するだけでもリストの要素 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n" alt=" n"/> 件に対し <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n" alt=" n"/> 件のコピーが走る。つまり <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28n%29" alt=" O(n)"/> の計算量が必要になってしまいます。永続データプログラミングは良いものですが、ナイーブに実装するとデータコピーによる計算量の増大を招きがちです。</p> <p>Haskell など、イミュータブルが前提のプログラミング言語はこの問題をどうしているのでしょうか?</p> <p>結論、データ構造全体をコピーするのではなく「変更されるノードとそのノードへ直接的・間接的に参照を持つノードだけをコピーする」ことによって計算量を抑え、不変でありながらも効率的なデータ更新が可能になるようにリストその他のデータ構造が実装されています。つまり同じ「リスト」でも、命令型プログラミングのそれと、不変なデータ構造のそれは実装自体が異なるのです。抽象は同じ「リスト」でも具体が違うと言えるでしょう。</p> <p>変更あったところだけをコピーし、それ以外は元の値と共有を行うこのデータ構造の実装手法は Structural Sharing と呼ばれることもあります。Structural Sharing により不変でありながら効率的に更新が可能な永続データのデータ構造を「永続データ構造」と呼びます。</p> <p>永続データ構造については、以下の書籍にその実装方法含め詳しく記載されています。</p> <p><a href="https://asciidwango.jp/post/160831986220/%E7%B4%94%E7%B2%8B%E9%96%A2%E6%95%B0%E5%9E%8B%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0">純粋関数型データ構造 - アスキードワンゴ</a></p> <p>もとい、例えば Haskell のリストは先頭の値を操作する場合は <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%281%29" alt=" O(1)"/> です。先頭要素だけがコピーされていて、それ以降の要素が更新前後の二つのリストで共有されるからです。</p> <p>同じく、先の実装でも利用した <code>Data.IntMap</code> という辞書、こちらも永続データ構造ですが、内部的にはパトリシア木で実装されていて、値の挿入やキーの探索は、整数のビット長程度の計算量 ··· <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20n" alt=" n"/> をデータサイズ、<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20W" alt=" W"/> をビット長としたとき <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28min%28n%2C%20W%29%29" alt=" O(min(n, W))"/> に収まります。</p> <p>Haskell で利用する標準的なデータ構造 ··· List、Map、Set、Sequence、Heap は、すべてイミュータブルでありながら、値の探索や変更が <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%281%29" alt=" O(1)"/> や <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28%5Clog%20n%29" alt=" O(\log n)"/> 程度の計算量で行える永続データ構造になっています。(なお、誤解の無いよう補足すると、ミュータブルなデータ構造もあります。ミュータブルなデータ構造は手続き的プログラミングで変更することになります)</p> <p>永続データ構造を利用することによって、永続データプログラミング時にもパフォーマンスをそれほど犠牲にせず、大量のデータを扱うことが可能になります。裏を返せば、永続データプログラミングをより広範囲に実践していくには、永続データ構造が必要不可欠であるとも言えます。関数型プログラミングは値が不変であることをよしとしますが、そのためには永続データ構造が必要かつ重要なパーツなのです。</p> <p>TypeScript その他のプログラミング言語で永続データプログラミングを実践するとき、純粋関数型言語とは異なり、素の状態では永続データ構造の支援がないということは念頭に置いておくべきでしょう。</p> <h2 id="TypeScript-や-Python-で永続データ構造を利用するには">TypeScript や Python で永続データ構造を利用するには?</h2> <p>TypeScript の Array、Map、Set などの標準的なデータ構造はすべて命令型データ構造、つまりミュータブルです。命令型のプログラミング言語においては、どの言語も同様でしょう。一方、プログラミング言語によっては List、Map、Set などの永続データ構造バージョンを提供するサードパーティライブラリがあります。</p> <ul> <li><a href="https://immutable-js.com/">Immutable.js</a> (JavaScript / TypeScript)</li> <li><a href="https://pypi.org/project/pyrsistent/">pyrsistent · PyPI</a> (Python)</li> </ul> <p>これらのライブラリを導入することで、TypeScript や Python で永続データ構造を利用することができます。しかし、実際のところこれらの永続データ構造の実装が、広く普及しているようには思えません。</p> <h2 id="永続データ構造は業務システム開発にも必須か">永続データ構造は業務システム開発にも必須か?</h2> <p>結論からいうと、命令型のプログラミング言語で業務システム開発をする場合には、必須ではないでしょう。</p> <p>永続データプログラミング自体は良い作法ですが、業務システムにおいては、大量データのナイーブなコピーが走るような実装をする場面が少ないから、というのが理由だと思います。</p> <p>Haskell のような関数型言語を使っているのであれば、永続データ構造は標準的に提供されていて、そもそも必須かどうかすら気にする必要がありません。永続データ構造のメカニズムを全く知らなくても、自然にそれを使ったプログラムを書くように導かれます。</p> <p>命令型言語を使いつつも、永続データプログラミングを実践するケースではどうでしょうか? 速度が必要な多くの場面では、いったん永続データを諦め、単に命令型データ構造を利用すれば事足りるので、わざわざ永続データ構造を持ち出す必要はないでしょう。ドメインオブジェクトの変更をイミュータブルに表現するためコピーする場合も、せいぜい 10 か 20 程度のプロパティをコピーする程度で、コピー 1回にあたり数万件といったオーダーのコピーが発生するようなことは希でしょう。</p> <p>よって業務システム開発において Immutable.js や pyrsistent のようなサードパーティライブラリを積極的に使いたい場面は、先に解いた競技プログラミング問題のように、永続データ構造の永続である特性そのものが機能要件として必要になるケースに限られるのではないか? と思います。</p> <blockquote><p>Immutable.js の開発が停滞しているのは、フロントエンドで永続データ構造の需要が乏しいからでしょう。このようなデータ構造自体は非常に重要な概念で、多くのプログラミング言語に存在します。我々フロントエンドエンジニアが依存するブラウザの内部でも、効率的なデータ処理のために多用されているはずです。しかし、フロントエンドエンジニアがイミュータブルに求めているのは処理速度ではなく設計の改善です。だからこそ、Immutable.js に代わって Immer が隆盛したのでしょう。</p> <p><a href="https://zenn.dev/uhyo/articles/immutable-immer">Immutable.jsとImmer、ちゃんと使い分けていますか?</a></p></blockquote> <p>一方、純粋関数型言語で競技プログラミングのような大きなデータを扱うプログラミングを行う場合、永続データ構造は必須ですし、また永続データ構造を利用していることを意識することでよりよい実装が可能になると思っています。個人的にはこの「永続データ構造によって、より良い実装が可能になる」点こそが本質的だと思っています。</p> <p>先の競技プログラミングの実装を改めてみてみます。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink>main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> q <span class="synStatement">&lt;-</span> readLn <span class="synStatement">@</span>Int qs <span class="synStatement">&lt;-</span> map words <span class="synStatement">&lt;$&gt;</span> replicateM q getLine <span class="synStatement">let</span> qs' <span class="synStatement">=</span> [<span class="synStatement">if</span> null args <span class="synStatement">then</span> (command, <span class="synStatement">-</span><span class="synConstant">1</span>) <span class="synStatement">else</span> (command, stringToInt (head args)) <span class="synStatement">|</span> command <span class="synStatement">:</span> args <span class="synStatement">&lt;-</span> qs] <span class="synStatement">let</span> res <span class="synStatement">=</span> scanl' f ([], IM.empty) qs' <span class="synType">where</span> f (xs, s) query <span class="synStatement">=</span> <span class="synStatement">case</span> query <span class="synStatement">of</span> (<span class="synConstant">&quot;ADD&quot;</span>, x) <span class="synStatement">-&gt;</span> (x <span class="synStatement">:</span> xs, s) <span class="synComment">-- リストに値を追加</span> (<span class="synConstant">&quot;DELETE&quot;</span>, _) <span class="synStatement">-&gt;</span> (drop1 xs, s) <span class="synComment">-- リストから値を削除</span> (<span class="synConstant">&quot;SAVE&quot;</span>, y) <span class="synStatement">-&gt;</span> (xs, IM.insert y xs s) <span class="synComment">-- 辞書にこの時点のリストを保存</span> (<span class="synConstant">&quot;LOAD&quot;</span>, z) <span class="synStatement">-&gt;</span> (IM.findWithDefault [] z s, s) <span class="synComment">-- 辞書から保存したリストを復元</span> _ <span class="synStatement">-&gt;</span> error <span class="synConstant">&quot;!?&quot;</span> printList [headDef (<span class="synStatement">-</span><span class="synConstant">1</span>) xs <span class="synStatement">|</span> (xs, _) <span class="synStatement">&lt;-</span> tail res] <span class="synComment">-- 各クエリのタイミングでのリストの先頭要素を得て、出力 (※)</span> </pre> <p>このプログラムでは、クエリのたびに、その時点でのリストの値を出力する必要があります。が、上記のプログラムでは (クエリのたびに都度出力を得ているのではなく) クエリを全部処理し終えてから、最終的な出力、つまりプレゼンテーションを組み立てています。(※) の実装です。</p> <p>データ構造が命令型データ構造の場合、こうはいきません。ある時点のデータ構造の状態はその時点にしか参照できないため、プレゼンテーションをそのタイミングで得る必要があります。</p> <p>一方、永続データ構造の場合、各々時点のデータ構造の状態を後からでも参照できますし、メモリ上にデータ構造を保持しておいても Structural Sharing によりそれが肥大化することもありません。このプログラムのように、中核になる計算 ··· つまりドメインロジックをすべて処理し終えてから、改めてプレゼンテーションに変換することが可能です。プレゼンテーション・ドメイン分離の観点において、永続データ構造が重要な役割を果たしています。この考え方は、実装スタイルに大きな影響を与えます。</p> <p>この点に関する詳細は、競技プログラミング文脈を絡めて話す必要もあり長くなりそうなので改めて別の記事にしようと思います。</p> <p>さて、業務システム開発には必須とは言えないと私見は述べましたが、命令型プログラミング言語でも値を不変に扱うとき、このナイーブなコピーが走る問題を意識できているかどうかは重要でしょう。多くの関数型言語においてはこの課題を永続データ構造によって解消しているということは、知っておいて損はありません。</p> <h2 id="永続データ構造の実装例">永続データ構造の実装例</h2> <p>「永続データ構造」というと字面から何かすごそうなものを思い浮かべるかもしれませんが、その実装方法を知っておくともう少し身近なものに感じられると思います。永続データ構造の中でも比較的実装が簡単な、永続スタックと永続配列の実装を紹介して終わりにしたいと思います。実装の詳細については解説しませんが、雰囲気だけみてもらって「何か特別なことをしなくても普通に実装できるんだな」という雰囲気を掴んでもらえたらと思います。</p> <h3 id="永続スタック">永続スタック</h3> <p>Haskell で実装した永続スタックの一例です。再帰データ型でリストのようなデータ構造を宣言し、API として <code>head</code> <code>tail</code> <code>(++)</code> など基本的な関数を実装します。</p> <p>代数的データ型でリンクリスト構造を宣言し、先頭要素への参照を返すように実装します。先頭要素を参照したいとき (<code>head</code>) は、先頭要素への参照からそれを取り出し値を得るだけ。先頭以外の要素を得る、つまり分解したいとき (<code>tail</code>) は次の要素への参照を返す。これだけで永続スタックが実装できます。</p> <p>二つのスタックを結合する (<code>(++)</code>) ときはどうしても <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28n%29" alt=" O(n)"/> かかってしまいますが、その際も双方のリストをコピーするのではなく古いリストの一方だけをコピーし、のこりの一つは新しいリストで共有されるように実装しています。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synPreProc">import</span> Prelude <span class="synPreProc">hiding</span> ((++)) <span class="synType">data</span> Stack a <span class="synStatement">=</span> Nil <span class="synStatement">|</span> Cons a (Stack a) <span class="synType">deriving</span> (Show, Eq) empty <span class="synStatement">::</span> Stack a empty <span class="synStatement">=</span> Nil isEmpty <span class="synStatement">::</span> Stack a <span class="synStatement">-&gt;</span> Bool isEmpty Nil <span class="synStatement">=</span> True isEmpty _ <span class="synStatement">=</span> False cons <span class="synStatement">::</span> a <span class="synStatement">-&gt;</span> Stack a <span class="synStatement">-&gt;</span> Stack a cons <span class="synStatement">=</span> Cons head <span class="synStatement">::</span> Stack a <span class="synStatement">-&gt;</span> a head Nil <span class="synStatement">=</span> error <span class="synConstant">&quot;EMPTY&quot;</span> head (Cons x _) <span class="synStatement">=</span> x tail <span class="synStatement">::</span> Stack a <span class="synStatement">-&gt;</span> Stack a tail Nil <span class="synStatement">=</span> error <span class="synConstant">&quot;EMPTY&quot;</span> tail (Cons _ xs) <span class="synStatement">=</span> xs (<span class="synStatement">++</span>) <span class="synStatement">::</span> Stack a <span class="synStatement">-&gt;</span> Stack a <span class="synStatement">-&gt;</span> Stack a Nil <span class="synStatement">++</span> ys <span class="synStatement">=</span> ys Cons x xs <span class="synStatement">++</span> ys <span class="synStatement">=</span> Cons x (xs <span class="synStatement">++</span> ys) main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> s0 <span class="synStatement">::</span> Stack Int s0 <span class="synStatement">=</span> empty s1 <span class="synStatement">=</span> cons (<span class="synConstant">1</span> <span class="synStatement">::</span> Int) s0 s2 <span class="synStatement">=</span> cons (<span class="synConstant">2</span> <span class="synStatement">::</span> Int) s1 s3 <span class="synStatement">=</span> cons (<span class="synConstant">3</span> <span class="synStatement">::</span> Int) s1 s4 <span class="synStatement">=</span> s1 <span class="synStatement">++</span> s3 print s0 print s1 print s2 print s3 print s4 </pre> <p>出力結果は以下です。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink>Nil Cons_1_Nil Cons_2_(Cons_1_Nil) Cons_3_(Cons_1_Nil) Cons_1_(Cons_3_(Cons_1_Nil)) </pre> <h3 id="永続配列">永続配列</h3> <p>永続配列は、配列といっても命令型の配列のように連続した領域を索引で参照できるようにするモデルではなく、完全二分木で表現します。</p> <p>値は葉に持たせて、インデックスによる参照時には根から二分木を辿って目的の葉を特定します。そのため、参照時の計算量は <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%281%29" alt=" O(1)"/> ではなく <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28%5Clog%20n%29" alt=" O(\log n)"/> となります。</p> <p><figure class="figure-image figure-image-fotolife" title="二分木による配列の表現"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/naoya/20241203/20241203090943.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>二分木による配列の表現</figcaption></figure></p> <p>更新時には「変更されるノードとそのノードへ直接的・間接的に参照を持つノードだけをコピーする」という考えに従い、根から更新対象の葉までを辿る経路上のノードをコピーする経路コピーという手法を使います。経路をコピーするといっても、木の高さ程度ですから更新も結局 <img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28%5Clog%20n%29" alt=" O(\log n)"/> になります。</p> <p>経路コピーについては <a href="https://speakerdeck.com/toyama1710/path-copying-niyoruyong-sok-detagou-zao">Path Copying による永続データ構造 - Speaker Deck</a> のスライドがわかりやすいと思います。</p> <pre class="code lang-haskell" data-lang="haskell" data-unlink><span class="synSpecial">{-# LANGUAGE DeriveFunctor #-}</span> <span class="synPreProc">import</span> Prelude <span class="synPreProc">hiding</span> (read) <span class="synType">data</span> Tree a <span class="synStatement">=</span> Leaf a <span class="synStatement">|</span> Node (Tree a) (Tree a) <span class="synType">deriving</span> (Show, Functor) fromList <span class="synStatement">::</span> [a] <span class="synStatement">-&gt;</span> Tree a fromList [] <span class="synStatement">=</span> error <span class="synConstant">&quot;Cannot build tree from empty list&quot;</span> fromList [x] <span class="synStatement">=</span> Leaf x fromList xs <span class="synStatement">=</span> <span class="synStatement">let</span> mid <span class="synStatement">=</span> length xs <span class="synStatement">`div`</span> <span class="synConstant">2</span> <span class="synStatement">in</span> Node (fromList (take mid xs)) (fromList (drop mid xs)) read <span class="synStatement">::</span> Int <span class="synStatement">-&gt;</span> Tree a <span class="synStatement">-&gt;</span> a read _ (Leaf x) <span class="synStatement">=</span> x read i (Node left right) <span class="synStatement">|</span> i <span class="synStatement">&lt;</span> size left <span class="synStatement">=</span> read i left <span class="synStatement">|</span> otherwise <span class="synStatement">=</span> read (i <span class="synStatement">-</span> size left) right write <span class="synStatement">::</span> Int <span class="synStatement">-&gt;</span> a <span class="synStatement">-&gt;</span> Tree a <span class="synStatement">-&gt;</span> Tree a write _ v (Leaf _) <span class="synStatement">=</span> Leaf v write i v (Node left right) <span class="synStatement">|</span> i <span class="synStatement">&lt;</span> size left <span class="synStatement">=</span> Node (write i v left) right <span class="synStatement">|</span> otherwise <span class="synStatement">=</span> Node left (write (i <span class="synStatement">-</span> size left) v right) size <span class="synStatement">::</span> Tree a <span class="synStatement">-&gt;</span> Int size (Leaf _) <span class="synStatement">=</span> <span class="synConstant">1</span> size (Node left right) <span class="synStatement">=</span> size left <span class="synStatement">+</span> size right main <span class="synStatement">::</span> IO () main <span class="synStatement">=</span> <span class="synStatement">do</span> <span class="synStatement">let</span> arr <span class="synStatement">=</span> fromList [<span class="synConstant">1</span> <span class="synStatement">..</span> <span class="synConstant">8</span> <span class="synStatement">::</span> Int] print arr print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr <span class="synStatement">let</span> arr' <span class="synStatement">=</span> write <span class="synConstant">3</span> <span class="synConstant">42</span> arr print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr' print <span class="synStatement">$</span> read <span class="synConstant">3</span> arr </pre> <p>永続スタック、永続配列の実装を簡単ですが紹介しました。</p> <p>何か特殊な技法を使うというものではなくスタック、配列などの抽象が要求する操作を考え、その抽象に適した具体的で効率的なデータ構造を用意する、というのが永続データ構造の実装です。</p> <h2 id="まとめ">まとめ</h2> <p>永続データプログラミングと永続データ構造について解説しました。</p> <ul> <li>不変な値を使い、式でプログラムを宣言すると永続データプログラミングになる</li> <li>永続データプログラミングでは、変更前の値を破壊しない。変更後も変更前の値を参照できるという特徴を持つ</li> <li>関数型プログラミングすなわち永続データプログラミングである、とも考えられる</li> <li>永続データプログラミングにおけるデータコピーを最小限に留め効率的な変更を可能にする不変データ構造が「永続データ構造」</li> <li>業務システム開発において、永続データ構造は必須とは言えない。パフォーマンスが必要な場面で、永続データ構造を持ち出す以外の解決方法がある</li> <li>大量データを扱うことが基本で、かつ値を不変に扱いたいなら永続データ構造は必須</li> <li>一般のシステム開発においても機能要件として「永続」データが必要になるなら、Immutable.js とかを利用しても良いかも</li> <li>関数型プログラミングが、不変でありながらも値の変更をどのように実現しているかは永続データ構造に着目するとよく理解できる</li> </ul> <p>というお話でした。</p> <p>途中少し触れた、永続データ構造を前提にした計算の分離については別途あらためて記事にしたいと思います。</p> <h2 id="追記">追記</h2> <p>以下に記事にしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fnaoya_ito%2Farticles%2Fabdcaa5a44b430" title="永続データプログラミングと競技プログラミング 〜 Haskell でがんばる競プロ" 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://zenn.dev/naoya_ito/articles/abdcaa5a44b430">zenn.dev</a></cite></p> Tue, 03 Dec 2024 09:11:33 +0900 hatenablog://entry/6802418398308634650 一休.com Developers Blogの執筆環境2024 https://user-first.ikyu.co.jp/entry/2024/12/02/165333 <p>この記事は<a href="https://qiita.com/advent-calendar/2024/ikyu">一休.com Advent Calendar 2024</a>の1日目の記事です。</p> <p><a href="https://x.com/kymmt90">kymmt</a>です。</p> <p>当ブログ「一休.com Developers Blog」は、以前からはてなブログで運用しています。そして、今年からは執筆環境を少し改善しました。具体的には、GitHubを用いて記事の作成や公開ができるようにしました。</p> <p>この記事では、当ブログの執筆環境をどのように改善し、ふだん運用しているかについて紹介します。</p> <h2 id="HatenaBlog-Workflows-Boilerplateの導入">HatenaBlog Workflows Boilerplateの導入</h2> <p>従来は、執筆者が記事をローカルやブログ管理画面のエディタ上で書き、なんらかの方法でレビューを受け、公開するというフローでした。このフローで一番ネックになりやすいのはレビューで、Slack上でレビューが展開されがちになり、議論を追いづらいという問題がありました。</p> <p>そこで、執筆環境の改善のために、はてなさんがβ版として公開しているHatenaBlog Workflows Boilerplateを利用して、記事執筆用のリポジトリを整備しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhatena%2FHatena-Blog-Workflows-Boilerplate" title="GitHub - hatena/Hatena-Blog-Workflows-Boilerplate" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hatena/Hatena-Blog-Workflows-Boilerplate">github.com</a></cite></p> <p>これは、GitHub Actionsのはてなブログ用reusable workflow集である<a href="https://github.com/hatena/hatenablog-workflows">hatenablog-workflows</a>を用いた、はてなブログ記事執筆用のリポジトリテンプレートです。</p> <p>リポジトリを整備した結果、GitHub上ではてなブログの記事をMarkdownファイルとして管理したり、GitHub Actionsで原稿の同期や公開などの操作を実行できるようになりました。</p> <p>現在は、記事執筆のフローは次のようになっています。</p> <ol> <li>下書き用pull request (PR)作成actionを実行</li> <li>手元にブランチを持ってきて執筆</li> <li>ときどきコミットをpushしてはてなブログに同期し、プレビュー画面で確認</li> <li>PR上でレビュー</li> <li>公開できる状態にしてPRをmainにマージし、自動で記事公開</li> </ol> <p>普段開発に携わっているメンバーはGitHubに慣れています。そのようなメンバーがPR上でブログ記事執筆やレビューができるようにすることでの執筆体験向上を図りました。とくに、レビューをPRで実施することで、あるコメントがどの文章に対するものなのか分かりやすくなる点は便利だと感じています。社内のメンバーからも、</p> <blockquote><p>ブログが GitHub で管理できるようになったのは、レビューの観点でホントありがたい</p></blockquote> <p>という感想をもらっています。</p> <h2 id="記事のネタ管理">記事のネタ管理</h2> <p>記事のネタはGitHubのissueとして管理しています。どの執筆PRでネタが記事になったのかや、ネタに関する細かいメモも記録しています。情報の一元化の観点では、スプレッドシートなどで別管理するよりは好ましいと思います。</p> <h2 id="校正">校正</h2> <p>校正は<a href="https://github.com/textlint/textlint">textlint</a>を利用しています。<code>pull_request</code>トリガーでtextlintによる校正を実行するGitHub Actionsのワークフローを設定しています。ワークフローは.github/workflows/textlint.yamlに次のようなものを置いています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> textlint <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">paths</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">&quot;draft_entries/*.md&quot;</span> <span class="synStatement">- </span><span class="synConstant">&quot;entries/*.md&quot;</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">fetch-depth</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-node@v4 <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> npm ci <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Add textlint problem matcher <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">&quot;::add-matcher::.github/textlint-unix.json&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Lint files added/modified in the PR <span class="synIdentifier">run</span><span class="synSpecial">:</span> | git diff --diff-filter=AM origin/main...HEAD --name-only -- <span class="synConstant">'*.md'</span> | xargs npx textlint --format unix </pre> <p>.github/textlint-unix.jsonは次のとおりです。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">problemMatcher</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">owner</span>&quot;: &quot;<span class="synConstant">textlint-unix</span>&quot;, &quot;<span class="synStatement">pattern</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">regexp</span>&quot;: &quot;<span class="synConstant">^(.+):(</span><span class="synSpecial">\\</span><span class="synConstant">d+):(</span><span class="synSpecial">\\</span><span class="synConstant">d+):</span><span class="synSpecial">\\</span><span class="synConstant">s(.*)$</span>&quot;, &quot;<span class="synStatement">file</span>&quot;: <span class="synConstant">1</span>, &quot;<span class="synStatement">line</span>&quot;: <span class="synConstant">2</span>, &quot;<span class="synStatement">column</span>&quot;: <span class="synConstant">3</span>, &quot;<span class="synStatement">message</span>&quot;: <span class="synConstant">4</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> </pre> <p>これでPR画面上にtextlintによる校正結果が表示されるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20241202/20241202000322.png" alt="problem matcher&#x306B;&#x3088;&#x308B;textlint&#x306E;&#x6821;&#x6B63;&#x7D50;&#x679C;&#x306E;&#x8868;&#x793A;" width="800" height="543" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>textlintで現在使っているルールは次のとおり最低限のものです。技術記事もテクニカルライティングの一種であるとは思いますが、Web上で気軽に読んでもらう類のものでもあるため、文体にある程度個性が出るのは問題ないと考え、そこまで厳しいルールにはしていません。</p> <ul> <li><a href="https://github.com/textlint-ja/textlint-rule-ja-no-abusage">ja-no-abusage: 日本語の誤用チェック</a></li> <li><a href="https://github.com/textlint-ja/textlint-rule-ja-unnatural-alphabet">ja-unnatural-alphabet: 不自然なアルファベット検知</a></li> <li><a href="https://github.com/textlint-ja/textlint-rule-preset-japanese">preset-japanese: 日本語文書向けのプリセット</a> <ul> <li>読点の数、てにをは、文の長さなど</li> </ul> </li> </ul> <h2 id="レビュアー">レビュアー</h2> <p>記事ファイルが配置されるディレクトリのコードオーナーに技術広報担当者を設定して、担当者のレビューが通れば記事を公開してOK、というフローにしました。現在は2名の担当者で回しています。</p> <p>HatenaBlog Workflows Boilerplateを利用しているという前提で、.github/CODEOWNERSを次のように設定しています。ここで<code>@org</code>にはGitHub orgの名前が、<code>team</code>には技術広報担当者からなるteamの名前が入ります。</p> <pre class="code" data-lang="" data-unlink>/draft_entries/ @org/team /entries/ @org/team</pre> <p>この設定に加えて、リポジトリのbranch protection rulesとしてmainブランチでコードオーナーのレビューを必須にすることで、必ず技術広報担当者が目を通せる仕組みとしています。</p> <p>とはいえ、各記事の内容については執筆者のチームメンバーのほうが深く理解していることも多いです。ですので、内容の正確性という観点ではチームメンバーどうしでレビューしてもらい、技術広報担当者は会社の名前で社外に公開する記事としてふさわしいかという観点でのチェックをすることが多いです。</p> <h3 id="余談-HatenaBlog-Workflows-Boilerplateへのフィーチャーリクエスト">余談: HatenaBlog Workflows Boilerplateへのフィーチャーリクエスト</h3> <p>上記のBoilerplateを用いてリポジトリを運用しているうちに、ほしい機能が1つ生まれたので、該当リポジトリのissueを通じて機能をリクエストしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhatena%2FHatena-Blog-Workflows-Boilerplate%2Fissues%2F35" title="下書きをdraft pull requestとして作成したい · Issue #35 · hatena/Hatena-Blog-Workflows-Boilerplate" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hatena/Hatena-Blog-Workflows-Boilerplate/issues/35">github.com</a></cite></p> <p>具体的には、下書きPRを作成したとき、そのPRはdraft状態になっていてほしいというものです。GitHubの仕様上、PRがdraftからready for reviewになるとき、コードオーナーへレビュー依頼の通知が送られるようになっています。ですので、記事を書き始めるときのPRとしてはdraftとするのが自然と考えた、というものです。</p> <p>結果、機能として無事取り込んでいただけました。ありがとうございました<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstaff.hatenablog.com%2Fentry%2F2024%2F06%2F25%2F163227" title="HatenaBlog Workflows Boilerplate で下書き記事を作成した際にドラフト状態のプルリクエストとして作成されるよう変更しました - はてなブログ開発ブログ" 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://staff.hatenablog.com/entry/2024/06/25/163227">staff.hatenablog.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>この記事では2024年の当ブログの執筆環境について紹介しました。GitHubを起点とする執筆環境や自動化を取り入れることで、執筆体験を向上できました。この記事も紹介した仕組みで書きました。引き続き、このブログで一休での開発における興味深いトピックをお届けしていければと思います!</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 機能リリース後に、draft機能がGitHubの有料プランの機能であることに伴うフォローアップもしていただきました。ありがとうございました<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> Mon, 02 Dec 2024 16:53:33 +0900 hatenablog://entry/6802418398308205516 一休はRust.Tokyo 2024にゴールドスポンサーとして協賛します https://user-first.ikyu.co.jp/entry/2024/11/29/121730 <p><a href="https://x.com/kymmt90">kymmt</a>です。</p> <p>11/30に開催されるRust.Tokyo 2024に一休はゴールドスポンサーとして協賛します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Frust.tokyo" title="Rust.Tokyo 2024" 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://rust.tokyo">rust.tokyo</a></cite></p> <h2 id="一休でのRustの活用">一休でのRustの活用</h2> <p>一休では一休.comレストランにおいてRustの活用を進めています。昨年に当ブログで活用の様子を紹介した際は、当時開発が進んでいたレストラン予約サービスWeb UIのバックエンドにおけるユースケースだけに触れていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F12%2F25%2F132215" title="一休レストランのふつうのRustバックエンド開発 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/12/25/132215">user-first.ikyu.co.jp</a></cite></p> <p>それから1年弱経過した現在では、Rust活用の場はさらに広がっており、Rustを書いているメンバーも増えてきています。</p> <h2 id="RustTokyoのゴールドスポンサー">Rust.Tokyoのゴールドスポンサー</h2> <p>2024年はRustでWebサービスを開発するのがますます現実的な選択肢として挙がるようになった年だったのではないかと思います。たとえば、RustでWebアプリケーションを開発するための本が複数発売されるなど<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>、学習リソースは着実に揃ってきています。このような環境のなかで、一休もRust採用企業として活用事例の共有などを通じてコミュニティを活性化させることが重要だと考えています。</p> <p>以上のような状況に基づいて、一休はこのたびRust.Tokyo 2024にゴールドスポンサーとして協賛させていただくことになりました。当日は会場で</p> <ul> <li>スポンサーセッション登壇</li> <li>ブースの出展</li> </ul> <p>をやります。</p> <p>スポンサーセッションでは、トラックAで15:25から<a href="https://rust.tokyo/lineup/15">「総会員数1,500万人のレストランWeb予約サービスにおけるRustの活用」</a>と題してkymmtが発表します。2024年現在の一休.comレストランにおけるRust活用の様子のスナップショットとして、システムの設計や技術的なトピックについて紹介する予定です。</p> <p>また、会場ではブースを出展する予定です。技術広報チームを中心に、一休.comレストランのエンジニアとしてkymmtが、ほかにも宿泊予約サービスの一休.comのエンジニアとEMがブースに参加する予定です。一休のサービス開発の様子について興味があるかたはぜひお越しください。</p> <h2 id="おわりに">おわりに</h2> <p>11/30に一休はRust.Tokyo 2024にゴールドスポンサーとして協賛します。参加者のかたは当日会場でお会いしましょう!</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> <a href="https://bookclub.kodansha.co.jp/product?item=0000398182">https://bookclub.kodansha.co.jp/product?item=0000398182</a> や <a href="https://www.shoeisha.co.jp/book/detail/9784798186016">https://www.shoeisha.co.jp/book/detail/9784798186016</a> など<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> Fri, 29 Nov 2024 12:17:30 +0900 hatenablog://entry/6802418398306924099 TSKaigi Kansai 2024とJSConf JP 2024に一休のエンジニアが登壇します https://user-first.ikyu.co.jp/entry/2024/11/15/113658 <p><a href="https://x.com/kymmt90">kymmt</a>です。</p> <p>今月は</p> <ul> <li>11月16日に京都で開催される<a href="https://kansai.tskaigi.org/">TSKaigi Kansai 2024</a></li> <li>11月23日に東京で開催される<a href="https://jsconf.jp/2024/">JSConf JP 2024</a></li> </ul> <p>と、JavaScript/TypeScriptに関するカンファレンスが2つ開催されます。今年は、一休.comレストランのフロントエンドアーキテクトを務めるエンジニア恩田 (<a href="https://x.com/takashi_onda">@takashi_onda</a>)がこれらのカンファレンス両方に登壇します。</p> <p>1週違いで開催されるそれぞれのカンファレンスでは、一休.comレストランのフロントエンド開発をきっかけとする内容のトークテーマを携えて登壇します。この記事では、現在絶賛発表準備中の本人からのコメントも交えつつ、発表内容について紹介します!</p> <h2 id="TSKaigi-Kansai-2024-構造的型付けとserialize境界">TSKaigi Kansai 2024: 構造的型付けとserialize境界</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkansai.tskaigi.org%2Ftalks%2Ftak-onda" title="構造的型付けと serialize 境界" 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://kansai.tskaigi.org/talks/tak-onda">kansai.tskaigi.org</a></cite></p> <p>TSKaigi Kansai 2024は、TypeScriptをテーマとして2024年5月に東京で開催されたTSKaigi 2024から派生した地域型カンファレンスです。</p> <p>本カンファレンスでは、16:00-16:30に「カミナシ堂」会場で「構造的型付けとserialize境界」というタイトルで恩田が発表します。</p> <p>以下、恩田からのコメントです。</p> <blockquote><p>京都在住なので、地元でお話しできるのが楽しみです。一休では僕のように関西やその他の地域に在住しているエンジニアも複数います。発表を聞いていただき興味を持たれた方は、お気軽にお声がけください。</p></blockquote> <h2 id="JSConf-JP-2024-Reactへの依存を最小にするフロントエンドの設計">JSConf JP 2024: Reactへの依存を最小にするフロントエンドの設計</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fjsconf.jp%2F2024%2Ftalk%2Ftakashi-onda%2F" title="React への依存を最小にするフロントエンドの設計 - 恩田 崇 | JSConf JP" 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://jsconf.jp/2024/talk/takashi-onda/">jsconf.jp</a></cite></p> <p>JSConf JP 2024は、Japan Node.js Association様により運営されている、JavaScriptをテーマとする大規模カンファレンスです。</p> <p>本カンファレンスでは、11:10-11:40にトラックBで「Reactへの依存を最小にするフロントエンドの設計」というタイトルで発表します。この発表は、本ブログで以前公開した記事「React / Remixへの依存を最小にするフロントエンド設計」をもとに、今回あらためてJSConfでお話しするものです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2024%2F08%2F05%2F142626" title="React / Remix への依存を最小にするフロントエンド設計 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2024/08/05/142626">user-first.ikyu.co.jp</a></cite></p> <p>以下、恩田からのコメントです。</p> <blockquote><p>一休レストランのフロントエンド開発では、疎結合な設計を心がけていて、ほとんどのロジックは Vanilla JS だけで完結しています。そのような書き方を可能とする、バックエンド開発で培われた知見をフロントエンドに適用する手法についてお話ししたいと思います。</p></blockquote> <h2 id="おわりに">おわりに</h2> <p>この記事では、TSKaigi Kansai 2024とJSConf JP 2024でのエンジニア恩田 (@takashi_onda)の発表内容について紹介しました。</p> <p>参加者の方は、それぞれの会場でぜひ発表を聞きに来ていただければと思います!</p> Fri, 15 Nov 2024 11:36:58 +0900 hatenablog://entry/6802418398303705618 Vue Fes Japan 2024に登壇 & ランチスポンサーをしました https://user-first.ikyu.co.jp/entry/2024/10/29/163102 <p>CTO室プラットフォーム開発チームのいがにんこと山口(<a href="https://x.com/igayamaguchi">@igayamaguchi</a>)です。</p> <p>先日、Vue Fes Japan 2024が開催され、一休は登壇とスポンサーをしました。その紹介をします。</p> <h2 id="Vue-Fes-Japan-2024が開催">Vue Fes Japan 2024が開催</h2> <p>10月19日(土)に日本最大級の Vue.js カンファレンス、<a href="https://vuefes.jp/2024/">Vue Fes Japan 2024</a> が開催されました。一休は当カンファレンスでプロポーザル採択による発表と、ランチスポンサーとしてランチセッションでの発表を行いました。この記事ではその発表の概要を紹介します。</p> <p>一休で行った発表は2つです。</p> <ul> <li>Vue.js、Nuxtの機能を使い、 大量のコピペコードをリファクタリングする</li> <li>Nuxtベースの「WXT」でChrome拡張を作成する</li> </ul> <h3 id="VuejsNuxtの機能を使い-大量のコピペコードをリファクタリングする">Vue.js、Nuxtの機能を使い、 大量のコピペコードをリファクタリングする</h3> <p>1つ目の発表は自分がプロポーザルを送り採択された発表です。一休.com、Yahoo!トラベルで発生したコピペ問題の原因と解決策の話です。</p> <p><iframe id="talk_frame_1263347" class="speakerdeck-iframe" src="//speakerdeck.com/player/38e14879cd874059b67ff0bf9942b793" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/igayamaguchi/vue-dot-js-nuxtnoji-neng-woshi-i-da-liang-nokopipekodoworihuakutaringusuru">speakerdeck.com</a></cite></p> <p>一休.com、Yahoo!トラベルでは大量のコピペコードが発生していました。それにより何かを変更するときに作業がN倍になってしまい、開発スピードが落ちていました。 今回の発表ではこの問題の原因を紐解き解決していった話をしました。 コピペ問題といっても原因は様々です。ワークフローに問題があったり、コピペが発生せざるを得ない技術的背景があったり。それぞれに対応しています。 内容をざっくり抜き出すと、以下のようになります。</p> <ul> <li>開発ワークフローに問題がありUIのコピペが横行していたので、ワークフローの見直し、UIコンポーネントの作成</li> <li>デザイン差分を小さくしてコードの統一</li> <li>Option APIでロジックが共通化できていなかったので、Composition APIを導入して共通化、さらには責務わけのためのコンポーネント分割</li> </ul> <p>詳細は発表資料をご覧ください。</p> <h3 id="NuxtベースのWXTでChrome拡張を作成する">Nuxtベースの「WXT」でChrome拡張を作成する</h3> <p>2つ目は新規プロダクト開発部の星野によるランチセッションでの発表です。「WXT」というOSSを使用し、Chrome拡張を作成してみた話です。</p> <p><iframe id="talk_frame_1263281" class="speakerdeck-iframe" src="//speakerdeck.com/player/b04f3124415c4a31815f056977c6c00d" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/moshi1121/nuxtbesuno-wxt-dechromekuo-zhang-wozuo-cheng-suru-vue-fes-2024-rantisetusiyon">speakerdeck.com</a></cite></p> <p>何かを便利にしようとChrome拡張を使おうと思った時、自分の求めるChrome拡張が存在しなかったり、信用できない開発者のChrome拡張を使用したくないということがあります。この問題を解決するために「WXT」というOSSを使用してChrome拡張を開発した話をしました。</p> <p>WXTはNuxtベースで開発できる拡張機能開発フレームワークです。Chrome拡張を開発するにはいくつか設定ファイルを作成したりChrome拡張特有の作業が必要でとても面倒なのですが、WXTを使用することでこの作業がとても簡単になります。しかもNuxtを使用している人には分かりやすいインターフェースになっており、Vue.js/Nuxtを触っている開発者にはとても開発しやすいものになっています。さらにはVue.jsだけでなくReactなど他のUIフレームワークを使用して開発することもできます。</p> <p>自分でChrome拡張を作りたいという方はこの発表を参考にWXTを試してみてください。</p> <h2 id="おわりに">おわりに</h2> <p>Vue Fes Japan 2024での一休の発表を紹介させていただきました。</p> <p>Vue Fesですが、発表者、かつ一視聴者として参加してみて、すごく熱があるカンファレンスでした。参加者も多く、Vue.jsだけでなくViteの話も聞けて、さらにRolldownやoxcの話を開発者から聞けるという場は国内ではなかなかないと思います。そのような場で発表できたこと、スポンサーをできたことはとてもありがたかったです。</p> <p>2025年も開催されるとのことなのでまた楽しみです。</p> <hr /> <p>一休では、ともに良いサービスをつくっていく仲間を募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Tue, 29 Oct 2024 16:31:02 +0900 hatenablog://entry/6802418398299804724 「一休×AEON 事業会社のサービスを支える基盤開発トーク」を開催しました https://user-first.ikyu.co.jp/entry/2024/10/04/121028 <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20241002/20241002184521.jpg" alt="&#x4E00;&#x4F11;&times;AEON&#x30A4;&#x30D9;&#x30F3;&#x30C8;&#x306E;&#x69D8;&#x5B50;" width="800" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="はじめに">はじめに</h2> <p><a href="https://x.com/kymmt90">kymmt</a>です。</p> <p>先日2024年9月18日に、「事業会社のサービスを支える基盤開発トーク」と題してイオンスマートテクノロジー(以下AST)さんと合同で技術イベントを実施しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fikyu.connpass.com%2Fevent%2F327095%2F" title="一休×AEON 事業会社のサービスを支える基盤開発トーク (2024/09/18 19:00〜)" 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://ikyu.connpass.com/event/327095/">ikyu.connpass.com</a></cite></p> <p>イベントでは、各会社の事業を支える基盤プロダクトの開発や運用における苦労や工夫について登壇者の方々にお話しいただきました。</p> <p>この記事では、このイベントの様子や発表の内容を紹介します。なお、X(旧Twitter)でも<a href="https://x.com/search?q=%23ikyu_aeon&amp;src=typed_query">#ikyu_aeon</a>というハッシュタグで当日の様子がご覧になれます。</p> <h2 id="会場">会場</h2> <p>会場は、一休のオフィスも入っている東京ガーデンテラス紀尾井町内の<a href="https://www.z-lodge.com/">LODGE</a>でした。</p> <p>当日は、一休/ASTのメンバーも合わせて計30人程度の方にご来場いただきました。また、イベント開始前に、ASTのもりはやさんからトップバリュ製品やイオンで販売中のお菓子を来場者向けに提供していただきました。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/ikyu_aeon?src=hash&amp;ref_src=twsrc%5Etfw">#ikyu_aeon</a><br>わいわい!!トップバリューとイオンで買ってきたお菓子をデプロイしました!! <a href="https://t.co/p3qZmhdD2K">pic.twitter.com/p3qZmhdD2K</a></p>&mdash; もりはや (@morihaya55) <a href="https://twitter.com/morihaya55/status/1836345532803092637?ref_src=twsrc%5Etfw">September 18, 2024</a></blockquote> <p> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <h2 id="発表">発表</h2> <h3 id="歴史あるプロダクトにマイクロサービスを導入するプロセス">『歴史あるプロダクトにマイクロサービスを導入するプロセス』</h3> <p>1つ目の発表は、一休の菊地さんによる『歴史あるプロダクトにマイクロサービスを導入するプロセス』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/42ab9beb050a40168896bced9d961cff" title="歴史あるプロダクトにマイクロサービスを導入するプロセス" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>一休のプロダクトを横断して利用されるマイクロサービス群(社内では一休プラットフォームと呼んでいます)を開発するチームでは、個別のプロダクトへのマイクロサービス導入も進めています。長い歴史を持ちコードベースが複雑なプロダクトにマイクロサービスを導入するには、技術課題の解決にとどまらず、積み重なった仕様を解きほぐして関係者と調整を進める必要もあります。</p> <p>この発表では、そのような課題に直面した一休.comレストランへのポイント管理マイクロサービス導入の事例を紹介いただきました。発表のなかで、デッドコード削除や仕様の調整のような地道な取り組みから、プロダクトとマイクロサービスを疎結合に保ちつつ連携する仕組みを解説していただきました。</p> <h3 id="1000万DLを超えたiAEONアプリ完全停止を防ぐ耐障害性と信頼性の設計">『1000万DLを超えたiAEONアプリ:完全停止を防ぐ耐障害性と信頼性の設計』</h3> <p>2つ目の発表は、ASTの范さんによる『1000万DLを超えたiAEONアプリ:完全停止を防ぐ耐障害性と信頼性の設計』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/d2df167241c14834a9f5161d0ba336ca" title="1000万DL人を支えるiAEONアプリ:完全停止を防ぐ耐障害性の設計/iAEON app supporting 10 million users" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>iAEONアプリは実店舗のレジ前で会員バーコードを表示するようなユースケースを想定しており、障害が発生して利用不可になることを可能な限り避けたいという要件があります。一方で、複雑なバックエンド側の一部で発生した障害がアプリに伝播することで、アプリが利用不可になることも過去あったとのことでした。</p> <p>この発表では、上述したような特性を持つiAEONアプリの耐障害性を高めるための取り組みについてお話しいただきました。とくに、デバイス上のキャッシュなどを活用してフロントエンドであるiAEONアプリ上のエラーをできるだけ局所化する手法について詳しく解説いただきました。App StoreやGoogle Play StoreにおけるiAEONアプリの評価が結果的に向上したとのことで素晴らしいと感じました。</p> <h3 id="宿泊予約サイトにおける検索と料金計算の両立">『宿泊予約サイトにおける検索と料金計算の両立』</h3> <p>3つ目の発表は、一休の鍛治さんから『宿泊予約サイトにおける検索と料金計算の両立』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/ff24fdf7034e46e1a42155b3bc38555f" title="宿泊予約サイトにおける検索と料金計算の両立" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 420;" data-ratio="1.3333333333333333"></iframe> <p>宿泊予約サイトである一休.comやYahoo!トラベルでの料金に関する業務ルールは複雑です。また、ユーザーが宿泊したい部屋をさまざまな条件に基づいて検索するときに、その複雑な業務ルールで算出される料金の検索サーバであるSolrで計算する必要があります。</p> <p>この発表では、そのような一休.comの検索要件の複雑さの解説や、要件を満たすためにSolrのプラグイン機構を活用していることについて解説いただきました。SolrのプラグインはJavaで開発することができ、Javaの機能を活用できるので、Solrクエリをメンテナブルに保ちつつ、複雑な料金計算を検索時に実行できているとのことでした。</p> <h3 id="SRE改善サイクルはチームを超えて---ダッシュボードを眺める会の取り組み">『SRE改善サイクルはチームを超えて - ダッシュボードを眺める会の取り組み』</h3> <p>4つ目の発表は、ASTのもりはやさんから『SRE改善サイクルはチームを超えて - ダッシュボードを眺める会の取り組み』でした。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/7b609ddb605b436a8cb1296f8d868deb" title="SRE改善サイクルはチームを超えて - ダッシュボードを眺める会の取り組み/SRE improvement cycle across teams" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>New Relicなどを活用したシステムメトリクスを概観できるダッシュボードは、非常時に障害の原因を探るためのものとして非常に便利です。一方で、定点観測することで、日々のシステムの状態や、障害になり得る変化を見つけることができます。</p> <p>この発表では、ASTのSREチームを中心にダッシュボードを定期的に眺める会を設けることで、メトリクスの変化や改善ポイントを発見して深掘りする機会を意図的に作り出す取り組みについてお話しいただきました。システムの信頼性を高める機会を作り出すのはもちろん、会を通じて「ザイオンス効果(単純接触効果)」でチームビルディングにも寄与している点が素晴らしいと感じました。</p> <h2 id="おわりに">おわりに</h2> <p>「事業会社のサービスを支える基盤開発トーク」の様子について紹介しました。</p> <p>各社のサービスの基盤プロダクト開発について社内外のエンジニアの工夫や知見が聞ける、とてもおもしろいイベントになりました。ご来場いただいたみなさま、ありがとうございました!</p> <hr /> <p>一休では、ともに良いサービスをつくっていく仲間を募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Fri, 04 Oct 2024 12:10:28 +0900 hatenablog://entry/6802340630909711107 情シスカンファレンス「Business Technology Conference Japan 2024」にブロンズスポンサーとして協賛します https://user-first.ikyu.co.jp/entry/btconjp2024 <p>コーポレート本部 社内情報システム部 兼 CISO室 <a href="http://blog.hatena.ne.jp/rotom/" class="hatena-id-icon"><img src="https://cdn.profile-image.st-hatena.com/users/rotom/profile.png" width="16" height="16" alt="" class="hatena-id-icon">id:rotom</a> です。</p> <p>10/12(土) にハイブリッド形式で情シス向けのテックカンファレンス<strong>「Business Technology Conference Japan 2024(BTCONJP 2024)」</strong>が開催されます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbtcon.jp%2F2024" title="- ビジネスを加速させる情シスのためのカンファレンス - Business Technology Conference Japan 2024 " 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://btcon.jp/2024">btcon.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbtajp.connpass.com%2Fevent%2F328706%2F" title="Business Technology Conference Japan 2024 (2024/10/12 11:00〜)" 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://btajp.connpass.com/event/328706/">btajp.connpass.com</a></cite></p> <p>昨年オンライン開催された <a href="https://btcon.jp/2023">BTCONJP 2023</a> では私が登壇者として <a href="https://user-first.ikyu.co.jp/entry/office-relocation">本社を東京ガーデンテラス紀尾井町へ移転し、オフィスファシリティ・コーポレートIT を刷新した話</a> というテーマで発表させていただきましたが、今年は core staff として運営に参画しており、所属企業である一休はブロンズスポンサーとして協賛しております。</p> <p>Identity, DX &amp; AI, Device Management, Zero Trust, Cyber Security, Business Technology の6つのテーマに沿った12のセッションや、イオン / イオンスマートテクノロジー CTO の 山﨑 賢氏による基調講演を用意しています。 詳細は以下のプレスリリースをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprtimes.jp%2Fmain%2Fhtml%2Frd%2Fp%2F000000003.000104640.html" title="1.2万人の日本最大級の情シスコミュニティの有志が主催 BTCONJP 10月12日(土) 開催" 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://prtimes.jp/main/html/rd/p/000000003.000104640.html">prtimes.jp</a></cite></p> <p>オフライン会場は一休 本社も入居する、東京ガーデンテラス紀尾井町 紀尾井タワーのLINEヤフー株式会社 本社です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmap.yahoo.co.jp%2Fv2%2Fplace%2FIj-b7CaIeE2" title="LINEヤフー株式会社 ・ 企業・オフィス" 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://map.yahoo.co.jp/v2/place/Ij-b7CaIeE2">map.yahoo.co.jp</a></cite></p> <p>LODGE での懇親会も予定しておりますので、ぜひ現地でご参加ください!</p> Thu, 12 Sep 2024 16:25:06 +0900 hatenablog://entry/6802340630905709469 プロンプトエンジニアリングをしよう - 一休.comでの検索システム改善事例 https://user-first.ikyu.co.jp/entry/2024/09/10/180410 <h1 id="はじめに">はじめに</h1> <p>こんにちは。宿泊プロダクト開発部の宮崎です。</p> <p>みなさん、生成 AI 使ってますか?</p> <p>近年、AI の進歩はめざましく、文章生成や画像生成はもちろん、動画生成も実用的なレベルで出来るようになっています。</p> <p>ChatGPT が話題になったのが 2022 年の 11 月なので、たった 2 年足らずでここまで来ているという事実に少し恐ろしくもありますね。AGI(汎用人工知能)の実現もそう遠くないのかもしれません。</p> <p>一休でも AI 技術は注目していて今年の 6 月に、まさに生成 AI を使ってホテル検索システムの改善を行いました。</p> <p>この記事では、その時に学んだ<strong>プロンプトエンジニアリングの重要性</strong>について書いていこうと思います。</p> <h2 id="生成-AI-を使ったホテル検索システム">生成 AI を使ったホテル検索システム</h2> <p>今回我々が実装したのはフリーワード・文章でもホテルを検索できるシステムです。</p> <p>以下のようなユーザーの自由な入力に対して、適切なホテル・旅館を返したいというのが目的でした。</p> <ul> <li>福井でおいしいご飯が食べられる宿</li> <li>沖縄 子供と楽しめる宿</li> <li>静岡で犬と泊まれる宿</li> </ul> <p>今までは、このような入力の場合、形態素解析をして検索を行っていました。しかしその場合、「子供と楽しめる」だと「子供」「楽しめる」に分割して検索するので、本来の意味「子供と楽しめる」とは少し結果が違っていました。</p> <p>よって文章の入力に対しては意味を考慮して検索ワードを切り出す必要があります。</p> <p>私たちが行ったのは、AI にユーザーの入力を解析させ意味毎に区切り、検索条件 area, condition に分解させるというものです。以下に具体的な例を示します。</p> <p>例: ユーザーの入力:「銀座の夜景が楽しめる」</p> <p>↓ これを AI で解析して分解する。</p> <p>求める出力: <code>{ area:銀座, condition:[夜景が人気] }</code></p> <p>従来は「銀座」「夜景」「楽しめる」で分解されていたのが、「銀座」「夜景が人気」に分解する。 このようにして json 形式で返してくれれば、既存のシステムで検索できます。この分解を生成 AI にやってもらおうと考えました。</p> <h2 id="実際の検索結果">実際の検索結果</h2> <p>具体的にどんな検索になったのか、実際にリリースしたものを使ってみましょう。</p> <p>まず、行きたい施設について文章を入力し、検索ボタンを押す。</p> <p>今回は「<strong>東京近郊でサウナがあって禁煙</strong>」と入力してみました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240904145126" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240904/20240904145126.png" alt="top &#x753B;&#x50CF;" width="800" height="294" loading="lazy" title="top" class="hatena-fotolife" itemprop="image"></a></span></p> <p>以下の結果が表示されます。 東京近郊の都市で、サウナが人気、そして禁煙で検索されていますね。 このように、ユーザーの入力ワードをシステムで検索できるよう AI で分解するというのが今回行った改善です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240904145136" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240904/20240904145136.png" alt="list &#x753B;&#x50CF;" width="800" height="412" loading="lazy" title="list" class="hatena-fotolife" itemprop="image"></a></span></p> <h2 id="実装における課題">実装における課題</h2> <p>ただ、この仕組みを実装する上で、1 つ大きな課題を抱えていました。</p> <p>それは単純な話で、コストの問題です。</p> <p>当時、私たちはこの分解に GPT-4 を使っていました。 理由は他の AI と比較して GPT-4 が一番結果が良かったからです。(このプロジェクトは去年の秋ごろに始まったため、選択肢があまりなかったというのもあります。)</p> <p>しかし、GPT-4 だとコストが高く、1 回の検索に 10 円以上かかることが分かりました。</p> <p>どうしてそれほどコストがかかるのかというと、プロンプトが長いからです。</p> <p>一休では、例えば近畿というエリアは「近畿(大阪以外)」と「大阪」という風に分けられています。 このような一休独自のエリアの分け方や、用語を AI が解釈できるようにプロンプトに詰め込んでいるので、どうしてもその分プロンプトが長くなってしまうのです。</p> <p>プロンプト例:</p> <pre class="code" data-lang="" data-unlink>扱うエリア名は以下です。 - 北海道 - 札幌 - 函館・湯の川・大沼 - 旭川・富良野・稚内 - ... - ... &lt;&lt;以下、地名がずらりと後に続く…&gt;&gt; </pre> <h2 id="解決案">解決案</h2> <p>GPT-4 ではコストがかかる。プロンプトを短くするのも難しい。 そこで考えたのは、GPT-3.5 Turbo を使うことでした。 これを使えば GPT-4 の 1/60 の値段になるので、コストの問題は簡単に解決できます。</p> <p>ただし、これが最初からできていれば苦労はしません。</p> <p>例えば「大阪でサウナと温泉が楽しめる」なら、GPT-4 は</p> <p><code>{ area:大阪, condition:[サウナ,温泉] }</code></p> <p>と出力されるのに対して、GPT-3.5 Turbo は以下の様に間違うことが多かったです。</p> <ol> <li><p>いくつかのキーワードが欠けている</p> <p>例:<code>{area: 大阪,condition: [サウナ] }</code></p></li> <li><p>入力がそのまま入ってしまっている</p> <p>例:<code>{area: 大阪,condition: [サウナと温泉が楽しめる] }</code></p></li> </ol> <p>こういった間違いに対して、最初はファインチューニングで精度を上げようとしました。 ファインチューニングは既存のモデルに追加でデータを学習させて、微調整するというものです。</p> <p>参考:<a href="https://platform.openai.com/docs/guides/fine-tuning">https://platform.openai.com/docs/guides/fine-tuning</a></p> <p>しかし、これも上手くいきませんでした。</p> <p>例えば condition の解釈精度をチューニングすると area 解釈精度が悪化したり、逆もまた然りであちらをたてればこちらがたたず状態に…。</p> <p>結果的にプロンプトを 2 つに分けて area 用と condition 用、のようにしてみましたが、精度の割にはコストが 2 倍に増えてしまって断念しました。</p> <p>※補足:精度の検証には新しくデータセットを作成しました。入力キーワードと、どのように分解して欲しいかの回答のセットです。これらを用いて生成された回答の一致率を測定しています。</p> <h2 id="プロンプトエンジニアリングをしよう">プロンプトエンジニアリングをしよう</h2> <p>頼みの綱のファインチューニングもうまくいかず、頭を悩ませていた時のことです。</p> <p>私は OpenAI 社のファインチューニングのドキュメントを読んでいました。チューニング精度を上げるための秘訣が書かれていればと思ったのです。</p> <p>するとその時、以下の一文が目に留まりました。</p> <blockquote><p>Fine-tuning OpenAI text generation models can make them better for specific applications, but it requires a careful investment of time and effort. We recommend first attempting to get good results with prompt engineering, prompt chaining (breaking complex tasks into multiple prompts), and function calling,</p> <p>引用:Fine-tuning (<a href="https://platform.openai.com/docs/guides/fine-tuning/fine-tuning">https://platform.openai.com/docs/guides/fine-tuning/fine-tuning</a>)</p></blockquote> <p>簡単に言うと、ファインチューニングの前にまずはプロンプトエンジニアリングやプロンプトチェイニング(タスクを複数に分割する)をしましょう、ということです。</p> <p>私はこの時まで、プロンプトエンジニアリングを軽視していました。プロンプトの修正で出力が改善されれば苦労しないだろうと。</p> <p>ですが、このドキュメントを見ながら自分の今のプロンプトを見返すと、推奨されている書き方をほとんどしていないことが分かりました。</p> <p>よって、半信半疑でありながらも、とりあえずドキュメントに従ってプロンプトエンジニアリングを行いました。</p> <p>すると驚くべきことに、GPT-3.5 Turbo でも GPT-4 と同等の精度を出すことができたのです。</p> <p>難しいことは何もしていません。ただ、プロンプトを修正しただけです。</p> <p>それだけでコストが 1/60 になり、今年の 6 月にこの機能をリリースすることができました。 まず最初にやるべきはプロンプトエンジニアリングだったというわけです。</p> <p><strong>※補足:現在は GPT-3.5 Turbo ではなく、 GPT-4o mini を使っています。</strong></p> <h2 id="プロンプトエンジニアリングについて">プロンプトエンジニアリングについて</h2> <p>ここからは具体的にどういったプロンプトをどう修正したのかについて書きます。</p> <p>OpenAI のプロンプトエンジニアリングのガイドラインを参考に以下の修正を行いました。 参考:<a href="https://platform.openai.com/docs/guides/prompt-engineering">https://platform.openai.com/docs/guides/prompt-engineering</a></p> <h3 id="はっきりと簡潔な指示を出す">はっきりと簡潔な指示を出す</h3> <hr> <p>簡潔な言葉で何をしてほしいかを指示しました。</p> <p><strong>★ 修正前のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink> 現在、日本のホテルを検索するシステムを開発しています。システムは以下の機能を持っています。 <<システムの説明 ※ここでは省略。実際は 50 行くらい使ってホテルや宿泊条件など、機能の詳細な説明をしていた。>> ここで、以下のユーザーのホテル検索クエリに対して、上記の機能を使って絞り込むのが適切だと判断したのなら、その条件を教えてください。 </pre> <p><strong>★ 修正後のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>あなたは優秀なクエリ分析システムです。 ユーザーのホテル検索クエリが与えられますので、それを分析して、どんな条件で検索すればいいのかをjson形式で回答してください。回答には、三重引用符で提供されたデータのみを使って答えてください。</pre> <p>このヒントは以下に書かれていました。 <a href="https://platform.openai.com/docs/guides/prompt-engineering/tactic-include-details-in-your-query-to-get-more-relevant-answers">https://platform.openai.com/docs/guides/prompt-engineering/tactic-include-details-in-your-query-to-get-more-relevant-answers</a></p> <h3 id="ステップバイステップで考えさせる">ステップバイステップで考えさせる</h3> <hr> <p>以前は 1 つの指示+複数のルールで指示していましたが、これをステップバイステップの指示に切り替えました。</p> <p><strong>★ 修正前のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>ここで、以下のユーザーのホテル検索クエリに対して、上記の機能を使って絞り込むのが適切だと判断したのなら、その条件を教えてください。 その際、以下のルールを守ってください。 1. JSON形式で教えてください。 2. たとえば、ユーザーのホテル検索クエリ「ペットと泊まれる 新潟」の場合、 {area: [&#34;新潟&#34;], condition: [&#34;ペット&#34;]} というJSONを期待します。 3. ... 4. ...</pre> <p><strong>★ 修正後のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>以下の思考フローを用いて、ステップバイステップで絞り込んでいくことを想定してください。 ステップ1:ユーザーのクエリを確認し、エリアに該当するものを抽出してください。 ステップ2:ステップ1で抽出したワードを出力のareaのキーに追加してください。 ステップ3:... ステップ4:...</pre> <p>このヒントは以下に書かれていました。 <a href="https://platform.openai.com/docs/guides/prompt-engineering/tactic-specify-the-steps-required-to-complete-a-task">https://platform.openai.com/docs/guides/prompt-engineering/tactic-specify-the-steps-required-to-complete-a-task</a></p> <h3 id="セクションを区別する">セクションを区別する</h3> <hr> <p>エリア名称や、用語一覧を渡しているのですが、ここを引用符などで区切りました。</p> <p><strong>★ 修正前のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>以下にシステムで扱える用語一覧を示します。 * サウナ * 禁煙 * 喫煙 * 室内プール * ... * ...</pre> <p><strong>★ 修正後のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>以下にシステムで扱える用語一覧を示します。 &#34;&#34;&#34; * サウナ * 禁煙 * 喫煙 * 室内プール * ... * ... &#34;&#34;&#34;</pre> <p>このヒントは以下に書かれていました。 <a href="https://platform.openai.com/docs/guides/prompt-engineering/tactic-use-delimiters-to-clearly-indicate-distinct-parts-of-the-input">https://platform.openai.com/docs/guides/prompt-engineering/tactic-use-delimiters-to-clearly-indicate-distinct-parts-of-the-input</a></p> <h3 id="例を付ける">例を付ける</h3> <hr> <p>出力の例を付けました。上述したように、以前はルールを使って回答を制御していて、1 つか 2 つしか例を付けていませんでした。</p> <p>それを以下の様に例を増やしました。</p> <p><strong>★ 修正後のプロンプト</strong></p> <pre class="code" data-lang="" data-unlink>以下は、出力のjsonの例です。 例1:「絶景と温泉」というユーザーのクエリの場合、以下の条件を出力してください。 { condition : [&#34;絶景&#34;,&#34;温泉&#34;] } 例2:「浜松でうなぎ料理を楽しめる旅館」というユーザーのクエリの場合、以下の条件を出力してください。 { area:[&#34;浜松&#34;] condition: [&#34;うなぎ料理&#34;] } 例3:... 例4:... </pre> <p>このヒントは以下に書かれていました。 <a href="https://platform.openai.com/docs/guides/prompt-engineering/tactic-provide-examples">https://platform.openai.com/docs/guides/prompt-engineering/tactic-provide-examples</a></p> <p>以上がメインとなるプロンプトの修正です。これらは本当にガイドラインに従って足りていないものを書き換えただけです。</p> <p>それだけで約 35% の精度改善となったので、同じような悩みを抱えている方はぜひ試していただきたいです。</p> <h2 id="おわりに">おわりに</h2> <p>今回、プロンプトエンジニアリングの効果を十分に理解することができました。</p> <p>ただ、考えてみれば、人が人に仕事を任せる時は当然相手の立場やスキルを考えて指示を出します。 これをどうして AI でやらないのか。 AI も AI の理解しやすい命令の構造があり、それをプロンプトエンジニアリングで最適化する。</p> <p>人の気持ちを考えるように AI の気持ちを考える。</p> <p>いい仕事をするためには人も、 AI も関係なく、相手を思い遣る心が大切なのだと思いました。</p> Tue, 10 Sep 2024 18:04:10 +0900 hatenablog://entry/6802340630900570718 オフライン技術勉強会「事業会社のサービスを支える基盤開発トーク」を開催します! https://user-first.ikyu.co.jp/entry/2024/09/03/175808 <p>CTO室プラットフォーム開発チームの山口(<a href="https://x.com/igayamaguchi">@igayamaguchi</a>)です。</p> <p>この度、一休とイオンスマートテクノロジー合同で技術勉強会「事業会社のサービスを支える基盤開発トーク」を開催します! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fikyu.connpass.com%2Fevent%2F327095%2F" title="一休×AEON 事業会社のサービスを支える基盤開発トーク (2024/09/18 19:00〜)" 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://ikyu.connpass.com/event/327095/">ikyu.connpass.com</a></cite></p> <p>このイベントでは</p> <ul> <li><a href="https://www.ikyu.com/">一休.com</a>、<a href="https://travel.yahoo.co.jp/">Yahoo!トラベル</a>、<a href="https://restaurant.ikyu.com/">一休.comレストラン</a>を運営する<a href="https://www.ikyu.co.jp/">一休</a></li> <li><a href="https://www.aeon.com/aeonapp/">iAEONアプリ</a>等を開発・運営する<a href="https://www.aeon-st.co.jp/">イオンスマートテクノロジー</a></li> </ul> <p>両社のシステムを支える基盤の開発についてお話しします。</p> <p>様々なお話をしていただく予定ですが、一休から1つ「歴史あるプロダクトにマイクロサービスを導入するプロセス」について紹介します。<br/> この発表では一休.comレストランにマイクロサービスを導入したときの取り組みについてお話しします。一休.comレストランはWebサービスとして18年もの歴史があるサービスです。歴史の長いサービスになると全てが新しい技術でできているわけではなくレガシーなアーキテクチャのものもあります。それらを考慮しながらマイクロサービスを導入していった泥臭い取り組みについてお話しする予定です。</p> <p>他にも以下のようなトークを予定しています。</p> <ul> <li>宿泊予約サイトにおける料金計算の格闘</li> <li>1000万DLを超えたiAEONアプリ:完全停止を防ぐ耐障害性と信頼性の設計</li> <li>SRE改善サイクルはチームを超えて - ダッシュボードを眺める会の取り組み</li> </ul> <p>それぞれの開発で取り組んでいる課題についてリアルな話をお送りします。どのトークもとても面白い内容になると思います!</p> <p>そして今回はオフラインで開催します。場所は一休本社も入居する東京ガーデンテラス紀尾井町17階にあるLODGEです。 <a href="https://www.z-lodge.com/">https://www.z-lodge.com/</a><cite class="hatena-citation"><a href="https://www.z-lodge.com/">www.z-lodge.com</a></cite></p> <p>懇親会もありますので、ご来場いただいた方はぜひ登壇者や他の参加者の方と交流していただければと思います。<br/> ぜひお越しください!</p> Tue, 03 Sep 2024 17:58:08 +0900 hatenablog://entry/6802340630903471245 React / Remix への依存を最小にするフロントエンド設計 https://user-first.ikyu.co.jp/entry/2024/08/05/142626 <p>CTO 室の恩田(<a href="https://x.com/takashi_onda">@takashi_onda</a>)です。</p> <p>一休レストランのフロントエンドアーキテクトを担当しています。</p> <h1 id="Intro">Intro</h1> <p>一休レストランでは、以前ご紹介したようにフロントエンドで React / Remix を利用しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F12%2F15%2F093427" title="一休レストランで Next.js App Router から Remix に乗り換えた話 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/12/15/093427">user-first.ikyu.co.jp</a></cite></p> <p>一方、設計方針としては、React / Remix への依存が最小になるように心掛けています。</p> <p>今日は、そんな一見矛盾するような設計方針について、ご紹介したいと思います。</p> <p>この記事を読んでいただき Remix に興味をもたれたら、明後日 2024/8/7(水) 19:00〜 のオンラインイベント</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Foffers-jp.connpass.com%2Fevent%2F324745" title="[一休恩田氏×溝口氏]向こう一年どうなる?次世代ReactフレームワークRemix活用の現状と今後 (2024/08/07 19:00〜)" 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://offers-jp.connpass.com/event/324745">offers-jp.connpass.com</a></cite></p> <p>にもご参加いただけると嬉しいです。 この記事でご紹介している疎結合なフロントエンドアーキテクチャを実現する Remix の魅力についてお話します。</p> <h1 id="なぜ依存を最小にするのか">なぜ依存を最小にするのか?</h1> <p>React / Remix を使っていて依存しないってどういうこと?と疑問を持たれる方も多いでしょう。 まずはその動機からご説明します。</p> <p>フレームワークやライブラリより寿命が長いプロダクトは珍しくありません。 栄枯盛衰の激しいフロントエンドでは、そういったサービスの方がむしろ多いのではないか、とも感じます。</p> <p>一休レストランのサービス開始は2006年です。 同じ2006年に jQuery 1.0 がリリースされました。 まだ、フロントエンドという言葉が生まれる前の時代です。</p> <p>一休レストランは、そんな時代から二度のリニューアルを経ながら(今は二度目の真っ最中)継続しているサービスです。 これからも広く使っていただけるよう日々、開発を進めています。</p> <p>したがって、将来、エコシステムが大きく変わったとしても、その変化に少ない労力で追随し続けられることが重要となります。</p> <p>フロントエンド領域も成熟してきているので、今後は、エコシステムが激しく入れ替わるような状況はもう訪れないかもしれません。 それでも、フレームワークやライブラリレベルでみると、メジャーバージョンアップは数年単位で見れば避けられず<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>、その中には破壊的変更を伴うものもあるでしょう。</p> <h1 id="バックエンド設計に倣う">バックエンド設計に倣う</h1> <p>バックエンド設計では、フレームワークへの依存を最小化する、という考え方が古くからあります。</p> <p>すなわち、バックエンドの世界では、指針となる原則や、依存を最小にして疎結合にする技術が確立しているのです。</p> <p>偉大な先人達の肩の上に乗るため、その知見はどういったものなのかを確認したいと思います。</p> <h2 id="依存性逆転の原則-Dependency-Inversion-Principle">依存性逆転の原則 (Dependency Inversion Principle)</h2> <p>ボブおじさんこと Robert C. Martin が提唱した SOLID 原則をご存知の方は多いと思います。その SOLID 原則の D 、依存性逆転の原則がフレームワークへの依存を最小化するための根幹の考え方になります。</p> <p>(少々長くなるので、ご存知の方は読み飛ばしてください)</p> <p>簡潔に説明すると、</p> <ul> <li>高レベルモジュールであるドメイン層が自身の必要とする抽象 (interface) を定義し</li> <li>低レベルモジュールにあたるインフラ層がその抽象を実現する詳細 (class) を実装する</li> </ul> <p>ように設計すべし、という原則です。</p> <p>高レベルモジュールが低レベルモジュールに直接依存するのではなく、低レベルモジュールが、高レベルモジュールの要求しているインターフェースの実装を提供する。 言い換えると、低レベルモジュールが高レベルモジュールの抽象に依存することから、依存性逆転の原則と呼ばれます。</p> <p>少し雑に言えば、高レベルモジュールにあたるのが我々の開発するプロダクトです。 低レベルモジュールがフレームワークやライブラリと、それらを使ったインフラ層に相当します。</p> <p>よく用いられる Repository の例で説明します。</p> <p>プロダクトが知っているのは自身で定義した Repository の interface のみです。 プロダクトが Repository の実装を構成するライブラリやフレームワークの API を直接呼ぶことはありません。 Repository の interface もフレームワークが要求する interface から独立しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240804124647" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240804/20240804124647.png" alt="DIP example" width="800" height="435" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></a></span></p> <p>フレームワークへの依存が疎結合になりました。</p> <h2 id="腐敗防止層-Anti-Corruption-Layer">腐敗防止層 (Anti Corruption Layer)</h2> <p>他にもバックエンドで培われた依存を最小化するテクニックとして、腐敗防止層は欠かせません。</p> <p>DIP に比べると、シンプルでわかりやすいと思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240804123141" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240804/20240804123141.png" alt="Anti Corruption Layer" width="800" height="143" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></a></span></p> <p>ライブラリや外部のサービスを利用するときに wrapper を挟み、変更の影響をその wrapper に閉じこめるという手法です。 インターフェースや実装が安定しなかったり、将来交換する可能性が考えられるような場合に、特に有効です。</p> <h1 id="フロントエンドにどう適用するか">フロントエンドにどう適用するか?</h1> <h2 id="余計なことをしない素直なフレームワークが大前提">「余計なことをしない」素直なフレームワークが大前提</h2> <p>現代のフロントエンド開発では、(メタ)フレームワークを利用することが一般的です。</p> <p>まず、フレームワークに求められる要件を考えたいと思います。</p> <h3 id="余計なことをしないとは">「余計なことをしない」とは?</h3> <p>逆を考えるとわかりやすいかもしれません。</p> <p>全部面倒を見てくれる、手厚いけれど、複雑な規約があり、その裏側がどうなっているかわからないフレームワークを思い浮かべましょう。フロントエンド・バックエンドは問いません。過去を振り返れば、だいたいどの言語にも一つはありそうです。</p> <p>このような全部入りフレームワークは、</p> <ul> <li>そのフレームワークが想定しているユースケースの範疇でコードを書けていて、</li> <li>運用面でも安定しているのであれば、</li> </ul> <p>とても優れた開発効率や開発体験が得られるでしょう。</p> <p>ですが、フレームワークが用意してくれているレールから外れないと実現が難しい要件は往々にして存在します。 そんな要件に遭遇してしまうと、フレームワークそのものに手を入れる以外の回避策しか見つからないことが多く、詰みます。<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup></p> <p>また、そんな要件に出会わなかったとしても、そのフレームワークに密結合状態で依存しているだけで弊害は存在します。 たとえばメジャーバージョンアップで破壊的な変更が加えられたとき、その変更に追随するための苦労は過去にそのようなフレームワークを使ったことがある方なら想像に難くないと思います。</p> <h3 id="Remix-の採用">Remix の採用</h3> <p>さて、逆の状況を踏まえた上で、あらためてフロントエンドの世界で「余計なことをしない」フレームワークの条件を考えてみましょう。</p> <p>標準 API の尊重、ブラックボックスがない、なだらかなアップデートパス、実装がシンプル、といった条件が挙げられます。</p> <p>現時点において React ベースでは Remix が条件を最も満たしていると判断し、以前ご紹介したように、<a href="https://user-first.ikyu.co.jp/entry/2023/12/15/093427">一休レストランでは Remix に乗り換える</a>ことを決断しました。</p> <p>その上でバックエンド設計の原則に従い Remix API への依存は最小限になるよう努めています。</p> <p>疎結合なアーキテクチャを実現するためには、逆説的ではありますが、フレームワークがそれを可能とする作りになっていることが重要なのです。</p> <p>Remix だと、なぜそれが可能になるのかは、最初にご紹介した<a href="https://offers-jp.connpass.com/event/324745">オンラインイベント</a>でもお話する予定です。</p> <h2 id="React-非依存の-Vanilla-JS-だけで使えるライブラリを選ぶ">React 非依存の Vanilla JS だけで使えるライブラリを選ぶ</h2> <p>一休レストランでは<a href="https://user-first.ikyu.co.jp/entry/2023/12/22/190342">以前ご紹介した XState</a> に加えて <a href="https://jotai.org/">Jotai</a> と <a href="https://tanstack.com/query/latest">TanStack Query</a> を利用しています。</p> <p>いずれも React に依存していないという共通点を持ちます。</p> <p>TanStack シリーズは Vanilla JS のコアと各フレームワークへのアダプタで構成されています。</p> <p>Jotai は TanStack シリーズのようにフレームワーク独立を謳っているわけではなく、React で使うことを前提とした状態管理ライブラリですが、公式サイトにも</p> <blockquote><p>Now with a store interface that can be used outside of React.</p></blockquote> <p>とあるように v2 からは、Vanilla JS で利用できる状態管理ライブラリとしての側面も持つようになりました。</p> <p>コアの Vanilla JS 部分と React アダプタが別れているため、他のフレームワークと組み合わせて使うことが可能です。 たとえば <a href="https://github.com/wobsoriano/jotai-vue">Vue</a> や <a href="https://github.com/wobsoriano/jotai-svelte">Svelte</a>, <a href="https://github.com/wobsoriano/solid-jotai">SolidJS</a> のアダプタを作っている方もいるようです。</p> <h2 id="React--Remix-での-DIP">React / Remix での DIP</h2> <p>具体例として、一休レストランの soft navigation 機能をご紹介します。</p> <p>一休レストランでの soft navigation では Remix の <a href="https://remix.run/docs/en/main/hooks/use-location"><code>useLocation</code></a> や <a href="https://remix.run/docs/en/main/hooks/use-navigate"><code>useNavigate</code></a> を直接使っていません。</p> <p>アプリ側で、</p> <ul> <li><code>useLocation</code> に対応する <code>useNavigationContext</code></li> <li><code>useNavigate</code> に対応する <code>useEventNavigate</code></li> </ul> <p>というカスタムフックをそれぞれ定義しています。</p> <p>DIP の観点から見ると、<code>useNavigationContext</code>, <code>useEventNavigate</code> の signature が、高レベルモジュールの定義する抽象に相当します。</p> <p>具体的に見てみましょう。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synSpecial">export</span> <span class="synStatement">function</span> <span class="synIdentifier">useNavigationContext</span>(): <span class="synIdentifier">NavigationContext</span> <span class="synIdentifier">{</span> <span class="synComment">// implementation</span> <span class="synIdentifier">}</span> </pre> <p>関数が first-class citizen なので signature (interface) さえ変わらなければ、</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">type </span><span class="synIdentifier">UseNavigationContext </span><span class="synStatement">=</span> () <span class="synIdentifier">=&gt;</span> <span class="synIdentifier">NavigationContext</span> <span class="synSpecial">export</span> <span class="synIdentifier">const</span> useNavigationContext: <span class="synIdentifier">UseNavigationContext</span> = createUseNavigationContext() </pre> <p>極端な例ですが、呼び出し元のコードを変えずに、後から実装を動的に差し替える形にすることも可能です。<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup></p> <p>さて、これらのフックは Remix の <code>useLocation</code> や <code>useNavigate</code> に加え Jotai, XState を利用して実装しています。 このフックの実装自体が、DIP における低レベルモジュールが実装する詳細にあたります。</p> <p>もちろん、アプリで定義しているこれらのフックは、単に DIP を実現するためだけに導入しているわけではありません。 あくまで、サービス固有のユースケースを実現するための機能を付加した抽象層になっています。<sup id="fnref:4"><a href="#fn:4" rel="footnote">4</a></sup></p> <p>ユースケースを具体的に見てみましょう。</p> <p>ユーザーが一休レストランで予約をするとき、人数日時などの予約条件や空席状況に応じて、次に表示するステップを切り替えたい場面があります。 このステップ、すなわち遷移先の切り替えは、固定的なナビゲーションではなかなか実現が難しい機能です。</p> <p>また、レストランの空席状況は刻々と変化するので、その時点の状況に応じた動的な画面遷移が必要となります。</p> <p>このようなユースケースに対応するため、ナビゲーションロジックは XState を使ったステートマシンで定義しています。 そして、ステートマシンが次の操作に相当するイベントをもとに次の画面(状態遷移)を決定する仕組みを取りました。</p> <p><code>useNavigationContext</code> はステートマシンの現在のステートとそのコンテキストを返し、<code>useEventNavigate</code> はステートマシンにイベントを送信する関数を返しています。</p> <h2 id="コンポーネント設計">コンポーネント設計</h2> <p>最後に、フロントエンドのアーキテクチャにおいて、本丸となるコンポーネントの設計について説明します。</p> <p>コンポーネント、カスタムフック、Vanilla JS ロジックの三層で構成しています。</p> <h3 id="コンポーネント">コンポーネント</h3> <p>コンポーネントは表示だけの責務を担う、テンプレートエンジン的な位置付けです。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">CourseFilter</span>() <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> facets = useAvailableFacets() <span class="synStatement">return</span> ( <span class="synComment">&lt;</span><span class="synIdentifier">div</span><span class="synComment">&gt;</span> <span class="synSpecial">{</span>facets.<span class="synStatement">map</span>((<span class="synPreProc">facet</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> ( <span class="synComment">&lt;</span><span class="synIdentifier">FacetButton </span><span class="synType">key</span><span class="synStatement">=</span><span class="synSpecial">{</span>facet.<span class="synStatement">key</span><span class="synSpecial">}</span><span class="synIdentifier"> </span><span class="synType">facet</span><span class="synStatement">=</span><span class="synSpecial">{</span>facet<span class="synSpecial">}</span><span class="synIdentifier"> </span><span class="synComment">/&gt;</span> ))<span class="synSpecial">}</span> <span class="synComment">&lt;/</span><span class="synIdentifier">div</span><span class="synComment">&gt;</span> ) <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">FacetButton</span>(<span class="synPreProc">{facet}</span>:<span class="synPreProc"> </span><span class="synIdentifier">{facet</span>: <span class="synIdentifier">Facet}</span>) <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> toggle = useToggleFacet() <span class="synIdentifier">const</span> onClick = useCallback((<span class="synPreProc">_</span>:<span class="synPreProc"> </span><span class="synIdentifier">MouseEvent</span>&lt;<span class="synIdentifier">HTMLButtonElement</span>&gt;)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> toggle(facet) <span class="synIdentifier">}</span>, <span class="synIdentifier">[</span>toggle, facet<span class="synIdentifier">]</span>) <span class="synStatement">return</span> ( <span class="synComment">&lt;</span><span class="synIdentifier">button </span><span class="synType">type</span><span class="synStatement">=</span><span class="synConstant">&quot;button&quot;</span><span class="synIdentifier"> </span><span class="synType">aria-pressed</span><span class="synStatement">=</span><span class="synSpecial">{</span>facet.<span class="synStatement">selected</span><span class="synSpecial">}</span><span class="synIdentifier"> </span><span class="synType">onClick</span><span class="synStatement">=</span><span class="synSpecial">{</span>onClick<span class="synSpecial">}</span><span class="synComment">&gt;</span> <span class="synSpecial">{</span>facet.<span class="synStatement">label</span><span class="synSpecial">}</span> <span class="synComment">&lt;/</span><span class="synIdentifier">button</span><span class="synComment">&gt;</span> ) <span class="synIdentifier">}</span> </pre> <p>基本的に、フックで取得した値を表示したり、イベントハンドラにバインドしている以上の仕事はしていません。</p> <h3 id="カスタムフック">カスタムフック</h3> <p>カスタムフックは Vanilla JS ロジックとコンポーネントを繋ぐアダプター層です。</p> <pre class="code lang-tsx" data-lang="tsx" data-unlink><span class="synStatement">function</span> <span class="synIdentifier">useAvailableFacets</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> useAtomValue(availableFacetsAtom) <span class="synIdentifier">}</span> <span class="synStatement">function</span> <span class="synIdentifier">useToggleFacet</span>() <span class="synIdentifier">{</span> <span class="synIdentifier">const</span> set = useSetAtom(toggleFacetAtom) <span class="synStatement">return</span> useCallback((<span class="synPreProc">facet</span>:<span class="synPreProc"> </span><span class="synIdentifier">Facet</span>)<span class="synPreProc"> </span><span class="synType">=&gt;</span> <span class="synIdentifier">{</span> set(facet) <span class="synIdentifier">}</span>, <span class="synIdentifier">[</span>set<span class="synIdentifier">]</span>) <span class="synIdentifier">}</span> </pre> <p>Jotai の atom と接続するだけの薄いアダプターになります。<sup id="fnref:5"><a href="#fn:5" rel="footnote">5</a></sup></p> <h3 id="Vanilla-JS-ロジック">Vanilla JS ロジック</h3> <p>Jotai の atom から先が、Vanilla JS (TypeScript) で書かれたロジックです。</p> <p>ただの TypeScript コードなので、可搬性が保証されます。</p> <h2 id="自動テスト">自動テスト</h2> <p>自動テストは、単体テストと e2e テストのみでカバーしています。</p> <p>単体テストでは React Testing Library を一切使用していません。 このことからも、React への依存が最小限になっていることが伝わるかと思います。</p> <p>Vanilla JS ロジックは、そのほとんどを純粋関数として実装しているので、複雑な fixture のセットアップも不要で vitest で高速にテストが可能です。</p> <h1 id="依存の最小化で得られる利点">依存の最小化で得られる利点</h1> <p>確実に発生するシナリオとして、フレームワークのメジャーバージョンアップに伴う破壊的な変更の影響を受けにくくなる、という点が挙げられます。</p> <h2 id="React-19-や-React-Router-v7-fka-Remix-v3-でどう変わる">React 19 や React Router v7 (fka Remix v3) でどう変わる?</h2> <p>現時点で判明している範囲からの判断ではありますが、影響はほぼゼロになるだろうと見ています。</p> <p>直接 Remix API を呼んでいる箇所は、これまで見てきたように DIP の低レベルモジュールにあたるフックの実装内に閉じています。 また、その構造上、Remix API の呼び出し箇所も最小限に抑えられているため、仮に変更が必要になっても、手を入れないといけない箇所は必然的に最小限に留められます。</p> <p>加えて Remix 自体もその設計哲学で、メジャーバージョンアップで導入される機能や変更は <a href="https://remix.run/docs/en/main/guides/api-development-strategy"><strong>future</strong> flag として段階的に適用できる</a>形で提供されます。 破壊的な変更は伴うとはいえ、バージョンアップに追随する負担が抑えられたフレームワークと言えるでしょう。</p> <p>例えば Server Component もオプトイン的に必要な箇所にだけ適用できるような<a href="https://github.com/remix-run/remix/discussions/8048">仕様が検討</a>されています。</p> <p>React 19 や React Router v7 についても<a href="https://offers-jp.connpass.com/event/324745/">前述したオンラインイベント</a>でお話しする予定ですので、興味のある方はぜひご参加ください。</p> <h2 id="極端な例だが仮に他のフレームワークに置き換えないといけなくなったら">極端な例だが、仮に他のフレームワークに置き換えないといけなくなったら?</h2> <p>コンポーネント設計でお伝えしたように、薄いコンポーネント層を移植するだけで対応できます。</p> <p>TypeScript で書かれたロジックは、Vanilla JS というその性質上、変更することなくそのまま使えることには説明の必要もないでしょう。</p> <h1 id="Outro">Outro</h1> <p>依存ライブラリやフレームワークのメジャーバージョンアップ程度では、ほとんど揺らがないアーキテクチャができました。</p> <p>あらためて前提を振り返ってみると、長い歴史を持ち、継続可能性が極めて高いサービスだからこその選択であるとも言えます。 スタートアップで、MVP でとにかく試行錯誤を高速に繰り返したい、といった状況下では、また別の選択肢があると思います。</p> <p>かなり長くなってしまいましたが、ここまで読んでいただきありがとうございました。 この記事が、フロントエンドのアーキテクチャを考える上での一助となれば幸いです。</p> <hr /> <p>一休では、本記事でお伝えしたような課題をともに解決するフロントエンドエンジニアを募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>まずはカジュアル面談からお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> React 19 や React Router v7 のリリースが近づいています。<a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> Remix への移行を決めた理由の一つです<a href="#fnref:2" rev="footnote">&#8617;</a></li> <li id="fn:3"> 実際には素直に関数として実装しています。事前に定義しなくても関数の signature が interface として機能することを示すための例です。<a href="#fnref:3" rev="footnote">&#8617;</a></li> <li id="fn:4"> 実を言うと、最初から現在の設計に辿りつけていたわけではありません。当初は Remix の nested routes を駆使する案を検討していました。その過程でチームメンバーから示唆をもらい、ステートマシンを使ったアプローチに切り替えました。もし、当初案のままであれば DIP と正反対の状態になっていたと思います。<a href="#fnref:4" rev="footnote">&#8617;</a></li> <li id="fn:5"> Jotai のようなライブラリを利用しない場合は <a href="https://react.dev/reference/react/useSyncExternalStore">useSyncExternalStore</a> を使って、ロジックを React と疎結合にできます。<a href="#fnref:5" rev="footnote">&#8617;</a></li> </ol> </div> Mon, 05 Aug 2024 14:26:26 +0900 hatenablog://entry/6801883189126703992 エンジニア主導でデザインシステムを導入してみた https://user-first.ikyu.co.jp/entry/2024/07/17/152452 <p>レストランプロダクト開発部の矢澤です。</p> <p>一休では「<a href="https://reszaiko.com/about">RESZAIKO</a>」というプロダクトの開発を行っています。 この開発を進めるにあたり、UI/UX に関するいくつかの課題があり、エンジニア主導でデザインシステムを構築することにしました。</p> <p>本記事では、エンジニア主導でデザインシステムを構築することになった背景や、実際に取り組んだ内容について赤裸々にお話しします。</p> <p>デザインシステムの導入を検討しているものの、最初の一歩を踏み出せずにいる・あるいは何から始めればよいかわからないチームにとって参考になれば幸いです。</p> <h2 id="そもそも-RESZAIKO-とは">そもそも RESZAIKO とは</h2> <p>RESZAIKO は飲食店の予約管理を DX する SaaS 事業で、現在3つのプロダクトを提供しています。</p> <ul> <li>複数予約サイトの在庫を一括管理する「サイトコントローラー」</li> <li>予約や顧客情報を管理する「予約台帳」</li> <li>店舗独自の予約ページを提供する「Web予約」</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240624161143" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240624/20240624161143.png" alt="RESZAIKO&#x306E;3&#x30D7;&#x30ED;&#x30C0;&#x30AF;&#x30C8;" width="800" height="301" loading="lazy" title="RESZAIKO&#x306E;3&#x30D7;&#x30ED;&#x30C0;&#x30AF;&#x30C8;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>現在は3つのプロダクトがありますが、元々はサイトコントローラーだけを提供しており、予約台帳とWeb予約は後発のプロダクトとしてリリースしました。</p> <h2 id="デザインシステム作成の背景">デザインシステム作成の背景</h2> <h3 id="後発プロダクトの開発を進めていく中での課題">後発プロダクトの開発を進めていく中での課題</h3> <p>私たちはサイトコントローラーがある状態で、「予約台帳」と「Web予約」の開発に着手しました。 同時進行で開発することになったこの2つの後発プロダクトは、既存プロダクトであるサイトコントローラーの UI/UX を参考にして設計・開発を進めていましたが、さまざまな課題に直面しました。</p> <h4 id="課題-スピード感を持ってデザインを固めていくことが難しい">課題① スピード感を持ってデザインを固めていくことが難しい</h4> <p>RESZAIKO にはサイトコントローラーの開発時から社内に専任のデザイナーがいなかったため、社外のデザイナーと週一回のミーティングでデザインを進めています。 限られた時間で多くの画面のデザイン調整を行うことは難しく、簡単な画面はエンジニアがデザインすることもありました。しかし、デザイナー以外デザインルールの把握ができていない状況であったため都度認識を合わせる必要があり、スピード感を持って進めていくことができませんでした。</p> <h4 id="課題-プロダクト間で-UIUX-の統一感を生み出しづらい">課題② プロダクト間で UI/UX の統一感を生み出しづらい</h4> <p>RESZAIKO は飲食店のスタッフが3つのプロダクトを横断して使用することを想定しています。横断して使用しても違和感のない操作を提供するために UI/UX を合わせる必要がありましたが、デザインや操作感に関する知見を開発チーム間でうまく共有できず、差異が生じてしまうことがありました。</p> <p>これを防ぐために、社外のデザイナーに Figma 上でコンポーネントの置き場を作成してもらいました。 しかし、各プロダクトで使用するための仕組み化ができず、操作感の統一も Figma 上で十分に表現されていなかったため、うまく運用することができていませんでした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240624161150" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240624/20240624161150.png" alt="RESZAIKO UI Guide" width="800" height="463" loading="lazy" title="RESZAIKO UI Guide" class="hatena-fotolife" itemprop="image"></a></span></p> <h4 id="課題-デザインのクオリティの担保が難しい">課題③ デザインのクオリティの担保が難しい</h4> <p>同一プロダクト内で同じコンポーネントを使用していても、余白の取り方やコンポーネントの組み合わせ方など細かい部分まで統一することは難しいです。実際に、既存箇所を参考にしてできた画面は、大枠は同じでも完璧には揃えられておらず、画面ごとに少しずつ違いが出てしまいました。 また、デザインに関するチェック基準がないため、チーム内でのレビューもある程度までしか確認できず、デザイナーのレビューも毎回細部までチェックすることができない状態でした。</p> <p>このような課題を感じながら当初は開発を進めていましたが、リリースが近づくにつれ、デザインのスピード感がボトルネックになり始めました。 そこで、エンジニアの中から比較的 UI/UX にこだわりをもつ3人が集まり、RESZAIKO デザインシステムを作成することになりました。</p> <h2 id="デザインシステム構築の流れ">デザインシステム構築の流れ</h2> <p>このようにしてデザインシステムを作ろうという話になったものの、メンバーがエンジニアだけだったため、デザインシステムに関する知見が不足していました。 そこで、知見を得るために「ちいさくはじめるデザインシステム」という本を読むことにしました。</p> <p><a href="https://bnn.co.jp/products/9784802512480">&#x3061;&#x3044;&#x3055;&#x304F;&#x306F;&#x3058;&#x3081;&#x308B;&#x30C7;&#x30B6;&#x30A4;&#x30F3;&#x30B7;&#x30B9;&#x30C6;&#x30E0;</a><cite class="hatena-citation"><a href="https://bnn.co.jp/products/9784802512480">bnn.co.jp</a></cite></p> <p>この本は、SmartHR 社のデザインシステムの取り組みを例にデザインシステムの構築・運用の方法について書かれています。 私たちはこの本を参考に以下の流れで進めていきました。</p> <h3 id="コンセプト設定">コンセプト設定</h3> <p>デザインシステムは、流行っているからや、なんとなくデザインの課題が解決しそうという理由で作成しがちです。 しかし、「ちいさくはじめるデザインシステム」には前提として目的が必要であると記載されていたため、まずデザインシステムを作成する目的を改めて定めるところから始めることにしました。</p> <blockquote><p>デザインシステムには正解がなく、「デザイン」という何らかの目的を機能させるための「システム」であり、システムとして成立させるために何らかの目的が必要</p></blockquote> <p>集まったエンジニア同士で現在感じている課題点を挙げ、それらの解決には何が必要なのか、どのような状態が理想なのかを話し合いました。 その結果、以下のようなデザインシステムのコンセプトとして明文化しました。</p> <hr /> <h4 id="デザインシステム-コンセプト">デザインシステム コンセプト</h4> <ul> <li>デザインと開発を効率化し、課題解決に集中できる環境を作り、リリースや改善サイクルを早くする</li> <li>デザインに一貫性をもたせ、ユーザービリティとアクセシビリティを向上させる(サービス単体)</li> <li>3つのプロダクトを違和感のないユーザー体験を提供する</li> <li>非デザイナーとデザイナーとのコミュニケーションとして使用し、共通認識を作る手段とする</li> <li>非デザイナーが自走して簡易的なデザインを行えるようにする(デザインのよりどころにする)</li> </ul> <hr /> <h3 id="構成決め">構成決め</h3> <p>コンセプトを決めたところで、次にデザインシステムをどのような構成で実際に作成していくのかを検討しました。</p> <p>構成を決めるにあたって、まずは他社のデザインシステムではどのような項目を採用しているのか調べました。 「ちいさくはじめるデザインシステム」にあるとおり、全てを網羅する必要はないため、調べたすべての項目を採用するのではなく、必要なものを取捨選択したり独自で項目を追加したりしました。</p> <blockquote><p>スタイルガイドには様々な種類がありますが、すべてを網羅している必要はありません。必要や目的・組織などに応じて柔軟に選ぶことができます。</p></blockquote> <p>私たちが特に参考にしたデザインシステムは、「<a href="https://smarthr.design/">SmartHR Design System</a>」「<a href="https://spindle.ameba.design/">Spindle</a>」「<a href="https://www.digital.go.jp/policies/servicedesign/designsystem">デジタル庁 デザインシステム</a>」の3つです。 「SmartHR Design System」はトークンやコンポーネントなど基本要素が網羅されているため、アウトライン作成の参考にしました。 「Spindle」は定義したコンポーネントの使用ルールがわかりやすくまとめられていたため、デザインルール作成の参考にしました。 また、今回作成するデザインシステムは元々コンポーネント置き場として使用していた Figma に定義したいと考えていたため、「デジタル庁 デザインシステム」のまとめ方を参考にして作成することにしました。</p> <h3 id="レビュー">レビュー</h3> <p>デザインシステムのコンセプトと構成が決まったところで、他のプロダクトのデザインシステムを作成している方にレビューを依頼しようということになりました。 一休.com ではすでにデザインシステムが構築されているため、その作成に携わったデザイナーやエンジニアにレビューを依頼しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2022%2F06%2F22%2F142755" title="デザインシステム導入しました - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2022/06/22/142755">user-first.ikyu.co.jp</a></cite></p> <p>レビュー会では、定義するコンポーネントに対してどのように使うのかルールを記載した方が良いというアドバイスをいただきました。 これを受け、もともとルールは「デザインパターン」という名目でレイアウトに関することを定義する予定でしたが、これを「デザインルール」という名称に変更し、コンポーネントの使い方まで定義することにしました。 また当初は Figma 上でガイドラインの定義のみを行う予定でしたが、せっかくエンジニアが作成しているのだからライブラリまで作成したらどうかと提案をいただき、ライブラリの作成も試みることにしました。</p> <p>そして、最終的に決定した構成がこちらです。</p> <hr /> <ul> <li>利用の手引き: デザインシステム構築の目的・利用方法</li> <li>デザインフィロソフィー: RESZAIKOプロダクトが大切にしていること</li> <li>デザイントークン: デザインシステムにおける最小単位のスタイル定義</li> <li>コンポーネント: UIを構成するための最小単位のパーツやアイコン</li> <li>デザインルール: デザイントークンやコンポーネントを使用する際のルール</li> <li>ライティングガイド: です/ます調や句読点の打ち方 <ul> <li>チェックリスト: デザインシステムに則っているか判断するための確認項目</li> </ul> </li> </ul> <hr /> <p>この中でも、まずは最小限でリリースしてみようということになり、主要部分となる「デザイントークン」「コンポーネント」「デザインルール」の策定を初回リリースの目標として進めていくことにしました。</p> <h3 id="作成">作成</h3> <p>レビューのフィードバックを受けたところで、デザインシステムの作成に着手しました。 本業の実装も並行で行っていて、あまり時間を確保できない中、Figma に各自がトークンやコンポーネントを追加し、週に1回3人で集まってお互いが作成したものにフィードバックを行うという流れで進めました。 また、3人の中で合意が取れたものは、社外のデザイナーに確認してもらい、エンジニアとデザイナーの双方が問題なく使用できるように整えていきました。</p> <p> 実際にできたデザインシステムの一部をお見せします。</p> <h4 id="デザイントークン">デザイントークン</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240624161158" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240624/20240624161158.png" alt="RESZAIKO UI Guide" width="800" height="636" loading="lazy" title="RESZAIKO UI Guide" class="hatena-fotolife" itemprop="image"></a></span></p> <h4 id="コンポーネント">コンポーネント</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240624161205" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240624/20240624161205.png" alt="RESZAIKO UI Guide" width="800" height="571" loading="lazy" title="RESZAIKO UI Guide" class="hatena-fotolife" itemprop="image"></a></span></p> <h4 id="デザインルール">デザインルール</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240624161212" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240624/20240624161212.png" alt="RESZAIKO UI Guide" width="800" height="557" loading="lazy" title="RESZAIKO UI Guide" class="hatena-fotolife" itemprop="image"></a></span></p> <h2 id="β版リリース">β版リリース!</h2> <p>初回リリースとして掲げていた「デザイントークン」「コンポーネント」「デザインルール」の作成がある程度完了したところで、β版のデザインシステムとしてチームに展開しました。</p> <p>現在は、実際に社外のデザイナーや開発メンバーに利用してもらい、元々の定義で不十分だった内容を拡充したり、新たなUIについては都度定義を追加したりしています。 デザインシステムを通じて、デザイナーとのコミュニケーションが円滑になっただけでなく、エンジニア同士でもデザインに関する会話ができるようになり、コミュニケーションツールとしても機能しはじめています。</p> <h2 id="まとめ">まとめ</h2> <p>本記事では、RESZAIKO デザインシステムの導入背景からβ版リリースまでの流れをご紹介してきました。</p> <p>エンジニア主導でデザインシステムを構築してみると、前提知識が不足していたことから調査しなければならない事柄が多く、最初の段階ではリリースまでたどり着けるのか不安に感じることもありました。 しかし、実装で必要になるコンポーネントやデザインルールが明らかなので、Figma への定義のフェーズに移ってからは特に迷うことなく進めることができました。 自分たちが実装時に必要としているものをダイレクトに反映することができることは、エンジニア主導で作成するメリットだと思います。</p> <p>今回デザインシステムを導入したことにより、当初感じていた3つの課題は解決できたのかという点についてですが、解決できた部分もあればもう少し手を加える必要がある部分もあると感じています。</p> <h4 id="課題-スピード感を持ってデザインを固めていくことが難しい-1">課題① スピード感を持ってデザインを固めていくことが難しい</h4> <p>デザイナーとエンジニアの共通言語ができたことにより、デザインに対する質問の精度も上がり、コミュニケーションが取りやすくなったため、これまでよりスピード感を持って開発が進められるようになってきました。</p> <p>今後は、さらにデザインルールなどをアップデートし、デザインについて考えやすい環境を整えていければと思います。</p> <h4 id="課題-プロダクト間で-UIUX-の統一感を生み出しづらい-1">課題② プロダクト間で UI/UX の統一感を生み出しづらい</h4> <p>各プロダクト間でコンポーネントを検討しデザインに反映することがなくなったため、導入前よりも統一感を出すことができるようになりました。 しかし、実装レベルでの統一がまだ十分ではありません。これを解決するために、UI ライブラリとしてパッケージ化し、RESZAIKO の各サービスに反映していければ、さらに課題の解決につながると考えています。</p> <h4 id="課題-デザインのクオリティの担保が難しい-1">課題③ デザインのクオリティの担保が難しい</h4> <p>ルールが定まったことで、誰が作成しても差異が出ないようにする基盤は整えられましたが、チェック時の効率はまだ改善の余地があります。 より効率的に確認できるようにするため、チェックリストの作成など、デザインシステムの拡張も進めていきたいと考えています。</p> <p>運用を始めたばかりの現段階では、まだ全体的に改善の余地が多く残っています。 これからさらにデザインシステムをアップデートし、利便性を高めていきたいと考えています。</p> <h2 id="おわりに">おわりに</h2> <p>全く知見がない中で始めたデザインシステムの作成でしたが、ありがたいことにデザインシステムをオープンに運用している企業が多く、参考にできるものが豊富にありました。そのおかげで自分たちなりにアレンジしてなんとか形にし、プロトタイプまで持っていくことができました。 知見がない中で、またエンジニア主導でデザインシステムを導入しようとしている方々にとって、この経験が一つの手がかりになれば嬉しいです。</p> <p>RESZAIKO デザインシステムはこれからもっとアップデートさせていくので、今後もブログで発信していきます!</p> <p>一休ではともに良いサービスをつくっていける仲間を募集していますので、興味を持っていただけたら、ぜひ一休の<a href="https://www.ikyu.co.jp/recruit/engineer">採用サイト</a>をご覧ください。</p> Wed, 17 Jul 2024 15:24:52 +0900 hatenablog://entry/6801883189116685908 Go Conference 2024にスポンサーしました & 一休はGoを活用しています https://user-first.ikyu.co.jp/entry/2024/06/25/180312 <h2 id="Go-Conference-2024にスポンサーしました">Go Conference 2024にスポンサーしました</h2> <p>CTO室プラットフォーム開発チームの山口(<a href="https://x.com/igayamaguchi">@igayamaguchi</a>)です。</p> <p>先日6/8(土)に一休でGo Conference 2024にスポンサーをさせていただき、スポンサーブースを出展しました。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgocon.jp%2F2024" title="Home | Go Conference 2024" 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://gocon.jp/2024">gocon.jp</a></cite></p> <p>来ていただいた方はありがとうございます!</p> <p>来ていただいた方と話していく中で、一休がGoを使っていることを知らない方がたくさんいることに気づきました。逆に、最近使い始めたばかりのRustの事例についてご存知の方のほうが多かったのです。これは、次のRustについての記事が多くの方に読まれたことによる影響だと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F12%2F25%2F132215" title="一休レストランのふつうのRustバックエンド開発 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/12/25/132215">user-first.ikyu.co.jp</a></cite></p> <p>実際には一休はGoを使っているサービスがたくさんあります。その点をアピールするため、この記事では一休のどのサービスでGoが活用されているかを紹介します。</p> <h2 id="一休がどのサービスでGoを使っているか">一休がどのサービスでGoを使っているか</h2> <p>まず、一休ではいくつものサービスを提供しています。</p> <p><figure class="figure-image figure-image-fotolife" title="一休プラットフォーム"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620104603" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620104603.png" alt="&#x4E00;&#x4F11;&#x306E;&#x63D0;&#x4F9B;&#x3059;&#x308B;&#x30B5;&#x30FC;&#x30D3;&#x30B9;" width="800" height="520" loading="lazy" title="&#x4E00;&#x4F11;&#x306E;&#x63D0;&#x4F9B;&#x3059;&#x308B;&#x30B5;&#x30FC;&#x30D3;&#x30B9;" class="hatena-fotolife" itemprop="image"></a></span><figcaption><a href="https://speakerdeck.com/kensuketanaka/introduce-ikyu">会社紹介資料</a>より抜粋</figcaption></figure></p> <p>この中でGoが使われているサービスは以下の赤枠で囲われたサービスです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620115123" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620115123.png" alt="Go&#x304C;&#x4F7F;&#x7528;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x30B5;&#x30FC;&#x30D3;&#x30B9;" width="800" height="520" loading="lazy" title="Go&#x304C;&#x4F7F;&#x7528;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x30B5;&#x30FC;&#x30D3;&#x30B9;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>ご覧の通り、Goが使われているサービスは多いです。<br/> また、ユーザー向けのサービスとは別の社内プラットフォームでもGoが使われており、実際は上の図で表されている箇所以上にGoが活用されています。</p> <p>ここからは、各サービスでのGo利用事例を個別に紹介していきます。</p> <h3 id="国内宿泊予約サービスでの活用">国内宿泊予約サービスでの活用</h3> <p>まず、一休の中で最も大きなサービスである国内宿泊予約においてGoは活用されています。<br/> 一休では国内宿泊予約サービスとして一休.comとYahoo!トラベル(LINEヤフー株式会社から運営を委託)を運営しています。</p> <table> <thead> <tr> <th style="text-align:left;"><a href="https://www.ikyu.com">https://www.ikyu.com</a></th> <th style="text-align:left;"><a href="https://travel.yahoo.co.jp">https://travel.yahoo.co.jp</a></th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620115649" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620115649.png" alt="&#x4E00;&#x4F11;.com" width="800" height="514" loading="lazy" title="&#x4E00;&#x4F11;.com" class="hatena-fotolife" itemprop="image"></a></span></td> <td style="text-align:left;"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620115700" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620115700.png" alt="Yahoo!&#x30C8;&#x30E9;&#x30D9;&#x30EB;" width="800" height="514" loading="lazy" title="Yahoo!&#x30C8;&#x30E9;&#x30D9;&#x30EB;" class="hatena-fotolife" itemprop="image"></a></span></td> </tr> </tbody> </table> <p>宿泊施設を探すための検索を実行するバックエンドがGoで書かれています。バックエンドはgqlgenを用いたGraphQLサーバーになっており、ホテルやプランの検索、料金、在庫検索といったロジックが実装されています。他にも、全文検索エンジンであるSolrのインデクシングや、施設管理画面の一部APIなどもGoで書かれています。</p> <p>ホテルの予約を行う処理、予約情報の閲覧ページはVB.NETですが、こちらも後々Goに置き換えていく予定です。</p> <h3 id="ふるさと納税海外宿泊予約">ふるさと納税、海外宿泊予約</h3> <p>国内宿泊予約サービス以外にもいくつかのサービスでGoは活用されています。<br/> 例えば宿泊予約時に割引クーポンを受け取れるふるさと納税サービスや、海外宿泊予約です。</p> <table> <thead> <tr> <th style="text-align:left;"><a href="https://furusato.ikyu.com/">https://furusato.ikyu.com/</a></th> <th style="text-align:left;"><a href="https://www.ikyu.com/global/">https://www.ikyu.com/global/</a></th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620120940" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620120940.png" alt="&#x3075;&#x308B;&#x3055;&#x3068;&#x7D0D;&#x7A0E;" width="800" height="514" loading="lazy" title="&#x3075;&#x308B;&#x3055;&#x3068;&#x7D0D;&#x7A0E;" class="hatena-fotolife" itemprop="image"></a></span></td> <td style="text-align:left;"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620120951" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620120951.png" alt="&#x6D77;&#x5916;&#x30DB;&#x30C6;&#x30EB;&#x4E88;&#x7D04;" width="800" height="514" loading="lazy" title="&#x6D77;&#x5916;&#x30DB;&#x30C6;&#x30EB;&#x4E88;&#x7D04;" class="hatena-fotolife" itemprop="image"></a></span></td> </tr> </tbody> </table> <p>これらのサービスは、社内では新しめということもあり、バックエンドは検索から予約まですべてGoで実装されています。</p> <h3 id="一休プラットフォーム">一休プラットフォーム</h3> <p>ユーザー向けサービス以外に、社内向けプラットフォームでもGoは使われています。一休ではいくつものサービスを運営しており、サービス間で共通のアカウントを利用し、貯めたポイントをサービス横断で使用したり、同じ決済の仕組みを使ったりできます。</p> <p>そういった機能を各サービスで再実装することなく提供するために、一休プラットフォームとして複数のマイクロサービスを実装し、運用しています。具体的なサービスとして、現在は会員サービス、ポイントサービス、決済サービスがあり、これらはすべてGoで実装しています。</p> <p><figure class="figure-image figure-image-fotolife" title="一休プラットフォーム"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240620115138" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240620/20240620115138.png" alt="&#x4E00;&#x4F11;&#x30D7;&#x30E9;&#x30C3;&#x30C8;&#x30D5;&#x30A9;&#x30FC;&#x30E0;" width="800" height="330" loading="lazy" title="&#x4E00;&#x4F11;&#x30D7;&#x30E9;&#x30C3;&#x30C8;&#x30D5;&#x30A9;&#x30FC;&#x30E0;" class="hatena-fotolife" itemprop="image"></a></span><figcaption>現在移行中の一休プラットフォームの図</figcaption></figure></p> <p>一休プラットフォーム開発の実例については、2023年のイベントでの資料も参照してください。</p> <p><iframe id="talk_frame_1052943" class="speakerdeck-iframe" src="//speakerdeck.com/player/81b3a9e036f5456cab9968b3d86b15a5" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/eguchij/xin-tanamaikurosabisuqu-rizu-minoshi-li">speakerdeck.com</a></cite></p> <h2 id="Goを選定してよかったこと">Goを選定してよかったこと</h2> <p>実際に国内宿泊予約や一休プラットフォームの開発に携わっているメンバーから、Goを選んでみてよかったことを聞いてみました。</p> <ul> <li>並行処理が言語組み込みで入っている。しかもそれが使いやすい</li> <li>言語仕様がシンプルで、入門から使えるようになるまでの時間が短い</li> <li>業務ロジックが大事なので、シンプルかつ堅く書けるのがよい</li> <li>一括処理や会計管理で大きめのデータを扱うときは非同期処理も書ける</li> </ul> <p>総じて、Goは「シンプル、かつすばやく、それでいて堅牢に作れる」ことを重視する一休の技術選定方針に合致すると感じています。</p> <h2 id="おわりに">おわりに</h2> <p>この記事では、一休がGo Conference 2024にスポンサーさせていただいたこと、一休では幅広くGoが使われていることを紹介しました。一休では、これからも生産的かつ高効率にサービスを開発/運用できるGoを活用して、サービスを成長させていきます!</p> <hr /> <p>一休では、ともに良いサービスをつくっていく仲間を募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Tue, 25 Jun 2024 18:03:12 +0900 hatenablog://entry/6801883189115552006 エンジニア向け社内イベントのご紹介と運営を経験してわかったこと https://user-first.ikyu.co.jp/entry/2024/05/27/120328 <p>こんにちは。宿泊プラットフォーム開発チームの菊地です。</p> <p>一休では月に一度、社内エンジニア向けにIkyu Tech Talkを開催しています。2022年から始まり、ありがたいことに2024年3月で丸2年を迎えることができました。<br/> この記事では、<strong>Ikyu Tech Talkの2年間のふりかえり</strong>をしていきます。</p> <p>また、私は社内イベントの主催が初挑戦だったので、どうやったらイベントを盛り上げられるのかと悩んだときもありました。<br/> そこで、同じように<strong>自分の会社でTech Talkを開催してみたい人に向けてイベント運営の知見</strong>もお伝えしたいと思います。</p> <h1 id="開催のきっかけ">開催のきっかけ</h1> <p>もともと定期的なプロジェクトの成果報告会はあるものの、業務で得たエンジニアリングの知見の共有をする場は設けられていませんでした。<br/> あるとき「技術についてざっくばらんに話す場が定期的にあると楽しそう。一緒にやらない?」と声をかけてもらい、面白そうだったのでやってみることにしました。</p> <h1 id="Ikyu-Tech-Talkとは">Ikyu Tech Talkとは?</h1> <p>「技術のことならなんでもOK」と題して社内エンジニアに発表者をやってもらう60分の社内イベントです。 月に1回ペースでZoom開催しています。<br/> カテゴリ別に過去の発表を抜粋してご紹介します。</p> <h3 id="自己学習の発表">自己学習の発表</h3> <p>個々人の技術研鑽の発表回です。業務では知ることができない興味関心分野を知ることができました。</p> <ul> <li>GitHub Copilotで 次世代のコーディング体験 <ul> <li>正式リリース直後の2022年6月にGitHub Copilotについて発表してもらいました。これをきっかけにCopilotの業務利用を行うことになりました!</li> </ul> </li> <li>TypeScript による型レベルプログラミングに入門した話 <ul> <li>型定義の表現力の高さを活用して、tscにアルゴリズムを実行させるデモが鮮烈でした</li> </ul> </li> </ul> <h3 id="プロジェクトのふりかえり">プロジェクトのふりかえり</h3> <p>案件が終わったタイミングで、チームの皆さんに振り返りもかねて発表をしてもらいました。新しいフレームワーク・ツールを積極的に採用するスタンスなこともあり、初挑戦の技術のフィードバックが多かった印象です。チャット欄もおおいに盛り上がりました!</p> <ul> <li>宿特化型SNS YADOLINKでのアーキテクチャ選定 <ul> <li>一休で初めてReactを用いた事例でした。当時よくGraphQLクライアントとして選定されていたApollo Clientに対しての適不適の考察も興味深い内容でした。Apollo Clientについては以下の記事もご覧ください <ul> <li><a href="https://user-first.ikyu.co.jp/entry/2022/07/01/121325">https://user-first.ikyu.co.jp/entry/2022/07/01/121325</a></li> </ul> </li> </ul> </li> <li>宿泊予約サイトの検索処理チューニング <ul> <li>国内宿泊サイトの検索処理には複数のシステムが関わっています。それらを複合的にパフォーマンスチューニングしてレイテンシを半減させた実践的なテクニック紹介でした</li> </ul> </li> <li>レストラン予約サイトフロントエンドの今とこれから <ul> <li>YADOLINKに続きレストラン予約サイトもReact/Next.jsで構築していました。Next.jsによる状態管理の落し穴や今後の展望についての発表でした。詳細は他の技術ブログで詳しく書いているため、ご興味のある方は以下の記事もご覧ください <ul> <li><a href="https://user-first.ikyu.co.jp/entry/2023/12/15/093427">https://user-first.ikyu.co.jp/entry/2023/12/15/093427</a></li> <li><a href="https://user-first.ikyu.co.jp/entry/2023/12/22/190342">https://user-first.ikyu.co.jp/entry/2023/12/22/190342</a></li> </ul> </li> </ul> </li> </ul> <h3 id="専門性の高い部署の知見を広める">専門性の高い部署の知見を広める</h3> <p>一休には、データサイエンス部・アーキテクトチーム・SEO対策チームといった専門性の高い部署があります。なかには「もっと早く知りたかった」「入社時の資料にしてほしい」という声をいただく発表もありました。</p> <ul> <li>猫でもわかる一休のデータ分析基盤(参加型) <ul> <li>一休のデータサイエンス部は、各プロダクトのデータをもとに分析基盤を提供しています。分析基盤の全体像をキャッチアップできただけではなく、データ基盤を安定させるための実践的なテクニックが非常に面白い発表でした</li> </ul> </li> <li>一休のサービスを支える インフラのはなし <ul> <li>プロダクトのネットワーク構成やデプロイフローについて、SREチームが解説しました。特に入社したての人にとっては垂涎の資料でした</li> </ul> </li> </ul> <h1 id="Tech-Talkの成果">Tech Talkの成果</h1> <p>Ikyu Tech Talkは完全任意参加のイベントとして運営してきましたが、開発組織メンバーの半数以上が参加し続けてくれています!<br/> ここまで続けられてきたのは「エンジニアリングの話をするのが楽しいから」というのに尽きると思います。その一方、会社としてTech Talkを開催することで以下のような成果が得られました。</p> <h2 id="チームを超えてナレッジを共有できる">チームを超えてナレッジを共有できる</h2> <p>これまでは、成果報告会などのビジネス的な成果を知る場はあったものの、互いのナレッジを知る機会はなかなかありませんでした。Tech Talkはエンジニアリングの話を聞く場として貴重な機会になりました。</p> <p>実際にSlackを探してみたところ、Tech Talkの発表を受けて他のチームのソリューションを取り込んでいるやりとりもありました!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240512132444" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240512/20240512132444.png" alt="Tech Talk&#x3092;&#x53D7;&#x3051;&#x3066;Volta&#x3092;&#x5C0E;&#x5165;&#x3057;&#x305F;&#x4E8B;&#x4F8B;" width="800" height="357" loading="lazy" title="Tech Talk&#x3092;&#x53D7;&#x3051;&#x3066;Volta&#x3092;&#x5C0E;&#x5165;&#x3057;&#x305F;&#x4E8B;&#x4F8B;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>発表時のZoomのチャット欄では、「ウチでは○○を使ってます」というように参加者からの知見も多く寄せられ、双方向での知見交流が生まれたのも成果だと思います。</p> <h2 id="発表の機会があることで個々人の知識がよりブラッシュアップされる">発表の機会があることで、個々人の知識がよりブラッシュアップされる</h2> <p>プロジェクトに没頭している間は、知識が表面的なままになっていることがあります。Tech Talkを目標に、知識の精査や最新情報の確認をすることで、それらを自分の知見として昇華するきっかけにできます。</p> <p>たとえば、あるプロダクトの新規リリースを行ったチームに発表をお願いしたところ、初期開発時の技術選定の是非を振り返った発表をしてくれました。<br/> 選定したソリューションの選定基準だけではなく、不採用にした他の案の理由や今振り返るとその選択は妥当だったのかの洞察も述べていて、今後の技術選定にとって価値のある資料になったと思います。</p> <h2 id="カジュアルに自己発信の経験を積む場を提供できる">カジュアルに自己発信の経験を積む場を提供できる</h2> <p>自己発信の機会は貴重ですが、いざ外部の勉強会で発表しようとすると初心者には足が重いこともあります。 Tech Talkでは顔見知りが参加者なので、カジュアルに発表の経験を積むことができます。 採用活動をしている会社にとって社外で発表してくれるエンジニアは貴重ですが、Tech Talkを練習場として提供することができます。</p> <h1 id="社内イベントの運営をしてわかったこと">社内イベントの運営をしてわかったこと</h1> <p>この記事を読んでいる方のなかには、以下のような悩みを持った人もいるかと思います。</p> <ul> <li>自分の会社でも社内イベントを開催してみたいけど、どうやったら盛り上がるだろうか、どう始めたらいいだろうか?</li> <li>社内イベントを開催してるけど人がなかなか集まらない、集まっても盛り上がらない</li> <li>登壇をお願いしても断られる、つらい</li> </ul> <p>ここからは、社内イベントを開催したい方に向けて、Ikyu Tech Talkで得た運営のノウハウをお伝えします。</p> <h2 id="イベントがコンスタントに続けられる仕組みにする">イベントがコンスタントに続けられる仕組みにする</h2> <p>Tech Talkの運営方針として、<strong>エネルギーが必要すぎて続けられなくなるよりもかけるエネルギー少なく長く運営できるイベントにする</strong>ことを決めていました。<br/> Zoom開催としたのも、イベント設営と集客に疲弊したくなかったからです。開催頻度も月1回くらいで「たまにやればいい」という気持ちで始めました。</p> <p>発表を依頼したりイベント告知等の作業など、イベントの運営はただでさえ負担が大きいです。そのため、開催コストをできるだけ下げるのは非常に有用だったと感じました。<br/> また一休ではリモートワークが導入されておりオフラインイベントにすると参加側の敷居も高くなってしまうため、双方にとってオンライン開催が最適でした。</p> <h2 id="発表者に対するリターンを設定する">発表者に対するリターンを設定する</h2> <p>発表準備や当日の精神的な負担が大きいので、モチベーションを高めるためにリターンを設けました。 具体的には「Tech Talk賞の開催」と「発表ごとに感想・メッセージの受付」を行っています。<br/>   Tech Talk賞とは、半期に一度、最も面白い発表をしてくれた人を投票で決め表彰するイベントです。一休各サービスで使用できるポイントを贈っています。</p> <p>また、毎度の発表後には、参加者に発表の感想・メッセージを書いてもらってそれをまとめてお渡ししています。発表中はどうしても参加者のリアクションがわからなかったり、面白い発表だったかなど不安を感じる人も多いです。実際に発表者の方からも、発表のフィードバックがもらえてよかった、という声をいただきました。</p> <p>一方で、メッセージの回収率が20%程度にとどまっているのが今後の課題です。対策としてイベント中にメッセージの記入時間を設けたらどうかと検討中です。</p> <h2 id="Zoomのコメント機能を活用して積極的な参加を促す">Zoomのコメント機能を活用して積極的な参加を促す</h2> <p>多くの参加者にリアクションしてもらいイベントを盛り上げるため、Zoomのコメント機能を活用しました。<br/> 質問や感想をその場で話すには緊張してしまう人もいるため、テキストベースでコメント欄に書き込んでもらう形式にしました。書かれた質問は、発表の区切りの良いタイミングで司会が拾いその場で発表者に回答してもらいました。 <span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240512132452" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240512/20240512132452.png" alt="&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x3092;&#x547C;&#x3073;&#x639B;&#x3051;&#x3066;&#x3044;&#x307E;&#x3059;" width="800" height="153" loading="lazy" title="&#x30B3;&#x30E1;&#x30F3;&#x30C8;&#x3092;&#x547C;&#x3073;&#x639B;&#x3051;&#x3066;&#x3044;&#x307E;&#x3059;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>また、司会以外の運営はちょっとした感想も意識的に書き込むようにし、コメント欄を盛り上げることを心がけました。今ではコメント欄がフランクな感想を言える場所として定着したため、とてもよい試みだったと思います。</p> <h2 id="まとめ">まとめ</h2> <p>以上が社内イベント運営のノウハウです。 社内イベントでは、<strong>運営・発表者・参加者それぞれが継続できる仕組みを作る</strong>ことが最も重要だと感じました!</p> <h1 id="さいごに">さいごに</h1> <p>ここまで読んでくださりありがとうございます!<br/> 今回は社内イベント Ikyu Tech Talkの紹介と、社内イベントの運営をしてみて得た学びをまとめました。本記事が自社のエンジニア組織を盛り上げたい方の力になれたら幸いです。</p> <p>また、いつもIkyu Tech Talkに参加&登壇してくださっている一休のエンジニアの皆さんへ。<br/> この場を借りて感謝の気持ちを伝えさせてください。皆さんがポジティブに参加してくれるおかげで、Ikyu Tech Talkが楽しいイベントとして継続できています。いつも参加いただき本当にありがとうございます。</p> <p>さいごになりますが、一休では社内イベントに積極的に参加してくれる、アウトプットが得意なエンジニアを募集しています。<br/> 興味がわいた方は、以下のリンクから面接応募及びカジュアル面談へのご参加をぜひぜひお願いいたします!!!</p> <p><a href="https://www.ikyu.co.jp/recruit/engineer/">https://www.ikyu.co.jp/recruit/engineer/</a></p> <p><a href="https://hrmos.co/pages/ikyu/jobs/1745000651779629061">https://hrmos.co/pages/ikyu/jobs/1745000651779629061</a></p> Mon, 27 May 2024 12:03:28 +0900 hatenablog://entry/6801883189094267103 一休の社内勉強会のご紹介2024 https://user-first.ikyu.co.jp/entry/2024/05/21/095713 <p><a href="https://twitter.com/kymmt90">kymmt</a>です。</p> <p>一休では、技術力の底上げを目的として、さまざまな社内勉強会を開催しています。この記事では、今年2024年に入って社内で実施していた勉強会について紹介し、一休での勉強会の雰囲気を伝えられればと思います。</p> <ul class="table-of-contents"> <li><a href="#一休の社内勉強会">一休の社内勉強会</a></li> <li><a href="#2024年にこれまで実施した勉強会">2024年にこれまで実施した勉強会</a><ul> <li><a href="#A-Philosophy-of-Software-Design輪読会">『A Philosophy of Software Design』輪読会</a></li> <li><a href="#Webフロントエンド-ハイパフォーマンス-チューニング輪読会">『Webフロントエンド ハイパフォーマンス チューニング』輪読会</a></li> <li><a href="#フロントエンドワークショップ-Reactハンズオン">フロントエンドワークショップ: Reactハンズオン</a></li> </ul> </li> <li><a href="#2024年の今後の勉強会">2024年の今後の勉強会</a></li> </ul> <h2 id="一休の社内勉強会">一休の社内勉強会</h2> <p>社内勉強会は輪読会の形式で実施することが多いです。参加意欲の高い人ができるだけ多く参加できる曜日と時間を決めて、週次で開催する形式をとっています。</p> <p>読む本の決め方については、ボトムアップで決めることが多いです。先日は、次に読みたい本をSlack上でPollyを使った投票形式で募り、『なっとく!関数型プログラミング』を読むことになったりしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240517185432" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240517/20240517185432.png" alt="&#x8AAD;&#x3080;&#x672C;&#x306E;&#x6295;&#x7968;&#x306E;&#x69D8;&#x5B50;" width="800" height="657" loading="lazy" title="&#x8AAD;&#x3080;&#x672C;&#x306E;&#x6295;&#x7968;&#x306E;&#x69D8;&#x5B50;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>進め方については前日までの準備と当日で分かれています。</p> <ul> <li>前日まで: 章ごとに担当を決める場合は担当者が、決めない場合は参加者全員がその回で読む範囲に基づいて概要や疑問点、コメントをConfluenceのページとしてまとめる</li> <li>当日: 担当者/ファシリテータが会を進行しつつ、資料に基づいて参加者が議論しつつ書籍を読み進める</li> </ul> <p>Confluenceに「勉強会」というスペースがあり、そこに勉強会の資料が集積されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240517185439" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240517/20240517185439.png" alt="&#x8F2A;&#x8AAD;&#x4F1A;&#x306E;&#x8CC7;&#x6599;" width="800" height="637" loading="lazy" title="&#x8F2A;&#x8AAD;&#x4F1A;&#x306E;&#x8CC7;&#x6599;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>また、のちほど説明するとおり、輪読会以外にハンズオン形式でのイベントを開催することもあります。</p> <h2 id="2024年にこれまで実施した勉強会">2024年にこれまで実施した勉強会</h2> <h3 id="A-Philosophy-of-Software-Design輪読会">『A Philosophy of Software Design』輪読会</h3> <p>ソフトウェアの複雑性に着目し、複雑性をいかに制御するかという観点でソフトウェア設計や実装の方法論が議論されている書籍です。</p> <p><a href="https://www.amazon.com/dp/173210221X">A Philosophy of Software Design, 2nd Edition: Ousterhout, John: 9781732102217: Amazon.com: Books</a></p> <p>この本はこれまでの通説とは異なる著者独自の観点からの主張が含まれる、という特徴があります。この主張を適切に咀嚼しつつ(いい意味で)批判的に読み進める態度が求められ、おもしろい輪読会になったと思います。</p> <p>また、基本的には原著しか選択肢がない本なので、洋書を読む練習にもなりました。英語自体は平易で、書評記事などがWeb上に多く存在することから、調べながらなら難しい本ではありません。とはいえ、平易な文であっても意味を取り違えてしまうケースもあるので、そのあたりも注意して読んでいくのは多くの参加者にとって勉強になりました。</p> <h3 id="Webフロントエンド-ハイパフォーマンス-チューニング輪読会">『Webフロントエンド ハイパフォーマンス チューニング』輪読会</h3> <p>タイトルから受ける印象とは異なり、実際はブラウザの仕組みについて詳しく書かれている書籍を読む会です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgihyo.jp%2Fbook%2F2017%2F978-4-7741-8967-3" title="Webフロントエンド ハイパフォーマンス チューニング" 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://gihyo.jp/book/2017/978-4-7741-8967-3">gihyo.jp</a></cite></p> <p>フロントエンドをこれから学習していきたいメンバーに、まずブラウザの仕組みを知ってもらうのがいいのではということで開催されました。</p> <p>この勉強会については、別の記事で詳しく紹介する予定です。</p> <h3 id="フロントエンドワークショップ-Reactハンズオン">フロントエンドワークショップ: Reactハンズオン</h3> <p>社内エンジニアのフロントエンドに関する技術力を底上げするための「フロントエンド技術力向上委員会」メンバーが主催しているのがフロントエンドワークショップです。第1回のワークショップでは、フロントエンドの経験が薄い人や、これまでReactを触ったことがない人向けに、Reactで簡単なToDoアプリを作るハンズオンを実施しました。</p> <p>ハンズオンでは <a href="https://github.com/tak-onda/frontend-workshop-react/">https://github.com/tak-onda/frontend-workshop-react/</a> のリポジトリを利用しました。</p> <p>輪読会は基本的にオンラインで実施していますが、このハンズオンは社内のラウンジと呼ばれるスペースを使って、みんなでワイワイ取り組むためにオフライン開催しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/ikyu_com/20240517185447" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20240517/20240517185447.jpg" alt="React&#x30CF;&#x30F3;&#x30BA;&#x30AA;&#x30F3;&#x306E;&#x69D8;&#x5B50;" width="800" height="600" loading="lazy" title="React&#x30CF;&#x30F3;&#x30BA;&#x30AA;&#x30F3;&#x306E;&#x69D8;&#x5B50;" class="hatena-fotolife" itemprop="image"></a></span></p> <p>ふだんサーバサイドやプラットフォーム中心に開発しているエンジニアが最近のフロントエンドについてキャッチアップできるイベントになりました。</p> <h2 id="2024年の今後の勉強会">2024年の今後の勉強会</h2> <p>2024年5月からは投票多数で選ばれた『なっとく!関数型プログラミング』の輪読会を開催しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.shoeisha.co.jp%2Fbook%2Fdetail%2F9784798179803" title="なっとく!関数型プログラミング | 翔泳社" 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://www.shoeisha.co.jp/book/detail/9784798179803">www.shoeisha.co.jp</a></cite></p> <p>近年は、関数型プログラミングのエッセンスを含んだ言語やライブラリが広く使われるようになっています。既存のコードベースが関数型ではないとしても、堅牢なソフトウェアを作るためには、関数型の考えに親しむことで得られるものも多いだろうと考えています。</p> <p>『なっとく!関数型プログラミング』を読み終えたら、次は『Domain Modeling Made Functional』を読むのがいいのではないかという話もすでに出ています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpragprog.com%2Ftitles%2Fswdddf%2Fdomain-modeling-made-functional%2F" title="Domain Modeling Made Functional" 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://pragprog.com/titles/swdddf/domain-modeling-made-functional/">pragprog.com</a></cite></p> <p>また、フロントエンドワークショップの第2弾として、Reactで作ったToDoアプリにJotaiで状態管理を、Vitestでテストを組み込むワークショップも開催予定です。</p> <p>一休では今後も継続的に勉強会を開催していく予定です!</p> <hr /> <p>一休では、ともに良いサービスをつくっていく仲間を募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Tue, 21 May 2024 09:57:13 +0900 hatenablog://entry/6801883189101443796 なぜ我々は GitHub Copilot Enterprise の導入を見送ったのか https://user-first.ikyu.co.jp/entry/2024/04/15/150249 <p>CTO 室の恩田です。</p> <p>今回は GitHub Copilot Enterprise を評価してみて、現時点ではまだ採用しないことを決めた、というお話をご紹介したいと思います。</p> <h2 id="きっかけ">きっかけ</h2> <p>とあるエンジニアが Slack で自身の times チャネルに時雨堂さんの <a href="https://voluntas.medium.com/github-copilot-enterprise-%E3%81%AE%E3%82%B9%E3%82%B9%E3%83%A1-d2f660355091">GitHub Copilot Enterprise のススメ</a>という記事を投稿したことが発端でした。特に感想はなく URL に 👀 だけが添えられていたので、後で見るぐらいのメモだったんだと思います。</p> <p>それを見かけた別のエンジニアが技術雑談チャネルにその投稿を共有して、これは凄そうと話題を向けたところ、CTO の「評価してみる?」の一言で、有志が集って評価プロジェクトが始まりました。</p> <p>雑談チャネルできっかけとなる投稿が共有されてから、30分足らずの出来事でした(笑)。</p> <p>この話題が出たのは金曜日でしたが、週明け早々に稟議を終え、火曜日の朝にアップグレードが完了しました。 GitHub Team から GitHub Enterprise Cloud に、Copilot Business から Copilot Enterprise への変更です。</p> <p>そうして評価プロジェクトが動きはじめました。</p> <h2 id="評価にあたって">評価にあたって</h2> <p>Copilot Enterprise が有効になったことを確認したあと、集った有志がどう評価を進めようか話しはじめたところで、CTO からレビューしてちゃんと意思決定しようね、との補足をもらいました。</p> <p>その要旨は次の二点です。</p> <ul> <li>会社で支払うライセンス管理は、ほとんど使ってないのにとりあえずもらっておくなど、なあなあになりがち</li> <li>評価は定性的でよい、インパクトのあるユースケースがどれだけ見つかるかが重要</li> </ul> <p>個人的には、コストに見合う価値をどう定量化するかという観点でばかり考えていたので、後者の指摘には新鮮な視点をもらえたように思います。 定量化を前提にすると評価プロセスが重たく固定的になってしまい、様々な視点からの素早い意思決定には繋がらなかったことでしょう。</p> <p>結果、プロジェクトに集まったメンバーが各自の興味のある観点で分担して、どういうユースケースが実現できると開発体験にインパクトを与えられるか、で評価することになりました。</p> <p>いくつか抜粋すると、</p> <ul> <li>ドキュメントを集約する場として knowledge bases は Confluence からの移行に値するか?</li> <li>レガシーコードの理解にあたり認知負荷をどれぐらい軽減してくれるか?</li> <li>PR サマリーの自動生成で開発プロセスがどの程度改善されるか?</li> </ul> <p>といった観点になります。</p> <h2 id="評価">評価</h2> <p>冒頭でお伝えしている通り、2024年4月現在、一休のコードベースやドキュメントを学習させた限りにおいては、GitHub Copilot Enterprise は時期尚早という結論になりました。</p> <p>ここでは評価していく中で、具体的にどんなことがわかったのかをご紹介したいと思います。</p> <h3 id="knowledge-bases-は使えるか">knowledge bases は使えるか?</h3> <p>もともとプラットフォームエンジニアリングの文脈で、開発者の認知負荷を軽減させるために、ドキュメントをどうしていくかという議論が少し前からありました。</p> <p><a href="https://docs.github.com/en/enterprise-cloud@latest/copilot/github-copilot-enterprise/copilot-chat-in-github/managing-copilot-knowledge-bases">knowledge bases</a> は2024年4月現在 GitHub リポジトリ内の markdown ファイルのみを学習します。 Copilot Enterprise を導入することになると、ドキュメントを今後は GitHub リポジトリで管理していく必要があります。</p> <p>一休では現在、多くのチームが Confluence を使ってドキュメントを管理しています。</p> <p>非エンジニアにとっても扱いやすく、階層的に情報を整理することができ、世界的に広く利用されているナレッジマネジメントサービスです。 特にドキュメントを同時編集したり、リアルタイムでインラインコメントを入れる機能は、一休でもミーティングの場で活用されています。</p> <p>そういった現在享受している Confluence の良い点を失ってでも、なお余りある価値を Copilot Enterprise がもたらしてくれるのかが焦点でした。</p> <p>上述の通り、knowledge bases にインデックスさせるデータは markdown ファイルで構成された GitHub リポジトリとして用意する必要があります。そこで、スペースを一括して markdown に出力する Confluence プラグインを導入し、それを使って knowledge bases 用のリポジトリをいくつか作成しました。</p> <p>その上で、様々な質問で評価してみましたが、概念の学習がまだまだ限定的であるように感じられました。</p> <p>ひとつ具体例を紹介したいと思います。</p> <p>一休レストランは現在3バージョン存在します。</p> <p>オリジナルの一休レストランは restaurant というリポジトリで作られました。 リニューアル時に restaurant2 が作られ、それ以後、オリジナルは restaurant1 や略して res1 と呼ばれるようになりました。</p> <p>今回 knowledge bases に取り込んだドキュメントにも restaurant1 や res1 という記述が多数あります。 にも関わらず、res1 などのキーワードを含めた検索では、オリジナルのリポジトリである restaurant に関する回答が返されることはほぼありませんでした。 数字の有無が影響しているのか、restaurant2 に関する情報ばかりが要約されて返ってくることが多かったです。</p> <p>他にも LLM でよく言われているように、knowledge bases においても、日本語で学習させたにも関わらず、英語で質問した方がより優れた回答になる傾向が見られました。</p> <h3 id="レガシーコードの理解にあたり認知負荷をどれぐらい軽減してくれるか">レガシーコードの理解にあたり認知負荷をどれぐらい軽減してくれるか?</h3> <p>一休は20年以上の歴史を持つサービスです。</p> <p>継続的にモダナイゼーションを進めてはいるものの、まだまだレガシーコードが残っています。</p> <p>そのようなレガシーコードを読んで理解することは、現状の振舞いや仕組み、そこに至った経緯を把握するために、避けて通れない作業です。</p> <p>レガシーコード上でわからないことを GitHub Copilot Chat が適切に要約して回答してくれると、あちこち行ったり来たりすることなく、着目すべきコードに集中して読むことが可能になります。 ひいては開発生産性の向上にも寄与してくれるのではないか、と期待していました。</p> <ul> <li>新しく入社した開発者がレガシーなリポジトリを見るとき、どこを読めばいいかを示してくれるか</li> <li>営業スタッフやカスタマーサービスから問い合わせがあったとき、現状や経緯についてのピンポイントな質問に答えられるか</li> <li>リポジトリの全体感がどうだ、とかそういう質問に答えられるか</li> <li>もっと踏み込んで、検索にとどまらず改善策など示唆にあたる情報を提示してくれるか</li> </ul> <p>具体的には上記のようなユースケースです。</p> <p>このようなシナリオを評価するために、評価者が十分に理解しているような内容について、適切なまとめを返せるか、というテストを行いました。</p> <p>このあたりは業務に深く関わってくる内容なので、具体例を紹介することは難しいのですが、たとえば、</p> <blockquote><p>複数のリポジトリを横断して内容をまとめる必要があるのですが、リポジトリ間でコードやコメントの質に差が大きく、より質の高いリポジトリに回答の内容が引っ張られてしまっている(ように見えた)</p> <p>無関係の情報が回答に含まれないように、プロンプトの書き方や、knowledge bases にインデックスさせる情報を工夫する必要がある</p> <p>XXX の API を呼びだしているところを探して、という質問で、関係のないプレゼンテーション層のコードを返してきたり、リポジトリのコードをあまり学習しているように感じられなかった</p></blockquote> <p>といった意見があり、期待した結果を得るにはハードルが高いなという印象でした。</p> <p>もちろん、命名やコメントを含めてレガシーコード自体の品質に問題があることは否定できません。 ですが、そのようなコードベースであっても適切に情報を抽出できなければ、レガシーコードを扱う上での助けにはならないのが実情なのです。</p> <h3 id="PR-メッセージ自動生成">PR メッセージ自動生成</h3> <p>Pull Requests のメッセージを自動で生成する機能は現状英語しかサポートされていません。</p> <p>ソフトウェアエンジニアとして英語ドキュメントに触れる機会は多いといっても、社内コミュニケーションはもちろんのこと日本語です。 人に読んでもらうための PR メッセージが母語でなければ、当然、その効率は著しく下がってしまいます。 ノンバーバルな情報が得られない文章によるコミュニケーションにおいて、重要となる細かなニュアンスを伝えることも難しくなります。</p> <p>また、英語であることを差し引いたとしても、生成される内容が現状ではそこまで有用とは言えませんでした。</p> <p>たとえば、どのようなファイルにどのような変更をおこなったかという what の情報はうまく要約してくれます。 しかし、その PR の変更が必要となった背景や変更の意図といった why の情報は期待したほどには盛り込まれません。</p> <p>レビューにあたって what は差分を見ればわかります。 ですが、その変更が適切かどうかを判断するために欲しい情報は why なのです。</p> <p>もちろん why をコードのみから読み取るのは人間でも難しいので、コメントの形で補足する必要があります。 しかし、コメントを書いたとしても、コードの変更箇所に関する限定的な内容となってしまいがちで、そもそもの背景や目的を網羅するのは現実的に難しいところがあります。 結果、PR の説明として期待するほどの内容にはなりませんでした。</p> <p>将来的には Issue や git の履歴を利用して、背景情報を補ってくれるようになることを期待しています。</p> <p>総じて PR メッセージ自動生成は機能自体が発展途上であり、現時点で導入したとしてもそこまで大きな恩恵を受けられるわけではなさそうだという結論に至っています。</p> <h3 id="学習の対象が限定的">学習の対象が限定的</h3> <p>他にも評価の過程で以下のような声が挙がりました。</p> <blockquote><p>DB 定義書を開くのが手間なので聞けたら便利だと思ったが、Excel ファイルを読み取ることはできなかった。</p></blockquote> <p>回避策として Excel の DB 定義ファイルから markdown に変換して knowledge bases リポジトリに登録してみました。</p> <p>自動生成された markdown という制限付きではあるものの、テーブル間の関係を学習できていないように思えます。 たとえば、ある機能に関連するテーブル定義の全体像を説明して、といった質問には適切な回答が得られませんでした。</p> <blockquote><p>ADRのリポジトリがあるので、これをインデックスして仕様を聞けたら便利だとおもったが、issueは対象外だったのでうまくいかず。。。</p></blockquote> <p>GitHub に蓄えられた情報は git リポジトリ以外にも Issues や Pull Requests, Wiki が存在します。</p> <p>LLM にとって、もっとも学習しやすい対象であるテキスト情報の上、過去の経緯を追う上でも重要な情報が含まれています。</p> <p>にも関わらず、Copilot の学習対象外であるため、これまで蓄積してきた情報にもとづく知見を抽出することはできませんでした。</p> <h2 id="近い将来の導入に向けて">近い将来の導入に向けて</h2> <p>上述した通り、残念ながら、現時点ではすぐに効果が得られるようなユースケースは見つかりませんでした。</p> <p>ですが、日進月歩を文字通り体現している LLM の発展を見る限り GitHub Copilot Enterprise を導入する未来は近いとは考えています。</p> <p>したがって、いざ導入するとなったとき、すぐに有効活用できるよう準備は進めておくのがよさそうです。</p> <p>今回は採用を見送ったものの、評価内容を踏まえてコードとドキュメントの二つの観点で、どういった準備をしておくべきかの認識を共有して評価プロジェクトを終了しました。 といっても頑張って準備する類の活動ではなく、頭の片隅においておこう、という程度の対策です。</p> <p>最後にその対策をご紹介して本記事を終えようと思います。</p> <h3 id="コードに意図が伝わるコメントを残す">コードに意図が伝わるコメントを残す</h3> <p>自動生成された Pull Requests のメッセージは修正した内容の要約という what であって、その修正がどういう意図でなされたか、レビューにあたって特に重要な why は含まれていません。</p> <p>もちろん、それはコードに意図や理由にあたる情報がないためであって、Copilot が why を説明するメッセージを生成できないのも当然です。</p> <p>GitHub の Blog でも、Copilot を使う上でのベストプラクティスとして <a href="https://github.blog/2024-03-25-how-to-use-github-copilot-in-your-ide-tips-tricks-and-best-practices/">LLM に context を提供することの重要性を説いている記事</a>が公開されています。</p> <p>ということで、ごく当たり前の結論ではありますが、コード自体に why や why not がわかるコメントをしっかり残すように意識していこう、となりました。</p> <p>奇しくも同時期に行っていた <a href="https://www.amazon.co.jp/Philosophy-Software-Design-2nd/dp/173210221X">A Philosophy of Software Design</a> の輪読会でコメントの重要性についての議論をしていたのも功を奏しました。 コードに意図が伝わるコメントを記述する、というプラクティスが各チームに浸透してきており、今後 LLM に与えられる context が増えていくと見込んでいます。</p> <p>また、副次的な効果として、フロー情報になってしまいがちな PR と異なり、ストック情報と言えるコード上のコメントは認知負荷を軽減してくれています。 普段から触れるコードだけで意図が伝わる状態と比べたとき、なにかしら問題が起きてから git の履歴や関連する PR を追う作業は、不要な課題外在性負荷でしかありません。</p> <p>なお、前段で PR メッセージ生成の評価の中でコメントを追加しても why を含んだ内容は生成されなかった、という評価結果をご紹介しました。 これは、あくまで現時点で未成熟なだけであって、将来に向けての布石としては、コメントを充実させていくことには意味があると捉えています。</p> <p>今後はコメントに加えて、どうすれば LLM フレンドリーなコードになるか、という観点での新しいコードの書き方も確立していくでしょう。引き続き動向を追いながら、新しい種類のコードの品質向上に努めていきたいと思います。</p> <p>ただ、このような新しいプラクティスが浸透するのには時間がかかります。 Copilot の今後の進化で、ブランチと紐付けた Issue や PR に代表される、コードに関わる既存の context をうまく利用してくれるのでは、と個人的には期待しています。</p> <h3 id="ドキュメントの準備はあえて何もしない">ドキュメントの準備はあえて何もしない</h3> <p>近い将来の導入に向けて、今からドキュメントを GitHub に移行していくことも検討しました。 しかし、現時点では、あえて何もしないことを選択しました。</p> <p>GitHub Copilot Enterprise がナレッジマネジメントの分野においても、LLM 時代のスタンダードになるかどうかはまだ判断しきれなかったからです。</p> <p>もちろん、コードそのものに加えて Issue や PR の情報を持っているという強みがあるので、非常に有力な候補であることには疑う余地はないでしょう。</p> <p>ですが、今はどの会社も LLM を自社製品にどう組み込むか最優先で試行錯誤しているのは間違いなく、どの製品が最終的に勝者となるかは未知数です。 Google が後発の検索エンジンであったことを忘れてはなりません。</p> <p>一休では、前述したように多くのチームでドキュメンテーションに Confluence を利用しています。 Atlassian でも <a href="https://www.atlassian.com/ja/blog/atlassian-intelligence-ga">Atlassian Intelligence</a> という AI 拡張機能が提供されはじめています。 Confluence には AI を利用した<a href="https://support.atlassian.com/ja/confluence-cloud/docs/summarize-a-page-or-blog-using-atlassian-intelligence/">要約</a>や<a href="https://support.atlassian.com/ja/confluence-cloud/docs/search-for-answers-using-atlassian-intelligence/">検索</a>、<a href="https://support.atlassian.com/ja/confluence-cloud/docs/define-terms-using-atlassian-intelligence/">社内用語やプロジェクト用語の自動定義</a>などの魅力的な機能が近日中に提供されるようです。</p> <p>GitHub もまた、knowledge bases を単にリポジトリ中の文書を学習するだけに留めず、Copilot の中核となる機能として発展させる施策を進めているのではないかと予想しています。 たとえば Issues や Discussions, Pull Requests など GitHub に蓄えられた他の情報との統合は容易に想像できるところです。</p> <p>加えて、忘れてはいけない観点として、ナレッジマネジメントサービスにとって、既存機能の重要性には変化がないことには触れておきたいと思います。 LLM による新しい検索体験は非常に強力で魅力的なフィーチャーであることは確かです。 しかし、情報の構造化やチームでの同時編集のしやすさ、他サービスとの連携といった、もともとの価値を見失うことがないよう留意していくつもりです。</p> <p>将来、他のナレッジマネジメントサービスを採用することになったとしても、knowledge bases リポジトリの準備でご紹介したようにデータ移行はさほど難しくはありません。加えて、この分野で高いシェアを持つ Confluence からの移行機能が提供されることも期待できます。</p> <p>引き続き動向を注視しながら、あらためて判断することになりそうです。</p> <h2 id="おわりに">おわりに</h2> <p>一休では、よりよい価値を素早くユーザーに提供できるよう、開発生産性の向上にもチャレンジしていただける仲間を募集しています。</p> <p>興味を持っていただけたら、ぜひ一休の<a href="https://www.ikyu.co.jp/recruit/engineer">採用サイト</a>をご覧ください。</p> Mon, 15 Apr 2024 15:02:49 +0900 hatenablog://entry/6801883189097639863 データベースの在庫の持ち方をビットで管理してる話 https://user-first.ikyu.co.jp/entry/2024/03/28/115631 <p>こんにちは、<a href="https://spa.ikyu.com/">一休.comスパ</a>(以下、「スパ」)の開発を担当しているshibataiと申します🙏 今回はスパのデータベースの在庫の持ち方で試行錯誤した話をさせていただきます。</p> <h2 id="背景">背景</h2> <h3 id="2024-03-29追記-一休comスパにおける在庫の特徴について">2024-03-29追記: 一休.comスパにおける在庫の特徴について</h3> <p>一休.comスパが扱う「在庫」は、「ある日付の特定の時間に対する空き枠」です。以降の説明では、スパ施設ごと、日付ごと、また時間ごとに増えていく「在庫」をいかに効率よく扱うかについて説明しています。</p> <p>詳細については次のスレッドも参照してください!</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://t.co/Y0SPmDE4yZ">https://t.co/Y0SPmDE4yZ</a><br><br>この記事のコメントみてると、少し我々のシステムの要件が伝わってないというかそこの説明が記事に不足しているように思った。ので以下その補足</p>&mdash; naoya (@naoya_ito) <a href="https://twitter.com/naoya_ito/status/1773550536735805584?ref_src=twsrc%5Etfw">March 29, 2024</a></blockquote> <p> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <h3 id="現在の実装">現在の実装</h3> <p>スパは予約を受け付けるために在庫の管理をしてます🎁 データベースで在庫テーブルを持っていますが、ベタな管理をしています。 特定の施設・日・在庫の数を00:00をt0000とみなして15分おきにt0000・t0015..t2345まで格納してます🤔 在庫テーブルのイメージは以下です。</p> <table> <thead> <tr> <th> shop_id </th> <th> inventory_id </th> <th> inventory_date </th> <th> t0000 </th> <th> t0015 </th> <th> (省略) </th> <th> t1300 </th> <th> t1315 </th> <th> (省略) </th> <th> t2345 </th> </tr> </thead> <tbody> <tr> <td> 1 </td> <td> 1 </td> <td> 2024-01-01 </td> <td> 0 </td> <td> 0 </td> <td> ... </td> <td> 1 </td> <td> 0 </td> <td> ... </td> <td> 0 </td> </tr> <tr> <td> 1 </td> <td> 2 </td> <td> 2024-01-01 </td> <td> 0 </td> <td> 0 </td> <td> ... </td> <td> 0 </td> <td> 1 </td> <td> ... </td> <td> 0 </td> </tr> </tbody> </table> <p>この設計は在庫の調査時に在庫数を確認しやすいのですが、レコード挿入時にtxxxの形にしたり、描画時にtxxxをtimeに変換する必要があったりと、実際に在庫を含めた描画を行う処理に難ありでした😞 チーム内で相談した結果、検索で描画する際は時間の配列(例: <code>['10:00', '11:15', '12:45']</code>)を圧縮したビットを使うようにしました。</p> <table> <thead> <tr> <th> shop_id </th> <th> inventory_id </th> <th> inventory_date </th> <th> timeBits1 </th> <th> timeBits2 </th> </tr> </thead> <tbody> <tr> <td> 1 </td> <td> 1 </td> <td> 2024-01-01 </td> <td> 1 </td> <td> 0 </td> </tr> <tr> <td> 1 </td> <td> 2 </td> <td> 2024-01-01 </td> <td> 64 </td> <td> 2 </td> </tr> </tbody> </table> <p>具体的な実装は後述しますが、カラムをビットで管理する場合のメリット・デメリットは以下です。</p> <p>【メリット】</p> <ul> <li>あるスパンごとのカラムを大量に持たずにビットの表現で圧縮できるのでデータ容量を抑えることができる</li> <li>動的にカラムを決めるために一般的にオーバーヘッドの大きいと言われるリフレクションを使わなくていいため、ビット値を用いると比較的高速に検索可能</li> <li>施設単位やプラン単位などで在庫有無をサマライズしたい時、ANDやOR検索で柔軟な条件指定が可能</li> </ul> <p>【デメリット】</p> <ul> <li>テーブルをSELECTで検索するだけでは状態がわからない(値を変換しなければならない)ため、デバッグやクエリ構築の難易度が上がる</li> <li>ビット値と時間の配列の間を相互変換するライブラリの用意が必要</li> <li>ビット値はBIGINT型でも桁溢れする場合があるので、Bit1とBit2といったようにある部分で分割する検討が必要</li> </ul> <p>以下からはビット演算の仕組みと、実際にどういうイメージで検索するかを説明します👀</p> <h2 id="ビット演算とは">ビット演算とは?</h2> <p>データをビット列(0 or 1で構成される)とみなして演算します。 メリットは、値に対してANDやOR検索ができることです。 例えば1/2/3をビット列で表した場合、<code>00000001</code>/<code>00000010</code>/<code>00000011</code>です。 1と2でビットOR演算を行うと、</p> <pre class="code" data-lang="" data-unlink> 00000001 OR 00000010 ------------- 00000011</pre> <p>各ビットを縦に見て、少なくとも一方に1がある場合、結果のそのビット位置は1になるので、演算結果は10進数の3です。 実際にSQLServerで検索する際にAND演算を使う例を出すと、</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">CREATE</span> <span class="synSpecial">TABLE</span> Example ( Bits <span class="synType">INT</span> ); <span class="synStatement">INSERT</span> <span class="synSpecial">INTO</span> Example(Bits) <span class="synSpecial">VALUES</span> (<span class="synConstant">3</span>); <span class="synStatement">SELECT</span> * <span class="synSpecial">FROM</span> Example <span class="synSpecial">WHERE</span> Bits &amp; <span class="synConstant">1</span> = <span class="synConstant">1</span>; // Bits列の値と<span class="synConstant">1</span>のビットANDが<span class="synConstant">1</span>に等しい行を選択するのでヒットする <span class="synStatement">SELECT</span> * <span class="synSpecial">FROM</span> Example <span class="synSpecial">WHERE</span> Bits &amp; <span class="synConstant">2</span> = <span class="synConstant">2</span>; // Bits列の値と<span class="synConstant">2</span>のビットANDが<span class="synConstant">2</span>に等しい行を選択するのでヒットする <span class="synStatement">SELECT</span> * <span class="synSpecial">FROM</span> Example <span class="synSpecial">WHERE</span> Bits &amp; <span class="synConstant">4</span> = <span class="synConstant">4</span>; // <span class="synConstant">3</span>(<span class="synConstant">00000011</span>)と<span class="synConstant">4</span>(<span class="synConstant">00000100</span>)はそれぞれに<span class="synConstant">1</span>が立っている位置が違うのでヒットしない </pre> <p>Pythonの代表的なORMであるSQLAlchemyを使う場合は以下のように書けます。</p> <pre class="code lang-python" data-lang="python" data-unlink>query.filter(Example.Bits.op(<span class="synConstant">&quot;&amp;&quot;</span>)(bits1) == bits1) </pre> <h2 id="実装例">実装例</h2> <p>ビット演算で在庫管理するには、たとえば次のように実装します。</p> <ol> <li><code>INSERT INTO Example(Bits) VALUES (n);</code>の nに相当する値を在庫がある時間帯からビットへ変換して格納</li> <li>検索時に時間を<code>query.filter(Example.Bits.op("&amp;")(bits1) == bits1)</code>として検索し、取得できたBitsカラムを時間帯に変換</li> </ol> <p>なので、デメリットでもお伝えしましたとおり、ビット値と時間の配列の間を相互変換するライブラリの用意が必要です。 今回は先人達が実装してくれていたライブラリが社内にあったため、ありがたく使わせていただきました。</p> <h2 id="変換の考え方">変換の考え方</h2> <p>例えば00:00-23:45で15分スパンとしたとき、1日は96区切りです。 <code>10:00 ~ 19:00に在庫が存在する</code>を表現すると以下のようになり、96bitsで時間が有効であれば1が立つと考えることができます👼 要件によっては00:00で終わりではなく、24時以降の表現をしたい場合もあるので、1日の区切り数やスパンをどうするかはプロジェクトの定義によって決めて下さい。</p> <pre class="code" data-lang="" data-unlink> |0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | || | | | | | | | | | | | | | | | | | | | | | | | | &lt;000000000000000000000000000000000000000011111111111111111111111111111111111100000000000000000000&gt;</pre> <p>1に関して、96bits(12bytes)のままではバイトオーダーの都合上扱いづらいので16bytesに変換すると、<code>b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xf0\x00\x00'</code>で、先頭~8bytesまでと9~16bytesまでの値を取得できます。これをbits1とbits2カラムとして格納します。 変換の一部をPythonでの実装してみると以下です。 実際の社内では複数のユースケースに対応できるように、より複雑なことをしてますが、社内のソースコードをそのまま載せられないのでサンプルコードのみです🙏</p> <pre class="code lang-python" data-lang="python" data-unlink>bits = <span class="synConstant">'000000000000000000000000000000000000000011111111111111111111111111111111111100000000000000000000'</span> bytes_array = <span class="synIdentifier">int</span>(bits, <span class="synConstant">2</span>).to_bytes(<span class="synConstant">16</span>, byteorder=<span class="synConstant">'big'</span>) bits_int1 = <span class="synIdentifier">int</span>.from_bytes(bytes_array[<span class="synConstant">0</span>:<span class="synConstant">8</span>], byteorder=<span class="synConstant">&quot;big&quot;</span>, signed=<span class="synIdentifier">True</span>) bits_int2 = <span class="synIdentifier">int</span>.from_bytes(bytes_array[<span class="synConstant">8</span>:<span class="synConstant">16</span>], byteorder=<span class="synConstant">&quot;big&quot;</span>, signed=<span class="synIdentifier">True</span>) <span class="synIdentifier">print</span>(bits_int1) <span class="synComment"># 0</span> <span class="synIdentifier">print</span>(bits_int2) <span class="synComment"># 72057594036879360</span> </pre> <p>2.に関しても逆の処理を行えば良く、検索したい時間をビットに変換し、データベースから時間帯をAND演算で取得。取得できたbits1/bitsをbytesに変換しつなげて、96bitsを復元します。 あとは0と1の状態によって、00:00から15分おきに繰り返しで判定することで時間帯を復元できます🍿 変換の一部をPythonでの実装してみると以下です。</p> <pre class="code lang-python" data-lang="python" data-unlink>bits_pair = (<span class="synConstant">0</span>, <span class="synConstant">72057594036879360</span>) bytes_int1 = bits_pair[<span class="synConstant">0</span>].to_bytes(<span class="synConstant">8</span>, byteorder=<span class="synConstant">&quot;big&quot;</span>, signed=<span class="synIdentifier">True</span>) bytes_int2 = bits_pair[<span class="synConstant">1</span>].to_bytes(<span class="synConstant">8</span>, byteorder=<span class="synConstant">&quot;big&quot;</span>, signed=<span class="synIdentifier">True</span>) reconstructed_bits = <span class="synIdentifier">format</span>(<span class="synIdentifier">int</span>.from_bytes(bytes_int1 + bytes_int2, byteorder=<span class="synConstant">&quot;big&quot;</span>), <span class="synConstant">'096b'</span>) <span class="synIdentifier">print</span>(reconstructed_bits) <span class="synComment"># 000000000000000000000000000000000000000011111111111111111111111111111111111100000000000000000000が復元される</span> </pre> <p>以上が相互変換するイメージでございます。</p> <h2 id="最後に">最後に</h2> <p>時間をビットで持つ実装の他にもチューニングしたため、単体での評価はできていませんが、今回の取り組みを通してスパの検索画面の描画は従来から1/3~1/5程度時間短縮することができました。 よって、ビットでの管理は今回スパの課題の解決手段としてはとても有効だったと考えます。 前述の通りデメリットもありますが、課題の解決手段の一つとして参考になれば幸いです!</p> <hr /> <p>一休では、ともに試行錯誤しながらよいサービスを作ってくれる仲間を募集しています!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施していますので、ぜひお気軽にご連絡ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> Thu, 28 Mar 2024 11:56:31 +0900 hatenablog://entry/6801883189091723652 開発プロセスをインクリメンタルに改善する https://user-first.ikyu.co.jp/entry/2024/03/13/124907 <p>一休.comレストランのエンジニアの<a href="https://twitter.com/kymmt90">kymmt</a>です。</p> <p>2023年度の下半期、一休.comレストランの開発チームでは開発プロセス改善に取り組みました。改善は小さい単位で徐々に進め、バックログの作りかたやカンバンの運用方法を改善することで、フロー効率の向上、開発ペースの把握、チーム内外からの進捗の見える化ができるようになりました。</p> <p>この記事では、このようなインクリメンタルな開発プロセス改善の取り組みについて紹介します。</p> <h2 id="従来の開発プロセス">従来の開発プロセス</h2> <p>主に2023年度前半の開発プロセスは次のような形でした<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>。</p> <ul> <li>プロダクトのリリースに必要なタスクが長いバックログとして存在し、ひたすらタスクを消化</li> <li>その状況に課題を感じ、区切りを入れるために2週間のスプリントを導入</li> </ul> <p>この時点では、スプリントは2週間ごとに状況を確認するためのもので、目標に対するふりかえりや、次のスプリントの計画を作るためのものとしては活用していませんでした。</p> <p>この開発プロセスに起因して、チームメンバーは次のような課題を感じていました。</p> <ul> <li>どの機能に紐づくかが一見してわかりにくい技術的タスクや、やることが曖昧な項目がバックログにある</li> <li>タスクは進んでいるが、ひとまとまりの機能ができるのに時間がかかる</li> <li>開発ペースを見通しにくく、今後の予定についてチーム内外に説明責任を果たしにくい</li> <li>スプリントを導入したものの、スプリント終了時の残項目が完了しなかった理由など、開発のボトルネックを深掘りできていない</li> </ul> <h2 id="改善の方針">改善の方針</h2> <p>先述した課題を受けて、開発プロセスをできるだけ早く改善したいという機運が生まれました。しかし、スクラムなど大きめの方法論をチームに導入するのはこれまで例がなく、ある種の理想的な開発プロセスには近づけますが、効果が出るまでに時間がかかりそうでした。また、著者(kymmt)は入社直後だったので、技術的なキャッチアップと並行してプロセス改善をサポートしたいという状況でした。</p> <p>そこで、アジャイル開発のプラクティスをインクリメンタルに導入してプロセスを改善することにしました。</p> <p>ここで、それらのプラクティスの生まれた理由や避けるべき罠は理解したうえで、課題の解決に必要なものを選択的に導入するという点に気を配りました。最近出た本だと<a href="https://www.shoeisha.co.jp/book/detail/9784798176826">『アジャイルプラクティスガイドブック』</a>は参考になりました。</p> <h2 id="2023年度後半からの開発プロセス">2023年度後半からの開発プロセス</h2> <p>上記の方針に基づいて、2023年度下半期からは、チームで次のような改善活動に取り組みました。</p> <ul> <li>顧客価値に直結する開発はユーザーストーリーとして項目を整理し、その下で技術的タスクを分解/整理する</li> <li>カンバン上でユーザーストーリーを左から右に流すようにして、顧客価値がどの程度生み出せているか、ボトルネックはどこかを見える化する</li> <li>ユーザーストーリーに対する規模の見積もりとベロシティの計測を繰り返し、開発の見通しを立てられるようにする</li> </ul> <p>これらの活動はある小規模なプロジェクトから始めて、次にもう1つの中規模なプロジェクトに横展開することで、徐々にチーム全体に活動範囲を広げました。</p> <h2 id="導入の様子">導入の様子</h2> <h3 id="小規模の開発プロジェクトへの導入">小規模の開発プロジェクトへの導入</h3> <p>すでに述べたとおり、2週間ごとに期間を区切るという枠組みだけ導入されていました。今回はそれを足がかりに、まずは小さい規模の開発プロジェクト(強いていうならエピック)に対してプラクティスを導入していきました。</p> <p>まず、事前にユーザーストーリーとして開発項目を改めて明らかにしつつ整理し直しました。そして、それらに優先度をつけてバックログ上で並び替えました。あくまでも例ですが、次のようなイメージです。</p> <table> <thead> <tr> <th style="text-align:left;">名前</th> <th style="text-align:left;">優先度</th> </tr> </thead> <tbody> <tr> <td style="text-align:left;">ユーザーが関連するレストランの一覧を閲覧できる</td> <td style="text-align:left;">高</td> </tr> <tr> <td style="text-align:left;">ユーザーが人気のレストランの一覧を閲覧できる</td> <td style="text-align:left;">中</td> </tr> <tr> <td style="text-align:left;">ユーザーが近隣のスポットに基づくレストランの一覧を閲覧できる</td> <td style="text-align:left;">低</td> </tr> </tbody> </table> <p>(ここでは一休.comレストランの利用者のことを「ユーザー」と呼んでいます)</p> <p>そのうえで、項目の規模を相対見積もりしました。ストーリーに必要な技術的タスクについて認識を合わせながら、それぞれの項目の相対的な規模を比較します。現在に至るまで、フィボナッチ数列に基づくストーリーポイント(1, 2, 3, 5, 8)を使っています。ここでは、プロジェクトに携わる3人ほどで、規模の感覚を揃えて見積もりをしました。古典ですが<a href="https://book.mynavi.jp/ec/products/detail/id=22141">『アジャイルな見積りと計画づくり』</a>もあらためて参考にしました。</p> <p>これらの項目を左から右に「To Do」、「In Progress」、「In Review」、「Done」のレーンを持つカンバンで管理します。これまでベロシティを計測したことがなかったので、見積もり実施後の初回スプリントでは、優先度に基づいてバックログの項目を「To Do」に並べ、優先度が高いものから取り組みました。また、できるだけ複数ストーリーを取らない(マルチタスクにならない)ように進めました<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>。</p> <p>この時点でバックログの項目が整理された状態でカンバン上に現れ、関係者から見て進捗がわかりやすくなりました。また、スプリントを繰り返すなかで、カンバン上にあるストーリーを左から右に流すために複数人で手分けするような動きもできるようになりました。この点が効いて、目標期日をきつめにとっていましたがプロジェクトの作業を完了できました。</p> <p>一方で、一部の開発プロジェクトだけに改善を適用していたので、チーム全体の開発ペースの計測ができていませんでした。これについては、次の中規模の開発プロジェクトであらためて進めました。</p> <h3 id="ツールの適切な運用">ツールの適切な運用</h3> <p>カンバン導入と前後して、コードベースとプロジェクト管理の距離が近いほうがチームの好みに合っていたので、従来Jiraを使っていたところをGitHub Projectsに移行し、これまで述べた運用に沿うようにカンバンや項目のメタデータを整備しました。また、チームで合意した運用方法はドキュメントとして明文化しました。</p> <p>GitHub Projectsの効果的な利用方法については、以前このブログでitinaoが紹介しているのでぜひご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F11%2F09%2F175121" title="GitHub Projects を利用したタスク管理 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/11/09/175121">user-first.ikyu.co.jp</a></cite></p> <p>できるだけ業務に支障がないように、Jiraにあったデータも移行しました。こういう移行はやり切るのが大事なので、GitHub APIを利用して必要なデータを極力自動でGitHub側にインポートしました。</p> <p><figure class="figure-image figure-image-fotolife" title="一休.comレストラン開発チームのカンバン"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kymmt90/20240308/20240308150355.png" alt="&#x4E00;&#x4F11;.com&#x30EC;&#x30B9;&#x30C8;&#x30E9;&#x30F3;&#x958B;&#x767A;&#x30C1;&#x30FC;&#x30E0;&#x306E;&#x30AB;&#x30F3;&#x30D0;&#x30F3;" width="1200" height="429" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>一休.comレストラン開発チームのカンバン</figcaption></figure></p> <p>項目間の依存関係を示しづらいなどの課題感もありますが、現在はおおむね現状を把握しやすいカンバンを運用できています。</p> <h3 id="中規模の開発プロジェクトへの導入">中規模の開発プロジェクトへの導入</h3> <p>前述のとおり、ある程度プラクティスの導入による効果が出てきたので、著者(kymmt)が直接担当しているわけではない別の中規模プロジェクトについても導入してみました。</p> <p>このフェイズでは、メンバー全員がプラクティスを実践できるように、プロジェクトを進めるメンバーと一緒にストーリーの単位で項目を整理し直し、方法のコツなどを共有しました。さらに、それらの相対規模の見積もりも一緒にやることで、規模に対する感覚をチーム全体で揃えていきました。</p> <p>もとは「状態管理追加」、「UI実装」のような技術的タスクの単位で項目が並べられていましたが、項目間の依存関係やまとまりを顧客価値として整理することで、何が実現できるか明確になりました。また、カンバン上でユーザーストーリーの粒度で左から右に1つずつ開発項目を流せるようになりました。チームメンバーからも作業が進めやすくなり、1つ1つのユーザーストーリーのリードタイムが向上したという声をもらいました。</p> <p>加えて、見積もりされたバックログ項目に取り組む中で、チーム全体のベロシティも安定して見えるようになってきたので、今後の開発の見通しを立てやすくなりました。</p> <p><figure class="figure-image figure-image-fotolife" title="一休.comレストラン開発チームのベロシティ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kymmt90/20240308/20240308150505.png" alt="&#x4E00;&#x4F11;.com&#x30EC;&#x30B9;&#x30C8;&#x30E9;&#x30F3;&#x958B;&#x767A;&#x30C1;&#x30FC;&#x30E0;&#x306E;&#x30D9;&#x30ED;&#x30B7;&#x30C6;&#x30A3;" width="1200" height="829" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>一休.comレストラン開発チームのベロシティ</figcaption></figure></p> <h3 id="スプリント開始時にチームで計画づくり">スプリント開始時にチームで計画づくり</h3> <p>以前は前のスプリントの残項目をそのまま次スプリントに移す<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup>というプロセスでしたが、現在はビジネスの状況やすべきことの優先度、またチームのベロシティも都度確認して、目標を決めてバックログを作っています。</p> <p>結果的に前スプリントで残った分も次のスプリントでやりましょうになることはあるのですが、なにも考えずに移すのではなく議論をしたうえで必要なら移すというプロセスを経るようにしています。</p> <h2 id="結果">結果</h2> <p>2023年度下半期に次のような開発プロセス改善活動をおこないました。</p> <ul> <li>顧客価値に直結する開発をユーザーストーリーとして項目を整理</li> <li>カンバン上で顧客価値につながる開発の進捗やボトルネックを見える化</li> <li>ユーザーストーリーに対する規模の見積もりとベロシティの計測で開発ペースを見える化</li> <li>スプリントの計画づくりで目標を定め、そのために必要なバックログを作る</li> </ul> <p>もともと技術的にしっかりしたチームだったので、これらの改善活動の結果でフロー効率をよくすることで、以前よりリードタイムの向上や安定が見られるようになりました。</p> <p>また、ストーリーに基づいた開発項目の見える化によって進捗がチーム内外からわかりやすくなり、デモやレポーティングなど組織運営に必要な業務も進めやすくなりました。先の計画を立てやすく、予定変更にも柔軟に対応できるようになってきています。</p> <p>他には、計画づくりに意識的に取り組むようになったので、ずるずると開発してしまうことが減りました。ビジネスの推進に必要なことがなにかを都度確認しながら開発を進められています。</p> <h2 id="これから">これから</h2> <p>すでに始めている取り組みとして、継続的に各チームメンバーがプロセス改善できるように、開発プロセスに関する知識をインプットする読書会を週次で開催しています。先日<a href="https://www.oreilly.co.jp/books/9784873117645/">『カンバン仕事術』</a>を読み終えたところです。</p> <p>課題としては、技術的に専門性のあるメンバーに下周りの整備のようなタスクが集中したり、緊急の差し込みタスクをシステムに詳しいメンバーが多めに取りがちだったりと、メンバー間のスキルの差によってWIPが多くなったりすることもあります。こういうときにタスクを取捨選択したり、メンバー間で知識を共有していく方法については、既存のプラクティスも参照しながら継続的にチームで考えていくつもりです。</p> <hr /> <p>一休では、ともに良いサービスをつくっていく仲間を募集中です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 著者(kymmt)は入社前〜入社直後なので聞いた話も含みます<a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> WIP制限に基づく方針ですが、このとき数値はとくに指定していませんでした<a href="#fnref:2" rev="footnote">&#8617;</a></li> <li id="fn:3"> Jiraの機能でそうなっていたというのもあります<a href="#fnref:3" rev="footnote">&#8617;</a></li> </ol> </div> Wed, 13 Mar 2024 12:49:07 +0900 hatenablog://entry/6801883189088581141 一休レストランのふつうのRustバックエンド開発 https://user-first.ikyu.co.jp/entry/2023/12/25/132215 <p>この記事は<a href="https://qiita.com/advent-calendar/2023/ikyu">一休.com Advent Calendar 2023</a> 25日目の記事です。</p> <p>一休レストランでは、よりスムーズな予約体験の提供を目的とするシステムのリニューアルを進めています。その一環として、2023年10月から、レストラン個別ページの表示から予約までのスマートフォンビューにおいて、バックエンドのサーバをRustで書かれたものに置き換えました。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">一休レストランの Rust バックエンドが正式リリースされました。<a href="https://t.co/7N4VGv5ej9">https://t.co/7N4VGv5ej9</a> このページのスマートフォンビューはバックエンドが Rust で書かれた GraphQL になってます</p>&mdash; naoya (@naoya_ito) <a href="https://twitter.com/naoya_ito/status/1709507132209680702?ref_src=twsrc%5Etfw">October 4, 2023</a></blockquote> <p> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <p>本番運用が始まって3か月近く経ちましたが、これまで安定して継続的な開発と運用ができています。これはRustだからと構えることなく、「ふつう」のバックエンド開発を心がけてきたからだと考えています。</p> <p>Advent Calendar 2023最終日は、一休レストランの開発チーム一同から、一休レストランのRustバックエンド開発の様子をお届けします。</p> <ul class="table-of-contents"> <li><a href="#Rustを選定した理由">Rustを選定した理由</a></li> <li><a href="#現在のバックエンドのユースケース">現在のバックエンドのユースケース</a><ul> <li><a href="#レストラン情報の取得">レストラン情報の取得</a></li> <li><a href="#予約の確保">予約の確保</a></li> </ul> </li> <li><a href="#現在のアーキテクチャ">現在のアーキテクチャ</a></li> <li><a href="#各モジュールの紹介">各モジュールの紹介</a><ul> <li><a href="#ドメインモデル">ドメインモデル</a></li> <li><a href="#データアクセス層">データアクセス層</a></li> <li><a href="#GraphQLとHTTPサーバ">GraphQLとHTTPサーバ</a></li> <li><a href="#ライブラリ">ライブラリ</a></li> </ul> </li> <li><a href="#Rustによる開発のふりかえり">Rustによる開発のふりかえり</a><ul> <li><a href="#よかったこと">よかったこと</a><ul> <li><a href="#Rustはビジネスロジックを書くのにも便利">Rustはビジネスロジックを書くのにも便利</a></li> <li><a href="#アプリケーションの各層で型安全にデータを変換">アプリケーションの各層で型安全にデータを変換</a></li> <li><a href="#Cargo-workspaceを活用した開発">Cargo workspaceを活用した開発</a></li> <li><a href="#パフォーマンスの向上">パフォーマンスの向上</a></li> </ul> </li> <li><a href="#もっとよくなると嬉しいこと">もっとよくなると嬉しいこと</a><ul> <li><a href="#エコシステムのさらなる成熟">エコシステムのさらなる成熟</a></li> </ul> </li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="Rustを選定した理由">Rustを選定した理由</h2> <p>一休レストランのリニューアル計画が始まったころ、一休では宿泊予約サービスや社内の基盤サービスを中心としてGoが標準的なバックエンドの技術スタックでした。</p> <p>一休レストランの開発でも、宿泊予約サービスでの経験があるメンバーのスキルセットに基づいてGoを使うこともできました。その一方で、この方針だと社内の技術ポートフォリオがGoに偏ってしまうという懸念もありました。</p> <p>一休では、社内で蓄積する技術的知見に多様性を持たせ、結果として状況に応じて最適な技術選定ができるように、複数のプログラミング言語を使うことを意図的に選択しています。</p> <p><figure class="figure-image figure-image-fotolife" title="一休の技術選定の方針について"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kymmt90/20231222/20231222151615.jpg" width="1200" height="780" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a href="https://speakerdeck.com/kensuketanaka/introduce-ikyu?slide=25">&#x682A;&#x5F0F;&#x4F1A;&#x793E;&#x4E00;&#x4F11; &#x4F1A;&#x793E;&#x7D39;&#x4ECB;&#x8CC7;&#x6599; / introduce-ikyu - Speaker Deck</a> より一休の技術選定の方針について</figcaption></figure></p> <p>そこで、チームメンバーの中にRustに詳しいエンジニアがいたことも助けになり、Rustをバックエンドの言語として採用するかどうかを検討しました。</p> <p>Rustの採用による狙いは次のとおりです。</p> <ul> <li>まず置き換えたい参照系処理のCPU利用効率を上げて、高速なバックエンドサーバとする</li> <li>今後のさらなる開発を見据え、メモリ安全、型安全な開発体験を実現する</li> <li>技術的知見の多様性という点で、関数型のメンタルモデルでプログラミングできるエンジニアを増やす</li> </ul> <p>同時に、Rustの採用に対する次のような懸念も上がりました。</p> <ul> <li>初めて使うエンジニアにとっては学習に時間がかかる</li> <li>ライブラリの自作が必要となるケースもありそう</li> </ul> <p>Rustは<a href="https://www.rust-lang.org/learn">公式ドキュメント</a>や<a href="https://docs.rs/">docs.rs</a>のリファレンスなどでドキュメントが充実しているので、学習曲線は急ではあるものの、学習自体は進めやすいと判断しました。</p> <p>ライブラリについては、Rustから一休の基幹DBであるSQL Serverにどうやって接続するかという技術的な検証が必要でした。最終的には、Prismaが公開している<a href="https://github.com/prisma/tiberius">Tiberius</a>というSQL Server用のDBドライバをベースとして、ある程度アプリケーションから使いやすいインタフェースのライブラリを整備することで開発が進められると判断できました。</p> <p>これらの議論や調査に基づいて、一休レストランのバックエンドでRustを採用することになりました。</p> <p>現在、一休レストランのバックエンドを開発するエンジニアは3人います。そのうち2人は、一休レストランの開発をきっかけに、はじめてRustを本格的に利用し始めました。豊富な学習リソースやRustに詳しいメンバーのヘルプを通じて、プロジェクト開始前の学習では<code>String</code>と<code>&amp;str</code>の違いを理解するところから始めたメンバーも、プロジェクト開始後はスムーズに開発できるようになりました。</p> <h2 id="現在のバックエンドのユースケース">現在のバックエンドのユースケース</h2> <p>ここからはRustでバックエンドを「ふつう」に開発するための、設計や実装における面白いポイントを紹介していきます。</p> <p>現在は主に次のユースケースでバックエンドを利用しています。</p> <h3 id="レストラン情報の取得">レストラン情報の取得</h3> <p>店舗情報や予約可能時間など、レストランの情報をお客様に提供するための情報を取得します。機能はGraphQLのクエリとして提供しています。</p> <p>今回はレストラン個別のページの表示から予約までのフローの置き換えを開発スコープとしたので、現在はこのユースケースが大半を占めています。後述のとおりコードベース上もデータの読み出しに関するコードが多いです。</p> <h3 id="予約の確保">予約の確保</h3> <p>お客様から入力いただいた情報をもとに予約を確保するエンドポイントをGraphQLのミューテーションとして提供しています。また、実際の予約処理は、予約処理モジュールを持つ既存の社内別サービスに委譲しています。</p> <h2 id="現在のアーキテクチャ">現在のアーキテクチャ</h2> <p>現在、アプリケーションのアーキテクチャとしてコマンドクエリ責務分離(CQRS)に基づいた構造を採用しています。つまり、データを読み出すだけのクエリと、データの作成や更新をするコマンドで、利用するモデルを分離する方式をとっています。</p> <p>また、たとえばクエリの場合、DBとSolrそれぞれについてデータアクセス層を設け、GraphQLのデータローダーのようなシステムの界面に近い層からは、データアクセス層を通じてクエリモデルの形式でデータを取得します。</p> <p><figure class="figure-image figure-image-fotolife" title="モジュールとその依存関係"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20231225/20231225120420.png" width="575" height="601" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>モジュールとその依存関係</figcaption></figure></p> <p>これらのモジュールはCargo workspaceを用いて管理しています。この点についてはあとで詳しく説明します。</p> <h2 id="各モジュールの紹介">各モジュールの紹介</h2> <p>上述した図における各層を構成するモジュールについて紹介します。</p> <h3 id="ドメインモデル">ドメインモデル</h3> <p>CQRSにおけるクエリとコマンドで利用するモデルを実装している層です。ドメインモデルは他のどのモジュールにも依存しません。また、クエリとコマンドは別モジュールとするためにcrateを分けています。</p> <p>クエリモデルの例としては、レストラン詳細画面で表示する店舗情報があります。これらのデータは実際は複数のテーブルに存在しますが、クエリモデルはそのような実装詳細には依存せず、クエリの結果としてほしい構造を定義しています。実際には、SQL ServerもしくはSolrから得たデータをクエリモデルに変換して利用します。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, </span><span class="synType">Clone</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Restaurant</span> { <span class="synStatement">pub</span> id: RestaurantId, <span class="synStatement">pub</span> name: <span class="synType">String</span>, <span class="synStatement">pub</span> description: <span class="synType">Option</span><span class="synStatement">&lt;</span><span class="synType">String</span><span class="synStatement">&gt;</span>, <span class="synComment">// ...</span> } </pre> <p>コマンドモデルの例としてはお気に入り店舗登録用のコマンドモデルなどが存在します。こちらはまだ数が少ないので割愛します。</p> <h3 id="データアクセス層">データアクセス層</h3> <p>実際のデータを取得するためのロジックを実装している層です。現在は、一休の基幹DBであるSQL Serverや、検索サーバであるSolrからデータを取得しています。このデータアクセス層の利用者に対して、取得したデータをもとにモデルのインスタンスを返します。つまり、ドメインモデルに依存します。</p> <p>クエリを実行するときは、<a href="https://github.com/serde-rs/serde">Serde</a>や<a href="https://docs.rs/serde_with/latest/serde_with/">serde_with</a>を利用して、データストアから取得した生データをDTOにデシリアライズします。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">mod</span> <span class="synIdentifier">dto</span> { <span class="synComment">// ...</span> <span class="synPreProc">#[serde_with::serde_as]</span> <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, serde::Deserialize)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Restaurant</span> { <span class="synPreProc">#[serde(rename = </span><span class="synConstant">&quot;restaurant_id&quot;</span><span class="synPreProc">)]</span> <span class="synPreProc">#[serde_as(as = </span><span class="synConstant">&quot;serde_with::TryFromInto&lt;i32&gt;&quot;</span><span class="synPreProc">)]</span> id: RestaurantId, <span class="synPreProc">#[serde(rename = </span><span class="synConstant">&quot;restaurant_name&quot;</span><span class="synPreProc">)]</span> name: <span class="synType">String</span>, <span class="synComment">// ...</span> } } </pre> <p>さらに、このDTOからクエリモデルに変換するために<code>std::convert</code>の<code>From</code>トレイトや<code>TryFrom</code>トレイトを活用しています。詳しくは後述します。</p> <h3 id="GraphQLとHTTPサーバ">GraphQLとHTTPサーバ</h3> <p>バックエンドはGraphQLを通じてフロントエンドにクエリとミューテーションを提供しています。このGraphQL APIの実装にはasync-graphqlを利用しています。async-graphqlはコードファーストでGraphQLスキーマを定義できるcrateです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fasync-graphql%2Fasync-graphql" title="GitHub - async-graphql/async-graphql: A GraphQL server library implemented in Rust" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/async-graphql/async-graphql">github.com</a></cite></p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// Restaurant {</span> <span class="synComment">// name</span> <span class="synComment">// }</span> <span class="synComment">// のようなスキーマをコードで定義</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Restaurant</span>(<span class="synStatement">pub</span> <span class="synPreProc">query_model</span><span class="synSpecial">::</span>Restaurant); <span class="synPreProc">#[async_graphql::Object]</span> <span class="synStatement">impl</span> Restaurant { async <span class="synStatement">fn</span> <span class="synIdentifier">name</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">&amp;str</span> { <span class="synType">&amp;</span><span class="synConstant">self</span>.<span class="synConstant">0</span>.name } <span class="synComment">// ...</span> } </pre> <p>また、HTTPサーバとしてはAxumを利用しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftokio-rs%2Faxum" title="GitHub - tokio-rs/axum: Ergonomic and modular web framework built with Tokio, Tower, and Hyper" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tokio-rs/axum">github.com</a></cite></p> <p>これまではGraphQLなのでエンドポイント1つで済んでいましたが、最近は社内の他サービスと通信するためにインターナルなREST APIを作る機会も増えてきています。</p> <h3 id="ライブラリ">ライブラリ</h3> <p>アプリケーションを構成するモジュールとは別に、独立したロジックをまとめたライブラリとしてのcrateもいくつか作成してworkspaceに含めています。これらのライブラリは他モジュールから利用されます。</p> <p>たとえば、先述したTiberiusをベースにしたDBドライバや社内サービスのクライアント、他にはログなどの横断的関心事を扱うライブラリが存在します。</p> <h2 id="Rustによる開発のふりかえり">Rustによる開発のふりかえり</h2> <h3 id="よかったこと">よかったこと</h3> <h4 id="Rustはビジネスロジックを書くのにも便利">Rustはビジネスロジックを書くのにも便利</h4> <p>Rustの言語機能として、所有権やライフタイムのようにメモリ安全性を意識したものがよく注目されます。さらに、Webアプリケーションバックエンドを書くうえでは、<a href="https://doc.rust-lang.org/std/option/index.html"><code>Option</code></a>や<a href="https://doc.rust-lang.org/std/result/index.html"><code>Result</code></a>に代表される関数型言語のエッセンスを取り込んだ機能や、データ変換にまつわる機能も非常に便利だとあらためて感じました。</p> <p>一休レストランは15年以上の歴史があるサービスです。このようなサービスは、しばしば歴史的事情からなるデータ構造やコードを多く持っています。たとえば有効な値とnullの両方が存在しうるカラムを扱うこともあります。このときに<code>Option</code>を利用することで、ビジネスロジック上でnullにまつわるバグを避け、match式やif let式によって値がないケースをつねに考慮できます。</p> <p>また、Webアプリケーションは無効な値を入力されたり外部のサービスとの通信に失敗するなど、つねにロジックが失敗する可能性があります。そのようなロジックでは返り値として<code>Result</code><sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>を使うことで、確実にエラーをハンドリングできます。また、<code>?</code>演算子を利用することで、コードを簡潔に保ちつつエラーハンドリングできるのも便利な点です。</p> <p>他には、一休レストランだと予約可能な時間や食事コースの検索結果などでコレクションを操作する場面が数多くあります。このようなときに、イテレータと<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map"><code>map</code></a>や<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter"><code>filter</code></a>のようなイテレータアダプタを利用することで、コレクションにまつわるビジネスロジックを簡潔に書けるのもよい点だと感じています。</p> <h4 id="アプリケーションの各層で型安全にデータを変換">アプリケーションの各層で型安全にデータを変換</h4> <p>先述したように、このアプリケーションでは複数のモジュールで責務を分けています。よって、そのままではデータアクセス層でデータストアから取得した生のデータをDTOを経由してクエリモデルに変換するロジックを書く必要が出てきます。</p> <p>ここで、<a href="https://doc.rust-lang.org/std/convert/trait.From.html"><code>From</code></a>トレイトや<a href="https://doc.rust-lang.org/std/convert/trait.TryFrom.html"><code>TryFrom</code></a>トレイトを用いて型安全なデータの変換を実装することで、層の間で安全にデータを受け渡しできます。たとえばDTOをクエリモデルに変換するために<code>From</code>トレイトや<code>TryFrom</code>トレイトをDTOに対して実装し、適切にモデルへ変換できるようにしています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl</span> <span class="synType">From</span><span class="synStatement">&lt;</span><span class="synPreProc">dto</span><span class="synSpecial">::</span>Restaurant<span class="synStatement">&gt;</span> <span class="synStatement">for</span> <span class="synPreProc">query_model</span><span class="synSpecial">::</span>Restaurant { <span class="synStatement">fn</span> <span class="synIdentifier">from</span>(d: <span class="synPreProc">dto</span><span class="synSpecial">::</span>Restaurant) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synPreProc">query_model</span><span class="synSpecial">::</span>Restaurant { id: d.id, name: d.name, <span class="synComment">// ...</span> } } } </pre> <p>このようにモデルに対して変換のためのトレイトを実装しておけば、あとは<code>from</code>/<code>try_from</code>や<a href="https://doc.rust-lang.org/std/convert/trait.Into.html#tymethod.into"><code>into</code></a>/<a href="https://doc.rust-lang.org/std/convert/trait.TryInto.html#tymethod.try_into"><code>try_into</code></a>を使うだけで層の間の型安全なデータ変換が可能になります。</p> <h4 id="Cargo-workspaceを活用した開発">Cargo workspaceを活用した開発</h4> <p>Cargo workspaceを活用してモジュール間の依存関係を制御しながら開発できているのもよい点です。</p> <p>リポジトリのルートディレクトリにあるCargo.tomlでは、workspaceのmembersとしてアプリケーション内の各モジュールを指定しています。そして、それらのモジュールをcrateとして実装し、各crateのCargo.tomlではアーキテクチャを意識して他のcrateへの依存関係を設定することで、意図しない依存はコンパイラによってエラーにできる構造にしています。</p> <pre class="code toml" data-lang="toml" data-unlink># ルートディレクトリのCargo.toml [workspace] resolver = &#34;2&#34; members = [ &#34;backend/*&#34;, ] # データアクセス層のCargo.toml [package] name = &#34;backend-data-access&#34; version.workspace = true authors.workspace = true edition.workspace = true publish.workspace = true [dependencies] backend-query-model = { workspace = true }</pre> <p>また、モジュールをcrateに分離したことで、コードを変更したときに、変更のあったcrateとそのcrateに依存するcrateだけを再ビルドすればよくなりました。結果として、毎回アプリケーション全体をビルドせずに済み、開発時のビルド時間の短縮にも貢献しています。</p> <h4 id="パフォーマンスの向上">パフォーマンスの向上</h4> <p>もちろんパフォーマンスの向上も当初の狙いどおり達成できた点であり、よかったことの1つです。</p> <p>バックエンドはGoogle Cloud Runで運用しています。現在は年末年始でレストラン予約が非常に増える時期ですが、ピーク時でも3台程度のインスタンスでリクエストを受けることができています。</p> <p>また、一休レストランのバックエンドの一部をRustに移行したことで、従来のPythonのバックエンドにおけるKubernetes DeploymentのReplicaSet数を次のように60程度から40程度に減らすことができました。</p> <p><figure class="figure-image figure-image-fotolife" title="Wed 4以降はPythonバックエンドの負荷をオフロードできた"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/ikyu_com/20231224/20231224133930.png" width="697" height="703" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Wed 4以降はPythonバックエンドの負荷をオフロードできた</figcaption></figure></p> <p>他には、バックエンドの高速化にともなってサービス全体の構成を最適化することで、一休レストラン全体のパフォーマンスが向上しました。こちらについてはチームメンバーのkozaiyが次の記事に詳しく書いたのでご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F12%2F06%2F173215" title="Solr クエリを速度改善したら Solr 全体のパフォーマンスが向上した - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/12/06/173215">user-first.ikyu.co.jp</a></cite></p> <h3 id="もっとよくなると嬉しいこと">もっとよくなると嬉しいこと</h3> <h4 id="エコシステムのさらなる成熟">エコシステムのさらなる成熟</h4> <p>Webアプリケーションバックエンドを開発するうえで、さらにプラットフォームのRust対応が拡充されると開発が楽になりそうです。</p> <p>たとえば、現在はCloud Runを使っているので、APMとしてCloud Traceを利用することにしました。しかし、公式にはRustのSDKが提供されていないことから、独自のライブラリを開発することで対応しています。</p> <h2 id="まとめ">まとめ</h2> <p>この記事では、一休レストランにおいてRustを採用した理由と、Rustによる「ふつう」のWebアプリケーションバックエンド開発の様子について紹介しました。</p> <p>Rustを採用したことで、期待どおり性能面で大きなメリットを得ることができました。また、RustやCargoの機能を適切に活用することで、生産性を保ちつつ今後の継続性も考慮した設計で開発を進めることができています。</p> <p>新たにRustを利用し始めたチームメンバーからは、Rustに対する感想として</p> <blockquote><ul> <li>自分自身にプログラミングを教えてくれる言語だなと思いました</li> <li>プログラミングする上で、気にすべきポイントを気にさせてくれる言語</li> </ul> </blockquote> <p>という声もあがっています。</p> <p>今後のバックエンドの展望としては、よりよい予約体験の提供やレガシーシステムの改善を目的として、</p> <ul> <li>高速なレスポンスが求められるレストラン検索</li> <li>レストラン予約のロジックなどのレガシーかつコアドメインであるモジュール</li> </ul> <p>についてもRustで置き換えていく予定です。このような箇所では、高いパフォーマンスや型に守られた開発体験を提供してくれるRustを活かすことができるだろうと考えています。</p> <p>このような技術的なチャレンジができる一休レストランのバックエンド開発に興味があるかたは、ぜひカジュアル面談応募ページや求人ページからご連絡ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 一休レストランでは<a href="https://docs.rs/anyhow/latest/anyhow/type.Result.html"><code>anyhow::Result</code></a>を利用しています<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> Mon, 25 Dec 2023 13:22:15 +0900 hatenablog://entry/6801883189068906224 開発ディレクターの進化と挑戦 https://user-first.ikyu.co.jp/entry/2023/12/24/090902 <p>この記事は <a href="https://qiita.com/advent-calendar/2023/ikyu">一休.com Advent Calendar 2023</a> 24日目の記事です。</p> <hr /> <p>宿泊プロダクト開発で開発ディレクターをしています、橋本と申します。<br/> ついにクリスマスイブ。残すところこの記事を含めて2つとなりました。<br/> 本日の記事では開発ディレクター1年目の奮闘劇を皆さんに紹介したいと思います。<br/> 同じディレクターの方はもちろん、何か新しいことに挑戦している皆さんに届くと嬉しいです。</p> <h1 id="簡単に経歴紹介">簡単に経歴紹介</h1> <p>新卒でNWインフラの会社に入社し、 エンジニアとして法人顧客のサービス導入をサポートをしてきました。 AWSの運用、セキュリティ商材の導入、NW機器の導入運用、スマホ管理サービス導入など様々な分野を担当し、直近では技術営業として提案メインでの活動に従事していました。</p> <p>5年目になったころ、サービスの導入ではなく、サービスを作ることに興味を持ち、プロダクト開発という新しい分野にチャレンジすることを決めました。<br/> そこから社内の制度を活用し、現在は一休にお世話になっています。</p> <h1 id="奮闘劇">奮闘劇</h1> <h2 id="インプット多量死をなんとか免れた序盤">インプット多量死をなんとか免れた序盤</h2> <p>入社前に開発ディレクターとはなんぞやということですごく簡単な資格だけ取りました。<br/> <a href="https://webken.jp/direction/">Web&#x30C7;&#x30A3;&#x30EC;&#x30AF;&#x30B7;&#x30E7;&#x30F3; | Web&#x691C;&#x5B9A;&#xFF08;&#x30A6;&#x30A7;&#x30D6;&#x30B1;&#x30F3;&#xFF09;</a><br/> 一般的なWeb業界の用語がメインで、実際に何をする役割なのかはふわっとだけ学びました。ただ、実際に入社してみると、言葉通り「右も左もわからない」状況でした。</p> <p>社内で使われているツールで触ったことがあったのがGmailとSlackだけで、進め方以前に使い方がわからない。。<br/> 業務フローについて説明を受けるも、表面的なところだけ分かった気になってしまう。。<br/> SQLも書けないのでデータ抽出を頼まれても時間がかかる。。<br/> 操作方法について聞くも1回では理解できないので、録画をして後で自分でコンフルにまとめる日々。。<br/> なによりアウトプットが何もできない状態でした。</p> <p>そこで自分が意識したのは、</p> <ul> <li>わからないことはわからないままにしない</li> <li>一度教えてもらったことは、次回からは一人でできるようになる</li> </ul> <p>とにかく周りに迷惑をかけないように、渡されたタスクはミスなくこなせるようにすることを日々考えていました。<br/> まずは仕事に慣れること、一人分の仕事ができるようになることを目指してがむしゃらに取り組む日々でした。</p> <p>とにかくインプット量が多くて整理しきれなくなりそうになるのをなんとか踏ん張った2カ月。<br/> 2カ月目で起きたのが常に追いかけまわしていたディレクターの先輩が産休に入られるという出来事。。。</p> <h2 id="必死に犬掻きをする中盤">必死に犬掻きをする中盤</h2> <p>産休に入られた先輩から複数プロジェクトのディレクションを引き継ぎました。<br/> 正直、やってやる!という気持ちと、自分が主体になることでプロジェクトが失敗するのではないかという不安で、精神的には余裕のない状態でした。<br/> 実際に業務に取り掛かると、<br/> 引き継いだ業務をうまく進めようと意気込むが、頑張りどころと向かう先がイマイチ合っておらず、日々犬掻き状態。。<br/> チームからはディレクションとしての役割を求められるが調整業務にも何日も時間をかけてしまう状態。。</p> <p>今振り返るとこんな状態でした。</p> <ul> <li>進め方や要件、仕様について各所と調整をしているつもりが、状況や要望を聞いてきて持って帰るだけの伝書鳩になっていた</li> <li>ユースケースを複数考慮できず出戻りが発生することが多かった</li> <li>自分がやるべき最低限タスクができていないのに、改善や新規の提案など背伸びをして何か価値を出そうと空回りしていた</li> </ul> <p>振り返るとなかなか恥ずかしいですね、、</p> <p>この状況を打破すべく意識したことは</p> <ul> <li>取り組む前に進もうとしている方向の認識合わせを行う</li> <li>悩むポイントはこまめに壁打ちを行う</li> </ul> <p>チームマネージャーに週1回、プロジェクトの進め方やチームのコンディションについて会話をする時間をいただきました。 これがとても大きかった…!<br/> この時に必ず自分の考えをもって臨み、ギャップを埋めていくことに努めた結果、<br/> 敷いてもらったレール上を進めることはできるようになってきました。</p> <p>では次は自ら動けるようにならねば。。。</p> <h2 id="自分の役割が何となくわかってきた今">自分の役割が何となくわかってきた今</h2> <p>複数のプロジェクトを経験することで、プロジェクトの初期、中盤、リリース前、リリース後のぞれぞれのタイミングでディレクションがやるべきことがわかってきました。<br/> 「あのプロジェクトと同じように、こう進めていきます」といえるようになったのは大きい。<br/> 今後の動きを予測して動けるようになったこともあり、チームメンバーや他部署から依頼をされることも増えてきました。<br/> さらに成長を感じたところとしては、【考えるタスク】を少しずつこなせるようになったこと。<br/> 調整業務やチームの開発を前に進めることだけではなく、本来のディレクション(方向を示す)という意味での【考えるタスク】を担当し、チームがその方向に進んでいくという体験が少しずつできるようになってきています。</p> <p>最近ではこのようなことに悩んでいます。</p> <ul> <li>開発目線になりすぎてビジネス観点(価値あるもの適切なタイミングで世に出すためにはどうすべきか)が漏れてしまうことがある</li> </ul> <p>これは同じ悩みをお持ちの方もいらっしゃるのではないでしょうか。<br/> 開発ディレクターは開発メンバーと過ごす時間が多いこともあり陥りがちな思考だと思います。 安全にミスなく進めるためにはとても重要ですが、忘れてはいけないのは、<br/> リリースをすることがゴールではなく、【価値のあるプロダクトを生み出すことがゴールである】ということです。</p> <p>例えばA案が良いと思って進めていたけれど、リリース直前になってB案の方が顧客の満足度も高く、売上にもつながるとわかったケースがあるとします。<br/> 開発チームとしては、直前で変更を加えなくてはいけない、リリース日の延長はなるべく避けたい、という状況はストレスにつながると思います。<br/> しかし、私たちが進むべきゴールは【価値あるプロダクトを生み出すこと】です。<br/> 開発チームには負荷がかかりますがディレクターとしてはサンクコストではなく、プロダクトの価値を見るべきです。</p> <p>私は同様の経験を通じで、ディレクターは開発目線とプロダクトオーナーのどちらの目線も持つことがとても重要だと身をもって学びました。<br/> ディレクターとして働き始めた当初は、ディレクターって正直いなくても開発は進むよな…と自分の価値を見つけられずにいました。<br/> 今では、プロダクトの価値を最大化すること、さらに開発チームとプロダクトオーナーの両者が最も進めやすい方法を模索することがディレクションの価値だと思っています。</p> <h1 id="今後に向けて">今後に向けて</h1> <p>今の私が意識し、目標にしていることを宣言させてください。<br/> 私は、エンジニアより技術力はない。<br/> 私は、マーケターよりも市場の把握や予測に強くない。<br/> 私は、営業よりも現場の考え方が理解できていない。<br/> けれど、チームの推進力を高め、開発によって生み出されるプロダクトを価値あるものにする力は誰よりも持てるようになりたい。</p> <p>そのために2つのことを意識していきたいと考えています。</p> <ol> <li>チームマネジメントについて学び、チームに合った進め方でさらに推進力を上げていく</li> <li>ビジネス目線を常に意識し、開発で生まれるサービスが価値あるものになるようにディレクションを行う</li> </ol> <p>一人のディレクターとしてチームや会社にとってなくてはならない存在になることを目指してきます。</p> <h1 id="最後に一言">最後に一言</h1> <p>勇気を出して、別業界かつ別職種にチャレンジしたことを本当によかったと思っています。<br/> 辛い時もありますが、日々自己成長できていると実感することができています。<br/> この場を借りてチームメンバー、同じエンジニアメンバー、一休の皆さんに感謝の気持を伝えたいです。いつも温かいアドバイス、ありがとうございます。<br/> これからも明るさと元気を取柄に頑張ります!</p> Sun, 24 Dec 2023 09:09:02 +0900 hatenablog://entry/6801883189069279420 一休レストランの XState 導入記 https://user-first.ikyu.co.jp/entry/2023/12/22/190342 <p>このエントリーは <a href="https://qiita.com/advent-calendar/2023/ikyu">&#x4E00;&#x4F11;.com&#x306E;&#x30AB;&#x30EC;&#x30F3;&#x30C0;&#x30FC; | Advent Calendar 2023 - Qiita</a> の22日目の記事です。</p> <p>レストランプロダクトUI開発チームの鍛治です。 一休レストランのフロントエンドを担当しています。</p> <p>一休レストランでは <s>Next.js App Router</s> Remix を採用しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fuser-first.ikyu.co.jp%2Fentry%2F2023%2F12%2F15%2F093427" title="一休レストランで Next.js App Router から Remix に乗り換えた話 - 一休.com Developers 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://user-first.ikyu.co.jp/entry/2023/12/15/093427">user-first.ikyu.co.jp</a></cite></p> <p>昨年の終わり頃から始まった一休レストランのリニューアルですが、フロントエンドは Nuxt v2 (Vue 2) から Next.js App Router (React) に、という大きな切り替えで、不慣れだった我々は React 初心者がひっかかる落とし穴を全部踏み抜いてきました。</p> <p>例えば、チュートリアルに従って useState で変化する状態を定義して、最初はそれで全てがうまくいっていました。機能追加していく過程でいつの間にか一つ増え二つ増え、あとはズルズルと。 ふと我に返ると一つのコンポーネントに10個もの useState が生えてしまっていました。 その結果、 <code>&amp;&amp;</code>, <code>||</code>, <code>??</code> のオンパレードと三項演算子だらけの JSX だけが残りました。何度も何度も読み返してるのに、コンポーネントが今どんな状態にあるのか、さっぱり把握できない…</p> <p>他にも、</p> <ul> <li>バケツリレー コールバック</li> <li>useEffect 問題</li> </ul> <p>といった落とし穴を踏み抜いてきました。</p> <p>フロントエンドの状態管理って本当に難しいですよね。</p> <p>あらためて本日は React 状態管理改善の第一弾として useState 濫用からどう抜け出したのかについてお話しします。</p> <p>コールバックや useEffect 問題は来月以降の記事でご紹介する予定です。</p> <h2 id="useState-の難しさ">useState の難しさ</h2> <p>まずは一番初歩的なところから考えてみましょう。</p> <p>複数のuseStateフックを使用する場合、予期しない状態の組み合わせが発生する可能性があります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> Sample<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">[</span>show<span class="synStatement">,</span> setShow<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">false</span><span class="synStatement">);</span> <span class="synType">const</span> <span class="synIdentifier">[</span>disabled<span class="synStatement">,</span> setDisabled<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synConstant">false</span><span class="synStatement">);</span> <span class="synType">const</span> toggle <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> setShow<span class="synStatement">((</span>prev<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synConstant">!</span>prev<span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">[]</span><span class="synStatement">);</span> <span class="synType">const</span> toggleDisabled <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> setDisabled<span class="synStatement">((</span>prev<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synConstant">!</span>prev<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;&gt;</span> <span class="synStatement">&lt;</span>button <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{</span>toggle<span class="synIdentifier">}</span> disabled<span class="synStatement">=</span><span class="synIdentifier">{</span>disabled<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> show <span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>button <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{</span>toggleDisabled<span class="synIdentifier">}</span><span class="synStatement">&gt;</span>disable<span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>SampleModal show<span class="synStatement">=</span><span class="synIdentifier">{</span>show<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span> </pre> <p>このシンプルな例では、show(モーダル表示用)と disabled(ボタン無効化用)の二つの状態を管理しています。</p> <p>しかし、たった二つしかないのに <code>show === true &amp;&amp; disabled === true</code> のように、ボタンが無効化されているにも関わらずモーダルが表示されている、という矛盾した状態を表現できてしまいます。useState で管理する状態が増えれば増えるほど、矛盾した状態を生んでしまう可能性は高くなります。</p> <p>この問題を解決するためには、コンポーネントの粒度を小さくし、useState には primitive 値を入れず構造化されたデータを用いて、ありえない状態を生まないようにするのが自然な発想でしょう。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink> <span class="synStatement">type</span> State <span class="synStatement">=</span> Initial | Disabled | Modal <span class="synStatement">type</span> Initial <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Initial'</span> disabled: <span class="synConstant">false</span> show: <span class="synType">boolean</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> Disabled <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Disabled'</span> disabled: <span class="synConstant">true</span> show: <span class="synConstant">false</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> Modal <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Modal'</span> disabled: <span class="synConstant">false</span> show: <span class="synConstant">true</span> modalData: ModalData <span class="synIdentifier">}</span> <span class="synStatement">function</span> Sample<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">[</span>state<span class="synStatement">,</span> setState<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">&lt;</span>State<span class="synStatement">&gt;(</span><span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Initial'</span><span class="synStatement">,</span> disabled: <span class="synConstant">false</span><span class="synStatement">,</span> show: <span class="synConstant">false</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synType">const</span> open <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> setState<span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Modal'</span><span class="synStatement">,</span> modalData: <span class="synConstant">'data'</span><span class="synStatement">,</span> disabled: <span class="synConstant">false</span><span class="synStatement">,</span> show: <span class="synConstant">true</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">[</span>setState<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synType">const</span> toggleDisabled <span class="synStatement">=</span> useCallback<span class="synStatement">(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">if(</span>state.disabled<span class="synStatement">)</span> <span class="synIdentifier">{</span> setState<span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Disabled'</span><span class="synStatement">,</span> disabled: <span class="synConstant">true</span><span class="synStatement">,</span> show: <span class="synConstant">false</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span> setState<span class="synStatement">(</span><span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'Initial'</span><span class="synStatement">,</span> disabled: <span class="synConstant">false</span><span class="synStatement">,</span> show: <span class="synConstant">false</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">[</span>setState<span class="synIdentifier">]</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;&gt;</span> <span class="synStatement">&lt;</span>button <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{</span>open<span class="synIdentifier">}</span> disabled<span class="synStatement">=</span><span class="synIdentifier">{</span>state.disabled<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> show <span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>button <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{</span>toggleDisabled<span class="synIdentifier">}</span><span class="synStatement">&gt;</span>disable<span class="synStatement">&lt;</span>/button<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>SampleModal show<span class="synStatement">=</span><span class="synIdentifier">{</span>state.show<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/<span class="synStatement">&gt;</span> <span class="synStatement">)</span> </pre> <h2 id="useState--union-型では足りなかった">useState + union 型では足りなかった</h2> <p>上述した実装のように、union 型によって不正な状態が作られなくなりました。</p> <p>遷移はイベントハンドラ内で暗黙的に記述されます。上記のモーダルでは状態が2つしかなく、シンプルな実装なので遷移の全体像を把握できていますが、状態の数が増え遷移が複雑になると遷移の全体を把握するのが困難になり、人為的に遷移先を決定するロジックをテストする必要があります。結果、誤って不正な遷移が紛れ込む場合があります。</p> <p>例えば、一休レストランでは空席確認カレンダーという機能があります。</p> <p><figure class="figure-image figure-image-fotolife" title="空席確認カレンダー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kajimm/20231222/20231222170804.png" width="775" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>空席確認カレンダー</figcaption></figure></p> <p>上記空席確認カレンダーの状態遷移図は以下のようになります。黒色で囲われているのが状態で、灰色で囲われているのが遷移イベントです。</p> <p><figure class="figure-image figure-image-fotolife" title="カレンダーの状態遷移図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kajimm/20231222/20231222145359.png" width="1200" height="329" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>カレンダーの状態遷移図</figcaption></figure></p> <p>状態が7個、遷移イベントが20個あり、イベントハンドラ内での遷移先を決めるロジックが複雑になってしまい不正な遷移を起こしてしまう可能性がありました。</p> <p>このような不正な遷移を人為的ではなく機械的に防ぐために、state machine を導入します。</p> <h3 id="state-machine-とは">state machine とは?</h3> <p>state machine は複数の「状態」と「状態間の遷移」で構成されます。</p> <p>上述した web 画面のシナリオを例にすると「フラットな状態」(通常の状態)から「モーダルが開いた状態」への遷移は「 show ボタンをクリックする」というイベントによって行われます。</p> <p>「モーダルが開いた状態」では再度 「show クリック」イベントが発生しても、そのイベントに対応する状態遷移は定義されていないので、それ以上何も起きません。</p> <p>また「フラットな状態」から最初に disabled ボタンが押されて (disable イベントが発火して)「ボタンが無効化された状態」になると、そこで仮に show イベントが発火しても、同様に show イベントに対応する状態遷移が定義されていないので、「ボタンが無効なのにモーダルが開いてしまう」という矛盾した状態が生じません。</p> <p><figure class="figure-image figure-image-fotolife" title="モーダルの状態遷移"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kajimm/20231221/20231221092434.png" width="1200" height="686" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>モーダルの状態遷移図</figcaption></figure></p> <p>state machine では、あらかじめ定義した状態とその状態間の遷移しか存在しないので、予期しない状態に陥ることがありません。state machine を導入すると、アプリケーションロジックを明確かつ宣言的に定義できるのが非常に魅力的なポイントです。</p> <h1 id="XState-state-machine-の導入">XState (state machine) の導入</h1> <p>state mcahine を導入するために、<a href="https://stately.ai/docs/xstate">XState</a> を使った状態管理方法を導入することを決定しました。</p> <p>もちろん他の解決策もあったと思います。</p> <p>例えば、弊社 CTO が以前ご紹介した TypeScript の discriminated union (タグ付きユニオン型)で状態を、関数で遷移を表現する手法はその一つであり、弊社プロダクトで実績あるソリューションであることは間違いありません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechplay.jp%2Fcolumn%2F1631" title="TypeScriptによるGraphQLバックエンド開発 ──TypeScriptの型システムとデータフローに着目した宣言的プログラミング - TECH PLAY Magazine" 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://techplay.jp/column/1631">techplay.jp</a></cite></p> <p>ただ、現在の自分達では、制約のない状況下でうまく型を定義して、状態を完全にコントロールできるという自信は持てませんでした。state machine もどきの不完全な物を生み出してしまわないか不安があったのです。</p> <p>XState であれば state machine を正しく定義することを強制されます。技術としてのフレームワークに留まらず、思考のフレームワークとしてガイドレールを提示してくれる点を評価しました。</p> <h2 id="XState-とは">XState とは?</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstately.ai%2Fdocs%2Fxstate" title="XState | Stately" 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://stately.ai/docs/xstate">stately.ai</a></cite></p> <p>state machineを作成することができる非常に高機能なライブラリです。</p> <p>例えば、フロントエンドのサンプルとしてよく用いられる TODO リストを XState で実装<a href="#f-bbc46b43" id="fn-bbc46b43" name="fn-bbc46b43" title="XState 4 ベースのコードです。XState 5には近日中に移行予定です">*1</a>すると以下のようになります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> TodoList <span class="synStatement">=</span> <span class="synIdentifier">{</span> items: <span class="synIdentifier">{</span> id: <span class="synType">number</span> name: <span class="synType">string</span> completed: <span class="synType">boolean</span> <span class="synIdentifier">}[]</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> TodoEvent <span class="synStatement">=</span> Add | Toggle | Disable | Enable <span class="synStatement">type</span> Add <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'ADD'</span> item: <span class="synIdentifier">{</span> id: <span class="synType">number</span> name: <span class="synType">string</span> completed: <span class="synType">boolean</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> Toggle <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'TOGGLE'</span> id: <span class="synType">number</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> Disable <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'DISABLE'</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> Enable <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synStatement">type</span>: <span class="synConstant">'ENABLE'</span> <span class="synIdentifier">}</span> <span class="synStatement">type</span> TodoState <span class="synStatement">=</span> <span class="synIdentifier">{</span> value: <span class="synConstant">'ACTIVE'</span><span class="synStatement">;</span> context: TodoList <span class="synIdentifier">}</span> | <span class="synIdentifier">{</span> value: <span class="synConstant">'INACTIVE'</span><span class="synStatement">;</span> context: TodoList <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synType">const</span> machine <span class="synStatement">=</span> createMachine<span class="synStatement">&lt;</span>TodoList<span class="synStatement">,</span> TodoEvent<span class="synStatement">,</span> TodoState<span class="synStatement">&gt;(</span><span class="synIdentifier">{</span> initial: <span class="synConstant">'ACTIVE'</span><span class="synStatement">,</span> states: <span class="synIdentifier">{</span> ACTIVE: <span class="synIdentifier">{</span> on: <span class="synIdentifier">{</span> ADD: <span class="synIdentifier">{</span> target: <span class="synConstant">'ACTIVE'</span><span class="synStatement">,</span> actions: assign<span class="synStatement">((</span>ctx<span class="synStatement">,</span> event<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> items: <span class="synIdentifier">[</span>...ctx.items<span class="synStatement">,</span> event.item<span class="synIdentifier">]</span> <span class="synIdentifier">}</span><span class="synStatement">)),</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> TOGGLE: <span class="synIdentifier">{</span> target: <span class="synConstant">'ACTIVE'</span><span class="synStatement">,</span> actions: assign<span class="synStatement">((</span>ctx<span class="synStatement">,</span> event<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> items: ctx.items.map<span class="synStatement">((</span>item<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> item.id <span class="synStatement">===</span> event.id ? <span class="synIdentifier">{</span> ...item<span class="synStatement">,</span> completed: <span class="synConstant">!</span>item.completed <span class="synIdentifier">}</span> : item <span class="synStatement">),</span> <span class="synIdentifier">}</span><span class="synStatement">)),</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> DISABLE: <span class="synConstant">'INACTIVE'</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> INACTIVE: <span class="synIdentifier">{</span> on: <span class="synIdentifier">{</span> ENABLE: <span class="synConstant">'ACTIVE'</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> </pre> <p>まず state として TODO を追加したりトグルを変更が可能な状態の <code>ACTIVE</code> と、なにもできない状態の <code>INACTIVE</code> を定義します。</p> <p>次に、各 state が各イベントを受け取った時にどの状態に遷移するか、すなわち状態遷移を <code>on</code> で定義し、その状態遷移時の副作用としてのデータ更新を <code>actions</code> で指定することで、state machine が完成します。</p> <p>XStateでは、内部情報として context (詳しいことは後のセクションで説明します)を持ちます。<code>ADD</code> イベントでは context である <code>items</code> に 新しい TODO を追加しています。</p> <p>XState で定義した state mahine では、<code>INACTIVE</code> の状態で <code>ADD</code> や <code>TOGGLE</code> のイベントに対する状態遷移を定義していないので、<strong>ありえない状態に遷移しないことが保証されます。</strong>。</p> <h3 id="context">context</h3> <p>context とは、state machine が扱う状態の「詳細」や「変動する部分」を吸収して、複雑な状況に対応する仕組みです。</p> <p>state machine 、厳密には有限状態機械(FSM: Finite State Machine)の「有限」は、あくまで数学的な「有限」です。</p> <p>実際のアプリケーションでは、管理しなければならない状態に紐づくデータや条件が複雑で、有限状態機械を原理的に適用すると、たとえ「有限」であっても、人間の認知能力ではとうてい把握しきれない膨大なバリエーションを生み出してしまいます。</p> <p>有限状態機械を現実的に利用するために 状態とその状態に関連するデータを分離して、context という形で保存・管理します。</p> <p>例えば以下のように、ユーザーの入力やアプリケーションの現在の状態など、状態自体ではなく、状態の「内容」を表すデータのことです。</p> <ul> <li>予約する人数日時</li> <li>予約の際に選択する支払い方法</li> <li>使用するクーポン情報</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="予約入力の状態遷移図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kajimm/20231222/20231222144556.png" width="1200" height="521" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>予約入力の状態遷移図</figcaption></figure></p> <h2 id="XState-で管理すべきでない状態">XState で管理すべきでない状態</h2> <p>XState で全ての状態を管理すべきと言ってるわけではありません。ボタンを押すとモーダルが表示される状態遷移は、XState で管理してしまうと却ってオーバーエンジニアリングになってしまいます。</p> <p>また、以下の場合は状態として持つべきではありません。</p> <ul> <li><strong>状態遷移から独立しており、値が操作の過程で変化しないもの</strong></li> </ul> <p>例えば、API レスポンスは state machine の遷移に変化する値ではないので XState で管理すべきではなく、useState で管理すべきです。</p> <p>XStateで管理すべき基準としては</p> <ul> <li>1つのコンポーネントで useState が3つ以上定義されている</li> <li>何かアクションを起こした時の遷移先が2つ以上ある</li> </ul> <p>場合だと思ってます。(プロダクトによって基準は違うと思うのであくまで目安です)</p> <h1 id="XStateを導入して良かったこと">XStateを導入して良かったこと</h1> <h2 id="フロントエンドの改修が容易になった">フロントエンドの改修が容易になった</h2> <p>state machine によりありえない状態ができないことが担保されているので、フロントエンドの改修をする際に大きいバグが起きなくなりました。</p> <h2 id="実装前の仕様--モデリングの議論ができるようになった">実装前の仕様 / モデリングの議論ができるようになった</h2> <p>state machine が画面のドメインモデルとなるので、画面や機能を作成する際にどのような state machine にするか議論することで、意図せずも画面や機能のモデリングの議論ができるようになりました。</p> <h1 id="所感">所感</h1> <p>XState による state machine という考え方のガイドレールができたことで、条件文を最小限にする state mahine のメンタルモデルが形成されてきたように思います。</p> <p>また、上述したように全て XState で管理すべきだとは思ってません。適材適所で XState をうまく活用していきたいです。</p> <h1 id="さいごに">さいごに</h1> <p>一休では、より良いサービスを作ってくれる仲間を募集しています!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>カジュアル面談も実施していますので、ぜひお気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-bbc46b43" id="f-bbc46b43" name="f-bbc46b43" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">XState 4 ベースのコードです。XState 5には近日中に移行予定です</span></p> </div> Fri, 22 Dec 2023 19:03:42 +0900 hatenablog://entry/6801883189068150637 Cloud Runで開発用環境を沢山作る https://user-first.ikyu.co.jp/entry/2023/12/16/112705 <h1 id="概要">概要</h1> <p>この記事は <a href="https://qiita.com/advent-calendar/2023/ikyu">一休.com Advent Calendar 2023</a> 16日目の記事です。</p> <p>RESZAIKO開発チームの松村です。</p> <p>一休では各サービス毎に、開発中のサービスの動作を社内で確認できる環境があります。 それぞれmain(master)ブランチと自動的に同期している環境と、特定のブランチを指定して利用できる環境の2種類があります。</p> <p>今回、RESZAIKOの新規サービス(予約画面)に対してブランチを指定してデプロイできる環境を作成したので、その方針と反省点と今後について記述していきます。</p> <ul> <li>現在運用中の予約画面</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="web予約サービス画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/narumatt/20231215/20231215134136.png" width="303" height="651" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <h1 id="開発環境を作る理由">開発環境を作る理由</h1> <p>一休では長らく、EKS上に複数の環境を用意して、ブランチを指定すると開発環境にデプロイするシステムが利用されてきました。 一般的にこのような環境を構築するのは以下のような理由が挙げられます。</p> <ul> <li>動作確認 <ul> <li>マイクロサービスで、異なるブランチ同士の組み合わせで動作確認がしたい</li> <li>ローカルだと何故か再現しない</li> <li>デプロイがちゃんと動くか確認したい</li> </ul> </li> <li>他人と成果物の共有 <ul> <li>リリースできるほど動作に自信は無いが、ステークホルダーと内容を共有したい</li> </ul> </li> </ul> <p>本サービスでは<a href="https://www.prisma.io/">Prisma</a>を利用してDBのスキーマをアプリのコードと同じリポジトリで管理しているため、 複数の新機能を平行して開発していく場合に開発環境が1つだと、DB定義が衝突したりして尚更大変です。 そこで、複数の開発環境を作成できるようにしました。</p> <p>本サービスは基盤にGoogle Cloudの<a href="https://cloud.google.com/run?hl=ja">Cloud Run</a>を使用しています。 Cloud Runは特に設定しなければアクセスがある時だけコンテナが起動するようになっているので、EKSを使用した場合よりスペックやコストをあまり気にせず環境を増やしていけます。</p> <h1 id="実現方法">実現方法</h1> <p>サーバはCloud Runで動いていて、デプロイは Github Actionsで行っています。 そのため、開発環境用のGithub Actions Workflowを作成していきます。</p> <h2 id="デプロイを行うGithub-Actions-Workflowの作成">デプロイを行うGithub Actions Workflowの作成</h2> <p>本記事の主旨から外れるので詳しく説明しませんが、 Google Cloudには<a href="https://cloud.google.com/blog/ja/products/devops-sre/using-github-actions-with-google-cloud-deploy">Github Actionsと連携してデプロイを行うための機能</a> が各種用意されているので、参考にしてWorkflowのyamlファイルを作成します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> backend.demo.create <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> string <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Environment name to deploy&quot;</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">build</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Checkout&quot;</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synComment"> # SecretからGCPの認証用のjsonを読み出す</span> <span class="synStatement">- </span><span class="synIdentifier">id</span><span class="synSpecial">:</span> <span class="synConstant">&quot;auth&quot;</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> <span class="synConstant">&quot;google-github-actions/auth@v0&quot;</span> <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">credentials_json</span><span class="synSpecial">:</span> <span class="synConstant">&quot;${{ secrets.gcp-dev-service-accont-key }}&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Set up Cloud SDK&quot;</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> <span class="synConstant">&quot;google-github-actions/setup-gcloud@v0&quot;</span> <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">install_components</span><span class="synSpecial">:</span> <span class="synConstant">'alpha,beta'</span> <span class="synComment"> # 以下ビルド・デプロイの記述</span> </pre> <h2 id="Workflowの呼び出し">Workflowの呼び出し</h2> <p>Workflowに <code>workflow_dispatch</code> を定義することで、 <a href="https://docs.github.com/ja/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event">外部からREST APIでWorkflowを呼び出す</a>ことができます。 開発環境用のアプリを作成して、そちらからREST APIで必要に応じてWorkflowを呼び出してあげます。</p> <pre class="code" data-lang="" data-unlink>POST https://api.github.com/repos/test/test-repo/actions/workflows/backend.demo.create/dispatches Content-Type: application/json Accept: application/vnd.github+json Authorization: Bearer &lt;TOKEN&gt; X-GitHub-Api-Version: 2022-11-28 { &#34;ref&#34;:&#34;feature/branch-to-test&#34;, &#34;inputs&#34;:{&#34;name&#34;:&#34;demo-1&#34;} }</pre> <h2 id="実装された運用">実装された運用</h2> <p><figure class="figure-image figure-image-fotolife" title="ブランチデプロイサービス"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/narumatt/20231215/20231215134204.png" width="1200" height="414" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ブランチデプロイサービス画面</figcaption></figure></p> <p>こんな感じのアプリを作成しました。 ブランチ名を入力して <code>Deploy</code> を押すと、デモ環境に該当のブランチがデプロイされます。 いつ、誰が、どのブランチをデプロイしたかを記録するようになっています。 削除機能はまだ実装していないので、使い終わったらmainブランチを手動で適用する運用になっています。</p> <h1 id="反省と将来">反省と将来</h1> <p>折角Cloud Runを使っているのに、既存の他サービスの仕様に引きずられた実装にしてしまいました。 特に以下の点が良くないです。</p> <ul> <li>設定ファイルをコピペして増やしていたので、環境を増やす毎に同じような設定ファイルが増える</li> <li>環境毎に社内用のドメイン( <code>[env-name].dev.reszaiko.com</code> のような)を作っていたので、環境を増やす度にDNSとSSLの設定が必要になる</li> </ul> <p>このため、気軽に環境を増減させる事が困難になっていて、既存の問題をそのまま引き継いでいます。</p> <ul> <li>使わなくなった環境を戻し忘れてそのまま占有し続ける</li> <li>空いている環境がない場合、他の環境を使っている人とコミュニケーションして融通してもらう必要がある</li> </ul> <h2 id="このままデプロイ環境を作るなら">このままデプロイ環境を作るなら</h2> <p>ブランチデプロイ環境として、全てのブランチに対して自動的にデモ環境を作成、破棄するのが理想です。 コンテナのビルドやDBやサーバの用意、デプロイは既にGithub Actionsで行うようにしていますし、 開発環境へのアクセスはCloud Routerを利用して振り分けているため、 <code>dev.reszaiko.com/[branch-name]/</code> のように環境毎のパスの追加もGithub Action上で構築できます。</p> <p>また、特に開発環境を必要としない軽微な修正に対しても無制限に環境を作るのを防ぐために、以下の手段が考えられます。</p> <ul> <li><code>dev-****</code> のように、特定のprefixを持つブランチに対して自動で環境を作る</li> <li>既存のデプロイ用UIを拡張して、環境数を増やしたり減らしたりできるようにする</li> </ul> <p>前者はブランチが消えれば自動で環境が消えるので、使わなくなった環境が残ってしまうというよくある問題が解消できます。 後者はUI上で存在する環境の把握やアプリへのリンク、DBのリセットなど機能を追加する事ができて便利です。</p> <h2 id="開発環境を作らないと駄目なのか">開発環境を作らないと駄目なのか</h2> <p>そもそもブランチデプロイ環境が必要か、という問題もあります。</p> <p>開発中のブランチを長期間利用していると本番環境との乖離が大きくなり、mainブランチにマージする際に入念なチェックが必要になります。 RESZAIKOの予約チームでは <a href="https://cloud.google.com/architecture/devops/devops-tech-trunk-based-development?hl=ja">トランクベース開発</a> のように 頻繁にリリースする手法を導入するか議論していますが、 このような手法では開発中の機能はフィーチャーフラグを利用して出し分けるのが適しています。</p> <p>RESZAIKOでは <a href="https://launchdarkly.com/">LaunchDarkly</a> というフィーチャーフラグ機能を提供してくれるSaasを導入しているため、 コストをかけてブランチデプロイ環境を開発していくよりは、フィーチャーフラグを適切に利用する体制を整備し、開発環境はmainブランチと同期したものだけで運用していく方がいいかもしれません。</p> <h1 id="まとめ">まとめ</h1> <p>使用している技術やサービスは日々新しい物が導入対象になるので、最適な開発手法というのはその時に合わせて検討する必要があります。 次に記事を書くときは「トランクベース開発に合わせたフィーチャーフラグの運用法」みたいなのが書けるように頑張ります。</p> <p>一休では、共に働くエンジニアを募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>カジュアル面談も実施しているので、お気軽にご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> Sat, 16 Dec 2023 11:27:05 +0900 hatenablog://entry/6801883189067031238 一休レストランで Next.js App Router から Remix に乗り換えた話 https://user-first.ikyu.co.jp/entry/2023/12/15/093427 <p>このエントリーは<a href="https://qiita.com/advent-calendar/2023/ikyu">一休.com Advent Calendar 2023</a>の15日目の記事になります。</p> <hr /> <p>CTO 室の恩田です。</p> <p>現在は<a href="https://restaurant.ikyu.com/">一休レストラン</a>のフロントエンドのリアーキテクトを手がけています。 今日はその中で <a href="https://nextjs.org/docs/app">Next.js App Router</a> から <a href="https://remix.run/">Remix</a> に乗り換えた話をご紹介したいと思います<a href="#f-bd9d7065" id="fn-bd9d7065" name="fn-bd9d7065" title="同じ一休レストランフロントエンドのリアーキテクトの一環で XState を導入した話は22日目の記事でご紹介しています。">*1</a>。</p> <h1 id="背景">背景</h1> <p><a href="https://user-first.ikyu.co.jp/entry/2023/12/06/173215">6日目</a>の記事で香西から紹介させていただきましたが、2023年10月に<a href="https://restaurant.ikyu.com/">一休レストラン</a>のスマートフォン用レストラン詳細ページをリニューアルしました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">一休レストランの Rust バックエンドが正式リリースされました。<a href="https://t.co/7N4VGv5ej9">https://t.co/7N4VGv5ej9</a> このページのスマートフォンビューはバックエンドが Rust で書かれた GraphQL になってます</p>&mdash; naoya (@naoya_ito) <a href="https://twitter.com/naoya_ito/status/1709507132209680702?ref_src=twsrc%5Etfw">2023年10月4日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ちなみにフロントエンドも、旧バージョンは Nuxt v2 で、新バージョンは Next.js です。一休レストラン React に寄せることに決めました。React Server Component を使った実装になっており、こちらも後者の方が体感速度は速いと思います。</p>&mdash; naoya (@naoya_ito) <a href="https://twitter.com/naoya_ito/status/1709740327375086038?ref_src=twsrc%5Etfw">2023年10月5日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>あらためてリニューアルでの技術的な変更点を再掲すると:</p> <ul> <li>バックエンド言語:Python から Rust へ</li> <li>フロントエンドフレームワーク:Nuxt v2 から Next.js App Router へ</li> </ul> <p>つまり、このエントリは先日リリースしたばかりの Next.js から Remix に乗り換えた、という話になります。</p> <p>図らずも、昨今盛り上がっている Next.js 論争<a href="#f-92d901d1" id="fn-92d901d1" name="fn-92d901d1" title="後段で紹介します。">*2</a>に足を踏み入れることになりました。</p> <h1 id="Nextjs-App-Router-について">Next.js App Router について</h1> <p>まずは disclaimer として、あくまで一休レストランにおいて Next.js App Router が "not for us" であっただけで Next.js そのものに対する評価ではないことは申し添えておきます。</p> <p>その上で、ここでは Next.js App Router を採用した経緯と、実際に採用してみてどんな課題に遭遇したのかを簡単に説明したいと思います。</p> <h2 id="当初-Nextjs-を採用した経緯">当初 Next.js を採用した経緯</h2> <p>採用を決めたのは <a href="https://nextjs.org/blog/next-13">Next.js 13</a> の発表直後、一休レストランのリニューアル計画が動きはじめた頃になります。</p> <p>以下が主に評価した点ですが、</p> <ul> <li>メタフレームワークとしてデファクトスタンダードとしての地歩を固めつつあったこと</li> <li>弊社内の別プロダクトで Next.js (Pages Router) の採用実績が複数あること</li> <li>そして toC サービスである一休レストランにとって、カリカリにチューニングできそうな React Server Component が非常に魅力的なフィーチャーであったこと</li> </ul> <p>特に最後の React Server Component が採用の決め手となりました。</p> <p>先日の <a href="https://nextjs.org/blog/next-14">Next.js 14</a> で発表された <a href="https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model">Partial Prerendering</a> もそうですが、toC サービスの欲しい機能をピンポイントに突いてくるニクいフレームワークです。</p> <h2 id="Nextjs-の-Pain-Points">Next.js の Pain Points</h2> <p>そもそも今回のリニューアルにおけるビジネス上のゴールは、一休レストランで予約するとき、お店に電話をかけたときのようなスムーズな体験を提供する、というものでした。</p> <p>しかし、社内レビューや canary release の過程で見つかったユーザー体験の問題を改善するにあたって、Next.js App Router では実現が難しそうな課題がいくつか見つかってきました。</p> <h3 id="History-API-の-state-を触れない">History API の state を触れない</h3> <p>リニューアルしたスマートフォン版一休レストランは以下のような画面遷移になります。</p> <p><figure class="figure-image figure-image-fotolife" title="レストラン詳細ページ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tak-onda/20231213/20231213191246.jpg" width="540" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>レストラン詳細ページ</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="空席確認カレンダーモーダル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kajimm/20231222/20231222170804.png" width="775" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>空席確認カレンダーモーダル</figcaption></figure></p> <p>人数・日時を選択する空席確認カレンダーのモーダル表示がポイントです。<a href="#f-87804ca9" id="fn-87804ca9" name="fn-87804ca9" title="カレンダーの状態管理についての紆余曲折については22日目の XState の記事で紹介しているので、ご笑覧いただければ幸いです。">*3</a></p> <p>ここでの選択は予約にいたるまでの一連の流れのワンステップなので、操作中はブラウザの「戻る」やリロードで開いた状態を維持したいモーダルです。</p> <p>ただ、その状態で URL が LINE などで共有されたときは、モーダルのない詳細ページが開いて欲しい場面でもあります。</p> <p>Next.js App Router の <a href="https://nextjs.org/docs/app/api-reference/components/link">Link</a> コンポーネントや <a href="https://nextjs.org/docs/app/api-reference/functions/use-router">useRouter</a> フックでは <a href="https://developer.mozilla.org/en-US/docs/Web/API/History">History API</a> の state を操作することはできず、URL を変更せずにブラウザ履歴を積んだ上で画面表示を変更することができません。</p> <h3 id="Cache-Control-ヘッダを自由に設定できない">Cache-Control ヘッダを自由に設定できない</h3> <p>Next.js App Router では <code>Cache-Control</code> ヘッダは <a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers#dynamic-functions">Dynamic Functions</a> が利用されたかどうかと <a href="https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config">Route Segment Config</a> で設定した値を元に Next.js 自身が出力する仕様となっており、利用者が自由に値を設定することはできません。</p> <p>例えば <a href="https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional">searchParams</a> を参照しただけで Dynamic Functions と判定され、強制的に <code>Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate</code> が出力されてしまいます。</p> <p>Fastly を CDN として利用している一休では、<code>Cache-Control</code> ヘッダを制御できない<a href="#f-24191d26" id="fn-24191d26" name="fn-24191d26" title="Fastly のキャッシュ制御は Surrogate-Control ヘッダで、ブラウザキャッシュのための Cache-Control ヘッダは VCL など他の手段で上書きすることはできますが...">*4</a>という制限は、パフォーマンスやインフラ負荷に影響を与える大きな問題です。</p> <p>また、レストラン詳細ページ以降のページだけが今回のリニューアル範囲のため、 <a href="https://web.dev/articles/bfcache?hl=en">bfcache</a> が無効になってしまうのも、既存ページとの遷移でユーザー体験に悪影響を及ぼします。</p> <h3 id="継続的なアップデートに懸念を覚えた">継続的なアップデートに懸念を覚えた</h3> <p>Next.js のパッチバージョンを上げたときに production build でだけ 500 エラーが発生するという問題に幾度か苦しめられました。</p> <p>App Router で運用している世界の様々なサイトで同じ問題が発生していたら大きな Issue になっているはずで、一休レストランのコード、もしくは利用ライブラリのいずれかに原因があったことには間違いないとは思います。</p> <p>現象の再現状況の特定が難しく、加えて調査に十分なリソースを割けなかったという背景もありましたが、正確な原因が掴めず仕舞いとなってしまったことには歯痒い思いとともに、懸念が残りました。</p> <h1 id="Remix-への乗り換え">Remix への乗り換え</h1> <p>上記の課題を解決するため、最終的には <a href="https://remix.run/">Remix</a> に乗り換えることを決定しました。</p> <h2 id="Remix-を採用した理由">Remix を採用した理由</h2> <p>Next.js App Router で抱えていた課題の裏返しになるのですが、そもそもの Remix の設計指針である、<strong>Web 標準 API を尊重している点</strong><a href="#f-83bbccc5" id="fn-83bbccc5" name="fn-83bbccc5" title="Remix サイトのトップページに &quot;Focused on web standards and modern web app UX&quot; と掲げられています。">*5</a>を特に重視しました。</p> <h3 id="History-API">History API</h3> <p>改善したかったクライアントサイドのナビゲーションを例に取ると、Remix の提供している <a href="https://remix.run/docs/en/main/components/link">Link</a> コンポーネントや <a href="https://remix.run/docs/en/main/hooks/use-navigate">useNavigate</a> フックは <a href="https://developer.mozilla.org/en-US/docs/Web/API/History_API">History API</a> <a href="#f-5e1662a6" id="fn-5e1662a6" name="fn-5e1662a6" title="Navigation API が早く普及して欲しい...">*6</a> の薄い wrapper になっていて <a href="https://remix.run/docs/en/main/components/link#state">state</a> を利用することが可能です。</p> <p>具体的には、Remix 自身もスクロール位置の維持をはじめとするクライアントサイドナビゲーションの管理に <a href="https://developer.mozilla.org/en-US/docs/Web/API/History/state">History API state</a> を利用していて、Remix API で利用者が指定した <a href="https://remix.run/docs/en/main/components/link#state">state</a> は <a href="https://developer.mozilla.org/en-US/docs/Web/API/History/state">History API state</a> では、</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">usr</span>&quot;: <span class="synSpecial">{</span>&quot;<span class="synStatement">state</span>&quot;: <span class="synSpecial">[</span>&quot;<span class="synConstant">set</span>&quot;, &quot;<span class="synConstant">from</span>&quot;, &quot;<span class="synConstant">Remix API</span>&quot;<span class="synSpecial">]}</span>, &quot;<span class="synStatement">key</span>&quot;: &quot;<span class="synConstant">dgfkntlh</span>&quot;, &quot;<span class="synStatement">idx</span>&quot;: <span class="synConstant">2</span> <span class="synSpecial">}</span> </pre> <p>上記の例のように Remix が定義する History state の構造の中の <code>"usr"</code> キーの中に格納されます。</p> <p>この構造を理解していれば、直接 <a href="https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState">History API replaceState</a> を呼ぶことで Remix の遷移は抑止しつつ state だけを置き換えるような運用も実現できます。</p> <h3 id="Cache-Control-ヘッダ">Cache-Control ヘッダ</h3> <p>Next.js Pages Router の <a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props">getServerSideProps</a> に相当する Remix の機能に <a href="https://remix.run/docs/en/main/route/loader">loader</a> があります。</p> <p>loader の引数や返り値は Web 標準の <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request">Request</a> / <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">Response</a> なので <code>Cache-Control</code> にも出力したかった値を設定でき、CDN やブラウザキャッシュをコントロールする自由を取り戻しました。</p> <h3 id="その他">その他</h3> <p>他にも Next.js App Router の Async Server Component に相当する効果<a href="#f-3be90aa8" id="fn-3be90aa8" name="fn-3be90aa8" title="正確に述べると fetch 処理は loader に一元化して Promise を defer を使って返す必要があります。">*7</a>が得られる <a href="https://remix.run/docs/en/main/guides/streaming">defer</a> など、toC サービスである一休レストランにとって魅力的な機能を備えています。</p> <h2 id="検討した代替案">検討した代替案</h2> <p>Remix 以外に検討した対策についても簡単にご紹介します。</p> <h3 id="Nextjs-に-patch-をあてる">Next.js に patch をあてる</h3> <p><code>Cache-Control</code> ヘッダの問題は Next.js の設計方針そのものでどうしようもないので、 <a href="https://pnpm.io/cli/patch">pnpm patch</a> でヘッダを出力している Next.js の当該コードを上書きしてしまう対策<a href="#f-d19bf24c" id="fn-d19bf24c" name="fn-d19bf24c" title="この問題は他の利用者も困っているようで Next.js の Issue 内に patch をあてる workaround が紹介されています。">*8</a>も試しました。</p> <p>ですが <code>Cache-Control</code> を制御したい path が増える度に patch を更新するのは手間がかかって煩わしいし、ヘッダを書き換えられるようになるだけで、ナビゲーション問題は解決できません。</p> <h3 id="Pages-Router-への切り替え">Pages Router への切り替え</h3> <p><a href="https://nextjs.org/docs/pages">Pages Router</a> への切り替えも少しだけ検討しました。</p> <p>一休の他プロダクトで Pages Router の実績はあるので安定性に不安はありませんが、React Server Component に期待したパフォーマンス面はあまり期待できそうにありません。<a href="#f-de74717a" id="fn-de74717a" name="fn-de74717a" title="Remix 公式ブログの Next.js との比較記事 で詳解されていますが Pages Router と比較すると Remix に軍配があがるようです。 ">*9</a></p> <p>また Vercel の開発リソースも App Router にほぼ向けられているだろうし、現時点において Pages Router を選択するのは将来性も見込めないと判断しました。</p> <h1 id="Remix-置き換えで得られた効果">Remix 置き換えで得られた効果</h1> <p>ちょうど Remix 版をリリースして一週間経過したところですが、以下のような効果が得られています。</p> <h2 id="継続的なアップデート">継続的なアップデート</h2> <p>2023-12-18 追記</p> <p>つい先日の 12/14 にリリースされたばかりの <a href="https://github.com/remix-run/remix/releases/tag/remix%402.4.0">Remix 2.4.0</a> まで、問題なく追随できていることをご報告しておきます。</p> <h2 id="Fastly-の-cache-hit-ratio-が-63--68-に">Fastly の cache hit ratio が 63% → 68% に</h2> <p>置き換えの目的の内の一つである CDN とブラウザキャッシュの有効活用です。</p> <p>背景で紹介していますが、リニューアル対象はスマートフォン用のレストラン詳細ページ以降のみで、一休レストラン全体から見れば、ごく限られた範囲でしかありません。</p> <p>にも関わらず、一休レストラン全体の cache hit ratio を 5% ポイント近く向上させることができました。</p> <p>インフラの効率化もさることながら、Fastly のキャッシュから返ってくるときのレスポンス速度は圧倒的に高速なので、ユーザー体験を向上させる改善に繋がったことが何よりも嬉しい成果です。</p> <h2 id="Cloud-Run-の効率化">Cloud Run の効率化</h2> <p>ここは意図していませんでしたが Remix 乗り換えで得られた嬉しい副作用です。</p> <h3 id="メモリ使用量が-14-に">メモリ使用量が 1/4 に</h3> <p><figure class="figure-image figure-image-fotolife" title="Cloud Run Memory Utilization"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tak-onda/20231213/20231213164640.jpg" width="1200" height="453" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Cloud Run Memory Utilization</figcaption></figure></p> <p>グラフの通りメモリ使用量が 1/4 に減りました。 一休レストランは夕方から夜にかけてアクセスのピークを迎えるのですが、その間も安定して同じ水準を保っています。</p> <h3 id="コンテナ起動時間が-12-に">コンテナ起動時間が 1/2 に</h3> <p><figure class="figure-image figure-image-fotolife" title="Cloud Run Startup Latency"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tak-onda/20231213/20231213164943.jpg" width="1200" height="518" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Cloud Run Startup Latency</figcaption></figure></p> <p>Next.js では 20 秒強かかっていたコンテナ起動時間が 10 秒に縮まりました。</p> <p>Next.js 時代からの課題ですが、ローカルでは一瞬で起動するのに、Cloud Run だと起動に時間がかかってしまう問題は調査中です。</p> <h1 id="所感と最近の議論">所感と最近の議論</h1> <p>Remix に乗り換えての<strong>個人的な</strong>所感になりますが、Web 標準 API がそのまま使えて、利用者が思った通りにコントロールできる非常に扱いやすいフレームワークだと感じています。</p> <p>上記はあくまで私の印象になるので、最近の Next.js の議論で特に参考にさせていただいたリソースを紹介します。</p> <ul> <li><a href="https://www.epicweb.dev/why-i-wont-use-nextjs">Why I Won't Use Next.js</a><br>Next.js 論争の火種になった Kent C. Dodds の記事</li> <li><a href="https://leerob.io/blog/using-nextjs">Why I'm Using Next.js</a><br>Kent C. Dodds の記事に対する Lee Robinson によるアンサー記事</li> <li><a href="https://mozaic.fm/episodes/135/monthly-ecosystem-202311.html">Mozaic.fm ep135 Monthly Ecosystem 202311</a><br>Next.js 14 や上記の議論について</li> <li><a href="https://speakerdeck.com/mugi_uno/next-dot-js-app-router-deno-mpa-hurontoendoshua-xin?slide=66">Next.js App Router での MPA フロントエンド刷新</a><br>サイボウズさんの App Router 導入知見。所感が趣き深い。</li> <li><a href="https://zenn.dev/catnose99/articles/f8a90a1616dfb3">しずかなインターネットの技術構成</a><br>Zenn の作者でも知られる catnose さんの記事。App Router を見送った理由を参照されたい。</li> </ul> <h1 id="今後の展望">今後の展望</h1> <p>現時点ではまだ Remix に置き換えただけで、ようやく改善のための足回りが整った、という段階です。</p> <p>引き続きよりよいユーザー体験を目指して、本丸のナビゲーションの改善、CDN キャッシュ効率向上によるレスポンスの高速化を進めていきたいと思います。</p> <h1 id="おわりに">おわりに</h1> <p>今回の一休レストランの問題だけでなく、フロントエンド領域で難しい課題をまだまだ抱えています。</p> <p>一休では、事業の成功を技術面からともに支える仲間を募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>まずはカジュアル面談からお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-bd9d7065" id="f-bd9d7065" name="f-bd9d7065" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">同じ一休レストランフロントエンドのリアーキテクトの一環で <a href="https://stately.ai/docs/xstate">XState</a> を導入した話は<a href="https://user-first.ikyu.co.jp/entry/2023/12/22/190342">22日目の記事</a>でご紹介しています。</span></p> <p class="footnote"><a href="#fn-92d901d1" id="f-92d901d1" name="f-92d901d1" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">後段で紹介します。</span></p> <p class="footnote"><a href="#fn-87804ca9" id="f-87804ca9" name="f-87804ca9" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">カレンダーの状態管理についての紆余曲折については<a href="https://user-first.ikyu.co.jp/entry/2023/12/22/190342">22日目の XState の記事</a>で紹介しているので、ご笑覧いただければ幸いです。</span></p> <p class="footnote"><a href="#fn-24191d26" id="f-24191d26" name="f-24191d26" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">Fastly のキャッシュ制御は <a href="https://developer.fastly.com/reference/http/http-headers/Surrogate-Control/">Surrogate-Control</a> ヘッダで、ブラウザキャッシュのための Cache-Control ヘッダは VCL など他の手段で上書きすることはできますが...</span></p> <p class="footnote"><a href="#fn-83bbccc5" id="f-83bbccc5" name="f-83bbccc5" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://remix.run/">Remix サイトのトップページ</a>に "Focused on <strong>web standards</strong> and modern web app UX" と掲げられています。</span></p> <p class="footnote"><a href="#fn-5e1662a6" id="f-5e1662a6" name="f-5e1662a6" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API">Navigation API</a> が早く普及して欲しい...</span></p> <p class="footnote"><a href="#fn-3be90aa8" id="f-3be90aa8" name="f-3be90aa8" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">正確に述べると fetch 処理は <a href="https://remix.run/docs/en/main/route/loader">loader</a> に一元化して Promise を <a href="https://remix.run/docs/en/main/utils/defer">defer</a> を使って返す必要があります。</span></p> <p class="footnote"><a href="#fn-d19bf24c" id="f-d19bf24c" name="f-d19bf24c" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">この問題は他の利用者も困っているようで Next.js の <a href="https://github.com/vercel/next.js/issues/22319">Issue</a> 内に patch をあてる <a href="https://github.com/vercel/next.js/issues/22319#issuecomment-1565268095">workaround</a> が紹介されています。</span></p> <p class="footnote"><a href="#fn-de74717a" id="f-de74717a" name="f-de74717a" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">Remix 公式ブログの <a href="https://remix.run/blog/remix-vs-next">Next.js との比較記事</a> で詳解されていますが Pages Router と比較すると Remix に軍配があがるようです。 </span></p> </div> Fri, 15 Dec 2023 09:34:27 +0900 hatenablog://entry/6801883189066390971 宿泊管理システムのフロントエンド設計と改善の変遷 https://user-first.ikyu.co.jp/entry/2023/12/14/091116 <ul class="table-of-contents"> <li><a href="#宿泊の管理システムについて">宿泊の管理システムについて</a></li> <li><a href="#新しい管理システムについて">新しい管理システムについて</a></li> <li><a href="#開発初期のフロントエンド設計">開発初期のフロントエンド設計</a><ul> <li><a href="#コンポーネントは4レイヤー方式を採用">コンポーネントは4レイヤー方式を採用</a></li> <li><a href="#UIのコンポーネントライブラリを採用">UIのコンポーネントライブラリを採用</a></li> <li><a href="#これ以上の設計方針は決めなかった">これ以上の設計、方針は決めなかった</a></li> </ul> </li> <li><a href="#初期ローンチ後の課題">初期ローンチ後の課題</a></li> <li><a href="#改善した内容">改善した内容</a><ul> <li><a href="#1-コンポーネント設計の見直し">1. コンポーネント設計の見直し</a><ul> <li><a href="#ディレクトリ構成の変更">ディレクトリ構成の変更</a></li> <li><a href="#大きくなったコンポーネントの分割">大きくなったコンポーネントの分割</a></li> <li><a href="#Fragment-Colocationを導入してコンポーネントのインターフェースとFragmentを整理">Fragment Colocationを導入してコンポーネントのインターフェースとFragmentを整理</a></li> </ul> </li> <li><a href="#2-業務処理composablesの分割">2. 業務処理(composables)の分割</a></li> <li><a href="#3-型安全に開発できるように厳しいlint設定に変更">3. 型安全に開発できるように厳しいlint設定に変更</a></li> <li><a href="#4-秩序を保てる開発体制ドキュメントの整備">4. 秩序を保てる開発体制、ドキュメントの整備</a></li> </ul> </li> <li><a href="#現在と今後">現在と今後</a><ul> <li><a href="#今後やりたいこと">今後やりたいこと</a></li> <li><a href="#改善を継続するためのポイント">改善を継続するためのポイント</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <p>宿泊プロダクト開発部の田中(<a href="http://blog.hatena.ne.jp/kentana20/">id:kentana20</a>)です。</p> <p>このエントリーは<a href="https://qiita.com/advent-calendar/2023/ikyu">一休.com Advent Calendar 2023</a>の14日目の記事です。昨日は<a href="https://x.com/kosuke_yellow">@kosuke1012</a>による<a href="https://user-first.ikyu.co.jp/entry/2023/12/13/115112">ADR を1年間書いてみた感想</a>でした。このチームの活動に刺激を受けて、自分のチームでもADRを導入して現在も活用しています。</p> <p>今回は自分が担当している一休.com宿泊の管理システムのフロントエンド設計について、この1年ほどで行った改善をお話します。</p> <h1 id="宿泊の管理システムについて">宿泊の管理システムについて</h1> <p>一休.com宿泊の管理システムは、一休社内とホテルの2面で構成されていて、利用者は一休の社内スタッフとホテルの担当者がおり、それぞれ以下のような業務に活用しています。</p> <ul> <li>一休社内のスタッフ <ul> <li>ホテルの作成、一休全体の予約の管理 など</li> </ul> </li> <li>ホテル担当者 <ul> <li>ホテル情報の管理、商品の在庫や料金設定 など</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="宿泊の管理システムイメージ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20231213/20231213183046.png" width="1200" height="655" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>宿泊の管理システムイメージ</figcaption></figure></p> <h1 id="新しい管理システムについて">新しい管理システムについて</h1> <p>1年半ほど前から、この管理システムに大きめの機能追加をするプロジェクトが発足し、現在も続いています。</p> <p>このプロジェクトは社内スタッフ向け、ホテル担当者向けの両面をカバーする必要があったのですが、新機能を開発をするにあたり</p> <ul> <li>新機能は中長期での開発・運用を想定していること</li> <li>既存システムで採用しているフレームワークやコードベースが古くなっており、新機能をスピーディに開発していくのに難があったこと</li> <li>新機能は既存システムに依存せずに作れそうなこと</li> </ul> <p>などの点から、既存のシステムとは別に新システムをゼロから開発する方針を決めました。</p> <p>新システムのテクノロジースタックは、先行して刷新をしていた一休.com、Yahoo!トラベルの画面に合わせる形で</p> <ul> <li>フロントエンド: Nuxt.js、TypeScript、Apollo Client、Tailwind CSS</li> <li>バックエンド: Go、GraphQL(gqlgen)</li> </ul> <p>という構成にしました。 Nuxt.jsについては開発開始時点ではRC版だったv3を採用しました。</p> <h1 id="開発初期のフロントエンド設計">開発初期のフロントエンド設計</h1> <h2 id="コンポーネントは4レイヤー方式を採用">コンポーネントは4レイヤー方式を採用</h2> <p>Components配下は</p> <ul> <li>pages</li> <li>features</li> <li>objects</li> <li>elements</li> </ul> <p>の4レイヤー構成を採用しており、各レイヤーの役割は以下のとおりです。</p> <table> <thead> <tr> <th style="text-align:left;"> レイヤー </th> <th style="text-align:left;"> 役割 </th> <th style="text-align:left;"> 具体例 </th> <th style="text-align:left;"> 再利用性 </th> <th style="text-align:left;"> 外部アクセス </th> <th style="text-align:left;"> 反証 </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> pages </td> <td style="text-align:left;"> ページ固有のコンポーネント群<br />ページ固有の API アクセス、表示を担う </td> <td style="text-align:left;"> ホテル管理ページ </td> <td style="text-align:left;"> ✕ </td> <td style="text-align:left;"> ◯ </td> <td style="text-align:left;"> 複数ページで使われるもの </td> </tr> <tr> <td style="text-align:left;"> features </td> <td style="text-align:left;"> 機能を持った共通コンポーネント<br />API アクセスをする<br /> </td> <td style="text-align:left;"> グローバルヘッダー<br /> </td> <td style="text-align:left;"> ◯ </td> <td style="text-align:left;"> ◯ </td> <td style="text-align:left;"> API アクセスをしない<br />ページ固有の UI </td> </tr> <tr> <td style="text-align:left;"> objects </td> <td style="text-align:left;"> アプリケーション上の機能、デザインのひと固まりとなるコンポーネント<br /> </td> <td style="text-align:left;"> サイドメニュー </td> <td style="text-align:left;"> ◯ </td> <td style="text-align:left;"> ✕ </td> <td style="text-align:left;"> API アクセスをする<br />ページ全体を実装<br />ボタンなどプリミティブな要素 </td> </tr> <tr> <td style="text-align:left;"> elements </td> <td style="text-align:left;"> HTML のサブセットとなるもっともプリミティブなコンポーネント<br />アプリケーション全体の統一感に寄与するコンポーネント </td> <td style="text-align:left;"> チェックボックス<br />ボタン </td> <td style="text-align:left;"> ◯ </td> <td style="text-align:left;"> ✕ </td> <td style="text-align:left;"> API アクセスをする<br />様々なコンポーネントを用いたデザイン状のかたまり </td> </tr> </tbody> </table> <p>この設計は一休.comのユーザー向けシステムに倣った形で、<a href="https://atomicdesign.bradfrost.com/">Atomic Design</a>と当時の一休レストランで採用していた<a href="https://user-first.ikyu.co.jp/entry/2018/10/09/080000">ITCSSによるレイヤードアーキテクチャ</a>をベースに、宿泊サービスの開発に合わせてカスタマイズした設計となっています。</p> <p>実際の画面だと、こんな形で用途に応じて各レイヤーにコンポーネントを作成してUIの開発をしています。</p> <p><figure class="figure-image figure-image-fotolife" title="コンポーネントのレイヤー例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20231213/20231213185230.png" width="1200" height="883" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>コンポーネントのレイヤー例</figcaption></figure></p> <h2 id="UIのコンポーネントライブラリを採用">UIのコンポーネントライブラリを採用</h2> <ul> <li>デザイナーがいないプロジェクト</li> <li>一覧(テーブル)や入力フォームがよく登場する管理画面で一貫したUIを素早く提供したい</li> </ul> <p>という点から、Vue/Nuxtで利用できるUIコンポーネントライブラリとして、Alibabaグループが開発しているElementのVue3対応版であるElement Plusを採用しました。</p> <p><a href="https://element-plus.org/en-US/">A Vue 3 UI Framework | Element Plus</a></p> <p>当時はVuetifyとElement Plusを比較検討したのですが</p> <ul> <li>フォームの画面ではVuetifyよりも書きやすい</li> <li>当時のVuetifyはVue3サポートが完了していなかったがElement Plusは対応済(現在はVuetifyもVue3をサポートしています)</li> <li>Element Plusの方がTailwind CSSとの親和性が高い</li> </ul> <p>といった点からElement Plusを選択しました。</p> <p>当時RCだったNuxt.js v3に対応したUIコンポーネントライブラリは多くありませんでしたが、現在は<a href="https://vuetifyjs.com/en/">Vuetify</a>や<a href="https://quasar.dev/">Quasar</a>などのライブラリが対応しており、選択肢が広がっています</p> <h2 id="これ以上の設計方針は決めなかった">これ以上の設計、方針は決めなかった</h2> <p>ほかにも開発方針として</p> <ul> <li>コンポーネントの分割方針をどうするか</li> <li>Composition API(コンポーネントとロジックの分離)をどう活用するか</li> <li>社内スタッフ向け、ホテル向けと2面ある管理画面のUIでコンポーネントを共用するのか</li> </ul> <p>など、初期に決めるべきことはたくさんあったのですが、機能開発をいち早く進めるためにこれらの方針を明確に定めずに開発を進めてしまいました。</p> <p>振り返ると、これはとても良くない判断で、むしろ早く作るためにもっとじっくり設計や開発方針を練るべきだったと考えています。</p> <h1 id="初期ローンチ後の課題">初期ローンチ後の課題</h1> <ul> <li>2022年4月~9月 ... 初期開発</li> <li>2022年12月~2023年3月 ... 大きめな機能追加</li> </ul> <p>を経て、その後も機能追加や改善を続けていくことになったのですが、機能追加の際に以下のような課題を感じました。</p> <ul> <li>新たにコンポーネントを開発する際に迷うことが多い <ul> <li>コンポーネントのインターフェース(Props)をどう定義するか</li> <li>GraphQLのFragmentをどう使っていくべきか</li> <li>エラーメッセージをどこにどう書くか</li> </ul> </li> <li>コードの見通しが良くない <ul> <li>入力項目が多いフォーム画面のロジックを扱うcomposablesが肥大化していて、見通しが悪い</li> </ul> </li> <li>型を厳密に扱えていない <ul> <li>as, anyを使っている箇所があり、型の安全性を担保できていない記述がある</li> </ul> </li> </ul> <p>これらを踏まえて、チームメンバーとも相談をした上で中長期で開発・運用していくためにフロントエンドの設計を改善することにしました。</p> <h1 id="改善した内容">改善した内容</h1> <p>宿泊事業を成長させるためのプロジェクトという前提があるため、ビジネスとして必要な機能追加をしながら、少しずつ以下の改善を行い、現在も継続しています。</p> <h2 id="1-コンポーネント設計の見直し">1. コンポーネント設計の見直し</h2> <h3 id="ディレクトリ構成の変更">ディレクトリ構成の変更</h3> <p>前述のコンポーネントレイヤーのうち、特にobjects配下にコンポーネントが多く存在しており、見通しが悪かったため、以下のルールで分別しました。</p> <ul> <li>社内、ホテル、共通のコンポーネントを分別する構成に変更</li> </ul> <table> <thead> <tr> <th style="text-align:left;"> ディレクトリ </th> <th style="text-align:left;"> 役割 </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> inside </td> <td style="text-align:left;"> 一休社内スタッフ用の管理画面のみで使用するコンポーネント </td> </tr> <tr> <td style="text-align:left;"> accommodations </td> <td style="text-align:left;"> ホテル向けの管理画面のみで使用するコンポーネント </td> </tr> <tr> <td style="text-align:left;"> shared </td> <td style="text-align:left;"> 2つの管理画面で共用するコンポーネント </td> </tr> </tbody> </table> <h3 id="大きくなったコンポーネントの分割">大きくなったコンポーネントの分割</h3> <p>大きいものになると1コンポーネントで1,000行に近いサイズになっていて、見通しが悪かったため <strong>1コンポーネント350行程度を目安とする</strong> というガイドラインを定めてコンポーネントを分割しました。分割時にコンポーネントの依存関係を明確にするために、以下のルールで分割後に再配置をしました。</p> <pre class="code" data-lang="" data-unlink>components └objects └inside └HotelDescription └HotelDescription.vue(親コンポーネント) └components ├child1/child1.vue(親コンポーネントのみで使う子コンポーネントその1) └child2/child2.vue(親コンポーネントのみで使う子コンポーネントその2)</pre> <h3 id="Fragment-Colocationを導入してコンポーネントのインターフェースとFragmentを整理">Fragment Colocationを導入してコンポーネントのインターフェースとFragmentを整理</h3> <p>改善前はルールを敷かずにFragmentによるGraphQLクエリの共通化をしていました。 以下はコード例です。</p> <pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">fragment</span> <span class="synIdentifier">HotelFragment</span> <span class="synStatement">on</span> <span class="synType">Hotel</span> <span class="synSpecial">{</span> <span class="synIdentifier">id</span> <span class="synIdentifier">name</span> <span class="synIdentifier">description</span> <span class="synIdentifier">address</span> <span class="synIdentifier">rooms</span> <span class="synSpecial">}</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// HotelFragmentを必要とするコンポーネント</span> <span class="synComment">// idとdescriptionがあれば良いが他の情報も含んだFragmentをPropsとして要求してしまっている</span> <span class="synStatement">&lt;</span>template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span><span class="synIdentifier">{{</span> id <span class="synIdentifier">}}</span><span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span><span class="synIdentifier">{{</span> description <span class="synIdentifier">}}</span><span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>script setup lang<span class="synStatement">=</span><span class="synConstant">&quot;ts&quot;</span><span class="synStatement">&gt;</span> <span class="synStatement">interface</span> Props <span class="synIdentifier">{</span> hotel: HotelFragment <span class="synIdentifier">}</span> <span class="synStatement">&lt;</span>/script<span class="synStatement">&gt;</span> </pre> <p>これにより</p> <ul> <li>オーバーフェッチが発生していた<a href="#f-5e5e2faf" id="fn-5e5e2faf" name="fn-5e5e2faf" title="Apollo Clientのキャッシュや、GraphQLサーバのLoaderによって発生しないケースもあります">*1</a></li> <li>共通化しているFragmentの配置場所が定まっていない</li> </ul> <p>という課題があったため、Fragment Colocationを導入しました。</p> <p>Fragmentによるデータの宣言を強制しているRelayの設計を参考に、以下のようなルールでコンポーネントのインターフェースとFragmentを扱うようにしています。</p> <ul> <li>Fragmentファイルは利用するコンポーネントと同階層に配置する</li> <li>コンポーネントのインターフェース(Props)はFragmentの型で定義する</li> <li>Fragment名は「コンポーネント名 + GraphQLスキーマの型名」で命名する</li> </ul> <p>改善後のファイル配置とコード例はこんな形です。</p> <pre class="code" data-lang="" data-unlink>components └objects └inside └HotelDescription(コンポーネントのディレクトリ) ├HotelDescription.vue(ホテルの説明文を表示するコンポーネント) └HotelDescription_Hotel.frag.graphql(コンポーネントが利用するFragment)</pre> <ul> <li>コンポーネントのインターフェース</li> </ul> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">&lt;</span>script setup lang<span class="synStatement">=</span><span class="synConstant">&quot;ts&quot;</span><span class="synStatement">&gt;</span> <span class="synStatement">interface</span> Props <span class="synIdentifier">{</span> hotel: HotelDescriptionHotelFragment <span class="synIdentifier">}</span> <span class="synStatement">&lt;</span>/script<span class="synStatement">&gt;</span> </pre> <ul> <li>Fragment</li> </ul> <pre class="code lang-graphql" data-lang="graphql" data-unlink><span class="synType">fragment</span> <span class="synIdentifier">HotelDescriptionHotel</span> <span class="synStatement">on</span> <span class="synType">Hotel</span> <span class="synSpecial">{</span> <span class="synIdentifier">id</span> <span class="synIdentifier">description</span> <span class="synSpecial">}</span> </pre> <p>プロジェクトで利用しているGraphQL Code GeneratorのClient PresetではFragment Maskingという機能が提供されていて、これによってFragmentで取得するフィールドは利用するコンポーネント以外からは参照できないように隠蔽化もできますが、まだこの機能は有効にしていません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fthe-guild.dev%2Fgraphql%2Fcodegen%2Fplugins%2Fpresets%2Fpreset-client%23how-to-disable-fragment-masking" title="client-preset – GraphQL Code Generator" 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://the-guild.dev/graphql/codegen/plugins/presets/preset-client#how-to-disable-fragment-masking">the-guild.dev</a></cite></p> <h2 id="2-業務処理composablesの分割">2. 業務処理(composables)の分割</h2> <p>Vue.jsのComposition APIの設計に沿って、コンポーネント内のロジックをcomposablesに書いていく方針で進めていましたが、入力内容が多いフォームの画面では</p> <ul> <li>登録や変更処理などのふるまい</li> <li>フォームの初期状態</li> <li>Validation</li> </ul> <p>などが1箇所に書かれており、記述量が多く見通しが悪くなっていました。</p> <p>これを解決するために、ルートに <code>lib/domain</code> というディレクトリを設置して</p> <ul> <li>フォームの初期状態</li> <li>Validation</li> </ul> <p>を分離する設計に変更しました。</p> <pre class="code" data-lang="" data-unlink>lib └domain └Hotel ├HotelForm.ts └HoetlValidator.ts</pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// HotelForm.ts</span> <span class="synStatement">export</span> <span class="synStatement">type</span> HotelForm <span class="synStatement">=</span> <span class="synIdentifier">{</span> name?: Scalars<span class="synIdentifier">[</span><span class="synConstant">'String'</span><span class="synIdentifier">]</span> description?: Scalars<span class="synIdentifier">[</span><span class="synConstant">'String'</span><span class="synIdentifier">]</span> ... <span class="synIdentifier">}</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// HotelValidator.ts</span> <span class="synStatement">export</span> <span class="synStatement">function</span> useHotelValidator<span class="synStatement">(</span>form: HotelForm<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> descriptionCheck <span class="synStatement">=</span> <span class="synStatement">(</span>description: <span class="synType">string</span><span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synComment">// descriptionに対するチェック処理</span> <span class="synIdentifier">}</span> <span class="synType">const</span> rules <span class="synStatement">=</span> computed<span class="synStatement">&lt;</span>FormRules<span class="synStatement">&gt;(()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> description: <span class="synIdentifier">[</span> <span class="synIdentifier">{</span> validator: descriptionCheck<span class="synStatement">,</span> trigger: <span class="synConstant">'change'</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">,</span> <span class="synIdentifier">]</span><span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> rules<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// composables</span> <span class="synStatement">export</span> <span class="synStatement">function</span> useHotel<span class="synStatement">()</span> <span class="synIdentifier">{</span> <span class="synComment">// HotelFormの初期化</span> <span class="synType">const</span> hotelForm: HotelForm <span class="synStatement">=</span> reactive<span class="synStatement">(</span><span class="synIdentifier">{</span> name: <span class="synType">undefined</span><span class="synStatement">,</span> description: <span class="synType">undefined</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synComment">// Hotelに関する業務処理</span> ... <span class="synStatement">return</span> <span class="synIdentifier">{</span> validationRules: useHotelValidator<span class="synStatement">(</span>form<span class="synStatement">)</span>.rules<span class="synStatement">,</span> <span class="synIdentifier">}</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// validationを使うFormを持つVueコンポーネント</span> <span class="synStatement">&lt;</span>template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>Form :model<span class="synStatement">=</span><span class="synConstant">&quot;form&quot;</span> :rules<span class="synStatement">=</span><span class="synConstant">&quot;validationRules&quot;</span> <span class="synStatement">&gt;</span> ... <span class="synStatement">&lt;</span>/Form<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/template<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>script setup lang<span class="synStatement">=</span><span class="synConstant">&quot;ts&quot;</span><span class="synStatement">&gt;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> useHotel <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'./composables'</span> <span class="synType">const</span> <span class="synIdentifier">{</span> form<span class="synStatement">,</span> validationRules<span class="synStatement">,</span> <span class="synIdentifier">}</span> <span class="synStatement">=</span> useHotel<span class="synStatement">()</span> <span class="synStatement">&lt;</span>/script<span class="synStatement">&gt;</span> </pre> <h2 id="3-型安全に開発できるように厳しいlint設定に変更">3. 型安全に開発できるように厳しいlint設定に変更</h2> <p>初期開発時はeslint, prettierによるコードフォーマット、型検査は導入していましたが、非nullアサーション(!)や型アサーションによるasやany型の利用を制限していませんでした。</p> <p>この結果、本来は型ガードやアサーション関数を使って型を保証するべきところを!, asを使ってコンパイルエラーを回避したり、any型を不用意に使うケースが出てきてしまいました。 (以下でもasやanyの危険性について語られていて、TypeScriptによる型の安全性を享受するために避けるべき、と書かれています)</p> <p><a href="https://qiita.com/uhyo/items/aae57ba0734e36ee846a">&#x6557;&#x5317;&#x8005;&#x306E;TypeScript #TypeScript - Qiita</a></p> <p>これを踏まえて</p> <ul> <li>非nullアサーション</li> <li>型アサーション</li> <li>any型</li> </ul> <p>の利用箇所を撲滅してlintで制限することにしました。小さい単位で作業を分割して進められるように</p> <ol> <li>修正対象箇所がわかるようにwarningを出すようにlintを変更</li> <li>地道にwarningが出なくなるように書き換え</li> <li>warningがなくなったらlint設定を変更してerrorにしてCIで止まるようにする</li> </ol> <p>というステップで作業を実施しました。</p> <p><figure class="figure-image figure-image-fotolife" title="asの撲滅のためのpull request"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20231212/20231212204846.png" width="1161" height="491" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>asの撲滅のためのpull request</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="CIで止まるようにlintでエラーになるようにするpull request"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20231212/20231212210025.png" width="1150" height="803" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>CIで止まるようにlintでエラーになるようにするpull request</figcaption></figure></p> <p>非nullアサーションは完全に撲滅できましたが、型アサーションとanyの利用は改善の途中です。</p> <h2 id="4-秩序を保てる開発体制ドキュメントの整備">4. 秩序を保てる開発体制、ドキュメントの整備</h2> <p>1~3でだいぶコードに秩序がある状態になりましたが、今後の開発によって悪化しないように以下を実施しました。</p> <ul> <li>コードレビューの強化 <ul> <li>CODEOWNERによるレビューを必須にして、定めた設計方針に沿った内容になっているかを識者がレビューする体制に</li> </ul> </li> </ul> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fja%2Frepositories%2Fmanaging-your-repositorys-settings-and-features%2Fcustomizing-your-repository%2Fabout-code-owners" title="コードオーナーについて - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners">docs.github.com</a></cite></p> <ul> <li>ドキュメントの整備 <ul> <li>コンポーネントのレイヤーと役割、Fragmentの利用方針、スタイルガイドなどをリポジトリのWikiにまとめて開発やレビューでの指摘に活用</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="Wiki(抜粋)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kentana20/20231212/20231212210928.png" width="861" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Wiki(抜粋)</figcaption></figure></p> <h1 id="現在と今後">現在と今後</h1> <p>これらを積み重ねた結果</p> <ul> <li>components配下はかなり見通しがよくなり、秩序がある状態になった</li> <li>設計・開発をする際の指針ができており、レビューも指摘しやすくなった</li> </ul> <p>など、改善の効果を感じています。先月末~現在にかけて、新機能を開発しているのですが、フロントエンドの開発はとてもスムーズで、迷うことがほぼなくなってきました。</p> <h2 id="今後やりたいこと">今後やりたいこと</h2> <p>引き続きプロジェクトを進めながら改善を続ける状態を維持したいと思っています。 具体的に考えている大きめの改善テーマとしては</p> <ul> <li>@Vue/apollo(@vue/apollo-composable)の脱却 <ul> <li>v4のβ期間が長く、バージョンアップによって意図しない不具合が入ったことがあるため、別のGraphQLクライアントへの変更を検討中</li> </ul> </li> <li>E2Eテストの整備 <ul> <li>機能追加・変更時のリグレッションテストを効率的に行うため、Playwrightを導入してE2Eテストを整備、CIに組み込む予定で改善中</li> </ul> </li> </ul> <p>などがあります。</p> <h2 id="改善を継続するためのポイント">改善を継続するためのポイント</h2> <ul> <li>プロジェクトで開発する際に違和感を感じたら、熱量があるうちにIssueにする(コードレビューや開発しながらやるとよい)</li> <li>上がったIssueを開発者で議論・認識合わせをしておく</li> <li>機能開発とセットで改善することを常に考える</li> </ul> <p>改善のネタを常に仕込んでおいて、機能開発をする際に「あ、あれ一緒にやりません?」みたいな形で組み込んで機能追加とシステム改善を同時にやっていくのが理想だと考えています。</p> <h1 id="まとめ">まとめ</h1> <p>一休.com宿泊の管理画面のフロントエンド設計について、開発初期から現在までの変遷と今後について紹介しました。 本来は開発初期に決めておくべき内容を決めなかったことでローンチ後に改善することになってしまいましたが、まだシステムが大きくないタイミングで改善を進められたことは良かったと思っています。</p> <p>一緒に改善を進めてくれているチームメンバーにとても感謝しています。</p> <p>今後もこのシステムで</p> <ul> <li>大きなビジネス成果につなげる</li> <li>中長期で開発・運用していけるシステムにする</li> </ul> <p>を両立してやっていけるように、引き続きやっていきたいと思います。</p> <h1 id="おわりに">おわりに</h1> <p>一休では、技術的にも妥協せず、事業の成果をともに目指せる仲間を募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>まずはカジュアル面談からお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> <p>明日はtak-ondaの「一休レストランで Next.js App Router から Remix に乗り換えた話」です。お楽しみに!</p> <div class="footnote"> <p class="footnote"><a href="#fn-5e5e2faf" id="f-5e5e2faf" name="f-5e5e2faf" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">Apollo Clientのキャッシュや、GraphQLサーバのLoaderによって発生しないケースもあります</span></p> </div> Thu, 14 Dec 2023 09:11:16 +0900 hatenablog://entry/6801883189066199996 ADR を1年間書いてみた感想 https://user-first.ikyu.co.jp/entry/2023/12/13/115112 <p>宿泊開発チームでエンジニアをしている <a href="https://x.com/kosuke_yellow">@kosuke1012</a> です。チームで ADR を書き始めて1年くらい経ったので、その感想を書いてみたいと思います。</p> <p>この記事は <a href="https://qiita.com/advent-calendar/2023/ikyu">&#x4E00;&#x4F11;.com&#x306E;&#x30AB;&#x30EC;&#x30F3;&#x30C0;&#x30FC; | Advent Calendar 2023 - Qiita</a> の13日目の記事です。</p> <h1 id="ADRとは">ADRとは</h1> <p>アーキテクチャ・ディシジョン・レコードの略で、アーキテクチャに関する意思決定を軽量なテキストドキュメントで記録していくものです。</p> <p>出典はこちらで、</p> <ul> <li><a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions">Documenting Architecture Decisions</a></li> </ul> <p>わかりやすい和訳は以下の記事が、</p> <ul> <li><a href="https://cloud.google.com/architecture/architecture-decision-records?hl=ja">&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x6C7A;&#x5B9A;&#x30EC;&#x30B3;&#x30FC;&#x30C9;&#x306E;&#x6982;&#x8981; &nbsp;|&nbsp; Cloud &#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3; &#x30BB;&#x30F3;&#x30BF;&#x30FC; &nbsp;|&nbsp; Google Cloud</a></li> <li><a href="https://developer.mamezou-tech.com/blogs/2022/04/28/adr/">&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x30FB;&#x30C7;&#x30B7;&#x30B8;&#x30E7;&#x30F3;&#x30FB;&#x30EC;&#x30B3;&#x30FC;&#x30C9;&#x306E;&#x52E7;&#x3081; | &#x8C46;&#x8535;&#x30C7;&#x30D9;&#x30ED;&#x30C3;&#x30D1;&#x30FC;&#x30B5;&#x30A4;&#x30C8;</a></li> <li><a href="https://qiita.com/fuubit/items/dbb22435202acbe48849">&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x306E;&#x300C;&#x306A;&#x305C;&#xFF1F;&#x300D;&#x3092;&#x8A18;&#x9332;&#x3059;&#x308B;&#xFF01;ADR&#x3063;&#x3066;&#x306A;&#x3093;&#x305E;&#x3084;&#xFF1F; #&#x8A2D;&#x8A08; - Qiita</a></li> </ul> <p>事例は以下の記事が分かりやすかったです。</p> <ul> <li><a href="https://blog.studysapuri.jp/entry/architecture_decision_records">&#x301C;&#x305D;&#x306E;&#x610F;&#x601D;&#x6C7A;&#x5B9A;&#x3092;&#x523B;&#x3081;&#x301C;&#x300C;&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x30FB;&#x30C7;&#x30B7;&#x30B8;&#x30E7;&#x30F3;&#x30FB;&#x30EC;&#x30B3;&#x30FC;&#x30C9;(ADR)&#x300D;&#x3092;&#x5229;&#x7528;&#x3057;&#x305F;&#x8A2D;&#x8A08;&#x306E;&#x8A18;&#x9332; - &#x30B9;&#x30BF;&#x30C7;&#x30A3;&#x30B5;&#x30D7;&#x30EA; Product Team Blog</a></li> </ul> <h1 id="ADRを導入したねらい">ADRを導入したねらい</h1> <p>機能を追加したり改修したりする際は、チーム外のメンバー含む様々な人との議論を経て、仕様やアーキテクチャが決定されていくと思います。</p> <p>そうした議論を経た最終的な決定は実際のプロダクトやアーキテクチャ図などに表現されるのですが、「どうしてそのような仕様やアーキテクチャになっているのか」と言った部分を後から知りたくなったりすることがありました。</p> <p>これは ADR で解決したい課題そのものと言って良いものなので、チームで ADR を書いていってみよう!という話になりました。</p> <h1 id="採用したフォーマット">採用したフォーマット</h1> <p>いろいろなフォーマットがあるようなのですが、まずは以下のようなフォーマットで記載しました。</p> <pre style="white-space: pre-wrap; word-break: break-all"> # タイトル タイトルには、一目で論点がわかるタイトルを記載します。可能な限り具体的で、それでいて簡潔なタイトルを心がけると良さそうです。(これが難しい) # ステータス draft, proposed, accepted, rejected, deprecated, superseded 原典のフォーマットには draft はありませんが、この段階で決定を除いて記載しておいて、MTG で決定みたいに進めたいシチュエーションがあったので、追加してみました。 proposed で一旦完成で、チーム(またはチーム間)で合意ができたら accepted にするのが良いかと思います。 別な議論などで決定が覆された場合、当該 ADR の決定を修正するのではなく、当該 ADR (ADR: 1 とする) のステータスを ( rejected: ADR: 2 に伴い ) とした上で、別途新しく ADR を起こし ( ADR: 2 とする )、そのステータスを (proposed: ADR: 1は破棄 ) などとすると良いです。 # コンテキスト コンテキストには、その ADR の決定が求められている背景や、対応案、対応案に対する評価を記載します。 # 決定 コンテキストを踏まえた決定を、受動的ではなく、肯定的かつ能動的に記載します。 # 影響 この決定の結果生じる影響を記載します。これは、決定の結果得られるメリットのほか、コンテキストで記載した対案を選択しなかった故のデメリットであったりも記載すると良いと思いました。 また、決定の結果、今後チームで意識しなければならないことであったり、改めて必要になる機能やその ADR を記載しても良いと思います。 </pre> <p> <a href="https://github.com/joelparkerhenderson/architecture-decision-record/blob/ab49e5ecad09b0e80c6ebeaf4f41c7958a2ad291/locales/en/templates/decision-record-template-by-michael-nygard/index.md">Michael Nygard さんのフォーマット</a>そのままに draft ステータスだけを追加しています。「ADR を書くときのコツ」の項で後述しますが、draft ステータスは結論が決まっていない段階で ADR を書くのに便利です。このフォーマットで1年運用してみましたが、必要十分だなという感じでした。</p> <h1 id="ADRの格納場所">ADRの格納場所</h1> <p>私のチームではドキュメントシステムに Confluence を利用していたので、 ADR もそこに記載していきました。そのほかの選択肢としては、プロダクトの GitHub のリポジトリに置く案もあったのですが、そうするとプロダクトを横断する ADR や、具体的なプロダクトが決まっていない柔らかい段階での ADR の置き場に困ったりするので、 Confluence に落ち着きました。</p> <p>ADR は自分たち以外のいくつかのチームでも書くようになったのですが、その管理方法はチームによりけりでした。<br/> 例えば <a href="https://user-first.ikyu.co.jp/entry/2023/11/09/175121">GitHub Projects &#x3092;&#x5229;&#x7528;&#x3057;&#x305F;&#x30BF;&#x30B9;&#x30AF;&#x7BA1;&#x7406; - &#x4E00;&#x4F11;.com Developers Blog</a> のチームでは、ADR 専用のリポジトリを作った上で、GitHub Issues に記載していったようでした。これなら先述の問題はクリアできています。<br/> プロジェクト管理に GitHub Projects を用いている場合は GitHub に一元化することが出来て相性も良いため、GitHub Issues に記載していく方法が良いかもです。</p> <h1 id="書いてみたADRの例">書いてみたADRの例</h1> <h3 id="個々のADR">個々のADR</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/woodcock/20231211/20231211200121.png" width="658" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="ADRに番号を振ってプロダクトや案件ごとにまとめています">ADRに番号を振ってプロダクトや案件ごとにまとめています</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/woodcock/20231211/20231211123012.png" width="816" height="431" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="書いてみてよかったところ">書いてみてよかったところ</h1> <p>ADR を書いてみてよかったことをいくつか書いてみます。</p> <h3 id="1-ここの設計どうしてこうなってたんだっけに困らない">1. 「ここの設計どうしてこうなってたんだっけ?」に困らない</h3> <p>ADR を書いた1番のモチベーションです。これが解消するのは非常に助かりました。自チームだけではなく、他チームが困っているときに「スッ…」と ADR をスマートに差し出すこともできました。</p> <h3 id="2-議論の効率が上がる">2. 議論の効率が上がる</h3> <p>以下の複数のポイントで、開発する中での議論の効率が上がりました。</p> <h4 id="議論が蒸し返らない">議論が蒸し返らない</h4> <p>1.とも重なるのですが、議論になるような仕様上/設計上のポイントでその背景を思い出すのに手間取ったり、(新事実が見つからない限りは)「やっぱりこっちの方がいいのでは」みたいな話にならないので、議論の効率が上がります。</p> <h4 id="意思決定するべきことが明確になる">意思決定するべきことが明確になる</h4> <p>「ADR を書くときのコツ」項で後述するのですが、あらかじめ draft の状態で ADR を記載しておくことで、意思決定しなければいけない項目が明確になり、議論の中であいまいにせず意思決定するようになり、議論の効率が上がります。</p> <h4 id="意思決定したことが明確になる">意思決定したことが明確になる</h4> <p>ADR を導入してから、MTG の最後に「hoge の件 ADR に書いておきましょう」といった会話が増えました。これによって、意思決定したことをクリアに言語化することになり、議論の効率が上がります。</p> <h4 id="仕様検討決定までのフレームワークができる">仕様検討~決定までのフレームワークができる</h4> <p>チームで議論が必要になった際に「じゃ、ADR 書いてまとめておきましょう」という流れができるのが結構良く、検討の中心となるメンバーが増えたり変わったりしてもフレームワークに沿って進めることで議論のレベルを保ちやすくなります。</p> <h3 id="3-新規メンバーが立ち上がりやすくなる">3. 新規メンバーが立ち上がりやすくなる</h3> <p>新しく参画したメンバーが疑問に思うであろうポイントに ADR があるケースが多いので、キャッチアップしやすいという意見も上がりました。</p> <h1 id="ADR-を書くときのコツ">ADR を書くときのコツ</h1> <p>良い ADR を書くのには割とコツがあることがわかってきたので、気づいた点を書いてみます。</p> <h3 id="1-タイトルは体言止めにせず文にする">1. タイトルは体言止めにせず、文にする</h3> <p>「hoge について」や「hoge の設計」など、体言止めにするのではなく、「hoge は fuga とする」といったように、タイトルを文にします。</p> <p>こうすると、タイトルをみるだけで内容が一発でわかるほか、ADR を書く際にも論点がクリアになり、記載や議論の効率があがりました。</p> <h3 id="2-結論が決まっていなくても-ADR-を書きはじめてしまう">2. 結論が決まっていなくても ADR を書きはじめてしまう</h3> <p>結論が決まっていない段階であっても ADR を書きはじめることで、何を決める必要があるのかが明確になってよかったです。<br/> 未定のところは実際に hoge などと書いておいて、それを元に議論して、決定事項で hoge を埋める感じです。</p> <h3 id="3-コンテキストを-SCQA-フォーマットで書く">3. コンテキストを SCQA フォーマットで書く</h3> <p>コンテキストの章で、結論に至るまでのギャップをいかに埋めるかというのが大切なのですが、これが慣れるまで結構難しいです。</p> <p>その際のフォーマットとして、SCQA というのが有用でした。『考える・書く技術』という本で紹介されているフォーマットなのですが、</p> <ul> <li>S: Situation 状況</li> <li>C: Complication 複雑化</li> <li>Q: Question 疑問</li> <li>A: Answer 答え</li> </ul> <p>Situation でまず状況の説明をして、それに続く Complication で、今回の Question やその Answer が必要になるトリガーを説明します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.diamond.co.jp%2Fbook%2F9784478490273.html" title="[新版]考える技術・書く技術" 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://www.diamond.co.jp/book/9784478490273.html">www.diamond.co.jp</a></cite></p> <p>上で記載した ADR の例で行くと、</p> <h4 id="S">S:</h4> <blockquote><p>(Slack リンク) での記載の通り、未付与のトランザクションに対して、PayPayの取消が発生することは考えられる。</p></blockquote> <h4 id="C--Q-">C ~ Q :</h4> <blockquote><p>この場合に、<br/> 1. 新たに取消トランザクションを作成した上で、新規と取消のトランザクションを見て付与取消バッチに判断してもらうのか<br/> 2. 既存のトランザクションを論理削除するのか<br/> の2通りの対応がありうるが、どちらにするか。<br/> 1.のメリットとしては、<br/> ...<br/> などのメリットがある一方で、<br/> ...<br/> というデメリットはある。</p></blockquote> <p>のような感じです。</p> <p>このフレームワークは、ADRに限らず、割と複雑な PR の Description を書く際にも有用だなと思いました。 ちなみに SCQA フォーマットは『スタッフエンジニア』という本でも紹介されていて(私もそれで知りました)、<br/> 曰く、</p> <blockquote><p>多くの議論で、冒頭の段落が巧みに構成されているだけで重要な対話に火が灯る。</p></blockquote> <p>だそうです。シビれますね。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbookplus.nikkei.com%2Fatcl%2Fcatalog%2F23%2F04%2F07%2F00760%2F" title="<5/8新刊>『スタッフエンジニア マネジメントを超えるリーダーシップ』" 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://bookplus.nikkei.com/atcl/catalog/23/04/07/00760/">bookplus.nikkei.com</a></cite></p> <h3 id="4-コンテキストにもうほぼ結論の手前まで書いてしまう">4. コンテキストに、もうほぼ結論の手前まで書いてしまう</h3> <p>前述の通り、コンテキストで背景の共有 → 問題意識の共有、と進めた上で「決定」の項目で結論を書くのですが、コンテキストにどこまで書くかというのが悩みどころです。</p> <p>これは好みもありますが、もうほぼほぼ結論の手前まで「コンテキスト」の項目に書いてしまえば良いと思いました。</p> <p>コンテキストで結論の手前まで書いた結果、読み手が「決定」を読んだ感想としては『でしょうね~』となるくらいまで書いてしまって良いのではないかなと思います。</p> <h3 id="5-とにかく軽量にする">5. とにかく軽量にする</h3> <p>優先順位として、<br/> <strong>開発する中での重要な意思決定の記録を漏らさないこと > リッチな ADR を書くこと </strong><br/> として、1つ1つの ADR を軽量にして、記載するハードルを下げることを意識すると良さそうです。</p> <p>1つ1つの ADR にあまり力を入れすぎると、だんだんと書かなくなっていってしまうことがありました。</p> <h3 id="6-アーキテクチャに限らず仕様上の決定も-ADR-に記載していく">6. アーキテクチャに限らず、仕様上の決定も ADR に記載していく</h3> <p>ADR はアーキテクチャ以外の決定の記録にも有用でした。それらの決定が、アーキテクチャ上の決定に影響を与えることもあるため、同じ ADR として並べて管理しておくと便利でした。</p> <h1 id="ADR-では足りないところ">ADR では足りないところ</h1> <p>ここまで説明してきた ADR ですが、それだけでは足りないなと思う部分もありました。</p> <h3 id="検討する単位が大きいものを1つのADRで書こうとするのは厳しい">検討する単位が大きいものを1つのADRで書こうとするのは厳しい</h3> <p>「hogehoge の仕様検討、といった粒度のものを一つの ADR がチームで出てきたのですが、決める論点が多かったり、発散したりしてしまってあまりうまくいかなかった」という意見がありました。</p> <p>「ADR を書くときのコツ」の項に「タイトルは体言止めにせず、文にする」「とにかく軽量にする」と記載しましたが、逆に言うと、これが出来ないようなテーマについては、ADR には向かないのではないかと思いました。</p> <h3 id="全体として今どうなっているのかを示すドキュメントはADR-とは別にほしい">「全体として今どうなっているのか」を示すドキュメントはADR とは別にほしい</h3> <p>ADR は、ここの意思決定やその背景を記述するドキュメントですが、それに加えて、やはり「全体として今どのような設計になっているのか」といったドキュメントは必要だなと思いました。いわゆる Design Docs がそれにあたると思います。</p> <p>Design Docs があり、その個々の設計に至った意思決定やその背景がADRとして残されていると理想的なのではないかと思います。全体としての What を Design Docs に記載して、Why を ADR でサポートするイメージでしょうか。</p> <h1 id="Design-Docs-とのすみわけ">Design Docs とのすみわけ</h1> <p>Design Docs には、Why に答える項目を含めたフォーマットもあったりするので、チームの中で ADR と Design Docs のすみわけの指針がそろっていると良さそうです。一つの観点として「Design Docs が実装でのフィードバックに基づいて継続的に更新される性質を持ち、一方でADRはスナップショットである」という性質の違いがありそう、との意見が出ました。</p> <p>以上から、Design Docs と ADR の性質の違いをまとめてみます。</p> <table> <thead> <tr> <th></th> <th>反映するもの</th> <th>時間軸</th> <th>答える対象</th> </tr> </thead> <tbody> <tr> <td>Design Docs (特に実装以後) </td> <td>実装</td> <td>今</td> <td>What</td> </tr> <tr> <td>ADR</td> <td>意思決定</td> <td>スナップショット</td> <td>Why</td> </tr> </tbody> </table> <p>表中 Design Docs と ADR としてまとめていますが、必ずしもそれぞれのフォーマットでフルに記載する必要はないかもしれません。 例えば ADR はログの形で簡易的に記載していったり、逆に Design Docs も必要な部分だけ記載する、といった判断もあるかもしれません。</p> <p>これらの項目があることを考慮しておくと、必要十分なドキュメントを用意していけるのではないかと思いました。</p> <h1 id="さいごに">さいごに</h1> <p>一休では、ともに試行錯誤しながらよいサービスを作ってくれる仲間を募集しています! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ikyu.co.jp%2Frecruit%2Fengineer%2F" title="エンジニア採用 - 株式会社一休" 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://www.ikyu.co.jp/recruit/engineer/">www.ikyu.co.jp</a></cite></p> <p>カジュアル面談も実施していますので、ぜひお気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fikyu%2Fjobs%2F1745000651779629061" title="【エンジニア】カジュアル面談応募フォーム | 株式会社一休" 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://hrmos.co/pages/ikyu/jobs/1745000651779629061">hrmos.co</a></cite></p> Wed, 13 Dec 2023 11:51:12 +0900 hatenablog://entry/6801883189065866348