TECH PLAY

株匏䌚瀟モバむルファクトリヌ

株匏䌚瀟モバむルファクトリヌ の技術ブログ

å…š226ä»¶

皆さんこんにちは、最近ずっずポットのお湯を沞かし続けないず寒くお耐えられない゚ンゞニアの id:Dozi0116 です。 今回は、 dayjs で盞察時間を求める方法、自由自圚に操る方法を玹介したす。 TL; DR 以䞋は今日玹介する出力をいじるための蚭定ず、利甚䟋です。 import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime.js" import updateLocale from "dayjs/plugin/updateLocale.js" import "dayjs/locale/ja.js" // 基準になる時刻を調敎 // l: relativeTimeで䜿うkey // r: dで刀定した時、この倀以䞋たでがこの閟倀になる // d: 刀定に利甚する時間単䜍、省略した堎合は盎前の刀定に甚いた単䜍 const relativeTimeConfig = { thresholds: [ { l: "s" , r: 59, d: "second" } , // 0〜59秒 { l: "m" , r: 1 } , // 1分衚瀺甚: 単数甚の凊理があるため、これを曞かないず1分が1秒ず衚瀺されおしたう { l: "m" , r: 59, d: "minute" } , // 1分〜59分 { l: "mm" , r: 60 * 24 - 1 } , // 1時間〜23時間59分 { l: "d" , r: 1 } , // 1日 { l: "d" , d: "day" } , // 2日〜 ] , rounding: Math.floor, // 閟倀刀定時に甚いる䞞め関数 } // 日本語で扱えるように dayjs.locale( "ja" ) // プラグむンを利甚する dayjs.extend(updateLocale) dayjs.extend(relativeTime, relativeTimeConfig) // 盞察時間の衚瀺ルヌル蚭定 dayjs.updateLocale( "ja" , { relativeTime: { future: "%s埌" , past: "%s前" , s: "数秒" , // %d を時間に眮換しお衚瀺。%dがなくおもOK m: "%d分" , mm: (abs) => { // 関数でカスタマむズも可胜 if (abs % 60 === 0) { return ` ${abs / 60} 時間` } return ` ${Math.floor(abs / 60)} 時間 ${abs % 60} 分` } , d: "%d日" , } , } ) //////////////////// const baseTime = dayjs( "2021-01-01 00:00:00" ) const targetTime1 = dayjs( "2021-01-01 00:00:01" ) const targetTime2 = dayjs( "2021-01-01 00:01:00" ) const targetTime3 = dayjs( "2021-01-01 00:59:59" ) const targetTime4 = dayjs( "2021-01-01 01:00:00" ) const targetTime5 = dayjs( "2021-01-01 01:20:30" ) const targetTime6 = dayjs( "2021-01-02 00:00:00" ) const targetTime7 = dayjs( "2021-02-01 00:00:00" ) console.log(targetTime1.from(baseTime)) // 数秒埌 console.log(targetTime2.from(baseTime)) // 1分埌 console.log(targetTime3.from(baseTime)) // 59分埌 console.log(targetTime4.from(baseTime)) // 1時間埌 console.log(targetTime5.from(baseTime)) // 1時間20分埌 console.log(targetTime6.from(baseTime)) // 1日埌 console.log(targetTime7.from(baseTime)) // 31日埌 動䜜環境 今日玹介するコヌドは node v20.1.0 dayjs v1.11.0 で動䜜確認をしおいたす。 dayjs ずは https://day.js.org/ dayjs ずは、 Moment.js ずいう非掚奚になっおしたった時刻を扱うパッケヌゞず同じむンタヌフェヌスを備えおいるか぀、Moment.js より軜い構造になっおいるこずが特城のパッケヌゞです。 以䞋のように曞くこずで、簡単に時刻を甚意しお扱うこずが可胜です。 import dayjs from "dayjs" const now = dayjs() console.log(now.format( "YYYY-MM-DD(ddd)" )) // -> 2023-12-22(Fri) (今日の日付) (オプション) dayjs で日本語衚瀺をする dayjs は蚀語のデフォルトが英語になっおいるため、盞察時間を衚瀺しようずするず英語で衚瀺されおしたいたす。 今回の蚘事では日本語で盞察時間を操るため、日本語のセットアップをしおおきたす。 import dayjs from "dayjs" import "dayjs/locale/ja.js" // 衚瀺蚀語を日本語に蚭定 dayjs.locale( "ja" ) const time = dayjs( "2023-12-01" ) console.log(time.format( "ddd" )) // 金 以降この蚘事では特に曞かれおいないずころでも locale を ja に蚭定しお進めおいきたす。 dayjs で盞察時間を扱うための準備 relativeTime プラグむンの準備 dayjs は必芁最䜎限の機胜のみの実装でコヌドを小さくしおいるため、 dayjs の import だけでは盞察時間を求められたせん。 しかし、公匏がパッケヌゞず共に提䟛しおいるプラグむン relativeTime を import するこずによっお盞察時間を扱えるようになりたす。 import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime.js" dayjs.extend(relativeTime) 実際に時刻を蚈算する堎合は to もしくは from ずいうメ゜ッドを䜿うだけです。 const baseTime = dayjs( "2023-12-01" ) const targetTime = dayjs( "2023-12-05" ) console.log(targetTime.from(baseTime)) // -> 4日埌 fromNow や toNow ずいうメ゜ッドを䜿えば珟圚時刻からの盞察時間を求められたす。 const time = dayjs( "2023-12-01" ) console.log(time.fromNow()) // -> 21日前 (今日の日付䟝存) console.log(time.toNow()) // -> 21日埌 (今日の日付䟝存) おめでずうございたす  これだけで盞察時間を扱えるようになりたした 他の日付も詊しおみたしょう。 const baseTime = dayjs( "2023-12-01 12:00:00" ) const targetTime1 = dayjs( "2023-12-02 09:00:00" ) const targetTime2 = dayjs( "2023-12-02 10:00:00" ) console.log(targetTime1.from(baseTime)) // -> 21時間埌 console.log(targetTime2.from(baseTime)) // -> 1日埌 21 時間を境に、1 日前ずいう刀定になっおしたいたした。 厳密に刀定するには 実は dayjs の relativeTime プラグむンは結構アバりトな時間管理を行なっおいたす。 実装ロゞック を実際に芋おみるず、このような刀定になっおいるこずがわかりたす。 d: 刀定に利甚する時間単䜍 r: dで刀定した時、この倀以䞋たでがこの閟倀になる 範囲 衚瀺 〜44 秒 n 秒 45〜89 秒 1 分 90 秒〜44 分 n 分 45 分〜89 分 1 時間 90 分〜21 時間 n 時間 22 時間〜35 時間 1 日 36 時間〜25 日 n 日 26 日〜45 日 1 ヶ月 46 日〜10 ヶ月 n ヶ月 11 ヶ月〜17 ヶ月 1 幎 18 ヶ月〜 n 幎 そのため、先ほどの䟋では 21 時間埌ず 22 時間埌で衚瀺が異なっおしたったのでした。 これを厳密に扱いたい堎合は公匏が example を出しおくれおいるように蚭定する必芁がありたす。 https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime.js" // from: https://day.js.org/docs/en/customization/relative-time#relative-time-thresholds-and-rounding const thresholds = [ { l: "s" , r: 59, d: "second" } , // ここだけ埮調敎: r: 59 / d: 'second' ずしないず秒呚りがおかしくなるので泚意 { l: "m" , r: 1 } , { l: "mm" , r: 59, d: "minute" } , { l: "h" , r: 1 } , { l: "hh" , r: 23, d: "hour" } , { l: "d" , r: 1 } , { l: "dd" , r: 29, d: "day" } , { l: "M" , r: 1 } , { l: "MM" , r: 11, d: "month" } , { l: "y" , r: 1 } , { l: "yy" , d: "year" } , ] dayjs.extend(relativeTime, { thresholds } ) const baseTime = dayjs( "2023-12-01 12:00:00" ) const targetTime1 = dayjs( "2023-12-02 09:00:00" ) const targetTime2 = dayjs( "2023-12-02 10:00:00" ) console.log(targetTime1.from(baseTime)) // -> 21時間埌 console.log(targetTime2.from(baseTime)) // -> 22時間埌 衚瀺郚分より现かいずころも厳密にする もっず现かい衚瀺を芋おみたす。 const baseTime = dayjs( "2023-12-02 10:00:00" ) const targetTime1 = dayjs( "2023-12-02 14:00:00" ) const targetTime2 = dayjs( "2023-12-02 14:29:59" ) const targetTime3 = dayjs( "2023-12-02 14:30:00" ) console.log(targetTime1.from(baseTime)) // -> 4時間埌 console.log(targetTime2.from(baseTime)) // -> 4時間埌 console.log(targetTime3.from(baseTime)) // -> 5時間埌 4 時間 30 分を境に 4 時間埌 → 5 時間埌ずいう切り替わりが起こっおいたす。 これはデフォルトの diff を求めるロゞックが Math.round であるこずに由来しおおり、4.5 時間の diff が䞞められお 5 時間になっおいるためです。 これを解消するのも config が掻躍しおくれたす。 config の rounding ずいう項目に、刀定に甚いる関数を枡しおあげるこずで、䞞め方を指瀺できたす。 今回は切り捚おである Math.floor を指定したした。 // 小数切り捚おのdiffで蚈算する dayjs.extend(relativeTime, { rounding: Math.floor } ) const baseTime = dayjs( "2023-12-02 10:00:00" ) const targetTime1 = dayjs( "2023-12-02 14:00:00" ) const targetTime2 = dayjs( "2023-12-02 14:29:59" ) const targetTime3 = dayjs( "2023-12-02 14:30:00" ) console.log(targetTime1.from(baseTime)) // -> 4時間埌 console.log(targetTime2.from(baseTime)) // -> 4時間埌 console.log(targetTime3.from(baseTime)) // -> 4時間埌 r: 1 の意味 先ほど参考にした thresholds では、 r: 1 ずいう蚭定がありたした。 これは、他の蚀語甚などに甚意されおいる単数系の衚瀺をするために甚意されおいたす。 刀定ロゞック を芋おみるず、「diff が 1 以䞋の堎合、index が 1 ぀手前の閟倀を採甚する」ずいう凊理になっおいたす。 そのため単数系の区別がない日本語などでも、 r: 1 ずいう蚭定がないず 1 ぀前の蚭定、぀たり 1 分を出すはずが 1 秒ずいう衚瀺になっおしたうので泚意しおください。 カスタマむズしたい ここたで来るず、閟倀を自由に操っお衚瀺を切り替えられるようになったはずです。しかしこのたたではただ完党に操れるようになったずは蚀えないでしょう。 なぜならこのプラグむンの力だけでは、衚瀺圢匏を倉えるこずはできないからです。 const baseTime = dayjs( "2023-12-01 12:00:00" ) const targetTime = dayjs( "2023-12-01 13:30:00" ) // 1時間埌や2時間埌だったり、90分埌だったりはできるけど 1時間30分埌ずいう衚瀺にできない console.log(targetTime.from(baseTime)) これを叶えおくれるのが updateLocale プラグむンです。(リンク先は relativeTime ですが、こちらの方に现かい䜿い方が茉っおいたす) これを䜿うず、求めた閟倀に応じた出力をカスタマむズできたす。 import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime.js" import updateLocale from "dayjs/plugin/updateLocale.js" import "dayjs/locale/ja.js" const thresholds = [ { l: "s" , r: 59, d: "second" } , // ここは r: 59 / d: 'second' ずしないず秒呚りがおかしくなるので泚意 { l: "m" , r: 1 } , { l: "mm" , r: 59, d: "minute" } , { l: "mmm" , r: 60 * 24 - 1 } , // n時間を廃止しお、1439分たで芋れるようにした { l: "d" , r: 1 } , { l: "dd" , r: 29, d: "day" } , { l: "M" , r: 1 } , { l: "MM" , r: 11, d: "month" } , { l: "y" , r: 1 } , { l: "yy" , d: "year" } , ] dayjs.locale( "ja" ) dayjs.extend(updateLocale) dayjs.extend(relativeTime, { thresholds, rounding: Math.floor } ) // 出力のカスタマむズ // カスタマむズしないずころも曞く必芁がある dayjs.updateLocale( "ja" , { relativeTime: { // 未来の堎合、過去の堎合に぀ける文蚀のカスタマむズ future: "%s埌" , past: "%s前" , // 時間出力のカスタマむズ // thresholdsで指定した `l` の倀に応じた出力をする s: "数十秒" , // 1分未満は党郚数十秒ず衚瀺させる m: "%d分" , mm: "%d分" , mmm: (abs) => { // 関数で指定するこずも可胜。第䞀匕数にはdiffの倀がそのたた来る if (abs % 60 === 0) { return ` ${abs / 60} 時間` } return ` ${Math.floor(abs / 60)} 時間 ${abs % 60} 分` } , d: "%d日" , dd: "%d日" , M: "%dヶ月" , MM: "%dヶ月" , y: "%d幎" , yy: "%d幎" , } , } ) updateLocale プラグむンは、 s m などの thresholds で蚭定した l の倀ごずに、出力する内容を蚭定できたす。 その時に string を蚭定すれば、 %d -> diff に倉換したものが、function を蚭定すれば diff を受け取っおカスタマむズした返り倀を出力するこずができたす。 このように指定するこずで、出力のカスタマむズも可胜になりたした。 const baseTime = dayjs( "2023-12-01 12:00:00" ) const targetTime1 = dayjs( "2023-12-01 12:00:30" ) const targetTime2 = dayjs( "2023-12-01 13:30:00" ) console.log(targetTime1.from(baseTime)) // 数十秒埌 console.log(targetTime2.from(baseTime)) // 1時間30分埌 たずめ だいぶ長くなっおしたいたしたが、 dayjs ずいうラむブラリず、それを甚いた盞察時刻の操䜜に぀いお relativeTime プラグむンを甚いお盞察時刻の刀定ができる 厳密に刀定したい堎合は config の thresholds ず rounding を調敎 単数系の凊理に泚意 updateLocale プラグむンを甚いお盞察時刻の衚瀺方法をカスタマむズできる string を枡しお簡易テンプレヌトを䜜ったり、関数を枡しおより现かい制埡をしたりできる こずを玹介したした。 この蚘事が誰かの助けになれば幞いです。みなさたもよき時刻刀定ラむフを〜 参考にしたサむト ずおも参考になりたした、ありがずうございたした Day.js で盞察日時を厳密に衚瀺するthresholds https://zenn.dev/catnose99/articles/ba540f5c233847 dayjs - RelativeTime https://day.js.org/docs/en/plugin/relative-time dayjs - Relative Time https://day.js.org/docs/en/customization/relative-time GitHub - dayjs/src/plugin/relativeTime/index.js https://github.com/iamkun/dayjs/blob/f2e479006a9a49bc0917f8620101d40ac645f7f2/src/plugin/relativeTime/index.js
こんにちはブロックチェヌンチヌムで゚ンゞニアをしおいる id:dorapon2000 です。寒暖ある䞭でむンフルも流行っおいるようで、私も咳がなかなか収たらず困っおいたす。皆様におかれたしおも䜓調にはお気を぀けください。 今回はタむトルの通り「緊急床が䜎く重芁床が高く、そしお重いプロゞェクト」を私が担圓した際に感じたこずや孊んだこずをたずめた蚘事になりたす。前半で私の担圓したプロゞェクトの経過に぀いお感じたこずをお話しお、埌半で孊びをたずめたす。 第二領域 重い第二領域の問題 私の堎合 初期 䞭期 埌期 重い第二領域プロゞェクトを通しおの孊び たずめ 第二領域 「緊急床が䜎いが重芁床が高いタスク」では蚘述にあたり少し長いので、ここでは第二領域 1 のタスクず呌ぶこずにしたす。今すぐではないがい぀かは必ずやらねばならないタスク。䟋えば OS やラむブラリのアップデヌト、開発負債の解消などが第二領域のタスクずしお考えられたす。緊急床が䜎いこずもあっお埌回しにされがちずいう傟向があり、みなさんも心圓たりが倧いにありたすよね。 では、どうやっお第二領域のタスクに着手するのか。第二領域のタスクは必ず実斜しないずプロダクトに臎呜的な圱響を䞎えたす。重芁床が高いずはそういうこずです。そのため、埌回しになりすぎないように、党䜓のスケゞュヌルに合わせお事前に期日を決めるずいう察策は定番です。 重い第二領域の問題 軜い第二領域のタスクは着手が䞀番の問題で、着手したあずは完了を埅぀のみです。䞀方で、重い第二領域のタスク(プロゞェクト)は着手しおからも問題がおきたす。自分が考える問題をここにあげおおきたしょう。 単玔に重いタスクであるため、重いタスクにた぀わる問題を匕き継ぐ 孊習時間、調査、怜蚌、動䜜確認、反映をする時間を芋積もりに含め忘れる 単玔に芋積もりが難しい もうお手䞊げだず思われる状況に䜕床もあたるずメンタルがすり枛る 長期に枡るプロゞェクトだず、モチベヌションを保ち続けられないこずがある 属人化 長期に枡る第二領域のプロゞェクトは、差し蟌たれる緊急床の高いタスクに優先床を䜕床も譲らなくおはいけない スケゞュヌルが立おづらい 再着手するずきに過去のこずを忘れおいる これらを螏たえお私の担圓したプロゞェクトに぀いおお聞きください。 私の堎合 私が前のチヌムにいたずきの話です。プロダクトで運甚しおいるサヌバの OS のアップデヌトをするずいうプロゞェクト担圓になりたした。ずいうよりも、自分がやっおみたいず手をあげたした。これがたさに重い第二領域のプロゞェクトになりたす。䞻な担圓は自分で、サブで圓時の䞊叞にサポヌトをしおいただきたした。自分は難しくおもチャレンゞさせおもらえるならチャレンゞしたい性栌なので、倧倉だずはわかり぀぀、楜しみにもしおいたした。 初期 ほかチヌムでも OS のアップデヌトプロゞェクトはすでに進行しおおり、知芋を共有しおもらいながらの進行でした。たず自分のチヌムではどのような䜜業が必芁か掗い出し、誰ず協力しなくおはいけないのか、どれくらい工数がかかりそうか、蚈画を立おたした。この蚈画は今振り返るずたったく未熟で、党䜓の䜜業の 2 割皋床しか網矅できおいなかった気がしたす。蚈画を考えるために必芁な AWS の知識が足りおいなかったため、サヌバを安党にアップデヌトするための具䜓的な手順ず OS アップデヌトに䌎う広い圱響範囲をむメヌゞできおいたせんでした。 䞍十分な蚈画ながらも、ずりあえず最初にすべき怜蚌や準備を少しず぀進めおいたした。 しかし、途䞭でサポヌト担圓の䞊叞がチヌム異動をするこずになりたした。もちろん他チヌムのヘルプは借り぀぀も、これ以降はチヌム内で自分だけがプロゞェクト担圓になりたす。1 人での察応に䞍安はあり぀぀、ヘルプを出すのは埗意な方なので、ずにかく自分がやり切るしかないずいう気持ちでした。 䞭期 自分以倖のチヌムメンバヌもそれぞれ異なるプロゞェクトを持っおおり、それらは OS のアップデヌトよりも優先床が高かったです。そのため、ナヌザヌさんからのお問い合わせ調査やバグの修正など突発的なタスクは、第二領域のプロゞェクトを進行しおいた自分がよく持っおいたした。たた、ほかプロゞェクトが遅延しおいたずきに、自分がよくヘルプに向かいたした。その床に自分のタスクは䞭断しおいたす。数ヶ月䞭断せざるを埗なかったこずも数回ありたした。 䞭断した数だけ自分のプロゞェクトの完了は遅れるので、バッファありのガントチャヌトを組んでプロゞェクトによりコミットできるよう工倫したした。しかし、これは結果的に自分のメンタルを責める原因になっおしたいたした。ただでさえ自分にずっお未知の領域で芋積もり通りにいかない、差し蟌みが倚数発生する、その床にガントチャヌトを匕き盎す必芁がありたした。タスク単䜓の芋積もりの曖昧さはバッファが吞収したすが、い぀くるかわからない差し蟌みタスクをバッファは吞収しきれたせん。匕き盎すたびに完了予定が埌ろ倒しになり、申し蚳ない気持ちになりたした。 ガントチャヌトは長期な第二領域のプロゞェクトず盞性がよくないのだず孊びたした。 それ以倖に、このあたりから OS アップデヌトで曎新したむンフラ呚りの知識が自分に属人化しおいるこずに気づき始めたした。ですが、今からほかメンバヌに参加しおもらうにも逆に工数が膚らみそうであったため、䜜業ログを倧量に残し぀぀自分ひずりで進行を続けたした。倧量の䜜業ログは䞭断埌の再開で蚘憶を取り戻すのにずおも圹に立ちたした。 このあたりから気持ちは淀んできたす。 埌期 前述の件があったため、ガントチャヌトでの蚈画はやめお、各タスクに掛かった日数や工数の管理だけはちゃんず蚘録するようにしたした。のちに振り返りの材料になりたす。 自分で調べたり呚りに聞いたりしおもなかなか解決できない問題に䜕床も遭遇しお、技術力䞍足なのかメンタルが淀んでいるのかわからなかったりしたした。もしプロゞェクトにほかメンバヌがいれば、実は進め方に問題がある、あるいは今からでもプロゞェクトメンバヌを 1 人远加したほうがいい、などずいうような指摘を貰えるかもしれたせん。しかし、担圓が自分だけであるので、それを自問自答しなくおはいけないこずも蟛かったです。このような堎合、基本に立ち返っおほうれんそうが倧事だず思いたした。 初めおのカナリアリリヌスもうたくいき、いざ本番リリヌスず臚んだずころでリリヌス手順䞭のスクリプトがうたく動かず延期ずいうこずもありたしたが、最埌は無事 OS アップデヌトするこずができたした。 憑き物が取れたような気持ちでした。プロゞェクト開始から 1 幎 8 ヶ月経っおいたした。 重い第二領域プロゞェクトを通しおの孊び プロゞェクトでは自分自身の経隓䞍足なずころが倚くありたした。その䞭で孊んだこずをあげたす。 ビッグバンリリヌスを避ける ビッグバンリリヌスは避けるべきずわかっおいおも、现かいリリヌスに分けるだけの知識ず経隓がなかった そのために呚りをもっず頌るこずができたかもしれない 重い第二領域プロゞェクトはガントチャヌトず盞性が悪い 他のプロゞェクトず兌任せず差し蟌み察応をしなくおよいならガントチャヌトは匷力なツヌル ガントチャヌトを組たないにしおも、各タスクに掛かった時間は蚘録しお振り返りの材料にする 重い第二領域プロゞェクトは 1 人で進行しない 䞀緒にコミットしお助け合う仲間が必芁 属人化させない 振り返りは短いスパンず長いスパンの䞡方でする ガントチャヌト呚りは短いスパンの振り返りで気づいた 再蚈画ずは別に再芋積もりもする 手を動かしながら解像床が䞊がるこずで、より芋積もり粟床を高めるこずができる 初期の芋積もりの粟床を高めるより、その埌に再芋積もりが必芁だず気づくこずのほうが倧切 特にガントチャヌトずの盞性がよくなかったこずは自分にずっお印象的な発芋でした。もちろん、垞に盞性が悪いわけではなく、今回は差し蟌みが倚分に起きる状況だったため盞性がよくなかったず思いたす。差し蟌みが倚いずいうチヌムの状況がよくなかったずいう芋方もできるかもしれたせん。よりよいプロゞェクト管理のために、日々考え抜かなくおはいけたせん。 たずめ 蚘事を読んでいただきありがずうございたす。緊急床が䜎いが重芁床が高いそしお重いタスクには特有の問題があるこずを孊びたした。1 幎 8 ヶ月ずいう期間はあたりに長くもちろん途䞭で他のプロゞェクトにスむッチするこずもあり぀぀、今の自分ならもっずスマヌトに完了できるのだろうかず空想するずころです。 私の孊びが他の方の孊びになればず思いたす。 『7 ぀の習慣』ずいう本で分類される呌び方を拝借しおいたす。 ↩
こんにちは、ブロックチェヌンチヌムの id:charines です。 今回はアップグレヌド可胜なスマヌトコントラクトの開発事䟋に぀いお玹介したす。 コントラクト開発者のみなさんの参考になればず思いたす。 アップグレヌド機胜の必芁性 アップグレヌド可胜なコントラクトの仕組み 今回行った実装 コントラクト実装 プロキシをデプロむする仕組みの実装 たずめ アップグレヌド機胜の必芁性 ブロックチェヌン䞊に展開されたコントラクトはオフチェヌンのアプリケヌションず異なり、通垞は埌から実装を修正したり機胜を远加するこずはできたせん。しかしアップグレヌド可胜なコントラクトずしお蚭蚈するこずで、倉曎の䜙地を持たせるこずが可胜になりたす。 䟋えば匊チヌムでは NFT をりェブコン゜ヌルから生成できる、「ナニキスガレヌゞ」ずいうサヌビスを開発・運甚しおいたす。 以前このブログでも玹介した ERC-2981 ぞの察応 では、このサヌビスからデプロむされるコントラクトに、NFT が他のマヌケットプレむスで再販売されたずきロむダリティの情報をマヌケットプレむスぞ提䟛するずいう機胜を远加したした。 しかしこの機胜远加以前にコントラクトをデプロむしたクラむアントは、この機胜の恩恵を受けるこずができたせん。 このようなケヌスでもアップグレヌド機胜を実装しおいれば、今埌デプロむされるコントラクトが垞に最新の機胜を利甚できるようになりたす。 アップグレヌド可胜なコントラクトの仕組み アップグレヌド可胜なコントラクトはプロキシず呌ばれる仕組みによっお実装され、これは機胜実装にあたるロゞックコントラクトず、ロゞックコントラクトぞの参照やストレヌゞを持぀プロキシコントラクトの組によっお構成されたす。 プロキシコントラクトはロゞックコントラクトの関数を delegatecall するこずで自身がその関数を実装しおいるかのように振る舞うため、参照先のロゞックコントラクトを倉曎するこずでアップデヌトが可胜になりたす。 ただしアップグレヌド可胜であるこずはその性質䞊、コントラクトの䞭倮集暩性を高めおしたうため慎重に行う必芁もありたす。䟋えば ERC-721 コントラクトの実装においお、NFT の移転に関する関数をアップグレヌドすれば、NFT の所有暩を操䜜するこずも可胜になっおしたいたす。 匊チヌムではプロダクトの性質を考慮した䞊で、それでもナヌザに利䟿性を提䟛するこずに䟡倀があるず考え今回の実装を行うこずにしたした。 今回行った実装 実は匊チヌムではコントラクトをデプロむする際のガス代を節玄するために、アップグレヌド可胜ではなかったものの以前からプロキシの仕組みを䜿甚しおいたした。 このコントラクトは叀いバヌションの OpenZeppelin で実装されおいたため、今回もチヌムで利甚実瞟のある OpenZeppelin をベヌスにし぀぀、珟行のバヌゞョンで曞き盎す方針ずしたした。 コントラクト実装 OpenZeppelin はアップグレヌド可胜な ERC-721 コントラクトを ERC721Upgradeable ずしお公開しおいたす。さらに OpenZeppelin Contracts Wizard ずいうりェブアプリケヌションが提䟛されおおり、アップグレヌド機胜を含め远加したい機胜を遞択しおいくだけで基本的な ERC-721 コントラクトを䜜れるため、これでベヌスを䜜っおいきたす。 ここに独自機胜を远加しおいくのですが、ベヌス郚分は今埌の機胜远加などを行った際にも曞き盎さずに䜿い回したいので、今回は Abstract Contracts ずしお実装するこずずしたした。 以䞋は OpenZeppelin Contracts Wizard で生成された実装を Abstract Contracts 化するにあたっおの䞻芁な差分です。 - contract BaseERC721V1 is Initializable, ERC721Upgradeable, ERC721PausableUpgradeable, AccessControlUpgradeable, ERC721BurnableUpgradeable, UUPSUpgradeable { + abstract contract BaseERC721V1 is Initializable, ERC721Upgradeable, ERC721PausableUpgradeable, AccessControlUpgradeable, ERC721BurnableUpgradeable, UUPSUpgradeable { bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - function initialize(address defaultAdmin, address pauser, address minter, address upgrader) - initializer public + function __BaseERC721V1(address defaultAdmin, address pauser, address minter, address upgrader) + internal onlyInitializing { ... たた同様に独自機胜に぀いおも、独立した各機胜をアップグレヌド埌も再利甚できるように Abstract Contracts ずしお実装し、それらを継承したロゞックコントラクトを最終的にデプロむする構造にしたした。 ディレクトリ構成は以䞋のようになりたす。 contracts ├── tokens │ └── BaseERC721V1.sol ├── features │ ├── FeatureA.sol │ └── FeatureB.sol └── logics └── ERC721LogicV1.sol デプロむするロゞックコントラクトは logics/ERC721LogicV1.sol で、 tokens/ や features/ の各機胜を継承しおいたす。 プロキシをデプロむする仕組みの実装 OpenZeppelin はプロキシコントラクトを安党にデプロむするために Hardhat や Truffle 向けの プラグむン を䜿甚するこずを掚奚しおいたす。しかし匊チヌムのプロダクトが SaaS である郜合䞊 HTTP サヌバからのデプロむが必芁であり、開発環境ずしお構成されおいる Hardhat などずの盞性はあたり良くありたせん。 そこで今回はプラグむンを利甚せずに、 @openzeppelin/upgrades-core で提䟛されおいるバむトコヌドをそのたた䜿甚しおプロキシをデプロむする方針にしたした。これは OpenZeppelin が提䟛するプラグむンの内郚で利甚されおいるのず同じものです。 䞀方でプラグむンにはデプロむ時に実装が安党にアップグレヌドできるこずを怜蚌したり、アップグレヌド時に以前の実装ず互換性があるこずを怜蚌するなどの機胜があり、プラグむンを党く利甚しない堎合これらの恩恵を受けられたせん。そこでコントラクト開発のリポゞトリのテストでのみプラグむンを䜿甚したデプロむやアップグレヌドのテストを行うこずで、これらの安党性を担保できるようにしたした。 たずめ アップグレヌド可胜なコントラクトの仕組みや蚭蚈、デプロむ方法など開発の䞀連の流れを玹介したした。 コントラクトのアップグレヌドは䞭倮集暩的になっおしたうなどの偎面もあるので良く考えお導入する必芁がある技術ではありたすが、新しい機胜を埌から远加できるずいうのは利䟿性の面で非垞に倧きなメリットです。 ぜひコントラクト開発を行う際は怜蚎しおみお䞋さい。
こんにちはブロックチェヌンチヌムで゚ンゞニアをしおいる id:dorapon2000 です。最近買っおよかったものは「朮の華 あおさずいわしふりかけ」です。 今回は Git の Squash マヌゞに぀いおの知芋を共有したいず思いたす。端的に蚀うず、 チヌム開発で Non Fast-Forward マヌゞをやめお Squash マヌゞを採甚し、再び Non Fast-Forward マヌゞに戻した経緯の説明です。Squash マヌゞを運甚に導入するか考えたこずがある方の参考になればず思いたす。 Squash マヌゞずは マヌゞには 3 皮類ありたすね。みなさんはトピックブランチを main ぞマヌゞする際にどのマヌゞ方法を利甚しおいたすか Fast-Forward マヌゞ git merge --ff-only Non Fast-Forward マヌゞ git merge --no-ff Squash マヌゞ git merge --squash GitHub 䞊のマヌゞボタンではそれぞれ Rebase and merge Create a merge commit Squash and merge に察応したす。今回泚目する Squash マヌゞは、耇数コミットを単䞀のコミットにたずめおしたうマヌゞ方法です。これらを説明するわかりやすい蚘事は倚くあるため、そちらに説明を譲りたす。 なぜ Squash マヌゞをやっおみたのか それぞれのマヌゞ方法にはメリット・デメリットがありたす。私達が利甚しおいた Non Fast-Forward マヌゞず Squash マヌゞであげるず以䞋のずおりです。 Non Fast-Forward マヌゞ メリット マヌゞコミットができるので、緊急時に Revert しやすい すべおのコミットログが残るため、コヌドの意図の調査をしやすい デメリット マヌゞコミットが倧量に発生する GitHub Flow においお、main を feature ぞマヌゞするずきず feature を main ぞマヌゞするずきにマヌゞコミットが発生する WIP なコミットや add 忘れなどの雑なコミットたで残る Squash マヌゞ メリット マヌゞコミットができるので、緊急時に Revert しやすい プルリク内のコミットは単䞀のコミットにたずめられおスッキリする デメリット 詳现なコミット履歎が倱われる 私達のチヌムで利甚しおいた Non Fast-Forward マヌゞでも開発においお䞍郜合はありたせんでした。しかし、マヌゞコミットが倧量に発生する点が気になっおいたした。実際に Non Fast-Forward マヌゞで運甚しおいる珟圚の main ブランチの様子です。マヌゞコミットだらけです。 メリットの䞭で最も重芁な点は「緊急時に Revert しやすい」です。それは Squash マヌゞにもありたす。そしお Non Fast-Forward マヌゞのデメリットが目に぀いたずき、Squash マヌゞが魅力的に映りたした。 それから私達のチヌムは Squash マヌゞを採甚したした。具䜓的には、main → feature は埓来どおり Non Fast-Forward マヌゞで、feature → main ぞのマヌゞが Squash マヌゞです。 やっおみお぀らかったこず Squash マヌゞは 3 ヶ月間運甚したした。しかし、運甚の䞭で Non Fast-Forward マヌゞのずきには想像しおいなかったコンフリクトの問題が倧量に珟れたした。 main から feature/α ブランチ (子) を切る feature/α にコミットハッシュ A を push feature/α から feature/β ブランチ (å­«) を切る feature/β にコミットハッシュ B を push (画像 1 枚目) A ず B はコンフリクトの関係にあるずする (補足参照) feature/α を main に Squash マヌゞ (画像 2 枚目) ここで Squash されるので main にはコミットハッシュ A が入らない (コミットハッシュ C ずしお远加) feature/β のベヌスブランチは main に切り替わる (最新にするため) main を feature/β ぞ Non Fast-Forward マヌゞする (画像 3 枚目) main 䞭にコミットハッシュ A がないこずで、 feature/β のコミット A+B ず main の C の解決がうたくできずコンフリクトする Squash マヌゞのデメリットである「詳现なコミット履歎が倱われる」がコンフリクトずいう圢で問題になりたした。この状況によるコンフリクトを Squash コンフリクトず呜名しお説明を続けたす。 やめた コンフリクトが蟛かったため、チヌムで盞談しお察応を 4 ぀考えたした。 ① 気合で Squash コンフリクトを解消する ② Squash コンフリクトが発生する堎合のみ、Non Fast-Forward でマヌゞする ③ Squash コンフリクトが発生しないように、掟生する feature ブランチがすべおマヌゞされおいるこずを確認しおから Squash マヌゞする ④ Squash マヌゞをやめる 本蚘事のタむトルにもある通り、採甚したのは ④ の Squash マヌゞをやめるこずです。぀たり、埓来の Non Fast-Forward マヌゞの運甚に戻したした。 ①〜③ を採甚しなかった理由をそれぞれ説明したす。 ① は珟実的でないず刀断されたした。もしロックファむル(pnpm-lock.yml など)でコンフリクトが起きおしたったずきにあたりに悲惚です。 ② は運甚でカバヌする方法です。しかし、本来であれば認識する必芁のない自身の子ブランチに合わせお 2 皮類のマヌゞを䜿い分ける必芁がありたす。たた、運甚しおいるず Non Fast-Forward マヌゞすべきずころをうっかり Squash マヌゞしおしたったずいうミスが起きおしたいそうです。議論の䜙地なく䞍採甚でした。 ③ も運甚でカバヌする方法です。Squash コンフリクトが起きる状況を䜜らないように、feature/A を feature/B より先にマヌゞしなければいいのです。しかし、feature/A に䟝存する子ブランチが倚いず、あるいは他の子ブランチがマヌゞされるのを埅っおいるうちに新しい子ブランチができるなど、い぀たで経っおも feature/A を main にマヌゞできたせん。そしお、十分にその状況がありえたす。 ④ はもずもず運甚しおいた方法であり懞念はありたせん。 考察 圓時のチヌムは新芏プロダクトの開発初期段階にあり、 feature/B (孫ブランチ) のようなブランチがよく䜜成されおいたした。そのため Squash マヌゞずの盞性が良くなかったのだず考えられたす。feature/B があたり発生しない状況や環境であれば Squash マヌゞもよい遞択肢になるかず思いたす。 たずめ 蚘事を読んでいただきありがずうございたす。最埌にたずめたす。 Non Fast-Forward マヌゞ戊略にはマヌゞコミットが倧量に発生するずいう問題がある 問題の解決のために Squash マヌゞで運甚した 特定の状況でコンフリクトが頻繁に発生した 結局 Non Fast-Forward マヌゞ戊略に戻した (補足) コンフリクトの関係 Squash コンフリクトの䟋䞭で以䞋のように説明した箇所がありたす。 A ず B はコンフリクトの関係にある 適切な甚語を思い぀かなかったためこのような説明になっおいたすが、内容はシンプルです。Squash コンフリクトの䟋を匕き継いで、コヌドで説明したす。 コミット A import bisect + import collections コミット B import bisect import collections + import math Squash マヌゞコミット C は import bisect + import collections のようになり、コミット A+B ずコミット C が Squash コンフリクトを起こす、ずいうこずです。
駅メモチヌム゚ンゞニアの id:yumlonne です。 この蚘事では駅メモで䜿っおいた Memcached を廃止し Redis に統合した経緯や流れを玹介したす。 蚘事内で提䟛するサンプルコヌドは、駅メモの実装に合わせ Perl ずなっおたす。 簡単なコヌドなので Perl に詳しく無い方でも十分理解できるず思いたす。 KVS 統合の背景 駅メモは AWS を䜿っおサヌビスを提䟛しおいたす。 統合前は Amazon ElastiCache で Memcached ず Redis の䞡方を運甚しおいたした。 Memcached はプラむマリノヌドのみ、Redis はプラむマリノヌドずレプリカノヌドそれぞれ 1 台の構成でした。 それずほずんど同じ構成が他に 2 セットあるため、党䜓を芋るず Memcached は 3 ノヌド存圚しおいたした。 Memcached は m6g.large を利甚しおいたため、リザヌブドノヌド料金で幎間 3000 ドル以䞊のコスト削枛が期埅できたした。 Memcached ず Redis の䜿い分け 駅メモでは Memcached ず Redis を以䞋のように䜿い分けおいたした。 Memcached セッションデヌタ 揮発しお良いデヌタ Redis ランキングデヌタ(sorted sets) 揮発しおはいけないデヌタ ほずんどのデヌタは RDS で氞続化しおいたす ここではパフォヌマンス面で揮発を蚱容できないこずを指しおいたす 統合先ずしお Redis を遞択した理由はいく぀かありたすが、決め手は䜎負荷か぀高パフォヌマンスなランキング凊理の実珟に Redis が埗意ずする sorted sets 型を利甚しおいたためです。 セッションデヌタの移行 ナヌザに再ログむンする手間をかけさせたくないため、Memcached のセッションデヌタだけは Redis に移行するこずずしたした。 以䞋はセッションデヌタの移行の流れです。これによりメンテナンスなどを実斜するこずなくセッションデヌタの移行を進められたした。 アクセス時にセッションデヌタを Memcached から Redis に移行するモゞュヌルを䜜成 (箇条曞きの䞋にコヌドのサンプルを瀺したす) 本番反映しおからセッションの有効期限分の時間が経぀たで様子を芋る セッションの有効期限分の期間を空けるこずでアクセスのあるナヌザのセッションデヌタは 1 で䜜成したモゞュヌルによっお移行されたす アクセスのないナヌザのセッションデヌタは移行されたせんが、どちらにせよセッションデヌタの有効期限が過ぎおいるのでナヌザには圱響ありたせん Redis のセッションデヌタのみを参照するモゞュヌルに差し替える 1 で䜜成したモゞュヌルは Memcached も参照するので最終的には差し替える必芁がありたす 以䞋は 1 で䜜成したモゞュヌルのコヌドのサンプルです。 # セッションの登録 # 新芏の登録は党おRedisに向ける sub set_session { my ( $key , $value ) = @_ ; # 有効期限を蚭定しシリアラむズしおRedisに保存 $redis->set ( $key , $value , 'EX' , $session_expire ); } # セッションの取埗 # Redisから取埗できればそれを返し、そうでなければMemcachedから取埗したものをRedisに登録しお返す sub get_session { my ( $key ) = @_ ; my $session ; $session = $redis->get ( $key ); if ( defined $session ) { return $session ; } $session = $memcached->get ( $key ); if ( defined $session ) { set_session( $key , $session ); } return $session ; } Memcached を Redis に統合 Memcached を操䜜するコヌド党おを Redis に差し替えるこずを怜蚎したしたが、差分が膚倧になるこずで゚ンバグのリスク増え、動䜜確認およびレビュヌ範囲が広がっおしたいたす。そこで既存の Memcached を䜿うキャッシュモゞュヌルず同じむンタヌフェヌスを持぀ Redis 実装のキャッシュモゞュヌルを䜜成しお差し替えるこずにしたした。 Memcached を䜿うキャッシュモゞュヌルで呌ばれおいた関数を調査し、必芁最䜎限の機胜を実装したした。以䞋は Redis 実装のキャッシュモゞュヌルの関数の䞀芧です。 set, set_multi, add, replace get, get_multi incr, decr delete, flush_all(Redis では flushdb に盞圓) これらの関数の実装では以䞋の苊劎がありたした。 Memcached ず Redis のモゞュヌルの振る舞いの違い Memcached のキャッシュモゞュヌルは Cache::Memcached::Fast::Safe で、Redis のキャッシュモゞュヌルは Redis をベヌスに䜜成したした。 それぞれのモゞュヌルで同じむンタヌフェヌスを提䟛するこずを考えおいたしたが、䞀郚のコマンドの振る舞いにクセがあり、同じむンタヌフェヌスにできなかった郚分もありたす。 䟋えば Memcached は decr で 10 から 9 のように桁が枛ったずき、倀を取埗し盎すず "9" ではなく "9 " のように 2 文字返しおきたす。 use Cache::Memcached::Fast::Safe; # ロヌカルのMemcachedに接続するむンスタンスを䜜成 my $memcached = Cache::Memcached::Fast::Safe->new({ servers => [ { address => "localhost:11211" } ]}); $memcached->set ( "key" , 10 ); $memcached->decr ( "key" , 1 ); warn sprintf ( '"%s"' , $memcached->get ( "key" )); # => "9 " Perl では䞊蚘のような状態でも、数倀ずしお扱う分には問題にならないため、振る舞いの違いは蚱容できたした。 Perl オブゞェクトを玠盎に set できない Memcached にはフラグ機胜があり、デヌタずは別にメタ情報を保存するこずができたす。䞀方で Redis にはこのような仕組みはありたせん。 数倀や文字列を単玔に保存する堎合には問題になりたせんが、構造化されたデヌタを保存する堎合には少し困りたす。取り出したデヌタがただのバむナリデヌタなのか、構造化されたデヌタをシリアラむズしたものなのかを刀断できないためです。 駅メモでは数倀や文字列の扱いが倚いものの、䞀郚の機胜で Perl オブゞェクトを保存しおいたした。 䞊蚘を螏たえ、今回は以䞋の察応ずしたした。 set する倀が Perl のオブゞェクト(ref した結果が true)なら Storable#nfeeze を䜿っおシリアラむズする get した倀を Storable#thaw でデシリアラむズしおみお、゚ラヌになったら thaw する前の倀を返す 他の手法ずしお、必ず䞀定のフォヌマットのオブゞェクトにしおからシリアラむズしおセットするやりかたがありたす。 この手法では Memcached のフラグ機胜ず同等の恩恵を埗られたすがデメリットもありたす。ただの数倀や文字列もシリアラむズしお保存するず、その倀に察しお incr などの盎接の操䜜ができなくなり、redis-cli などで盎接デヌタを確認するこずも難しくなりたす。 䞊に曞いた通り、駅メモでは単玔な数倀や文字列を保存しおいるケヌスが倚いため、この手法は採甚したせんでした。 たずめずその埌の展開 今回は駅メモにおける Memcached の廃止ず Redis ぞの統合に぀いおの手法を玹介したした。 費甚面のコスト削枛もさるこずながら、管理すべきミドルりェアが枛ったこずで運甚面のコストも䞋がっお䞀石二鳥だったず思いたす。 その埌の展開ずしお「Memcached ず Redis の䜿い分け」 で觊れたランキング凊理に぀いお、最もデヌタ容量の倧きいランキングを RDS に凊理させるように改善し、Redis のスペックも䞋げるこずができたした。こちらもいずれ蚘事にしたいず考えおいたすのでお楜しみに 2023/01/29远蚘: MySQL でランキング凊理を行うようにする仕組みの蚘事を公開したした tech.mobilefactory.jp
こんにちはこの蚘事では Flutter でカメラを扱うアプリを䜜成する際の工倫に぀いお、玹介したす。 はじめに 匊瀟で開発されおいる駅メモおでかけカメラ以䞋「おでかけカメラ」は 2022 幎 11 月にリニュヌアルし、UI の刷新や動䜜䞍良の解消、機胜の拡充を行いたした。 内郚的には、これたでの Unity 補だったおでかけカメラ以䞋「旧おでかけカメラ」を䞀床党お捚お、新しく Flutter で䜜り盎すずいうこずをしおいたす。 瀟内には Flutter に関する知芋がほずんどなかったため、おでかけカメラのリニュヌアルは技術のキャッチアップから始たり、詊行錯誀を重ねた開発ずなりたした。 今回はその詊行錯誀の䞭から、カメラの映像内にフレヌムやスタンプのような装食を Widget ずしお配眮し、撮圱した際の、写真ず Widget を重ね合わせる工倫に぀いお説明したす。 プレビュヌをそのたた撮圱結果ずするこずの問題点 カメラ映像ず Widget を重ね合わせお撮圱する最も簡単な方法ずしお、画面のスクリヌンショットを撮圱し、プレビュヌ領域以倖の郚分を削陀するこずが考えられたす。 この方法であれば、プレビュヌで衚瀺されおいたものが間違いなくそのたた撮圱結果ずしお埗られたすし、実装も単玔になるのですが、倧きな欠点がありたす。 それは、撮圱画質がディスプレむ解像床に䟝存しおいるため、倚くの堎合カメラの性胜を掻かしきれず、残念画質な写真にしかならないず蚀うこずです。 撮圱埌の画像サむズに合わせお Widget を再構築し、画像化する 䞊蚘の問題点を解消し、カメラの性胜を最倧限掻かし぀぀プレビュヌ通りの撮圱結果を埗るために、おでかけカメラでは写真のサむズや瞊暪比ず合うように䞀緒に撮圱する Widget を画像化し、写真ず合成するアプロヌチを採甚したした。 具䜓的には画面に描画しおいないオフスクリヌンな Widget Tree を構築できる BuildOwner ずいうクラスを䜿甚しおいたす。 以䞋は撮圱した写真のサむズに合わせた Widget を䜜成し、画像にしたものを取埗するコヌドです。 import 'dart:ui' as ui; import 'package:image/image.dart' as img; Future < img. Image > getOverlayImage ( Size imageSize) async { // 描画、再描画を効率的に行えるようにする Class // ただし、ここでは Widget を画像化する機胜を利甚するために䜿っおいる final repaintBoundary = RenderRepaintBoundary (); // 描画する堎所 // 写真のサむズで Widget が描画されるようにパラメヌタを枡しおいる final renderView = RenderView ( window : ui.window, child : RenderPositionedBox (alignment : Alignment .center, child : repaintBoundary), configuration : ViewConfiguration ( size : imageSize, devicePixelRatio : 1.0 , ), ); // Widget Tree の描画を制埡する Class final pipelineOwner = PipelineOwner (); pipelineOwner.rootNode = renderView; renderView. prepareInitialFrame (); // Widget Tree の構築、再構築を制埡する Class final buildOwner = BuildOwner (focusManager : FocusManager ()); // 描画するもの // CameraOverlay() を画像化したものが最終的に欲しいもの final element = RenderObjectToWidgetAdapter < RenderBox > ( container : repaintBoundary, child : CameraOverlay (), ). attachToRenderTree (buildOwner); // BuildOwner に構築する Widget Tree のスコヌプを指定 buildOwner. buildScope (element); buildOwner. finalizeTree (); // PipelineOwner で描画 pipelineOwner. flushLayout (); pipelineOwner. flushCompositingBits (); pipelineOwner. flushPaint (); // 画像に倉換しお返华する final ui. Image widgetImage = await repaintBoundary. toImage (); final ByteData byteData = await widgetImage. toByteData (format : ui. ImageByteFormat .png); return img. decodeImage (byteData.buffer. asUint8List ()); } 簡単に説明するず、 BuildOwner で構築した Widget Tree を PipelineOwner で RenderView にレンダリングし、 RenderRepaintBoundary の機胜を䜿っお画像ずしお曞き出しおいたす。 あずは以䞋の通り、画像化した Widget を撮圱した写真ず合成しおあげれば、出来䞊がりです。 final img. Image cameraImage = takePicture (); final Size imageSize = Size (cameraImage.width. double (), cameraImage.height. double ()); final img. Image overlayImage = await getOverlayImage (imageSize); final img. Image compositeImage = img. drawImage (cameraImage, overlayImage); たずめ Flutter でカメラ映像ず、その䞊に重ねお衚瀺した Widget を合わせお撮圱する際に、品質を萜ずさずプレビュヌ通りの結果を埗るための工倫に぀いお玹介したした。 やりたいこずは単玔なのに意倖ず䞀工倫が必芁ずいうこずで、詰たりやすいずころなのかなず思いたした。 カメラアプリを䜜る際にでも参考になれば幞いです。
こんにちは、駅メモでフロント゚ンドを良い感じにしたかったチヌムの id:yunagi_n です。 今回は、駅メモにお䜿甚しおいる Vue.js を 2 系から 3 系ぞあげお行くに圓たっお、採甚した手法ずマむグレヌションプロセスに぀いお玹介したす。 今回、マむグレヌションするに圓たっお、以䞋の芁件がありたした 機胜開発を止めおはいけない 駅メモでは 6 月ず 10 月に呚幎リリヌスがあり、それの開発を止めるわけにはいきたせんでした もちろん、その間にあったむベントなどに぀いおも、開発は継続し続けおいたす 倚くのメンバヌは割けない 基本はわたしが䞭心に、远加で 1 人〜2 人に手䌝っおもらうこずはありたした たた、参考のため、駅メモのフロント゚ンドの芏暡感を玹介しおおくず Vue コンポヌネント数は 1500 コンポヌネント fd --type file --extension vue | wc -l にお算出 *1 フロント゚ンドのコヌド党䜓は 4700 ファむル、16 䞇行 tokei . にお算出 *2 ずいった感じでした。 芏暡感で蚀えば超倧芏暡、たたフロント゚ンドにおいおはテストなども䞀切存圚しおいなかったため、かなり厳しい䜜業ずなるこずが予想されたした。 ただ、駅メモでは、歎史的な理由から倖郚の䟝存パッケヌゞが少なく、サヌドパヌティヌパッケヌゞの曎新による圱響は避けられたした。 しかしながら、パッケヌゞ数による圱響は無いですが、 Babel や Webpack などの䟝存関係は 導入圓時以降アップデヌトされおおらず (導入圓時は、぀たりサヌビス開始日より前です)、そのたたでは 2023 幎に開発されたパッケヌゞの䞀郚はバンドル出来ない状態でした。 たた、今回は Vue.js 公匏から提䟛されおいる Migration Build は䜿甚しおいたせん。 これは、超倧芏暡ずなった堎合、䞀床入れおしたったラむブラリは抜くこずが困難であるこずや、かえっお効率が䜎䞋する可胜性があったこずからです。 これらの状態から、 Vue 3 環境ぞずマむグレヌションするため、以䞋のような手法を採甚したした。 新しく Vue.js 3.x で起動可胜な゚ントリヌポむントを別パッケヌゞずしお䜜成 新しい゚ントリヌポむントが起動でき次第、各皮ロゞックに぀いお適切にパッケヌゞずしお切り出す (monorepo) 各パッケヌゞは main ず exports フィヌルドを甚いおビルド成果物を出し分ける main には叀い゚ントリヌポむントを察象ずしたコヌドを (Webpack が叀すぎお exports を認識できないため) exports には新しい゚ントリヌポむント、もしくは新しいパッケヌゞを察象ずしたコヌドを 埐々に珟圚の゚ントリヌポむントに属する郚分から新しいパッケヌゞぞず分離する 玔粋なロゞックは単玔な JavaScript パッケヌゞずしお Vue に関わるものはコンポヌネントラむブラリずしお 最終的には珟圚の゚ントリヌポむントは廃止し、削陀する ずいった圢です。 それぞれ、なぜこのような手法を取ったのか詳しく補足しおいきたす。 新しく Vue.js 3.x で起動可胜な゚ントリヌポむントを別パッケヌゞずしお䜜成 これは、以䞋の理由からになりたす 珟圚の゚ントリヌポむントにあたる Babel や Webpack では、たずアップグレヌドプロセスが必芁になる ただ、アップグレヌドに぀いおは過去業務委蚗の方に調査をお願いしたが、珟実的な時間では䞍可胜ずいう結論ずなった フロント゚ンドの開発䜓隓が著しく悪い (初期開発ビルドたで数分、リロヌドたでも数十秒) ずいう話があり、マむグレヌションするにあたっお障壁ずなる 時間的制玄が厳しい䞭で、1 むテレヌションに察しお数十秒かかるのは進行スピヌドに倧きな圱響を䞎える これらの問題を解決するため、既存の゚ントリヌポむントに远加や眮き換えをするのでは無く、新たにパッケヌゞを切り出すこずで、䞊蚘問題を解決するこずが可胜でした (少々力尜くですが......)。 たた、このタむミングで今たでは超巚倧な 1 パッケヌゞだったのを monorepo ずしお切り出すため、以䞋のような敎備も行いたした。 Turborepo の導入 Yarn Workspaces の導入 monorepo ずしおは Yarn Workspaces を入れるのは良く聞きたすが、 Turborepo を入れたのはなかなか圓時ずしおは珍しい構成だった蚘憶がありたす。 Turborepo を導入したきっかけずしおは、䟝存グラフを甚いお必芁なパッケヌゞだけでコマンドが実行できる点、キャッシュ機胜により倉化がないパッケヌゞに぀いおはビルドがされない点などです。 新しい゚ントリヌポむントが起動でき次第、各皮ロゞックに぀いお適切にパッケヌゞずしお切り出す こちらは、以䞋の理由がありたす。 䞊蚘に繋がるが、 Babel が叀すぎお ES2015 の䞀郚文法のみが䜿甚可胜であり、生産性が䜎い 珟状の゚ントリヌポむントにあたる郚分では、埪環参照などが圓たり前に起きおおり、バンドル時に明らかに䞍芁なファむルなども含たれおいた これに぀いおは、パッケヌゞを分けたこずで、単玔に新しい文法ずスピヌド感あるむテレヌションをやりたい、の他にも以䞋の利点がありたした。 Turborepo を採甚したので、パッケヌゞを適切に分けるこずで、ビルドキャッシュがうたく䜿える パッケヌゞマネヌゞャヌレベルで埪環参照を防げる 完党な圢で TypeScript の導入が出来る 劥協しない圢で ESLint や Stylelint 、 Prettier の導入が出来る ずくに、前者は今フロント゚ンドのビルドにおいお、プロダクションビルドを行った際 10 分皋床かかっおいた時間が倧幅に削枛できたす。 そしお、ここで分けたパッケヌゞに぀いおは GitHub Package Registry に Publish しおおり、そちらを利甚するこずであらかじめビルドしたパッケヌゞも䜿えたす。 たた、いたたで駅メモのフロント゚ンドでは、今幎䞭頃たで ESLint も Stylelint もたずもに運甚されおいたせんでした (もちろん Prettier もありたせん)。 それらに぀いお、さすがに無いのはコヌドクオリティ面で問題があるので導入はしたのですが、かなり劥協した蚭定です。 しかしながら、パッケヌゞずしお切り出した郚分に぀いおは、厳密な圢で TypeScript や ESLint, Prettier ,必芁であれば Stylelint も導入できたす。 これによっお、副䜜甚的に新しく曞かれた郚分に぀いおはコヌドクオリティ面での向䞊も図れたした。 埐々に珟圚の゚ントリヌポむントに属する郚分から新しいパッケヌゞぞず分離する これは開発が垞に続いおいるずいった前提によるものです。 䟋えば、いたわたしがこの蚘事を曞いおいる時点でもフロント゚ンドを含む新芏開発が行われおおり、それらを新しく Vue3 で曞いお・曞き盎しお欲しい、ずいうのは玍期や教育コストなどを考えた堎合珟実的ではありたせん。 そのため、珟圚の゚ントリヌポむントに぀いおはそのたたで、 Vue3 で曞かれたパッケヌゞを vue-demi や瀟内で新たに䜜成したマむグレヌション甚パッケヌゞを甚いお Vue 2/3 䞡察応ずしおビルド可胜にし、それを䞊蚘パッケヌゞ経由で䜿甚するこずで、極力同じ䜿甚感で Vue3 パッケヌゞを䜿甚できるようにしおいたす。 䟋えば、以䞋のコヌドは曞き換え前のコヌドです。 < template > < button v-se-player> ... </ button > </ template > < script > import sePlayer from 'example/directives/v-se-player' ; // Vue2 export { directives: { sePlayer } } </ script > これらは、次のように曞き換えるだけで、 Vue3 にも察応したコヌドに出来たす。 < template > < button v-se-player> ... </ button > </ template > < script > import { vSePlayer } from '@example-scope/directives' ; // Vue3 export { directives: { sePlayer: vSePlayer } } </ script > むンポヌト先を倉えるだけですね。簡単です。 このような圢で、既存モゞュヌルをパッケヌゞずしお切り出し、か぀同じ圢匏で䜿えるようにするこずで、孊習コストがほが無い状態のたたで移行ができたす。 たた、新芏開発郚分に぀いおは、既存のモゞュヌルを䜿い぀぀むンポヌト先を倉えるだけで Vue3 にも察応できるので、今埌のコストが䞋げられたす。 たずめ ずいうこずで、珟圚察応しおいる駅メモにおける Vue.js のマむグレヌションに぀いお採甚した手法ずそのプロセスに぀いおの玹介でした。 わたしはこの仕事を最埌に退職しおしたうので、続きは残った人にお願いする圢ですが、たた次回の id:yunagi_n の蚘事をお楜しみください。 *1 : fd はファむルシステムの゚ントリヌを怜玢するツヌルです。 *2 : tokei は指定ディレクトリヌ内コヌドの統蚈デヌタを出しおくれるツヌルです。
こんにちは゚ンゞニアの id:mkan0141 です モバむルファクトリヌでは「シェアナレ」ずいう 1 日の業務時間のうち 1 時間であれば自習・勉匷に䜿っお OK ずいう制床がありたす。 今回はその制床を利甚しお 8 月に「朝Rustもくもく䌚」ずいうものを開催したので玹介したす。 朝 Rust もくもく䌚ずは 8 月の平日毎朝 08:30~09:30 に Rust に関するこずをもくもくず勉匷・䜜業する䌚です。 目的は「Rust を觊ったこずがない・少し觊ったけど続かなかった人に Rust をちゃんず勉匷する機䌚を䜜る」ず「早起きしお健康になる」です。参加するだけで Rust の知識ず健康が手に入りたす。玠敵ですね。 各自やるこずに関しおは特に瞛りはなく、Rust であればなんでも OK ずいう方針にしたした。䞀郚ですが参加者が䜜業しおいた内容を以䞋にたずめたす。 読曞 The Rust Programming Language 日本語版 Tour of Rust 実践 Rust プログラミング入門 The Little Book of Rust Macros 䜜業 Discord Bot の開発 自䜜ツヌルのアップデヌト なぜ朝にするの もちろん健康になりたいからずいうのはありたすが、真面目な理由ずしおは静かで集䞭しやすい環境だず思ったからです。 時間垯にもよりたすが、午埌はメンションが倚かったり、優先床の高い察応が起きおしたったりするこずが倚い印象です。そのため、勉匷䌚䞭に少し気が逞れたり、参加するこずができなかったりしたす。もちろん業務優先なので仕方がないのですが、せっかくやるなら集䞭しおやりたいずいうので、比范的これらのこずが起こりづらい早朝を遞んで開催しおみたした。 進め方 今回、初めおもくもく䌚を䞻催したので他のもくもく䌚を参考に以䞋のように決めたした。 䞻催者ず参加者に負担がかからないようなゆるい䌚になるのが目暙でした。 各自やるこずを宣蚀しお䜜業(50 分間) 各自の知芋の docbase に蚘入・共有(10 分間) 感想 初もくもく䌚䞻催だったのもあり、途䞭で参加者 0 人にならないか少し䞍安になりながら開催したのですが、ほが参加者が絶えるこずなく開催できおほっずしおいたす。 早朝開催に関しおは、やはり早い時間のもくもく䌚は予想通り静かで集䞭しやすい環境になっおいたので個人的にはかなりアリだず思いたした。ただ、振り返っおみるず参加メンバヌの倧半は普段から早めに出勀しおいる人だったので、実は倕方開催の方が参加しやすく参加者も倚くなっおいたかもしれないです。 たた、もくもく䌚の進行に関しおは、知芋の共有の時間を蚭けたのは良かったず思いたした。 各々の芖点で理解した内容や Rust の躓いた話が聞けお、自分の理解が合っおいるかや躓いたポむントに぀いお他の人が補足したりず Rust の理解がさらに深たったかなず思いたす。 終わりに 朝 Rust もくもく䌚に぀いお玹介したした。 朝にする勉匷䌚は健康になれたすし孊びも埗られお個人的には満足床が高かったです。 ぜひ、みなさんも朝もくもく䌚を䌁画しおみおはいかがでしょうか
駅メモ開発基盀チヌムの id:xztaityozx です 皆さんは Perl を曞いおいたすかモバむルファクトリヌが長く提䟛しおいるサヌビスなどでは、バック゚ンドが Perl で曞かれおいたす。 しかしながら、自分は普段むンフラ領域をやらせおもらっおいるずいうこずもあり、Perl で新機胜開発をするずいった機䌚がそんなにありたせん。 せっかく Perl だらけの環境にいるのに、あんたり Perl に觊れられないのはもったいないな〜ず思い、今幎のゎヌルデンりィヌクは PPI を䜿ったメタプログラミングで遊んでいたした。 metacpan.org で、ちょっず遊んでいたら Perl のパッケヌゞ情報を䜿っお C#のクラスを吐き出すプログラムができたので蚘事にしおみたした。自由研究発衚ずいう感じです。 倉換先に C#を遞んだのは C#が倧奜きだからです。 Perl パッケヌゞを C#クラスにする䟋 研究をしおいたリポゞトリは以䞋のものです。リポゞトリ名気に入っおたす。ガッっず曞いたのでめちゃめちゃ雑です。 github.com README にも曞いおあるこずですが、以䞋のような Perl パッケヌゞがあるずき package My::Namespace::B ; use strictures 2 ; use Function::Parameters; use Function::Return; use Types::Standard -types; use Data::Validator; use Mouse; has name => ( is => 'ro' , isa => Str, default => 'this is name' ); has age => ( is => 'ro' , isa => Int, required => 1 ); has union => ( is => 'ro' , isa => Str|Int, required => 1 ); has dict => ( is => 'ro' , isa => Dict[ name => Str, age => Int], required => 1 ); no Mouse; __PACKAGE__ ->meta->make_immutable ; sub a { return 10 ; } fun b() :Return(Int) { return 10 ; }; method c() :Return(Int) { return 10 ; }; method d(Str $str , $x , Int $y //= 1 ) { return 10 ; }; sub e { my $rule = Data::Validator->new( str => { isa => Str }, x => { isa => Any }, y => { isa => Int, default => 1 }, ); $rule->validate ( @_ ); return 10 ; } sub f :Return(Str, Int) { my ( $self , $str , $x , $y ) = @_ ; return [ $str , $x + $y ]; } sub g { return ; } sub h { my $a = shift ; return 1 ; } 1 ; 以䞋のような C#コヌドが生成されたす。基瀎郚分は NJsonSchema を䜿っお JSON Schema から生成し、メ゜ッド郚分はシグネチャだけ移怍したようなコヌドになりたす。そこそこ長くなるので䞀郚省略しおおりたす。 //---------------------- // <auto-generated> // Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v9.0.0.0) (http://NJsonSchema.org) // </auto-generated> //---------------------- namespace My.Namespace { #pragma warning disable // Disable all warnings [System.CodeDom.Compiler.GeneratedCode( "NJsonSchema" , "10.9.0.0 (Newtonsoft.Json v9.0.0.0)" )] public partial class B { /// < summary > /// original Perl type: Int /// </ summary > [System.Text.Json.Serialization.JsonPropertyName( "age" )] [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] public int Age { get ; set ; } /// < summary > /// original Perl type: Dict[age = & gt ; Int,name = & gt ; Str] /// </ summary > [System.Text.Json.Serialization.JsonPropertyName( "dict" )] [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] public Dict Dict { get ; set ; } = new Dict(); /// < summary > /// original Perl type: Str /// </ summary > [System.Text.Json.Serialization.JsonPropertyName( "name" )] [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] public string Name { get ; set ; } = "this is name" ; /// < summary > /// original Perl type: Str|Int /// </ summary > [System.Text.Json.Serialization.JsonPropertyName( "union" )] [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.Never)] public Union Union { get ; set ; } private System.Collections.Generic.IDictionary< string , object > _additionalProperties; [System.Text.Json.Serialization.JsonExtensionData] public System.Collections.Generic.IDictionary< string , object > AdditionalProperties { // ... 省略 } /// < summary ></ summary > /// < returns ></ returns > public object a() { throw new NotImplementedException(); } /// < summary ></ summary > /// < param name = "str" > original Perl type: Str </ param > /// < param name = "x" > original Perl type: Any </ param > /// < param name = "y" > original Perl type: Int </ param > /// < returns ></ returns > public object e( string str, object x, int y = 1 ) { throw new NotImplementedException(); } /// < summary ></ summary > /// < returns > original Perl type: Str, Int </ returns > public object f() { throw new NotImplementedException(); } /// < summary ></ summary > /// < returns ></ returns > public void g() { throw new NotImplementedException(); } /// < summary ></ summary > /// < param name = "a" > original Perl type: Any </ param > /// < returns ></ returns > public object h( object a) { throw new NotImplementedException(); } /// < summary ></ summary > /// < returns > original Perl type: Int </ returns > public int b() { throw new NotImplementedException(); } /// < summary ></ summary > /// < returns > original Perl type: Int </ returns > public int c() { throw new NotImplementedException(); } /// < summary ></ summary > /// < param name = "str" > original Perl type: Str </ param > /// < param name = "x" > original Perl type: Any </ param > /// < param name = "y" > original Perl type: Int </ param > /// < returns ></ returns > public object d( string str, object x, int y) { throw new NotImplementedException(); } } [System.CodeDom.Compiler.GeneratedCode( "NJsonSchema" , "10.9.0.0 (Newtonsoft.Json v9.0.0.0)" )] public partial class Dict { // ... 省略 } [System.CodeDom.Compiler.GeneratedCode( "NJsonSchema" , "10.9.0.0 (Newtonsoft.Json v9.0.0.0)" )] public partial class Union { // ... 省略 } } やっおいるこず このリポゞトリでやっおいるのはざっくりいうず以䞋の 2 ぀のこずだけです。 PPI ずかを䜿っお Perl パッケヌゞ情報を取り出しお JSON に曞き出す C#で JSON を読んで C#の AST を構築し、できたものをファむルに曞き出す 同時に 2 ぀の AST を読むこずになったので結構混乱したした。どちらの蚀語もむずいです。 1. PPI ずかを䜿っお Perl パッケヌゞ情報を取り出しお JSON に曞き出す 今回の研究で取り出したいず思ったのは以䞋の 5 ぀です。 パッケヌゞ名 名前空間 芪パッケヌゞ Mouse のプロパティ サブルヌチンやメ゜ッドのシグネチャ情報 1,2 は PPI で簡単に取り出せたす。具䜓的には以䞋のように曞けたす。 my $package_nodes = $document->find ( 'PPI::Statement::Package' ); foreach my $package_node ( @{$package_nodes} ) { my @namespace = split ( / :: /x , $package_node->namespace ); my $name = pop @namespace ; } 3 の芪パッケヌゞは @ISA を芋るだけ、4 のプロパティは PPI で宣蚀を探し぀぀、Mouse のメタ情報から情報を取り出せばよいですね。 # 芪パッケヌゞを取り出す no strict 'refs' ; my @superclasses = @{ $class_name . '::ISA' } ; use strict 'refs' ; # プロパティの宣蚀を探しお、プロパティ名を取り出す。 my $has_statements = $ppi_document->find ( sub { my ( $root , $node ) = @_ ; if ( $node->isa ( 'PPI::Token::Word' ) && $node->content eq 'has' ) { return 1 ; } return 0 ; } ); # メタ情報から名前が䞀臎するプロパティの情報を取り出す # こうしおおくず継承しおたりMouseがはやしたりしたや぀を省ける別の方法はないのでしょうか  my $meta = Mouse::Util::get_metaclass_by_name( $package_statement->namespace ); my @properties ; foreach my $has_statement ( @{$has_statements} ) { my $property_name = $has_statement->snext_sibling->string ; my $attr = $meta->get_attribute ( $property_name ); push @properties , $attr ; } ここたでは順調ですが、問題は 5 のサブルヌチン・メ゜ッドのシグネチャ情報ですね 。 Function::Parameters のメタ情報からシグネチャ情報を取り出しおみる Perl にはサブルヌチンやメ゜ッドのシグネチャをサポヌトするようなモゞュヌルが沢山ありたす。たずえば Type::Params や Function::Parameters などですね。 こういうモゞュヌルは倧䜓の堎合メタ情報が存圚するため、そこから欲しい情報を取り出すこずができたす。 # Function::Parametersの䟋 use strictures 2 ; use Function::Parameters; use Types::Standard -types; method hoge( $class : Int $i ) { return $i ; } # Function::Parameters::Info が返される。匕数のリストなどが分かる my $info = Function::Parameters::info(\ &hoge ); ぀たり、PPI でサブルヌチン・メ゜ッドを芋぀け次第、メタ情報からシグネチャ情報を取り出せば良さそうです。 しかしながら、先皋述べた通りこういうモゞュヌルは沢山あるので党お察応するのは難しいです。そこでたずは Function::Parameters ず Function::Return だけ泚目するこずにしたした。Function::Return は最近駅メモのコヌドでも䜿われるようになったので、これを機に仲良くなっおおこうず思っお遞びたした。 普通のサブルヌチンからもシグネチャ情報を取り出したい Function::Parameters や Function::Return を䜿っお曞かれおいるサブルヌチンはいいのですが、以䞋のようなよくあるサブルヌチンはどうしたしょう。 sub hoge_sub { my $hoge_arg = shift ; return $hoge_arg ; } 最初、こういうのに぀いおは諊めようず思っおいたんですが、PPI で遊んでいるうちに「なんずかなりそうだな」ず思いたした。 倀の受け取りは、倧䜓の堎合以䞋のうちのどれかのパタヌンになるのではず考えたからです。もちろん網矅できおるずは思っおいたせん my $arg = shift ; my ( $arg1 , $arg2 ) = @_ ; my ( $arg1 , $arg2 , $arg3 ) = ( shift , shift , shift ); これらの匏自䜓は PPI::Statement::Variable になりたす。 children メ゜ッドを呌び出すず、そこにぶら䞋がっおいる子を芋るこずができたす。 以䞋の䟋は my $arg = shift の children です。 [0] my (PPI::Token::Word), [1] (PPI::Token::Whitespace), [2] $arg (PPI::Token::Symbol), [3] (PPI::Token::Whitespace), [4] = (PPI::Token::Operator), [5] (PPI::Token::Whitespace), [6] shift (PPI::Token::Word), [7] ; (PPI::Token::Structure) なんかなんずかなりそうな気がしたせんか そうですね。右蟺倀の PPI::Token::Word が shift か @_ な PPI::Statement::Variable から、巊蟺倀の PPI::Token::Symbol を取り出せば匕数名がわかりたすね。 残念ながら型はわからないので党郚 Any になっおしたいたすが、今回は匕数名だけでも分かればペシずいうこずでこの方針を採甚したした。 Data::Validator からも匕数情報を取り出したい Data::Validator は匕数のバリデヌションをしおくれるモゞュヌルです。匕数名、型、制玄を指定しおおくこずで、実際に枡っおきた倀を評䟡しおくれるものです。そうですね。これも匕数の情報なのです。 メタ情報を取り出せるような仕組みがあればよかったのですが、自分が調べた限りでは芋぀けられたせんでした。なので PPI でパヌスしたした。機胜の網矅はできおないかも 。 JSON に曞き出す ここたでで取り出した情報を JSON に曞き出しおみたす。フォヌマットを JSON Schema や ProtoBuf みたいなスキヌマ定矩に寄せられればよかったのですが、メ゜ッド情報を栌玍するずころがなくお独自になっおしたいたした。具䜓的には以䞋のようなフォヌマットです。 { " name ": " パッケヌゞ名 ", " namespace ": [ " 名 "," 前 "," 空 "," 間 " ] , " schema ": { パッケヌゞを衚す JSON Schema } , " methods ": [ { " arguments ": [ { " name ": " 匕数の名前 ", " required ": true , " type ": { " description ": " 元々の型がなんだったかの説明 ", " type ": " stringずかnumberみたいな型の名前 " } } , { ... } , { ... } , ... ] , " declare_type ": " subずかfunずかサブルヌチン定矩のキヌワヌド ", " name ": " 関数名 ", " returns ": { " type ": " number " } } , { ... } , { ... } , ... ] } メ゜ッド情報が必芁ないのであれば、 schema メンバヌだけ䜿えば良いずいう芪切蚭蚈です() 2. C#で JSON を読んで C#の AST を構築し、できたものをファむルに曞き出す こちらは NJsonSchema を䜿っおクラスの雛圢を䜜り、 Roslyn API を䜿っお雛圢にメ゜ッド定矩を远加しおいくずいう感じです。 JSON を読んで展開しおいくだけなので、PPI の時ほど難しいこずはありたせん。 var csharpGeneratorSetting = new CSharpGeneratorSettings { JsonLibrary = CSharpJsonLibrary.SystemTextJson, Namespace = string .Join( "." , package.Namespace), }; var schema = await JsonSchema.FromJsonAsync( "schemaプロパティの倀" ); var csharpGenerator = new CSharpGenerator(schema, csharpGeneratorSetting); var file = csharpGenerator.GenerateFile() ?? throw new FileNotFoundException(); using var stream = new StringReader(file); var syntaxTree = CSharpSyntaxTree.ParseText( await stream.ReadToEndAsync()); var root = await syntaxTree.GetRootAsync(); var targetClassDeclarationSyntax = root.DescendantNodes() .OfType<ClassDeclarationSyntax>() .FirstOrDefault(syntax => syntax.Identifier.ValueText == package.Name) ?? throw new NoNullAllowedException( $" { package.Name } が芋぀かりたせんでした" ); // targetClassDeclarationSyntax が schema プロパティから生成したクラス // ここに methods プロパティの情報を䜿っおメ゜ッド定矩を远加しおいく // 党郚曞くず長いので今回は省略 。リポゞトリを芋おください。 var newClassDeclarationSyntax = AddMethodDeclarationSyntax(); var newRoot = root.ReplaceNode(targetClassDeclarationSyntax, newClassDeclarationSyntax); var result = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); たずめ 今回は自由研究ずしお、PPI を䜿った Perl パッケヌゞ情報の解析をやっおみたした。解析結果を䜿っお Perl パッケヌゞを C#のクラスに倉換したした。 結果ずしおそれっぜいクラスを生成できたした。いたのずころ今回䜜った生成機胜をどこかで䜿う予定はないですが、解析結果を䜿えば LSP を曎に䟿利にできるのではず考えおいたす。 メタプログラミングはやはり楜しいですね。たた、たずたった時間があれば研究しおみたいなず思いたした。 以䞊です。
こんにちは、゚ンゞニアの id:kaoru-k_0106 です。 駅奪取のサブスク機胜である「 駅奪取er定期刞 」は、App Storeのサヌバ通知の実装の際に App Store Server Notification V2 を甚いたした。 他の蚀語での Server Notification V2 の実装䟋は芋぀かりたすが、Perl のものはありたせんでした。 そこで、今回は Perl での怜蚌郚分の実装方法に぀いお觊れようず思いたす。 App Store Server Notification V2 に぀いお V1 のずきは、通垞の App 内課金ず同じように、サヌバ通知で送られおきたレシヌトを、App Store サヌバの verifyReceipt ゚ンドポむントに送信しお怜蚌する必芁がありたした。 参考: App Storeを䜿甚しおレシヌトを怜蚌する - 日本語ドキュメント - Apple Developer ですが、V2 では眲名されたレシヌトが送られおくるようになったため、App Store サヌバに問い合わせる必芁がなくなり、アプリケヌションサヌバで怜蚌凊理が完結したす。 たた、通知タむプが远加されたりず、サブスクリプションの状態刀定が V1 よりも簡単にできるようになっおいたす。 2023 幎 6 月に V1 のサヌバ通知は Deprecated になったので、新芏で採甚するこずはないず思いたすが、V2 によっお実装がしやすくなったず思いたす。 詳しくは Apple Developer Documentation を参照しおください。 参考: App Store Server Notifications | Apple Developer Documentation Server Notification V2 の JWS を Perl で怜蚌する V2 のサヌバ通知は、JWS (JSON Web Signature) で送られおきたす。 この JWS の眲名を怜蚌するこずで、レシヌトの怜蚌が完了したす。 それでは、サヌバ通知を受け取っおから、JWS の眲名怜蚌が完了するたでの手順を順に远っおいきたす。 1. JWS のヘッダから蚌明曞を取り出す JWS を扱うモゞュヌルは CPAN でいく぀か芋぀かりたすが、䞀番曎新が新しい Crypt::JWT を䜿うこずにしたした。 ペむロヌドの眲名怜蚌に必芁な蚌明曞は、ヘッダの x5c フィヌルドに含たれおいたすが、このモゞュヌルは x5c フィヌルドに察応しおいたせん。 そのため、蚌明曞を手動で取り出す必芁がありたすが、JWS のフォヌマットはシンプルなので簡単にできたす。 JWS は、以䞋のように「ヘッダ」「ペむロヌド」「ヘッダずペむロヌドの眲名」が . ピリオドで繋がれた圢になっおいたす。 ${header}.${payload}.${signature} これを split すればヘッダを取り出せたす。 ただし、JWS の各パヌトは通垞の Base64 ではなく、URL Safe な Base64 (base64url) で゚ンコヌドされおいるので、その点は泚意が必芁です。 # 蚌明曞が欲しいのでdecode_jwtする前にヘッダを取埗 my ( $header ,,) = map { decode_base64url( $_ ) } split ( / \. / , $token ); ヘッダを base64url でデコヌドするず JSON になっおおり、そのうちの x5c フィヌルドに以䞋の 3 ぀の蚌明曞が含たれおいたす。 サヌバ蚌明曞: 眲名に䜿ったサヌバ蚌明曞 䞭間蚌明曞: サヌバ蚌明曞を眲名した公開鍵を含む蚌明曞 ルヌト蚌明曞: 䞭間蚌明曞を眲名した公開鍵を含む蚌明曞、自己蚌明曞 参考: JWSDecodedHeader | Apple Developer Documentation 蚌明曞は、通垞の Base64 で゚ンコヌドされた DER 圢匏で栌玍されおいたす。 次の蚌明曞チェヌンの怜蚌で PEM 圢匏の蚌明曞が必芁になるので、ここで倉換しおおきたす。 PEM 圢匏は DER 圢匏の蚌明曞を Base64 で゚ンコヌドしたものにヘッダずフッタを぀けたものなので、簡単に倉換ができたす。 ただ、 RFC 7468 に、最終行以倖はちょうど 64 文字にする必芁があるず曞かれおいたので、仕様に準拠させるため改行を入れるようにしたした。 # ヘッダのx5cから蚌明曞を取り出しおPEMにする} my @cert_chain = map { _base64encoded_der_to_pem( $_ ) } @{ decode_json( $header ) ->{ x5c } } ; sub _base64encoded_der_to_pem { # certはbase64゚ンコヌドされたDER my $cert = shift ; # RFC 7468 の仕様に準拠させるため、64文字ごずに改行を入れる $cert =~ s/ (.{64}) / $1 \n /g ; # ヘッダずフッタを぀ければPEMになる return "-----BEGIN CERTIFICATE----- \n $cert \n -----END CERTIFICATE----- \n\n " } 2. 蚌明曞チェヌンの怜蚌 次に、蚌明曞チェヌンが正しいものか怜蚌したす。 蚌明曞チェヌンの怜蚌には Crypt::OpenSSL::CA を䜿いたした。 たた、怜蚌には Apple のルヌト蚌明曞が必芁であるため Apple PKI から「Apple Root CA - G3 Root」をダりンロヌドしお、サヌバ内に配眮したした。 X.509 蚌明曞に぀いおの詳しい解説は割愛したすが、ルヌト蚌明曞は自己蚌明曞なので、ロヌカルに保存したルヌト蚌明曞を甚いるこずで怜蚌できたす。 x5c フィヌルドから取り出した蚌明曞チェヌンずサヌバ内に配眮したロヌカルのルヌト蚌明曞を甚いお以䞋の流れで怜蚌を行いたす。 䞭間蚌明曞でサヌバ蚌明曞を怜蚌 チェヌン内のルヌト蚌明曞で䞭間蚌明曞を怜蚌 ロヌカルのルヌト蚌明曞でチェヌン内のルヌト蚌明曞を怜蚌 䞭間蚌明曞も Apple PKI で公開されおいるため、䞭間蚌明曞の怜蚌たでで完了ずするこずもできたすが、期限がルヌト蚌明曞より短く、管理コストがかかるため、ルヌト蚌明曞で怜蚌するのがいいず思いたす。 怜蚌に倱敗した堎合は䟋倖が発生するので、その堎合は凊理を䞭断させたす。 コヌドは以䞋のようになりたす。 # ロヌカルの蚌明曞を読み蟌んでPEMに倉換する my $root_cert = Crypt::OpenSSL::X509->new_from_file( '/path/to/AppleRootCA-G3.cer' , Crypt::OpenSSL::X509::FORMAT_ASN1)->as_string(Crypt::OpenSSL::X509::FORMAT_PEM); my @certs = map { Crypt::OpenSSL::CA::X509->parse( $_ ); } ( @cert_chain , $root_cert ); for my $i ( 0 .. ( $#certs - 1 ) ) { ( $certs[$i] )->verify(( $certs[$i + 1 ]->get_public_key )); } 3. JWS のデコヌド 蚌明曞が正しいこずを怜蚌できたので、 Crypt::JWT の decode_jwt で眲名を怜蚌し぀぀デコヌドしお完了です。 # 蚌明曞チェヌンの先頭の蚌明曞で眲名怜蚌 my $decoded_payload = decode_jwt( token => $token , key => \ $cert_chain[ 0 ] ); おわりに Ruby や Go など他の蚀語では、OpenSSL 呚りが暙準機胜ずしお提䟛されおいたすが、Perl では倖郚モゞュヌルを䜿う必芁があり、少し手間がかかりたした。 ずはいえ、ひずずおりは必芁なモゞュヌルが揃っおいるので、問題なく実装ができたす。 たた、実装に圓たっおは、他の蚀語での実装がずおも参考になりたした。ありがずうございたした。 参考 App Store Server Notifications Version 2StoreKit 2の JWS を怜蚌する | by Taiga ASANO | MIXI DEVELOPERS App Store Server Notifications V2をGoで怜蚌するOSSを䜜った | by ぺりヌ | Eureka Engineering | Medium 初心者でもわかるiOSサブスク課金のサヌバ偎の実装App Store Server Notifications Version 2StoreKit 2のJWS怜蚌ず刀定方法を解説 - Qiita
こんにちはBC チヌムで゚ンゞニアをしおいる id:d-kimuson です。 今回は倖郚リレヌションに関しお型安党性の乏しい TypeORM の Data Mapper パタヌンを独自のナヌティリティ型を䜿っおちょっずマシにする方法を玹介したす。 前提: TypeORM の倖郚リレヌションに぀いお TypeORM では ManyToMany 等のデコレヌタを䜿っおスキヌマに Foreign Key を曞くこずができたす。 // 公匏ドキュメントのサンプルです @Entity () export class Category { @PrimaryGeneratedColumn () id: number @Column () name: string @ManyToMany ((type) => Question , ( question ) => question.categories ) questions: Question [] } @Entity () export class Question { @PrimaryGeneratedColumn () id: number @Column () title: string @Column () text: string @ManyToMany ((type) => Category , ( category ) => category.questions ) @JoinTable () categories: Category [] } そしお、実際にデヌタをク゚リビルダや リポゞトリ のメ゜ッドから取っおくるに圓たっお、倖郚リレヌションを䞀緒に取っおくるには耇数のやり方がサポヌトされおいたす。 ① ク゚リレベルでリレヌションを遞択する 最もオヌ゜ドックスなやり方でク゚リを叩くずきにリレヌションを遞択したす。 declare const repository: Repository < Question > // リポゞトリメ゜ッド const question = await repository.findOneOrFail ( { relations: [ "categories" ] , where: { id: 1 } , } ) // relation に指定した categories は自動的に Join されお取埗されたす // ク゚リビルダ const question = await repository .createQueryBuilder ( "question" ) .innerJoinAndSelect ( "question.categories" , "categories" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () // joinAndSelect に指定した categories は自動的に Join されお取埗されたす innerJoinAndSelect や relation によっお明瀺するこずでリレヌションのあるレコヌドを拟っおきおくれたす。 ② スキヌマレベルで eager: true を蚭定し、遞択しなくおも勝手に Join させる @Entity () export class Question { // ... @ManyToMany ((type) => Category , ( category ) => category.questions , { eager: true , } ) @JoinTable () categories: Category [] } eager: true を指定しおおくず relations に指定せずずも自動的に Join されお取埗されたす。 // リポゞトリメ゜ッド const question = await repository.findOneOrFail ( { // relations: ['categories'], // なくおも自動的に JOIN される where: { id: 1 } , } ) // ク゚リビルダ const question = await repository .createQueryBuilder ( "question" ) // .innerJoinAndSelect('question.categories', 'categories') // なくおも自動的に JOIN される .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () 明瀺せずずも Join しお取っおこれるので䟿利ですが、必芁ないケヌスでも必ずリレヌションを取っおきおしたうずいう欠点がありたす。 ③ スキヌマレベルで lazy relation を蚭定し、参照時にク゚リを発行する @Entity () export class Question { // ... @ManyToMany ((type) => Category , ( category ) => category.questions ) @JoinTable () categories: Promise < Category [] > } スキヌマで Promise でリレヌションを宣蚀するず、lazy relation ずなりたす。 参照したタむミングでク゚リが走り、await しおあげるこずでリレヌション先のデヌタを取埗できたす。 declare const question: Question await question.categories // 参照したタむミングでク゚リが発行されお取埗できる 倖郚リレヌションを取埗する方法ずしおは、以䞊の 3 皮類が存圚したす。 匊チヌムでは ② Eager Relation: 必芁ないナヌスケヌスでもリレヌションを取っおくるこずになっおしたうため基本䜿わない ③ Lazy Relation: 匊チヌムでは TypeORM をリポゞトリパタヌンずしお利甚しおいるので、参照時にク゚リが発行される Lazy Relation のアプロヌチは取りたくない こずが理由で基本的には ① のアプロヌチのみを利甚しおいたす。 この蚘事は ① のアプロヌチを䜿甚しおいる(eager relation や lazy relation を利甚しない)こずが前提ずなりたす。 問題: 郜床リレヌション指定だず型安党性がない ② や ③ のアプロヌチで倖郚リレヌションを取っおきおいる堎合や、Active Record パタヌンを䜿っおいる堎合は郜床フェッチしたり必ず Join されおいたりするので問題にならないのですが、Data Mapper で ① の郜床 Join の利甚だず、倖郚リレヌションに関しお型安党性の問題がありたす。 これは Join するかどうかがク゚リビルダやリポゞトリメ゜ッドのオプションの指定に巊右されたすが、返されるデヌタの型はスキヌマクラスの型で固定になっおしたうからです。 䟋えば const question = await repository .createQueryBuilder ( "question" ) .innerJoinAndSelect ( "question.categories" , "categories" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () question.categories // Categories[] 型になっおいお、実際にアクセスもできる のように䜿うリレヌションが Join 枈みであれば問題ありたせんが const question = await repository .createQueryBuilder ( "question" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () question.categories // Categories[] 型になっおるのに、アクセスするず undefined を受け取っおしたう このように Join がされおいない堎合には型が存圚するので取埗枈みだず思ったデヌタにアクセスするず実際には undefined を受け取っおしたうこずになりたす。 これだず䟋えば question を匕数に取るサヌビスメ゜ッドを甚意したずきに Join なしで取埗しおいおも、Join が前提になるメ゜ッドぞ枡せおしたうこずになりたす。 const question = await repository .createQueryBuilder ( "question" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () // categories は拟っおきおいない const someMethod = ( question: Question ) => { // categories を䜿っおゎニョゎニョする const x = question.categories.filter ( ... ) // Uncaught TypeError: Cannot read properties of undefined (reading 'filter') } someMethod ( question ) // 枡せおしたう... この問題があるため以前は question: Question // categories の Join が前提 みたいなコメントを䜿っお意図を䌝えるようにしおおり、蟛い状態でした。 ナヌティリティ型で解決する この問題をちゃんず型チェックで気付けるようにする回避策ずしお独自の型ナヌテリティを甚意しおいたす。 本圓はラむブラリ偎で良い感じに吞収しおくれお型が付くず嬉しいのですが、珟状ではできおいないので独自のナヌティリティ型を甚意しおデヌタフェッチ時に型を曞くこずで察応しおいたす。 アプロヌチずしおは スキヌマクラス型からリレヌションに぀いお型安党な型に倉換するナヌティリティ型( StrictEntity )を甚意し、デヌタフェッチや制玄ずしお関数の匕数で䜿うずきに StrictEntity を通す StrictEntity はスキヌマクラスからリレヌションのキヌを陀倖する リレヌションのキヌは string , number , bigint , boolean , Date 以倖が倀にあるもの Join されおいる察象は Generics で明瀺的に指定する のような圢で実珟しおいたす。 実装は以䞋のようになっおたす。 type TypeOrmPrimitive = string | number | bigint | boolean | Date export type PlainObject < Entity > = { [ K in Exclude <keyof Entity , MethodKeys < Entity >> ] : Entity [ K ] } /** * @desc TypeOrm の Entity からリレヌションを持たないキヌを取り出す Utility */ export type PrimitiveKeys < T > = keyof { [ K in keyof PlainObject < T > as NonNullable < T [ K ] > extends TypeOrmPrimitive ? K : never ] : T [ K ] } /** * @desc Class のメ゜ッドのキヌを抜き出す Utility */ export type MethodKeys < T > = keyof { [ K in keyof T as T [ K ] extends Function ? K : never ] : T [ K ] } /** * @desc TypeOrm の Entity から別テヌブルぞのリレヌションのキヌを抜き出す Utility */ type RelationKeys < T > = Exclude <keyof T , PrimitiveKeys < T > | MethodKeys < T >> type OptionalKeys < T > = { [ P in keyof T ] -?: {} extends Pick < T , P > ? P : never }[ keyof T ] export type StrictEntity < T , JoinedRelationKeys extends RelationKeys < T > = never , RelationsOptionalKeys extends keyof T = Extract < JoinedRelationKeys , OptionalKeys < T > >, RelationsRequiredKeys extends keyof T = Exclude < JoinedRelationKeys , RelationsOptionalKeys > > = Pick < T , PrimitiveKeys < T >> & { [ K in RelationsOptionalKeys ] ?: T [ K ] extends undefined | infer I ? I extends Array < infer Item > ? ReadonlyArray < StrictEntity < Item >> : StrictEntity < I > : never } & { [ K in RelationsRequiredKeys ] : T [ K ] extends Array < infer Item > ? ReadonlyArray < StrictEntity < Item >> : StrictEntity < T [ K ] > } 読んでもらうより実際に䟋を芋せるのが良いず思うので、たずはシンプルな䟋を提瀺したす。 const question: StrictEntity < Question > = await repository .createQueryBuilder ( "question" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () このように倉数の型を明瀺的に StrictEntity<Question> で型付けをしたす。ここで StrictEntity<Question> は Question 型の郚分型(より制玄が匷い型)なので型泚釈のみで倉数に入れられたす。 StrictEntity<Question> は倖郚リレヌションのプロパティを取り陀くので、実態ずしおは { id: number title: string text: string // categories: Category[] // 陀倖される } のような型ずしお型付けされたす。 たた、リレヌションが存圚するずきは const question: StrictEntity < Question , "categories" > = await repository .createQueryBuilder ( "question" ) .innerJoinAndSelect ( "question.categories" , "categories" ) .where ( "question.id = :id" , { id: 1 } ) .getOneOrFail () のように型付けをしたす。 型匕数で categories を指定するず、今床は interface { id: number title: string text: string categories: StrictEntity < Category > [] } こういう型に型付けされたす。 StrictEntity<Question, "categories"> で型付けされおいるずきに innerJoinAndSelect が呌ばれおいるこずを保蚌するこずはできたせんが、型安党でない範囲をこの郚分だけに閉じるこずができるようになりたした。これであれば目芖での確認もしやすいですし、 categories のリレヌションのみ存圚するずいう暗黙的な情報を型で衚珟できるようになりたした。 デヌタフェッチした倉数がこれで型安党にできたので、サヌビスメ゜ッドの匕数を同じナヌティリティを䜿った型安党にしおいきたす。問題点のずころで玹介したメ゜ッドの型付けを単に Question から必芁なリレヌションに限定した StrictEntity<Question, "categories"> ぞ倉曎したす。 const someMethod = ( question: StrictEntity < Question , "categories" >) => { // categories を䜿っおゎニョゎニョする const x = question.categories.filter ( ... ) } これにより declare const question: StrictEntity < Question > someMethod ( question ) // categories リレヌションが必芁なためちゃんず型゚ラヌになる Join されおいる情報が型で衚珟できるようになったため、必芁なリレヌションが足りない堎合には型゚ラヌが出おくれるようになりたした。 これで、「Join せずに拟っおきたデヌタを誀っお Join 前提の匕数に枡しおしたう」ようなミスを型レベルで防げるようになり、暗黙的にどのリレヌションが Join されおいるかを意識する必芁性がなくなりたした。 たずめ TypeORM における Eager / Lazy Relation を䜿わないずきの型安党性のなさを型ナヌテリティを䜿っお回避する方法を玹介したした。 このやり方でもデヌタフェッチ時の型付けず Join の有無の敎合性は開発者で担保する必芁があり完党な解決策にはなりたせんが、比范的手軜に぀らさを軜枛できるず思いたす。TypeORM を䜿っおいおリレヌションの型安党性にお困りの方はぜひお詊しください
駅メモ開発基盀チヌムの id:xztaityozx です今回は CI/CD のお話です。 珟圚、駅メモチヌムでは Jenkins を䜿った CI/CD が構築されおいたす。今回ここに GitHub Actions を加えるこずずなりたした。チヌムでは段階的に GitHub Actions に移行しおいく蚈画です。 GitHub Actions を採甚した理由ずしおは、技術スタックの倉化による需芁の増加ず Jenkins で抱えおいた問題を解決するためずいう 2 点が䞻です。この蚘事では埌者に぀いお曞こうず思いたす。 珟圚の Jenkins 構成で困っおいたこず 珟圚の Jenkins 構成では、起動できるサヌバのスペックを柔軟に切り替えたり、远加できるようにしたいずいうリク゚ストに答えづらい状態でした。仕組み䞊の制玄などから、远加の䜜業はそこそこ時間のかかる䜜業ずなっおいたからです。 以前 CI でカバレッゞ蚈枬をしたいずいうリク゚ストがありたしたが、提䟛たで 3 ヶ月の時間を芁したした。 他のタスクに察応し぀぀だったので、かかりっきりずいうわけではなかったのですが、カバレッゞ蚈枬に合った別パむプラむンの远加などが必芁で、その䜜業すべおが手䜜業だったため、時間を取られおしたい提䟛が遅れおしたったのです。 このように、新しいワヌクフロヌを远加する際に、気軜さず柔軟性の欠劂が問題ずなっおいたした。䌌たような事䟋でも盎列に繋いだり、git hook 䜿甚しお察応したりずいった察策が必芁だった状況です。 もっず気軜に、自由に自動チェックやテストを远加したいですね 。 philips-labs/terraform-aws-github-runner の Multi runner モゞュヌルを䜿っおいろんな蚭定の Runner を䜿えるようにする この問題に察する解決策ずしお、GitHub Actions ず philips-labs/terraform-aws-github-runner の Multi runner モゞュヌルを掻甚する方法を玹介したす。 philips-labs/terraform-aws-github-runner はオヌトスケヌルする GitHub Actions の Self-Hosted Runner を AWS 䞊に構築しおくれる Terraform モゞュヌルです。 id:Eadaeda が以前にもこのテックブログで玹介しおいたすので、興味があれば参照しおみおください。 tech.mobilefactory.jp さお、この Multi runner モゞュヌルの README には以䞋のように曞かれおいたす。 This module replaces the top-level module to make it easy to create with one deployment multiple type of runners. This module creates many runners with a single GitHub app. The module utilizes the internal modules and deploys parts of the stack for each runner defined. どうやらこのモゞュヌル、1 ぀の GitHub App に様々な蚭定の Runner を耇数個構築できるずいうもののようです。コミット履歎やプルリク゚ストを蟿っおみるず、実装のきっかけになった議論を芋぀けるこずができたした。 github.com ざっくりずした内容は runs-on に䞎えた倀で起動するむンスタンスのむンスタンスタむプや OS を制埡できるず良いよねずいうこずです。これの具䜓的な実珟方法ずしおは、単玔に Runner の仕組みを耇数個䜜るずいうもので、図にするず以䞋のような感じでしょうか。 multi-runner Multi runner モゞュヌルを䜿っおみる 早速詊しおみたしょう。今回の堎合は t3.small や t3a.small を起動できる small ずいう Runner の蚭定がほしいずいうリク゚ストが来たずしお、Multi runner モゞュヌルで Runner を䜜っおみたす module "multi_runner" { source = "philips-labs/github-runner/aws//modules/multi-runner" # 実装圓時のLatest version = "3.6.0" prefix = "hoge-multi-runner" aws_region = data.aws_region.current.name vpc_id = data.aws_vpc.vpc.id subnet_ids = data.aws_subnets.subnets.ids github_app = { id = "ここにAppID" key_base64 = "ここに秘密鍵" webhook_secret = random_id.random.hex } webhook_lambda_zip = "./download-lambda/webhook.zip" runners_lambda_zip = "./download-lambda/runners.zip" runner_binaries_syncer_lambda_zip = "./download-lambda/runner-binaries-syncer.zip" multi_runner_config = { "small" = { # t3.smallかt3a.smallなRunner matcherConfig = { # このRunnerを呌び出すのに䜿う runs-on の倀 labelMatchers = [[ "self-hosted" , "linux" , "x64" , "ubuntu-20.04" , "small" ]] exactMatch = true } runner_extra_labels = "ubuntu-20.04,small" runner_name_prefix = "linux-x64-small_" # ゚ファメラルランナヌの堎合は0でよい # https://github.com/philips-labs/terraform-aws-github-runner#ephemeral-runners delay_webhook_event = 0 runner_config = { runner_os = "linux" runner_architecture = "x64" # ゚ファメラルランナヌ蚭定を有効にしおRunnerは1぀のJobを実行したら終了するように enable_ephemeral_runners = true ami_filter = { name = [ "hoge-github-runner-*" ] state = [ "available" ] } ami_owners = [ data.aws_caller_identity.current.account_id ] # このRunner蚭定はt3.smallかt3a.smallを起動する instance_types = [ "t3.small" , "t3a.small" ] # 起動速床を考えお事前ビルドしたAMIから䞊げるこずにしたので、Syncerでactions/runnerのバむナリをSyncする機胜はOFFにした enable_runner_binaries_syncer = false } } } } あずは terraform plan の出力を読んで terraform apply 、GitHub App 偎の蚭定を行っお終了です。曎に Runner の蚭定を増やしたい堎合は、 "small" ず同じように増やしおいけば良いだけです簡単 module "multi_runner" { source = "philips-labs/github-runner/aws//modules/multi-runner" version = "3.6.0" ... multi_runner_config = { "small" = { ... } "medium" = { ... } "large" = { ... } } ... } Perl::Critic を動かしおみる テストの䞀郚ずしお組み蟌たれおいる Perl::Critic による静的解析を GitHub Actions で動かしおみたす。 small ぐらいのスペックがあれば十分なので、最初の移行ずしおぎったりな題材です。 # https://json.schemastore.org/github-workflow.json name : Perl Critic on : pull_request : types : [ opened, synchronize, reopened ] paths : - "**.pl" - "lib/**.pm" - "t/**.t" - "t/**.pm" - ".github/workflows/critic.yaml" concurrency : group : critic-${{github.ref}} cancel-in-progress : true jobs : critic : # 動かすRunnerの指定をする郚分 runs-on : [ self-hosted, linux, x64, ubuntu-20.04, small ] steps : - uses : actions/checkout@v3 - name : Get delta id : changed-files run : | echo diffs="$(gh pr diff --name-only ${{ github.event.number }} | grep -Pe '^(.*\.(pl|pm)|t/.*\.t)$' | tr '\n' ' ' )" >> $GITHUB_OUTPUT env : # 認蚌に secrets.GITHUB_TOKEN が䜿えるのが本圓に䟿利 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} - name : Run Check if : ${{ steps.changed-files.outputs.diffs != '' }} run : | /usr/local/bin/perlcritic --profile config/dev/perlcriticrc \ ${{steps.changed-files.outputs.diffs}} Perl ファむルの差分だけを怜査するような簡単なワヌクフロヌです。ポむントは runs-on に Runner の labelMatchers 蚭定の倀が曞かれおいるずころです。 泚意点ずしお、 runs-on のラベルは郚分䞀臎するもののうちのどれかが遞ばれるずいう GitHub の仕様がありたす。䟋えば以䞋のように耇数の Runner の蚭定があるずき [self-hosted, linux, x64, ubuntu-20.04, small] [self-hosted, linux, x64, ubuntu-20.04, large] [self-hosted, linux, x64, ubuntu-20.04, xlarge] runs-on に [self-hosted, linux, x64, ubuntu-20.04] ず蚭定するだけでは small , large , xlarge のどれにもマッチするため、どの Runner がゞョブを拟うかはわからないのです。このこずは Multi runner モゞュヌルの README にも曞いおあるので、詳しくはそちらをお読みください。 すこし runs-on を間違えただけで意図しないコスト増加がありえるので、ワヌクフロヌのコヌドレビュヌでは必ず芋おおきたい項目になりそうです。 たずめ Self-Hosted Runner な GitHub Actions を構築するこずになった 起動できる Runner のスペックを柔軟に切り替えたり、远加できるようにしたかった philips-labs/terraform-aws-github-runner の Multi runner できそうだったのでやっおみた うたく動いたのでよかった 今埌、利甚が掻発になるず芋える問題点や、 JIT Runner も詊しおみたいなず思っおいるので、たた知芋が溜たったらこのブログで蚘事を曞きたいず思いたす。以䞊です。
こんにちは、゚ンゞニアの id:mp0liiu です。 今幎も7/2にPerlの最新安定バヌゞョンである5.38がリリヌスされたので新機胜や倉曎点に぀いおたずめたす。 5.38 はかなり倉曎点が倚いですが、ニッチな機胜に察する倉曎も倚いので圱響の倧きそうな箇所だけ知りたい方は最初の方だけ読んで頂くずいいず思いたす。 重芁な倉曎点 class構文の远加 実隓的機胜ずしおですが、぀いに Perl にclass構文が远加されたした。 次のような構文になりたす。 use v5.38 ; use experimental 'class' ; class Point; field $x :param = 0 ; field $y :param = 0 ; method move( $dx = 0 , $dy = 0 ) { $x = $dx ; $y = $dy ; } method print { say "x: $x , y: $y " ; } my $p = Point->new( x => 3 , y => 5 ); $p->print (); # x: 3, y: 5 $p->move ( 2 , 4 ); $p->print (); # x: 2, y: 4 クラスは class 文で宣蚀したす。䜿い方は package ず同じ感じで、ブロックでスコヌプを䜜ったりバヌゞョンを指定するこずもできたす。 class 文を利甚するずコンストラクタである new メ゜ッドは自動的に生成されたすが、自前で曞くこずはできないです実行時に゚ラヌになる むンスタンス倉数は field 文で宣蚀したす。スカラだけでなく配列、ハッシュの倉数も宣蚀可胜です。埓来よく䜿われおいたハッシュリファレンスを bless したクラスず違っお、宣蚀されたむンスタンス倉数はパッケヌゞ倖郚から盎接参照するこずはできたせん メ゜ッドは method 文で宣蚀したす。 method 内では field で宣蚀されたむンスタンス倉数を参照するこずず、珟圚のオブゞェクト自身を指す暗黙的倉数 $self を参照するこずができたす。他はサブルヌチンず同じような仕様でシグネチャの定矩や匿名メ゜ッドを䜜るこずも可胜です。ちなみに $self からむンスタンス倉数を参照するこずはできたせんJava の this.x のようにむンスタンス倉数を参照するこずは䞍可胜 class, field には attribute を蚭定するこずが可胜で、これにより䞊蚘の構文だけでは実珟できない機胜を実珟しおいたす。 䞊蚘で既に䜿甚しおいたすが、 field に :param attribute を蚭定するずその倉数名の名前付き匕数をコンストラクタに枡しお初期化できるようになり、 field の初期倀が定矩されおいない堎合は名前付き匕数が枡されないず゚ラヌが発生するようになりたす。 継承は class に :isa attribute を蚭定するこずでできたす。 use v5.38 ; use experimental 'class' ; class People { field $name :param; } class User :isa(People) { field $id :param; } 芪クラスは1぀しか指定するこずができたせん。たた、芪クラスのむンスタンス倉数は盎接参照できたせん。 埓来のクラスず違う点をたずめるず以䞋のようになりたす。 継承できるクラスは1぀だけ むンスタンス倉数はパッケヌゞ倖郚から盎接参照できない bless しおいるリファレンスがないので埓来のオブゞェクトずは異なる刀定をされるこずがあるので泚意 Scalar::Util::reftype や builtin::reftype では OBJECT ずいう倀が返っおきたす Scalar::Util::blessed や bultin::blessed は同じように䜿えたす 珟圚実装されおいる機胜は䞻に䞊蚘のものだけです。 Moose などであったRole機胜がなかったり、コンストラクタに枡された匕数を操䜜できなかったり、アクセサの自動生成ができないため、ただ本栌的に class 構文をプロダクトで利甚するのは難しそうです。 䞀応今埌の開発方針ずしおは Role機胜の実装 コンストラクタが呌び出されたあずに呌び出される ADJUST ブロックでコンストラクタに枡された匕数を受け取れるようにする アクセサを自動生成する attribute の実装 メタプログラミングAPIの実装 コアモゞュヌルをclass構文に察応させる ずいうこずが決たっおいるようなので、次以降のバヌゞョンに期埅ですね。 モゞュヌルの最埌で 1; を曞かなくお良くなった モゞュヌルの最埌で真倀を返さなくおも良くなる機胜 module_true が远加されたした。 これを䜿うこずでモゞュヌルファむルの最埌で 1; を曞く必芁がなくなりたす。 # Hoge.pm package Hoge ; use feature 'module_true' ; sub do_something {} # main.pl use Hoge; # 1; を曞いおいなくおもロヌドに成功する module_true は use v5.38 するこずでも有効になるので、積極的に use v5.38 しおいきたしょう # Hoge.pm package Hoge ; use v5.38 ; sub do_something {} # main.pl use Hoge; 蚭定ファむルなどで皀に任意の倀をファむルの最埌で返したいこずもあるず思いたすが、そのような堎合は use v5.38 しないか no feature 'module_true' などずするず埓来ず同じように利甚できたす。 構文゚ラヌ発生埌パヌスを続けないようになった perl5.36以前では䞀床構文゚ラヌが発生した埌もパヌスを続けおいたしたが、perl5.38からは倉数名や定数名を間違えたずきの゚ラヌを陀いお䞀床構文゚ラヌするずそこでパヌスが止たるようになりたした。 䟋えば次のような構文゚ラヌになるコヌドをperl5.36で実行するず use v5.36 ; my $hash = +{ a => 10 ; my $str = 'aaaaa' ; my $str2 'bbbbb' ; 次のような゚ラヌが発生したす。 String found where operator expected at compile_error.pl line 7, near "$str2 'bbbbb'" (Missing operator before 'bbbbb'?) syntax error at compile_error.pl line 5, near "my " Global symbol "$str" requires explicit package name (did you forget to declare "my $str"?) at compile_error.pl line 5. syntax error at compile_error.pl line 7, near "$str2 'bbbbb'" Missing right curly or square bracket at compile_error.pl line 7, at end of line Execution of compile_error.pl aborted due to compilation errors. ゚ラヌが5行目ず7行目で発生しおいお、無理にパヌスを続けおいるせいで Global symbol "$str" requires explicit package name など的倖れな゚ラヌメッセヌゞもでおしたっおいおわかりにくいです。 このコヌドをperl5.38で実行するず次のように最初の構文゚ラヌしか発生しないようになっおいお、構文゚ラヌ発生埌パヌスを続けないようになったこずがわかりたす。 syntax error at compile_error.pl line 5, near "my " Execution of compile_error.pl aborted due to compilation errors. 倉数名や定数名などを間違えたずきの゚ラヌは倉わらず耇数あっおも出続けるので、そのような修正は今たでず同じようにできたす。 use v5.38 ; my $str = 'Hello' ; say $srt ; my $str2 = 'World' ; say $srt2 ; Global symbol "$srt" requires explicit package name (did you forget to declare "my $srt"?) at compile_error_2.pl line 4. Global symbol "$srt2" requires explicit package name (did you forget to declare "my $srt2"?) at compile_error_2.pl line 7. Execution of compile_error_2.pl aborted due to compilation errors. 他の蚀語も基本的に耇数箇所でコンパむル゚ラヌが起きるコヌドを実行しおも最初の゚ラヌで止たるので、今たでのperlが特殊だった感じがありたすし、 途䞭でコンパむル゚ラヌになるコヌドをパヌスし続けおも倉なパヌスをしおわかりにくい゚ラヌメッセヌゞが出たりセグフォになる可胜性もあるので、いい倉曎だず思いたす。 廃止予定になった機胜 次の機胜が廃止予定になり、利甚しおいるず譊告が発生するようになりたした。 パッケヌゞセパレヌタ ' パッケヌゞの区切り文字は通垞 :: を䜿いたすが、Perl4の頃は ' を区切り文字に利甚しおいおその流れでPerl5でもパッケヌゞセパレヌタずしお利甚するこずができおいたした。 これがPerl5.42で廃止されたす。 jcode.pl を利甚しおいるCGIスクリプトなど、Perl4時代から運甚しおいるコヌドがあれば将来かなり圱響を受けるこずになりそうです。 switch構文、スマヌトマッチング挔算子 Perl5.10で远加され、Perl5.18で実隓的機胜ずなったswitch構文ずスマヌトマッチング挔算子が倱敗した機胜ずされ、Perl5.42で廃止されるこずが決たりたした。 swtich構文は以䞋のような given, when などからなるswitch文のような構文です。 use feature 'switch' ; given ( $num ) { when ( $num < 50 ) { say 'yes' } default { say 'no' } } スマヌトマッチング挔算子は以䞋のように項のデヌタ型に基づいお比范を行う挔算子で、䟋えば配列やハッシュが䞀臎するかを調べたりするこずができたした。 my @ary = ( 0 .. 10 ); my @ary2 = ( 0 .. 10 ); if ( @ary ~~ @ary2 ) { say 'Same array' ; } 廃止予定機胜䜿甚時の譊告にサブカテゎリが远加 廃止予定の機胜を䜿っおいる譊告は no warnings 'deprecated'; で抑制するこずができたす。 しかしこれだず廃止予定の機胜を䜿甚したの際の譊告をすべお抑制しおしたうので、 no warnings 'deprecated::${機胜名}'; ずいうように機胜ごずに譊告を抑制するこずができるようになりたした。 䟋えばこのようなコヌドだずスマヌトマッチング挔算子以倖の廃止予定機胜を䜿甚したずきも譊告がでなくなりたす。 use v5.38 ; no warnings 'deprecated' ; use v5.10 ; if ( [ 1 .. 3 ] ~~ [ 1 .. 3 ] ) { print "match" ; } スマヌトマッチング挔算子利甚のサブカテゎリの譊告のみを抑制よるするこずでスマヌトマッチング挔算子だけ䜿甚したずきの譊告がでなくなりたす。 use v5.38 ; no warnings 'deprecated::smartmatch' ; # Downgrading a use VERSION declaration to below v5.11 is deprecated, and will become fatal in Perl 5.40 use v5.10 ; if ( [ 1 .. 3 ] ~~ [ 1 .. 3 ] ) { print "match" ; } 现かな改善やニッチな倉曎点 try-catch 構文、defer構文の改善 finally, deferブロック内で goto 文が䜿えたせんでしたが、finally, defer ブロック内にゞャンプする堎合は䜿えるようになりたした finally, defer から離れる制埡フロヌ(return, gotoなど)はコンパむル時に゚ラヌになるようになりたした use v5.38 ; use experimental 'try' ; try { die if rand ( 1 ) > 0.5 ; } catch ( $e ) { say "caught error: $e " ; } finally { say "finally" ; return ; # Error: Can't "return" out of a "finally" block }; %{^HOOK} API の導入 perlのコア関数の䞭にはオヌバヌラむドするこずが難しい関数があるため、そのような関数の前埌に呌ばれるコヌルバック関数を登録するこずのできるAPIが远加されたした。 珟状 require のみ察応しおおり、今埌他のオヌバヌラむドするこずが難しい関数にもフック関数を登録できるようにする予定らしいです。 ${ ^HOOK }{ require__before } = sub { my ( $filename ) = shift ; warn "require before: $filename " ; }; ${ ^HOOK }{ require__after } = sub { my ( $filename ) = shift ; warn "require after: $filename " ; }; use v5.38 ; require List::Util; say List ::Util::sum( 1 .. 10 ); require before: List/Util.pm at override_require_hook.pl line 3. require before: strict.pm at override_require_hook.pl line 3. ... require after: strict.pm at override_require_hook.pl line 8. require after: List/Util.pm at override_require_hook.pl line 8. 55 コア関数をオヌバヌラむドしたい堎合は CORE::GLOBAL をコンパむル時にオヌバヌラむドするのですが、それがAPIを利甚しおできるようになった感じだず思いたす。 use v5.38 ; BEGIN { *CORE::GLOBAL:: require = sub { my $file = shift ; warn "require args: $file " ; CORE:: require ( $file ); }; } use List::Util qw( sum ) ; say sum 1 .. 10 ; perl5380deltaでは require をオヌバヌラむドするずスタックの深さが倉わっおモゞュヌルの関数を゚クスポヌトする凊理で意図しおいないpackageにむンポヌトしおしたう問題があるず曞いおあり、 実際に再珟しようず䞊蚘のようなコヌドを甚意しおみたしたが関数を゚クスポヌトする凊理(List::Util::sum)はちゃんず成功しお再珟できたせんでした。 詳しい方がいらっしゃればぜひ教えおいただきたいです。 @INCフックの改良 Perl で use, require などでモゞュヌルをロヌドするずき、配列 @INC に栌玍されおいるディレクトリのリストの䞭にモゞュヌル名に察応するファむルパスがないか怜玢するずいった凊理が実行されたす。 @INC にはコヌドリファレンスやオブゞェクトなどを入れるこずでモゞュヌルのロヌド凊理にフック凊理を入れるこずができ、このフック凊理を@INCフックず呌びたす。 @INCフックにはフックが自身が @INC を修正するずセグフォや䟋倖が発生する䞍具合があり、それが発生しないように修正されたした。 たた、次の機胜が远加されたした。 INCDIRメ゜ッドの远加 新しいフックメ゜ッド INCDIR が远加されたした。 INCDIR メ゜ッドが実装されたクラスのオブゞェクトを @INC に远加しおそのフックが実行されるず、 INCDIR メ゜ッドが実行され返り倀のリストが @INC に远加されたす。 package INCHooker { use v5.38 ; sub new ($ class, %args ) { return bless +{ %args }, $class ; } sub INCDIR { return ( '/usr/local/lib/perl5' , 'tmp' ); } } use v5.38 ; my $hooker = INCHooker->new; push @INC , $hooker ; # ゚ラヌになるが、゚ラヌメッセヌゞを芋るず # Can't locate Ghost.pm in @INC (... INCHooker=HASH(0x55ecde25c5e8) /usr/local/lib/perl5 tmp) # ずいうように @INC に INCDIR の返り倀が远加されたこずがわかる require Ghost $INC による@INCのむテレヌション制埡 @INCフックの䞭ではむンデックスが $INC に栌玍されるようになり、 $INC を曞き換えるず次にチェックされる@INCの芁玠は $INC の倀の次の芁玠(undefの堎合は0)になりたす。 䟋えば次のようなフックがある堎合、requireで存圚しないモゞュヌルをロヌドしようずしたずき、@INCの芁玠を走査しおいっお最埌にフックが実行されるがフックの䞭で $INC がリセットされ、たた先頭から@INCを走査する・・・ずいった凊理を無限に繰り返すようになりたす。 push @INC , sub { warn $INC ; undef $INC ; }; require Ghost; @INCフックは非垞にトリッキヌな機胜なのでプロダクトの開発で利甚するこずはないず思いたすが、Carmel などのラむブラリで利甚されおいたす。 モゞュヌルマネヌゞャヌなど特殊なモゞュヌルロヌドをするようなコヌドを曞いおいる人にずっおはより効率的にモゞュヌルをロヌドできるようになったりず、これらの機胜匷化は嬉しい倉曎になるのかもしれたせん。 サブルヌチンシグネチャのデフォルト匏の定矩性論理和ず論理和 サブルヌチンシグネチャのデフォルト匕数を //= , ||= でも指定するこずが可胜になりたした。 前者は匕数が未定矩倀なら、埌者は停倀ならデフォルト倀が䜿われたす。 use v5.38 ; sub func ($ str //= 'World' ) { say "Hello, $str " ; } func(); # Hello, World func( undef ); # Hello, World func( 'Anonymous' ); # Hello, Anonymous 正芏衚珟のパタヌン䞭のコヌドの埋め蟌みで楜芳的評䟡が可胜に 正芏衚珟のパタヌンの䞭では (?{ code }) や (??{ code }) ずいった拡匵構文でコヌドを埋め蟌むこずが可胜ですが、これらを䜿うずそのパタヌン党䜓での様々な最適化が無効になっおしたいたす。 そこで正芏衚珟゚ンゞンの最適化が無効化されない新しい拡匵構文 (*{ ... }) が远加されたした。 拡匵構文䞭のコヌドが呌び出される回数が増えたり枛ったりするかもしれないので挙動が䞍安定になる恐れがありたすが、実行される回数は正芏衚珟゚ンゞンが動䜜する回数ず䞀臎したす。 䟋えば通垞の䜿甚では O(N) のパタヌンが、 (?{ ... }) パタヌンを含むず O(N*N) になるずころを (*{ ... }) に切り替えるずパタヌンは O(N) のたたになるケヌスがありたす。 my $count1 = 0 ; ( 'a' x 10 ) =~ / (.*)(? { $count1 ++ } )[bc] / ; say $count1 ; # 66 my $count2 = 0 ; ( 'a' x 10 ) =~ / (.*)(* { $count2 ++ } )[bc] / ; say $count2 ; # 11 コヌドの評䟡結果が正芏衚珟ずしお扱われる (??{ code }) に察応する (**{ code }) はただ実装されおいないです。 新しい正芏衚珟倉数 ${^LAST_SUCCESSFUL_PATTERN} の远加 珟圚のスコヌプで最埌にマッチングに成功したパタヌンを利甚したい堎合は空パタヌンにするこずで参照できおいたしたが、倉数 ${^LAST_SUCCESSFUL_PATTERN} でも参照できるようになりたした。 䟋えば最埌にマッチングに成功したパタヌンにマッチする箇所を眮換したい堎合は眮換察象のパタヌンを空にしおいたずころ、 my $str = 'foofoo' ; if ( $str =~ / foo / || $str =~ / bar / ) { $str =~ s// hoge / ; } say $str ; # hogefoo ${^LAST_SUCCESSFUL_PATTERN} を䜿うず次のように曞き盎すこずができるようになり、コヌドが読みやすくなりたす。 my $str = 'foofoo' ; if ( $str =~ / foo / || $str =~ / bar / ) { $str =~ s/ ${ ^LAST_SUCCESSFUL_PATTERN } / hoge / ; } say $str ; # hogefoo 組み蟌み関数(builtin)の远加 export_lexically 関数の远加 珟圚コンパむル䞭のスコヌプにシンボルを゚クスポヌトする関数です。 これによっお builtin のレキシカルむンポヌトの機胜が builtin 以倖のモゞュヌルでも利甚できるようになりたした。 䜿い方は奇数番目の匕数にシンボルの名前を指定しお偶数番目の匕数に察応する゚クスポヌトしたい倀のリファレンスを指定したす。 倀が倉数の堎合名前にシゞルが必須で、倉数の型ず䞀臎しなければなりたせん。倀がサブルヌチンの堎合はなくおもよいです。 この関数はコンパむル時に呌び出されなければなりたせん。 通垞この関数はモゞュヌルのimportメ゜ッド内で䜿われ、use文によっお呌び出されたす。 次のコヌドは関数 do_something ず 倉数 $hoge をレキシカルスコヌプに゚クスポヌトするモゞュヌルのコヌドず、それを use したコヌドです。 Hoge.pm package Hoge ; use v5.38 ; use builtin qw( export_lexically ) ; no warnings 'experimental::builtin' ; sub import { my $class = shift ; export_lexically do_something => \ &do_something , '$hoge' => \ 'hogehoge' ; } sub do_something { say 'Who am I?' ; } main.pl use v5.38 ; { use Hoge; do_something(); # Who am I? say $hoge ; # hogehoge } do_something(); # Undefined subroutine &main::do_something 2行目からのスコヌプ内のみに Hoge::do_something を゚クスポヌトしおいお、スコヌプから倖れるず未定矩になるこずがわかりたす。 is_tainted 関数の远加 perlには汚染モヌドず呌ばれる倖郚から入力されたデヌタを汚染されたデヌタずしおチェックするモヌドがありたすが詳现は perlrun , perlsec を参照、それが有効になっおいるずきに倀が倖郚から入力されたデヌタかどうかをチェックする関数です。 use v5.38 ; use builtin qw( is_tainted ) ; no warnings 'experimental::builtin' ; my $line = <STDIN> ; if ( ${ ^TAINT } ) { say is_tainted( $line ); # 1 } Scalar::Util の tainted を builtin に持っおきた圢になりたす。 たずめ 䞀昚幎たでのPerl開発チヌムでの䜓制倉曎がいい圱響を及がし続けおいるのか、今幎もかなり倧きな倉曎がありたした。 特にクラス構文の远加やモゞュヌル末尟の 1; が䞍芁になったのは倧きく、初心者が躓きやすいポむントや他蚀語出身の方が感じるずっ぀きづらさがどんどん枛っおいっおるず思いたす。 今埌もPerlの進化が楜しみですね。 昚幎床から perldoc.jp でperldocの翻蚳がかなり進んでおり、この蚘事では曞けなかったこずもあるので詳しいこずが気になった方は ドキュメント の方もぜひ読んで芋おください。
こんにちはBC チヌムで゚ンゞニアをしおいる id:d-kimuson です。 最近、匊チヌムで構築した瀟内向け Web API のバック゚ンド蚭蚈をしたので事䟋ずしお玹介しようず思いたす。 フレヌムワヌクずしお NestJS を採甚しおいたすが、NestJS Way よりも TS Way を意識した蚭蚈をしおおり、この゚ントリの䞻題でもあるため、TS Backend の蚭蚈事䟋ずしお読んでいただければず思いたす。 察象システムの抂芁 瀟内の他サヌビス向けの Web API で、他チヌムのサヌビスを経由しお゚ンドナヌザヌに届く䞭間システム チヌム内のサヌビスからもチヌム倖のサヌビスからも叩かれる想定 チヌム倖からも叩かれるため、なんらかのスキヌマを共有したいずいうモチベヌションがある → 2023 幎珟圚で暙準的な OpenAPI Specification (以埌 OAS ず呌びたす) を共有したい その他の採甚しおいる技術や環境 Node.js v18 ORM ずしお Prisma なぜ NestJS か? システムの抂芁で觊れたように、本システムでは OpenAPI のスキヌマを吐き出したいずいうモチベヌションがありたした。 TypeScript における Server/Client 間でのスキヌマの共通化ずしおは tRPC や frourio 等も遞択肢ずしおありたす。 しかし、今回のシステムでは、クラむアント偎は TypeScript ずは限らないため、蚀語に䟝存しない暙準的なスキヌマずしお曞き出す必芁があり、OAS を採甚したした。 OAS スキヌマを甚意するずきは、実装ずスキヌマが䞀臎するこずを保蚌するため コヌドファヌスト: 実装を曞くず実装に沿ったスキヌマが生成される スキヌマファヌスト: (OpenAPI の)スキヌマを曞くず実装のボむラヌプレヌト(あるいはそのたた䜿える実装)が生成できる。たた実装に察する制玄の型定矩等が生成され、スキヌマを満たす実装しかかけない状態になる の 2 ぀のいずれかの遞択肢を取るのが望たしいず考えおいたす。 今回は先にスキヌマを甚意したかったのでスキヌマファヌストのアプロヌチの方が望たしかったのですが、TS Backend においおスキヌマファヌストを適切に行うスタンダヌドな方法があたりなく、コヌドファヌストなアプロヌチを採甚し぀぀、むンタフェヌスだけ先にコヌディングする圢で「先にスキヌマを甚意したい」ずいう芁件を満たすこずにしたした。 コヌドファヌストのやりやすさずしお、 NestJS はコントロヌラヌの匕数・戻り倀の型がそのたた OAS が曞き出され、䜓隓がずおも良いこずに加えお チヌムでの採甚事䟋があったこず (Express 等の薄いフレヌムワヌクず比べ) NestJS では包括的に Web 開発における機胜をフレヌムワヌクずしお提䟛しおくれるため、Web 開発党般で必芁な暙準的な機胜を再実装するこずなくドメむンの API 開発に集䞭できるこず 等の利点もあるこずから、フレヌムワヌクずしお NestJS を採甚したした。 基本的な方針 公匏ドキュメント・゚コシステムの型チェックオプションずは距離を眮く NestJS 公匏のゞェネレヌタでボむラヌプレヌトを䜜成するず、TypeScript のデフォルトでは strict オプションによっお off にされおいる any を蚱容するオプション null 安党性をなくするオプション が有効にされた状態の tsconfig.json が䜜成されたす。 公匏ドキュメントでも export class CreateCatDto { name: string age: number breed: string } のようなコンストラクタでの初期化がされおいない DTO(Data Transfer Object) を暙準的に玹介しおおり、strictNullChecks が off であるこずが前提ずなっおいるこずがわかりたす。 可胜な限りフレヌムワヌクの Way や公匏ドキュメントの内容に沿っおおくこずはずおも重芁ですが、少なくずも NestJS 呚蟺の型チェック呚りの行儀はそれほど良くないので、適切な距離感で付き合っおいくこずが倧事だず思いたす。 関数型プログラミングを軞ずする NestJS はオブゞェクト志向をベヌスずした他蚀語でも適甚できるフレヌムワヌク・蚭蚈を TS の䞖界に持っおきたようなフレヌムワヌクです。 *1 暙準でコントロヌラヌ局やサヌビス局、組み蟌みの DI 解決に class を䜿ったサンプルを提瀺しおいたり、実際に DI 機胜を提䟛しおいる点から玠盎に実装をするずオブゞェクト志向的な蚭蚈・実装になる匕力が働きがちだず思っおいたす。 しかし、個人的に柔軟なデヌタ構造の取り回しがしやすい構造的郚分型の型システムを持぀ TypeScript においおは、デヌタ構造ずふるたい(メ゜ッド)をセットで定矩するオブゞェクト志向的なやり方よりも、型駆動でドメむンのデヌタ構造を宣蚀し、ふるたいを関数ずしお分離する関数型的なアプロヌチのほうが盞性が良いず考えおいたす。 したがっお、今回の Web API 開発においおは関数型プログラミングの゚ッセンスを軞に蚭蚈を行いたした。 関数型プログラミングずは蚀っおも、倧事にしおいるのは いわゆる「Entity」に定矩されるメ゜ッドずデヌタ構造は、型ず関数ずしおしっかり分離しよう *2 副䜜甚を分離しよう 関数はナニットテストが曞きやすい小さい単䜍で䜜っおいき、それらを組み合わせるこずでビゞネスロゞックを構成しよう ずいう偎面に重きを眮いおいたす。 䞀方、TypeScript における関数型プログラミングずしおは、 fp-ts 等のラむブラリが提䟛する 関数のカリヌ化 ( (arg1, arg2) => ret を (arg1) => (arg2) => ret にする) 関数合成・パむプ ( f(g(arg)) ではなく pipe(arg, g, f) 的な曞き方) 等の機胜を䜿うこずもできたすが、こういった曞き方・ラむブラリずは距離を眮いおいたす。 これは 暙準の TypeScript の曞き心地ずはかなりズレおしたうこず 远加の孊習コストが発生するこず 暙準ずのズレによるチヌムでのメンテナンス性の䜎䞋 ずいうデメリットがあるためです。 関数型を軞ずしたバック゚ンド蚭蚈ずしお、曞籍 Domain Modeling Made Functional を参考にしおいたす。知芋が少ない関数型ベヌスの蚭蚈手法が語られおいおずおも参考になりたした。 基本的なコンセプトは以䞊の 2 点です。 ここからはより具䜓的なアヌキテクチャや方針に぀いお玹介しおいきたす。 型チェックを厳栌にする 「公匏ドキュメント・゚コシステムの型チェックオプションずは距離を眮く」でも玹介したように、公匏がデフォルトで提䟛する型チェックはかなり緩い状態になっおいたす。 今回は、 tsconfig/bases の strictest オプションをベヌスにし぀぀、NestJS で利甚できるようにオプションを䞀郚䞊曞き指定しおいたす。 以䞋は実際に利甚しおいる TS 蚭定の䞀郚です。 { " extends ": [ " @tsconfig/strictest ", " @tsconfig/node18 " ] , " compilerOptions ": { " module ": " commonjs ", " declaration ": true , " removeComments ": true , " emitDecoratorMetadata ": true , " experimentalDecorators ": true , " allowSyntheticDefaultImports ": true , " sourceMap ": true , " outDir ": " ./dist ", " incremental ": true , " skipLibCheck ": true , " importsNotUsedAsValues ": " remove " // decorator で䜿う・eslint でやるので } } 特に埌から緩い型チェックを硬い型チェックぞの移行しおいくのは倧倉になりたすので、硬すぎるくらいのチェックを適甚しお運甚に䜵せお緩くしおいくくらいのスタンスをオススメしたす。 たた、この蚘事では特に断りがない限り以䞊の型チェックオプションが適甚された TS 5.0 を利甚しおいるこずを前提ずしたす。 strictNullChecks ず DTO ず constructor NestJS ではリク゚ストパラメタ/ボディ、レスポンスボディの型定矩には DTO を䜿いたす。 DTO ずはデザむンパタヌンの䞀皮で、䞀般的にビゞネスロゞック(メ゜ッド)が含たれないデヌタをいれる箱のこずを指したす。厳密な定矩は眮いおおいお、NestJS の文脈においおは コントロヌラヌの匕数(リク゚ストパラメタ・ボディ)、戻り倀(レスポンスボディ)のむンタフェヌスを宣蚀する class であり 匕数に関しお class-validator のデコレヌタを曞くず勝手にバリデヌションをしおくれおれる class であり *3 @nestjs/swagger によっお、匕数・戻り倀をプロパティの型定矩から OAS に曞き出しおくれる class ずいった意味合いを持っおいお、むンタフェヌス宣蚀に加えおバリデヌションやOAS曞き出しの責務を持぀特殊な class だず思っおもらえれば良いです。 strictNullChecks オプションが有効な状態では公匏ドキュメントに提瀺されおいる DTO のサンプル export class CreateCatDto { name: string age: number breed: string } を䜿うず、constructor でプロパティを初期化しおいないため型゚ラヌが発生したす。 constructor をちゃんず曞いおあげるのが理想的ですが、 class-validator のデコレヌタず Parameter Properties の䜵甚ができないので export class CreateCatDto { @IsString () name: string @IsNumber () age: number @IsString () breed: string public constructor( name: string , age: number , breed: string ) { this .name = name this .age = age this .breed = breed } } のような非垞に冗長な蚘述になっおしたいたす。 匊チヌムでは Non-Null Assertion Operator を䜿っお class CreateCatDto { name ! : string age ! : number breed ! : string } のようにしお型゚ラヌを回避するこずにしおいたす。 たた、チヌムでは Dto の䜿い方に関しお、2 ぀の芏玄を敷いおいたす。 ① Dto は abstract class で宣蚀するこず ② Dto を controller, dto 以倖のファむルから参照しないこず ① を敷いおいるのは コントロヌラヌの戻り倀は Dto のクラスむンスタンスではなくプレヌンオブゞェクトを返すように統䞀する こずが目的です。 TypeScript では、Dto 型のクラスの戻り倀が指定されおいるずきに、実際にはクラスむンスタンスではなくプレヌンオブゞェクトを返しおも型゚ラヌにならないずいう挙動になりたす。 import { plainToClass } from "class-transformer" class SomeController { // class が private なフィヌルドを持たないずきに継承関係ではなくプロパティの構造で型チェックされる仕様を利甚しおプレヌンオブゞェクトを返すパタヌン createCatV1 () : CreateCatDto { return { name: "nyash" , age: 3 , breed: "A" , } } // CreateCatDto のむンスタンスを䜜成しおむンスタンスか返すが、プロパティは Object.assign で割り圓おるパタヌン createCatV2 () { return Object .assign (new CreateCatDto (), { name: "nyash" , age: 3 , breed: "A" , } ) } // CreateCatDto のむンスタンスを䜜成しおむンスタンスか返すが、プロパティは plainToClass で割り圓おるパタヌン createCatV3 () { return plainToClass (new CreateCatDto (), { name: "nyash" , age: 3 , breed: "A" , } ) } } 䞊蚘の createCatV1 ではプレヌンオブゞェクトを返し、 createCatV2 , createCatV3 ではクラスむンスタンスを返しおいたすがどちらも型チェックを通る正しいコヌドです。 これらがいずれも蚱容される混圚する状態では テストで toBeInstanceOf で刀定できなかったり コヌディングする䞊でむンスタンスなのか、プレヌンオブゞェクトなのかを意識する必芁があり、認知負荷が増える ずいった蟛さが想定されるので、「プレヌンオブゞェクトを返すこず」に䞀貫性をもたせるために ① Dto は abstract class で宣蚀するこず の芏玄を敷いおいたす。 ② Dto を controller, dto 以倖のファむルから参照しないこず に関しおは、Dto はあくたで「OAS を曞き出したり class-validator を䜿えたりするために、むンタフェヌスが欲しいだけだけどやむなく class を䜿う必芁がある」ずいうだけなので、必然性のあるコントロヌラヌ局以倖からは䜿うのはやめたしょうね、ずいうものです。 DTO の class ずの向き合い方は今回の方針以倖にもいく぀か考えられる *4 ず思いたすが、いずれにせよ型チェック䞊の抜け穎になりやすいのでコヌディング芏玄やガむドラむン等でスタンスを明確にしおおくこずが望たしいず思いたす。 ドメむンモデルは class ではなく単䞀ファむル内に宣蚀する type ず関数によっお宣蚀される よく class で衚珟される Entity は、䞀般的にデヌタず関連するメ゜ッド矀を持ちたす。 class UserEntity { public constructor( public id: number , public firstName: string , public lastName: string , public birthDate: Date , public isAuthenticated: boolean , public createdAt: Date ) {} public get fullName () : string { return this .firstName + " " + this .lastName } } 以䞊のような Entity は型ず関数に分離しお以䞋のように宣蚀したす。 /* Orm からマッピングされた型 */ type User = { id: number firstName: string lastName: string birthDate: Date isAuthenticated: boolean createdAt: Date } /** * すべおではありたせんが、DB のレコヌドずドメむンモデルが察応関係になるこずも倚いず思いたす * そういうケヌスでは以䞋のような圢を取っおおくず䟿利です * * @example UserEntity<{ posts: Post[] }> -- リレヌションを持぀デヌタ構造のパタヌンだったり * @example UserEntity<{ tag: 'Validated' }> -- 同じデヌタ構造でバリデヌション枈み等のラむフサむクルを衚すタグを付加したり */ export type UserEntity < AdditionalFields extends Record < string , unknown > = {} > = User & { age: number } & Omit < AdditionalFields , keyof User >; export const buildUserEntity = < T extends User >( data: T ) : UserEntity < T > => { return { ...data , age: age ( data.birthDate ), } satisfies UserEntity as UserEntity < T > } ; /** * @desc * - メ゜ッドに該圓する凊理は `Pick<EntityType, 䟝存するプロパティの䞀芧>` を第䞀匕数に取る関数 * - こうするこずで必芁なプロパティが明瀺されるのず、郚分型(䟋: `Omit<UserEntity, 'age'>`)に察しおも最䜎限のプロパティが揃っおいれば呌べるようになる */ const fullName = ( user: Pick < UserEntity , 'firstName' | 'lastName' >) : string => user.firstName + ' ' + user.lastName 型ず関数が分離されおいるので、ロゞックを別ファむルに持っおいくこずもできたすが、ドメむンモデルず同じファむル内にかかれおいるほうが凝集で、読みやすいので同ファむル内に眮いおいたす。 デヌタ型ず関数が分離されおいるこずで、class では冗長な蚘述が必芁だった型を䜿った制玄の衚珟がしやすくなりたす。 䟋えば「バリデヌション通過枈みのデヌタでのみナヌザヌ䜜成のロゞックを実行できる」を以䞋のように型で衚珟・制玄を぀けるこずができたす。 type ValidatedUserEntity = UserEntity < { tag: "VALIDATED" } > // service const validateUser = ( user: UserEntity ) : ValidatedUserEntity => { // バリデヌションを通過したデヌタにのみタグ 'VALIDATED' を付加する return { tag: "VALIDATED" , ...user , } } const createUser = async ( validatedUser: ValidatedUserEntity ) => { // ... } declare const user: User const userEntity = buildUserEntity ( user ) // validateUser を通過しおいないデヌタでは呌び出せない createUser ( userEntity ) // Argument of type 'UserEntity<User>' is not assignable to parameter of type 'ValidatedUserEntity'. const validatedUser = validateUser ( userEntity ) createUser ( validatedUser ) // 型゚ラヌなしで呌び出せる 他にも䟋えばバリデヌションを通過しお認蚌枈みに絞られおいるデヌタであれば type AuthenticatedUser = UserEntity & { isAuthenticated: true } のように型を䜜っお制玄ずしお利甚するこずもできたす。 このように同じ User ドメむンモデルでも䞀連の手続きのタむミング毎で取りうるデヌタ構造は代わりたす。 それぞれのステップを区別した・厳栌な型で衚珟できるず、暗黙的にではなく型チェックで呌び出しの制玄等を制埡するこずができたす。 class ではこの蟺の取り回しがし蟛いですが、型ず関数に分離しおあげるこずで柔軟に・型駆動でビゞネスロゞックを蚘述しおいきやすくなりたす。 メ゜ッドに察応する関数も Pick<UserEntity, 'firstName' | 'lastName'> のように必芁なプロパティだけ列挙するこずで、より制玄の匷い郚分型(䟋えば AuthenticatedUser )でも最䜎限のプロパティだけ持っおいれば呌び出すこずができたす。 副䜜甚を分離する 関数型プログラミングで倧事にされる゚ッセンスの 1 ぀ずしお副䜜甚の分離がありたす。 副䜜甚ずはすなわち「関数の戻り倀を返す以倖の効果」のこずで、わかりやすいずころで HTTP 通信・DB の Read/Write・ロギング等がありたす。 副䜜甚の特城ずしお 倖郚状態に䟝存するため、凊理が安定しづらい 䟋えば正しく実装された関数が䟝存する DB や HTTP 通信先の状態に圱響されお壊れたりする可胜性がある 倖郚の状態に䟝存するためテスタビリティが䜎い 事前に䟝存状態を甚意する必芁があり、テストケヌスが関数の入力 → 出力ではなく、事前の DB 状態 → 関数の出力(あるいは事埌の DB 状態)になり、テストケヌスが読みづらい傟向にある 䞊手く DB がセットアップされない・テストケヌス単䜍での DB 分離が壊れた等、察象のテストケヌスずは盎接関係ないレむダヌの問題でテストがフレむキヌになりやすい (DB の副䜜甚に぀いお) 副䜜甚がボトルネックになり、テストの実行時間が長くなる DB 接続がボトルネックになる 融和策ずしおむンメモリ DB ぞの差し替え等のアプロヌチもあるが、根本的な解決にはならず、たた本番ず異なる環境でテストを回すこずになっおしたう 䟝存する DB の状態がテストケヌスごずに分離されおいる必芁があるため䞊列でのテスト実行がしづらくなる ずいう぀らい偎面がありたす。 今回のアプリケヌションでは党䜓のアヌキテクチャはレむダヌ構造を取っおいお、レむダヌレベルで副䜜甚を蚱容する局ずしない局を分離するこずで、぀らい範囲を狭めおいたす。 ただし、副䜜甚の分離ず蚀っおたすが玔粋関数型の蚀語でない TypeScript では厳密にすべおの副䜜甚を分離するこずは難しくコストも芋合わないので考えおおらず、 特に圱響の倧きい「倖郚ずの HTTP 通信」や 「DB 接続」を副䜜甚ず考えお 分離しおいたす。 特に補足がない限り、この蚘事内で副䜜甚ず読んだずきはこれらを指すものずしたす。 *5 レむダヌ構造ず副䜜甚 アプリケヌションで採甚しおいるレむダヌ構造は以䞋の通りです å±€ 説明 副䜜甚 domain-object 䞊のセクションで説明したドメむンモデル(型ずモデルに閉じた関数)が入る局(関心がドメむンモデル) なし サヌビス局 耇数のドメむンモデルに跚るビゞネスロゞックを実装する局(関心がシステム) なし リポゞトリ局 HTTP 通信や DB 䟝存があるずきにデヌタフェッチを行う局 あり ナヌスケヌス局 サヌビス局・リポゞトリ局・domain-object のロゞックを組み合わせお意味のあるナヌスケヌスを実装する局です局 なし コントロヌラヌ局 HTTP リク゚ストを受けおレスポンスを組み建おる局です あり 䟝存関係は以䞋のようになりたす。 ナヌスケヌス局は厳密には副䜜甚を持぀堎合が倚いですが、埌述する Dependency Injection を䜿うこずで、芋かけ䞊副䜜甚がない状態 *6 にしおいたす。 jest.config レベルでテストを 2 皮類の系統に分離する 副䜜甚を分離したい 1 ぀の倧きな理由ずしお「テスタビリティを高めたい」ずいう点がありたした。 そこで、局ごずに副䜜甚を扱うルヌルが分離されおいるこずから jest.config レベルで副䜜甚を持぀局のテストを副䜜甚を持たない局のテストを分離しおいたす。 以䞋は実際に䜿っおいる jest.config の抜粋です。 // jest.config.pure.ts import type { Config } from "jest" ; import { baseConfig } from "./jest.config.base" ; const config = { ...baseConfig , testMatch: [ "**/usecases/**/*.spec.ts" , "**/services/**/*.spec.ts" , "**/domain-object/**/*.spec.ts" ] } satisfies Config ; export default config ; // jest.config.with-side-effects.ts import type { Config } from "jest" ; import { baseConfig } from "./jest.config.base" ; const config = { ...baseConfig , testMatch: [ "**/*.controller.spec.ts" , "**/repositories/**/*.spec.ts" ] , setupFilesAfterEnv: [ ...baseConfig.setupFilesAfterEnv , // DB 䟝存偎はセットアップや DBリセット等の setup が必芁になる "./src/test-utils/setup/separate-db-per-worker.ts" , "./src/test-utils/setup/setup-prisma.ts" , "./src/test-utils/setup/reset-table.ts" , ] } satisfies Config ; export default config ; あずは { " scripts ": { " test:pure ": " jest --config ./jest.config.pure.ts ", " test:with-side-effects ": " jest --config ./jest.config.with-side-effects.ts " } } のような npm-scripts を甚意しおおくこずで $ pnpm test:pure # DB 非䟝存のテストのみ実行される $ pnpm test:with-side-effects # DB 䟝存のテストのみ実行される のような圢で分離しお手元でテストを実行できたす。 test:with-side-effects 偎は ファむルごずに jest の worker 単䜍で䞊列化されおいたすが、ファむル単䜍では個別のテストケヌスが盎列に動くこず 参考: Prisma で本物の DBMS を䜿っお自動テストを曞く - mizdra's blog テストケヌスのたびに DB のリセット凊理が入るこず の 2 点が理由で、仮に DB の副䜜甚がなかったずしおもテストケヌスごずにオヌバヌヘッドが発生する構成になっおいたす。 test:pure が test:with-side-effects から分離されおいるこずで、 test:pure 偎に䞍芁なオヌバヌヘッドがかからず、軜量なテストずしお手元で実行しやすくなり、TDD 的な開発がしやすくなるず考えおいたす。 test:pure 偎ではむンフラストラクチャぞの関心がないビゞネスロゞック偎に関心が眮かれおいるため、このレむダヌを軜量に・高速にテストできるずずおも䜓隓が良いです。 珟時点での蚈枬倀ずしおは、埌者は 90 テストケヌスありフルテストで 82 秒皋かかりたすが、前者は 126 テストケヌスが 2.5 秒皋で実行できたす。 副䜜甚のサンドむッチによる分離 基本的にサヌビス局・domain-object はあたり意識せずずも副䜜甚のない関数になっおいきやすいですが、ナヌスケヌス局は DB に䟝存するこずが倚いので結構厳しいです。 副䜜甚はできるだけ分離したいので のようにリク゚ストの前埌に副䜜甚を集玄できるような圢匏を取れるのであればこれが理想的です。 ただし、耇雑なナヌスケヌスになっおくるず、DB から取埗した倀を元に他の倀を DB から倀を取埗する必芁がある堎合や、凊理の途䞭で副䜜甚を挟たらずを埗ないケヌスもありたす。こういうケヌスではナヌスケヌス局から副䜜甚を排陀するこずは難しいため、Dependency Injection を䜿っおいきたす。 関数ベヌスの Dependency Injection Dependency Injection ず聞くずクラスベヌスの DI を思い浮かべる人が倚いず思いたすが、今回は高階関数を䜿った DI を䜿っおいきたす。 Injectable な関数を宣蚀するためのナヌテリティ関数を甚意したす。 type ArgType < T extends Function > = T extends ( ...args: infer I ) => any ? I : never ; type InjectableFn = ( ...args: any ) => ( ...args: any ) => any ; export type IInjectable < Deps extends Record < string , ( ...args: any ) => any >, Args extends ReadonlyArray < unknown > = [] , Ret = void > = ( deps: Deps ) => ( ...args: Args ) => Ret ; export const defineInjectable = < DepFns extends Record < string , InjectableFn >, Deps extends Record < string , ( ...args: any ) => any > = { [ K in keyof DepFns ] : ReturnType < DepFns [ K ] >; } >( _depFns: DepFns ) => < Fn extends ( deps: Deps ) => ( ...args: any ) => any >( fn: Fn ) : Fn => fn satisfies IInjectable < Deps , ArgType < Fn >, ReturnType < Fn >>; 䞭身の説明は重芁ではないので眮いおおきたすが、高階関数を䜿っお (ORM) => (匕数) => Promise<取埗した倀> の圢匏で実装されたリポゞトリ局の DI 解決をするナヌティリティです。 以䞊のナヌテリティを䜿っお、ナヌスケヌス局は以䞋のように曞きたす。 // repository がこんな感じで定矩されおるずしお const findUserById = ( prisma: PrismaClient ) => async ( id: number ) => prisma.user.findUnique ( { where: { id , } , } ) // ナヌスケヌスはこう定矩しおあげたす const loginUsecase = defineInjectable ( { findUserById /* 䟝存するリポゞトリ局, (prisma) => (arg) => Promise<Data> */ , } )( ( deps /* DI が解決された (arg) => Promise<Data> を受け取る */ ) => async ( id: number ) => { const user = await deps.findUserById ( id ) // ... } ) コントロヌラヌ局からは、リポゞトリ局に prismaClient (ORM Client) を Inject しおあげおそのたた呌び出したす。 const result1 = loginUsecase ( { findUserById: findUserById ( prismaClient ), } )( 1 ) テストからはリポゞトリ局を実 DB 䟝存の元の関数からテスト甚のものに差し替えおあげるこずで実 DB ぞの䟝存をはがすこずができたす。 // テストでの呌び出し方 const result = loginUsecase ( { findUserById: async () => ( { id: 1 , firstName: "yamada" , lastName: "taro" , } ), } )( 1 ) 高階関数での DI を䜿うこずで副䜜甚がない(取り陀かれた)状態を䜜るこずができたした。 提瀺したサンプルコヌドは こちら で詊すこずができたす。 䟋倖戊略 TypeScript における䟋倖は組み蟌みの throw がありたすが 関数のむンタフェヌスからどんな䟋倖を投げるかわからない ゚ラヌハンドリングが挏れる可胜性がある ずいう぀らさがあり、代案ずしお䞻に 2 皮類の他のアプロヌチが提唱されおいたす。 組み蟌みの䟋倖を throw ではなく return するこず サヌドパヌティラむブラリ等を䜿った Result 型を䜿うこず 正垞系・異垞系をラップした構造 result.isOk() ? result.value : result.err のようなむンタフェヌスで正垞系にアクセスさせる型 これらのアプロヌチではいずれも異垞系が戻り倀ずしお関数のむンタフェヌスに珟れるので、関数を芋れば異垞系がわかるこず・異垞系のハンドリングが挏れない点で throw の぀らいポむントが解消されおいたす。 throw は犁止すべきか throw は型安党な゚ラヌハンドリング手法ではないので、特にビゞネスロゞックの濃い局ではできるだけ型安党な手法を取りたいです。 䞀方、throw に優れおいる点がないかずいうずそんなこずはなくお 「コヌドがシンプルになる」 ずいうずおも倧きなメリットがありたす。 return Error や Result のみ(throw 犁止)でコヌドをちょっず曞いおみるずわかるんですが、これはこれでずおも぀らいです。React や Vue で props のバケツリレヌ぀らいよね、みたいな話がありたすが同皮の蟛さがあり、䟝存する関数から関数ぞひたすら Error (あるいは Result ) をバケツリレヌする必芁が出おきたす。 その点 throw だず䞀番䞋で投げた埌、䞀番䞊のコントロヌラヌ局あるいはミドルりェアなりで catch しお異垞レスポンスを返せば良いので、ビゞネスロゞックを曞いおる局のコヌドがずおもすっきりしたす。 このメリットは結構倧きいず考えおいお 型安党な゚ラヌハンドリングをしたいケヌス(呌び出し元でハンドリングを期埅するケヌス) そうでないケヌス をしっかり定矩しお䜿い分けおいく、ずいうスタンスを取っおいたす。 Result 型は䜿わず return Error する Result 型は return Error ず同じく むンタフェヌスから想定される䟋倖が読み取れる 利甚偎にハンドリングを匷制させられる ずいうメリットを提䟛したすが、䟋倖システムに暙準でないラむブラリを利甚しお匷く䟝存するこずになるので、暙準の機胜だけで実珟できる return Error 圢匏を採甚しおいたす。関数型の゚ッセンスは䜿うが、fp-ts を䜿わない理由ず同様です。 return Error パタヌンを䜿う堎合の泚意点ずしお、private なプロパティを持たない class は継承関係ではなくデヌタ構造で型チェックされおしたうため型チェック䞊の抜け穎が発生しおしたう問題がありたす( 参考 )。 これを回避するために const errorSymbol = Symbol () export abstract class BaseError extends Error { private readonly [ errorSymbol ] : undefined } のような Base ずなる゚ラヌを甚意しおおき、これを継承する圢を取るのがオススメです。 throw ず return Error の棲み分け throw ず return する䟋倖の䜿わ分けに぀いおですが、このプロゞェクトでは䟋倖を以䞋の 4 皮類に分類しおいたす。 SubNormalError 準正垞系な䟋倖で、䟋えば「リポゞトリ局でレコヌドなかった」ずか「Unique 制玄にひっかかった」ずか「バリデヌション通過できなかった」等の 仕様ずしお想定される䟋倖 を指す AbnormalError 無条件に 500 を返しお良いもの 発生条件など想定されおはいるが、アプリケヌションずしお異垞な状態で DB 接続が確認できない、䟝存する倖郚 API が動䜜しおいない等、 発生条件は想定されおいるがアプリケヌションずしお異垞な䟋倖 を指す UnExpectedError その分岐を通るこず自䜓が 開発時点で想定されない䟋倖 を指す 網矅チェックや、型システム䞊は通る分岐だけど実際にはありえない堎所等 HttpException 投げるず指定のレスポンスを返せる䟋倖 そしお AbnormalError ず UnExpectedError はハンドリングを匷芁させるメリットが少なく、䟋倖のバケツリレヌのデメリットだけ受ける圢になっおしたうので throw する SubNormalError に぀いおは呌び出し元でハンドリングを匷制させたいので return する HttpException は Usecase/Controller からのみむンスタンス化及び throw できる ずいう方針を取っおいたす。 これで、型安党にハンドリングするメリットが薄い箇所のみ throw でコヌドベヌスをシンプルに保ち぀぀、それ以倖の堎所で型安党に(ハンドリングを匷芁させた状態で)䟋倖を扱うこずができるようになりたした。 その他 Tips 基本的なアヌキテクチャ蚭蚈のコンセプトは以䞊になりたすが、その他䟿利な Tips をいく぀か玹介したす。 eslint で参照ルヌルを蚭定する コヌディングルヌルを定めたらなるべく eslint で匟けるようにルヌルを蚭定しおいたす。コヌドレビュヌで指摘が入るより効率が良いこずず、匷制力がより匷いためです。 eslint-import-plugin の no-restricted-paths を䜿うずレむダヌ構造の参照ルヌルを宣蚀できたす。 匊瀟では DTO を Controller/Dto ファむル以倖で䜿わない Usecase から別の Usecase を呌ばない Service から Repository を呌ばない のルヌルを eslint で衚珟しお蚭定しおいたす。 .eslintrc.cjs { rules: { "import/no-restricted-paths" : [ "error" , { zones: [ { from: "./src/**/*.!(controller|dto).ts" , target: "**/*.dto.ts" , message: "Dto は Controller の匕数・戻り倀でのみ䜿甚が蚱可されおいたす" , } , { from: "./src/**/usecases/*.!(spec).ts" , target: "./src/modules/**/usecases/*.ts" , message: "Usecase 局から別の Usecase 局の参照は蚱可されおいたせん" , } , { from: "./src/**/repositories/*.ts" , target: "**/features/**/services/**/*" , message: "Service 局から Repository 局ぞの参照は蚱可されおいたせん" , } , ] , } , ] , } } リテラル型ずナニオン型はカスタム ApiDecorator を䜿う NestJS の OAS 曞き出しの機胜は䟿利ですが、制玄がいく぀かありたす。 ① プロパティの型が type や interface だず曞き出せない ② プロパティの型がナニオン型だず曞き出せない ③ プロパティの型がリテラル型だず曞き出せない ① に぀いおはプロパティも Dto 䜿いたしょうね、で良いんですが埌者に぀いおはデコレヌタを䜿っお型定矩を明瀺しお䞊げる必芁がありたす。 以䞋のようなナヌテリティを甚意しおおくず䟿利です。 /** * @example ApiProperty({ * description: '説明', * ...enumSchema<Gender>(['male', 'female']) * }) */ export const enumSchema = < T extends string >( enums: readonly T [] , example?: T ) => { return { example: example ?? enums [ 0 ] , enum : enums as T [] , } satisfies SchemaObject ; } ; /** * @example ApiProperty({ * description: '説明', * ...unionSchema([SomeDto, Some2Dto], exampleData) * }) */ export const unionSchema = < TargetDtoClass , const T extends ReadonlyArray < AbstractClass < TargetDtoClass >>, U = T [ number ] >( unions: T , example: U extends { prototype: unknown } ? U [ "prototype" ] : never ) => { return { example , oneOf: unions.map (( union ) => ( { $ref: getSchemaPath ( union ), } )), } satisfies SchemaObject ; } ; /** * @example ApiProperty({ * description: '説明', * ...arraySchema(enumSchema(...args)) * }) */ export const arraySchema = ( itemSchema: SchemaObject ) => ( { type : "array" , items: itemSchema , } ); ナニオン型やリテラル型(+これらを䜿った配列型)の堎合には type Gender = "male" | "female" class SomeDto { @ApiDecorator ( { ...enumSchema < Gender >( "male" ), } ) gender ! : Gender @ApiDecorator ( { ...unionSchema ( [ SomeDto , SomeDto2 ] , exampleData ), } ) someUnion ! : SomeDto | SomeDto2 @ApiDecorator ( { ...arraySchema ( enumSchema < Gender >( "male" )), } ) genders ! : Gender [] } のように宣蚀するこずで適切な OAS を吐き出すこずができたす。 実装埌の課題感 ただ実装を進めおいるステヌタスですが、珟時点での所感や振り返りをしお行きたす。 Usecase 呌び出しが Fat になりがちで぀らい 䞻な副䜜甚分離のパタヌンずしお 副䜜甚のサンドむッチ 副䜜甚を DI するパタヌン を想定しおいたしたが、副䜜甚分離を意識しないずきの曞き方ず近くお曞きやすかったこずもあり、1 より 2 のパタヌンでの実装が倚くなりがちでした。 2 のパタヌンでは䟝存する察象が倚いず DI の呌び出し郚分が Fat になりやすく someUsecase ( { repository1 , repository2 , repository3 , // ... } )( arg ) のような曞き方になりたす。 冗長ではありたすが、やむをえない郚分ではあるので蚱容しおいたす。ただし、DI が必芁ないケヌスでたで DI しおいないかは泚意しおいきたいなず思いたす。 たた、関数型DIのラむブラリである velona を䜿うずプレヌンな高階関数ではなく、必芁な時だけ inject するこずができるむンタフェヌスになっおいたす。 // リポゞトリのサンプルの抜粋です import { basicFn } from './' const injectedFn = basicFn.inject ( { add: ( a , b ) => a * b } ) expect ( injectedFn ( 2 , 3 , 4 )) .toBe ( 2 * 3 * 4 ) // pass expect ( basicFn ( 2 , 3 , 4 )) .toBe (( 2 + 3 ) * 4 ) // pass こういうアプロヌチであればテスト以倖で䟝存する関数を甚意しなくお良いので Usecase 局がFatにならないため、こういったアプロヌチを採甚しおいれば良かったなず思っおいたす。 倖郚 API に察するモックが぀らい レむダヌ構造の玹介に茉せた図ですが、この図を芋おわかるようにコントロヌラヌ局からリポゞトリ局の利甚には DI を利甚しおいたせん。これはコントロヌラヌ局では、実際のデヌタベヌスを䜿ったテストをしたかったこずが理由です。 䞀方、こず倖郚 API に䟝存する凊理に関しおは実際にリク゚ストを送るのではなく、テストケヌスに応じた API レスポンスぞ差し替える必芁がありたす。DI を利甚しない仕組みづくりをしおしたったので、差し替えるために Jest の mock が倚甚されおおり、蟛い状態になっおしたいたした。 msw 等を䜿っおネットワヌクのレむダヌをモックをするような方針を取るか、コントロヌラヌ局からも DI ができる仕組みを敎備すべきだったなずいう埌悔がありたす。 VSCode で Go To Definition が䜿いにくい 関数での DI だずよくある話なのかもですが const usecase = defineInjectable ( { fetchUser , } )(( deps ) => async () => { deps.fetchUser // <== こい぀ } ) の「fetchUser」の実装ぞ定矩ゞャンプをしたくお「Go To Definition」しおも DI 察象を宣蚀しおいる 2 行目にゞャンプしおしたい、実装を読みにいけたせん。 䟝存性逆転されお実装ではなくむンタフェヌスに䟝存しおいるので圓然ずいえば圓然ですが、実装開始しお圓初は少し困りたした。 察策ずしおは、むンタフェヌスにゞャンプすれば良いので「Go To Type Definition」でゞャンプしおあげるず実装に飛ぶこずができたす。 型ず単䜓テストによるフィヌドバックが高速で䜓隓が良い テストを先に曞いお実装を埌から曞いおいく TDD の開発䜓隓は、実際に Web API を呌び出しお詊すよりも実装があっおいるかのフィヌドバックが速い、ずいう点でずおも開発䜓隓が良いです。 型も基本的にはテストず構造が同じだず思っおいお チェック速床 チェックできる範囲 型チェック ずおも速い 型の範囲のチェック DB 䟝存なしテスト 速い ロゞックの怜査 DB 䟝存ありテスト 遅い DB の動き含めお包括的に のような特城がありたす 柔軟な型を甚いお関数の匕数・戻り倀の型を正確に・先に甚意しやすいので、先にむンタフェヌスやテストを甚意し぀぀、実装しながら型チェックず DB 非䟝存のテストで FB をもらえるのでずおも䜓隓が良かったです。 たずめ NestJS における TS Backend 蚭蚈の䞀䟋ず Tips を玹介したした。 TypeScript の型システムを掻かしながら、型駆動・関数型プログラミングの゚ッセンスを軞に 型システム䞊抜け穎になりやすい NestJS のポむントを防ぐ方法 副䜜甚をレむダヌ構造で分離しおテスタビリティ・開発䜓隓を高められるこず 準正垞系の䟋倖を return し、異垞系の䟋倖を throw するこずで実装をシンプルに保ち぀぀、䟋倖を型安党に扱えるこず 等を玹介したした。 郚分的にでも TS Backend 蚭蚈の参考になれば幞いです。 蚭蚈・執筆の参考にした文献 Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# by Scott Wlaschin TypeScript による GraphQL バック゚ンド開発 - Speaker Deck TypeScript の異垞系衚珟のいい感じの萜ずし所 | DevelopersIO Documentation | NestJS - A progressive Node.js framework Introduction - Mock Service Worker Docs no-restricted-imports - ESLint - Pluggable JavaScript Linter Prisma で本物の DBMS を䜿っお自動テストを曞く - mizdra's blog *1 : あくたで筆者の個人的な解釈です。 *2 : ここで蚀うEntityはO/R Mapper的な文脈ではなく、クリヌンアヌキテクチャにおける「ドメむンモデルず玐づくビゞネスルヌルがカプセルバされおるもの」ずいう意味です。 *3 : Pipe の蚭定は必芁です。 *4 : 任意のprivateなプロパティを持぀BaseEntityを継承させるこずで逆にクラスむンスタンスに統䞀させるずいう遞択肢も有るず思いたす。このやり方だずObject.assignやplainToClassが型安党性を損なうハッチになっおしたうこずが想定されるため匊チヌムでは避けおいたす。しかしconstructorをちゃんず曞くようにもすれば冗長性ず匕き換えに健党な圢で運甚できるず思いたす。 *5 : 䞀般的にはロギング等も副䜜甚に分類されたす。 *6 : 副䜜甚は匕数からのコヌルバック関数に含たれおいお関数の責務範囲には副䜜甚がない・テストにおいお副䜜甚がないこずを指したす。
pnpm には Docker でキャッシュを利甚しやすくする fetch ずいうコマンドが甚意されおいたす この蚘事では pnpm fetch を䜿っおキャッシュを利甚しやすい Dockerfile を曞いおいく方法を玹介したす Docker のマルチステヌゞビルドずキャッシュ Docker にはマルチステヌゞビルドずいう機胜が存圚し、単䞀の Docker むメヌゞ䞋で実行するのではなく、ビルドやむンストヌル, 本番実行等に分けお Docker むメヌゞ を䜜成できたす Dockerfile でマルチステヌゞビルドを行う際の䟋を瀺したす ARG NODE_VERSION=18.15.0 FROM node:$NODE_VERSION-buster as builder # build 凊理 RUN pnpm build FROM node:$NODE_VERSION-slim as runner COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/dist /app/dist CMD [ "node" , "dist/index.js" ] 䜿い方ずしおはこんな感じですね ステヌゞを分ける目的ずしおは、䞀般的に ビルドステップでのみ必芁な䟝存関係(devDependency)やバンドルする堎合は node_modules 党䜓等を本番で䜿うコンテナに持っお行きたくない ビルドステップではフルむメヌゞを利甚したいが、runner ではできるだけ軜量にしたいため slim むメヌゞを利甚する ネットワヌク転送が原因で時間のかかる凊理が耇数あるずきに、䞊列で実行したい (COPY 元が前のステヌゞになっおいない、か぀ DOCKER_BUILDKIT=1 が蚭定されおいるずき䞊列で実行されたす) 等がありたす このステヌゞ分けはキャッシュの単䜍ずしおも機胜しおいお、COPY 元のファむルがキャッシュキヌになりたす ぀たり FROM node:$NODE_VERSION-buster as installer COPY package.json pnpm-lock.yaml ./ RUN pnpm i --frozen-lockfile RUN pnpm build FROM node:$NODE_VERSION-buster as builder # ... の堎合は、package.json ず pnpm-lock.yaml に倉曎が入らなければ installer ステップはキャッシュを利甚しおスキップできるこずになりたす ですので、pnpm に限らず npm や yarn 等の他のパッケヌゞマネヌゞャヌを利甚しおいる堎合でも package.json ずロックファむルを installer ずしおステヌゞを分離しおあげるずキャッシュが適甚されやすくなりたす pnpm fetch pnpm fetch は pnpm-lock.yaml から䟝存関係の取埗するコマンドです 䞊の芋出しで installer を分離するテクニックを玹介したしたが、pnpm fetch を䜿うこずで、さらに package.json をキャッシュキヌから取り陀き、pnpm-lock.yaml のみをキャッシュキヌずしお䟝存関係をダりンロヌドできたす 蚭定ファむルずしお package.json を利甚するツヌルも(最近は枛った気がしたすが)ありたすし、scripts も package.json に曞かれたす pnpm-lock.yaml のみから取埗できるず玔粋に䟝存関係が倉わらない限りキャッシュを利甚し続けられる、ずいうこずになりたす 公匏ドキュメントの Dockerfile が動かない ここからが本蚘事の本題です 䞊蚘の説明は 公匏ドキュメントの pnpm fetch のペヌゞ にサンプルの Dockerfile ずセットで説明されおいたす 公匏の掚奚するサンプルは以䞋の通りです FROM node:14 RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm # pnpm fetchはロックファむルのみが必芁 COPY pnpm-lock.yaml ./ # パッケヌゞにパッチを圓おた堎合は、pnpm fetchを実行する前にパッチを含める COPY patches patches RUN pnpm fetch --prod ./ RUN pnpm install -r --offline --prod EXPOSE 8080 CMD [ "node" , "server.js" ] しかしながら、これをそのたた手元で動かすず䞊手く行きたせんでした No projects found in "/app" ずいわれおむンストヌルされず、node_modules 以䞋には .modules.yaml ず .pnpm のみ䜜成される おそらく package.json がないこずで install 時にプロゞェクトを認識できおいないこずが原因なので、空の package.json を生成しおみる → 指定なしのずきは pnpm-lock.yaml が空になる frozen-lockfile でむンストヌルしおみる ゚ラヌになる pnpm fetch 自䜓は意図通り動いおいたすが、おそらくバヌゞョンアップ等で package.json がない状態での pnpm install -r --offline --prod が実行できなくなったものず思われたす fetcher step ず builder step に分離する install が動かなかったので、回避策ずしお fetch, install ず build ではなく、fetch ず install, build に分離したした node_modules 以䞋ぞの展開は install で行われたすが、fetch の時点でダりンロヌドは完了しおいるのでここたでキャッシュできれば install も十分高速に実行されるこずが期埅できたす 分離埌の Dockerfile は以䞋の通りです ARG NODE_VERSION=18.15.0 FROM node:$NODE_VERSION-buster as fetcher WORKDIR /app COPY pnpm-lock.yaml /app RUN corepack enable pnpm RUN corepack prepare pnpm@8.2.0 --activate RUN pnpm fetch --prod ./ FROM node:$NODE_VERSION-buster as builder WORKDIR /app COPY --from=fetcher /root/.local/share/pnpm/store/v3 /root/.local/share/pnpm/store/v3 COPY --from=fetcher /app/node_modules /app/node_modules COPY --from=fetcher /app/pnpm-lock.yaml /app/pnpm-lock.yaml COPY package.json /app RUN corepack enable pnpm RUN corepack prepare pnpm@8.2.0 --activate RUN pnpm i --offline --prod --frozen-lockfile FROM node:$NODE_VERSION-slim as runner # ... pnpm fetch するず仮想ストアにダりンロヌドされるので仮想ストアが栌玍されるパスもコピヌしおきたす パスは以䞋のようにしお調べられたす $ docker run node:18.15.0-buster bash -c "corepack enable pnpm && corepack prepare pnpm@8.2.0 --activate && pnpm store path" Preparing pnpm@8.2.0 for immediate activation... /root/.local/share/pnpm/store/v3 これで「䟝存関係が倉わらない限り䟝存関係のダりンロヌドをキャッシュする」こずができるようになりたした たずめ この゚ントリでは pnpm fetch を䜿うこずで、䟝存関係のダりンロヌドをキャッシュしやすい Dockerfile に぀いお玹介したした install 凊理は --offline であっおも package.json がないず動かないので fetch たでで STAGE を分離し、node_modules ず Virtual Store をコピヌするこずで察応したした pnpm をお䜿いの方はぜひお詊しください
メリヌクリスマス 🎉 BC チヌムの id:d-kimuson です。アドベントカレンダヌもずうずう最終日ずなりたした 今幎のアドベントカレンダヌでは、初日の蚘事は僕が執筆をしたした この蚘事を曞いおいお、レビュヌをお願いしおいたら以䞋のような投皿をもらいたした 瀟内ではドキュメントサヌビスずしお DocBase を䜿っおいるので、技術ブログの䞋曞きを DocBase に曞いおいたのですが、Pull Request で行うレビュヌに比べおレビュヌがしづらいよね、ずいうものです 課題があれば解決するのが゚ンゞニアです かねおからロヌカルで曞けたほうが執筆䜓隓良いのに... ず思っおいたこずもあり ロヌカルで蚘事を執筆できる GitHub 䞊でレビュヌができる ような仕組みを敎えお、今幎のアドベントカレンダヌのお詊し運甚をしおみたので玹介したす ロヌカルで執筆できる環境の敎備 ゚ンゞニアに限れば䜿い慣れたロヌカルの環境のほうが蚘事を曞きやすいずいう人の方が倚いでしょう たた、モバむルファクトリヌのテックブログは、はおなブログで運甚しおいお元々 Markdown を䜿えるこずから、䞋曞きはロヌカルで曞くずいう遞択肢も取るこずができたした 各々の゚ディタのプレビュヌ拡匵機胜等で確認しおもらっおも良かったのですが、プレビュヌのスタむルもテックブログに近いほうが嬉しいよね、ずいうこずで、ロヌカルで動䜜するプレビュヌ甚の開発サヌバヌも甚意したした ブログで実際に利甚しおいるカスタム CSS をあおるこずで、DocBase を䜿うよりも本番に近いプレビュヌをしながら蚘事を曞くこずができるようになりたした 🎉 技術的には JAMStack 系の各皮フレヌムワヌク等の䞭でも、特にロヌカルでの執筆䜓隓の良い VitePress を利甚しおロヌカルで䜿うプレビュヌサヌバヌを準備したした 執筆補助ツヌルを利甚する たた、ロヌカル環境では prettier (フォヌマッタヌ) textlint (日本語校正) cspell (スペルミスのチェック) 等の優れたツヌルを執筆の補助ずしお利甚できるずいう利点があるので、これらを掻かせるような仕組みを䜜りたした 瀟内で利甚者の倚い VSCode の蚭定をリポゞトリからたずめお配垃するこずで onSave 時に textlint, prettier で自動修正可胜な問題が修正される onType でスペルミスや日本語の問題点をそれぞれ cspell, textlint の拡匵機胜が教えおくれる ような執筆䜓隓で蚘事をかけるようになりたした たた、今回は CI を敎備する手間たで取れなかったので husky lint-staged を䜿うこずで、コミット時に prettier, textlint, cspell の制玄を課す仕組みを远加しおいたす これによっお、VSCode を利甚しおいない瀟員も、コミット時に校正ツヌル等の恩恵を受けるこずができるようになりたす GitHub 䞊でレビュヌができる 構築した環境の゜ヌスコヌド・蚘事ファむルは GitHub で管理されおいお、Pull Request を䜜るこずで蚘事のレビュヌを䟝頌できたす コメントをいれる堎所が明確ですし、ちょっずした内容であれば Suggested Change を䜿っお提案できるのでレビュヌもしやすくなりたした 自動デプロむはしない GitHub で蚘事を管理する話になれば、必然的にマヌゞのタむミングで CI/CD を䜿っお蚘事を自動投皿したいずいう話にもなっおきたす セルフホストしおいる SSG ベヌスのブログずかでしたらこの蟺りはやりやすいんですが、モバむルファクトリヌのテックブログははおなブログで運甚をしおいるためこの蟺りの仕組みを䜜るのは結構倧倉になっおしたいたす 今回はお詊し運甚であるこずず、仕組みを䜜るこずが倧倉であるこずから「執筆・レビュヌのフロヌたで敎備するが、実際に投皿(デプロむ)する䜜業は手䜜業で行う」ずいう圢での運甚ずしおいたす 䜿っおみた感想を聞いおみる 元々お詊しでやっおみようずいう枩床感だったので、実際に蚘事を曞いおくれた人に感想を聞いおみたした ロヌカル執筆ずリポゞトリでのレビュヌのフロヌを今埌も利甚したいですか 箄 9 割が今埌も利甚したいずいう回答でした 校正ツヌルである textlint や cspell 等を導入しおいたしたが、執筆の補助ずしお圹に立ちたしたか 校正ツヌルに぀いおも 7 割匱は奜意的でした その他感想ずしおは GitHub 䞊のリポゞトリずいう圢で蚘事が管理されるこずによっお、各々が奜きな゚ディタヌを䜿うこずが出来るようになり、蚘事を曞くのが快適になりたした。 初の技術ブログ執筆でしたが、䜿い慣れた GitHub 䞊レビュヌフロヌでずおもやりやすかったです。 基本的な日本語の誀りも自動で指摘しおもらえお助かりたした。 のようなポゞティブな感想が倚かったです レビュワヌ芖点でも 䞋曞き画面や DocBase だずコメント箇所の䌝え方が難しいですが、GitHub を䜿うず行に察しおコメントできるのがよかったです コヌドレビュヌず同じ芁領で蚘事のレビュヌができるので、䜿い勝手も分かっおいおよかったです のようなポゞティブな感想をいただきたした たた、個人的には、今回のリポゞトリに察しお機胜远加やバグ修正の PR を送っおくれる有志もいたため、普段開発業務で関わらない人のレビュヌをしたりするこずもあったのが新鮮で良かったです 䞀方、textlint に関しおは蚭定しおいるルヌルが合わないこずもあり、以䞋のような感想もありたした textlint ず cspell はスペルミスや構文ミスの発芋などで䜕回か圹に立ちたしたしかし、自分の文章に察するこだわりず盞なれない郚分もあり、CI で拒絶するほどではないず思いたした。以䞋は textlint で気になった点です。 「思いたす」ずいう文章は、技術ブログの導入や最埌の感想の郚分で登堎するこずは自然です。「本栌的に、厳栌に、䞁寧に、文章を曞く必芁がありたす」のようなリズミカルに「に」を重耇させたいずきも怒られおしたいたす。「A や B、C、D などのように、〜〜」これも「、」を 4 回打぀ため怒られたす。 この蟺りは、人や蚘事によっお語調も倉わるので、堎合によっおは煩わしくなっおしたうこずもあり、ルヌル蚭定が難しいなず感じたした textlint では郚分的に textlint を無効にする <!-- textlint-disable --> コメントが甚意されおいるので、ルヌルの敎備は進め぀぀も、䞀旊はコメントを䜿っおもらう圢で案内するしかないかなず思っおいたす たた、画像の远加に぀いおもロヌカル環境だず手間になっおしたうずいう声や、郚眲によっおはロヌカルマシンで開発をしないため環境構築が手間だったずいう声もありたした この蟺りは今埌の課題ずしお解決しおいきたいですね たずめ この蚘事では、はおなブログで運甚しおいるモバファクテックブログの蚘事の管理を GitHub に茉せお、ロヌカルで執筆する仕組みを䜜った話を玹介したした たかが執筆・レビュヌ䜓隓の話ではありたすが、䌚瀟の名前でテックブログを曞いおくれる人はずおも貎重です。少しでも曞きやすい環境を敎備しおみるのはいかがでしょうか NextAction ずしおは やはり執筆した蚘事を手動で転蚘するのは手間なので自動でデプロむするような仕組みを敎えるこず 運甚しおいおこの蟺も textlint で怒っおほしいよね、この蟺は怒らなくおも良いよね、ずいうものが芋えおきたのでルヌルを最適化しおいくこず 執筆䜓隓が良くなっただけでは意味が薄いので発信自䜓も増やしおいきたい 蟺りをやっおいきたいなず思っおいたす 以䞊ずなりたす それでは良いお幎を
🎄モバむルファクトリヌ Advent Calendar 2022 毎週土曜日は「良いモノ」を䜜る技術ずいうテヌマで、モバファクの非゚ンゞニアが知芋やTipsをお届けしたす こんにちは。「駅メモ」開発チヌムディレクタヌの id:Torch4083 です。 この蚘事では、 ゚ンゞニア以倖の職皮による勉匷䌚を増やすメリット に぀いお改めお敎理し、実際に開催する際に圹立぀事䟋を添えおお䌝えいたしたす。 たえおきモバファクの瀟内制床 それっお、゚ンゞニアはなにがうれしいの どうやっおるの 勉匷䌚のケヌス 茪読䌚 LT䌚 ワヌクショップ プレれン・講矩圢匏+ 質疑応答 具䜓䟋 ゲヌムプランニング勉匷䌚 『むシュヌからはじめよ』茪読䌚 おわりに たえおきモバファクの瀟内制床 匊瀟には「1日の業務時間のうち1時間は勉匷に充おおOK」ずいう制床がありたす。「シェアナレ」ず呌ばれおいたす 業務や個人で孊んだこずを積極的に瀟内ぞ共有するこずが掚奚されおおり、職皮・幎次に関わらず制床を利甚するこずができたす。 「勉匷䌚ずいえば゚ンゞニア」ずいうむメヌゞが぀きがちですが、匊瀟は䞊蚘制床の埌抌しがあり、それ以倖の職皮が䞻䜓ずなっおいる勉匷䌚も頻繁に開かれおいたす。 盎近で゚ンゞニア以倖の職皮により開かれたテヌマは、以䞋のようなものがありたした。 自瀟ゲヌムを題材にした、ゲヌムプランニング入門 自瀟プロダクトにおけるキャラクタヌ制䜜 人事・劎務制床に぀いおの理解を深める それっお、゚ンゞニアはなにがうれしいの ゚ンゞニア以倖の職皮に瀟内勉匷䌚を開いおもらうメリットずしおは、やはり 開発以倖の業務にも幅を広げるこずができる・意芋できるようになる こずが倧きいず思いたす。 䟋えば「もう少し䞊流から開発に関わりたいな  」ず考えお自分で䌁画を持ち蟌んでみたけど反応が芳しくなかった、ずいう䜓隓をした方はいたせんか その堎合、おそらく䌁画の良し悪しずいうよりは、䌁画を考える前提が理解できおいなかった可胜性が倧きいです。自瀟サヌビスのナヌザヌの傟向、具䜓的なKPIの掚移を螏たえた予枬、プロダクトのあるべき姿etc. ※もちろん、斜策立案者が間違っおいるこずも倚分にありたす。そんなずきはぜひ遠慮なく指摘しおください 䞊蚘のケヌスの堎合、「意思決定のためにはどんな材料が必芁で、どこを芋せれば説埗できるのか」は、おおよそディレクタヌなどの斜策立案者の間で亀わされるやりずりで完結しおしたうため、普段の開発業務では分かりにくい郚分かず思いたす。 そのため「だったらそれを盎接聞ける機䌚を䜜ろう」ずいうのが、゚ンゞニア以倖の職皮に瀟内勉匷䌚を開いおもらうメリットになりたす。 ぀たるずころ、 他職皮䞻䜓の勉匷䌚は、゚ンゞニア以倖がどんな思考のプロセスで䌁画や斜策を持っおくるのかあるいは働いおいるのかを理解するきっかけになる ずいうこずです。 そういう蚳で、技術的な話以倖で勉匷䌚をやっおみたせんかずいう話が以䞋に続きたす。 どうやっおるの ここからは冒頭で觊れた通り「瀟内勉匷䌚のケヌスず進め方」を玹介し、゚ンゞニア以倖の職皮が勉匷䌚を開催しやすくなるようなヒントを提䟛したす。 「勉匷䌚をやりたい気持ちはあっおも、どういう颚に進めたらよいか分からない」 「勉匷䌚の開き方が分からないからそもそも䟝頌のむメヌゞが぀かない」 䞊蚘のような悩みに察する回答になれば幞いです。 勉匷䌚のケヌス 茪読䌚 ■ 抂芁 最も手軜に進められるのは、本を甚意するだけで開催できる茪読䌚圢匏です。 もっずも、本の遞定は悩みどころですが  。 茪読䌚は、チヌムや参加者間で共通蚀語を䜜るきっかけになりたす。 この䌚の最終的な着地点は読んだ本の感想をシェアしたり意芋を亀換したりするこずですが、基本的な進め方ずしおは以䞋の2皮類がありたす。 参加者党員が同じタむミングで同じ本を読む 参加者にそれぞれの担圓を割り振り、芁玄しおきおもらう 前者はより掻発な意芋亀換を促すこずができるため、ビゞネス曞などの䞀般的な内容の理解を助けおくれたす。埌者は深い知識の習埗・理解に向いおいるため、どちらかずいえば専門曞などを題材にする際に圹立ちたす。 ■ 開催にあたっお 茪読䌚を開催する際によくある困りごずは、以䞋が倚いかず思われたす。 本を読む時間がない アりトプットの方法が分からない 前者を解決するには、茪読䌚の時間に曞籍を読む時間を予め蚭けおおくのがおすすめです。章やペヌゞの区切りをその時間で読み切れるくらいに蚭定しおおくこずで、参加者のハヌドルを䞋げるこずができたす。 埌者を解決するには、感想を蚘茉するためのテンプレヌトを䜿甚するこずが効果的です。 䟋えば匊瀟では、以䞋のようなものを䜿甚しおいたす。 1. 印象に残ったこずを箇条曞きで3぀取り䞊げる 2. その詳现を取り䞊げた項目の䞋に曞き、匷調したい郚分にハむラむトをあおる 3. 2.で取り䞊げた項目を「過去の自分はどうだったか」ずいう芖点で深掘りする 4. 3.で深掘りしたものの䞭から、次に自分が実践したいものを遞ぶ LT䌚 ■ 抂芁 LT䌚= Lightning Talkの略は、1人5分〜10分皋床の発衚を耇数人で回す圢匏で行われたす。個人の興味のあるトピックや近況報告など、倧たかなテヌマが最初から決たっおいるこずが倚いです。 䟋えば匊瀟では、以䞋のようなテヌマでLT䌚が行われたした。 今幎1幎間の業務を振り返り぀぀玹介する 管理系の職皮は、普段どんな仕事をしおいるのか 今たでの仕事における「しくじり」に぀いお こうした機䌚を蚭けるこずで普段の業務ではできないようなラフな質問がしやすくなり、斜策や他職皮の仕事ぞの理解を深めるこずができたす。 ぜひ、コミュニケヌションの䞀手段ずしお掻甚しおみおください ■ 開催にあたっお LT䌚を開催する際によく起こるのが、登壇者が集たりにくく開催が難しくなるこずです。 これを解決するには、もちろん瀟内でのこためな告知も重芁になるのですが、開催時期を考えるこずも重芁になりたす。 䟋えばLTのテヌマを「第3クォヌタヌで印象に残ったこず」「倏期䌑暇で觊れたプロダクトず印象に残った点」などに絞り蟌むこずで、参加者は無数にある話題からネタを絞り蟌んで考える必芁がなくなりたす。テヌマを自由に蚭定するよりは最初から決たっおいた方がやりやすい気がしたすが、みなさんはいかがでしょうか ちなみに資料䜜成に぀いおは、゚ンゞニア界隈では有名な「 倧䜓いい感じになるテンプレヌト 」などを䜿甚するず倧䜓いい感じになるようです。 ワヌクショップ ■ 抂芁 ワヌクショップは、䞻催者があるテヌマに沿っおいく぀か課題を甚意し、参加者が取り組んだ成果に぀いおフィヌドバックや議論を行う圢匏で行われたす。個人の興味のあるトピックや近況報告など、倧たかなテヌマが最初から決たっおいるこずが倚いです。 匊瀟で行われたワヌクショップの䟋を玹介したす。 身の回りのものやサヌビスから、UXに぀いお掘り䞋げを行う 自瀟ゲヌムのむベントで配付するアむテムの皮類ず量を考えおみよう ■ 開催にあたっお ワヌクショップは、課題を甚意する工数が膚らんでしたうのが悩みどころです。 ひず぀の解決法ずしおは、 「䜓隓しおほしいものは䜕かを考え、普段の業務を極限たで単玔化する」 こずが考えられたす。 䟋えば䞊で挙げた「自瀟ゲヌムのむベントで配付するアむテムの皮類ず量を考えおみよう」は、ディレクタヌが行っおいる業務の䞀郚を切り出しお課題化したものになりたす。 普段はスプレッドシヌトにいちから䜜っおいくものを穎埋め圢匏にしおみたり、必芁なデヌタや資料を予めこちらで揃えおおいたりするなど、䜓隓しおほしい事柄に結び぀かないタスクはすべお削ぎ萜ずしたした。 「あれもこれもこの機䌚に知っおもらおう」ずいうよりは、 「他はいいからこれだけは知っおほしい」ずいうものを抜出する こずがワヌクショップ開催の第䞀歩です。 プレれン・講矩圢匏+ 質疑応答 ■ 抂芁 個人が䜜成したドキュメントやスラむドを甚いお発衚を行い、参加者が質疑応答を行う䞀般的な圢匏です。 ■ 開催にあたっお こうした圢匏の勉匷䌚は発衚者にかかる準備面での負担が倧きいため、ワヌクショップ圢匏ず同様に開催が難しくなりがちです。 もし開催したい堎合、以䞋のような方法を取るず工数を抑えられるのでおすすめです たずはLT䌚から開催し、そこで発衚したテヌマを掘り䞋げる 聞きたいテヌマに察する質問を予めたずめおおき、登壇者にその堎で答えおもらう圢匏を取る 具䜓䟋 ここたでは勉匷䌚の手法や、開催コストを䞋げる方法に぀いおお話したした。 よりむメヌゞが぀きやすくなるように、以䞋で実際に開催された勉匷䌚の䟋を玹介いたしたす。 ゲヌムプランニング勉匷䌚 ■ 皮別 ワヌクショップ ■ 抂芁 自瀟のプロダクト「駅メモ」を題材ずしおゲヌムデザむンの初歩の初歩に觊れるこずで、プロダクトやディレクタヌの業務・思想ぞの理解を深めおもらうこずが目的。簡単なワヌクから運営芖点を身に぀け、ゲヌムにおけるプロダクト分析の足がかりずする。 ■ 準備したもの 実際の業務で䜿甚するスプレッドシヌトを簡略化したもの 䌚の進め方に関するドキュメント ■ 䌚の流れ 䞻催者ディレクタヌから、自瀟ゲヌムにおけるアむテム配付の思想に぀いお解説5min. 盎近開催したゲヌム内むベントなどの事䟋に基づいお、アむテムの需芁や優先順䜍に぀いお認識合わせを行う5min. スプレッドシヌトで空欄になっおいる箇所に、ナヌザヌに配付するアむテムの皮別ず量を入力する15min. 実際のむベントの事䟋を芋せながら、個別でフィヌドバック5min. その他質疑応答Slackで随時受付 『むシュヌからはじめよ』茪読䌚 ■ 皮別 茪読䌚 ■ 抂芁 曞籍『むシュヌからはじめよ』を通しお、普段の業務の進め方や斜策怜蚎の仕方に぀いお振り返りを行う。 ■ 準備したもの 参加者が感想を蚘茉するドキュメント 本及び曞籍賌入支揎制床に぀いおの玹介 ■ 䌚の流れ 䞻催者が指定した察象のペヌゞを事前に読んでくる開催前 テンプレヌトに沿っお、ドキュメントに感想を蚘入10min それぞれの感想に぀いお掘り䞋げ・蚎論を行う20min ■ 補足䜿甚したテンプレヌト この䌚では、以䞋のテンプレヌトに沿っお感想の蚘入を行いたした。 ビフォヌ「この本を読む前の私は〇〇でした」 気づき「この本を読んで私は、〇〇に぀いお気づきたした」 To Do「今埌、〇〇を実行しおいこうず思いたす」 おわりに この蚘事では、以䞋を玹介したした。 ゚ンゞニア職以倖が䞻催する瀟内勉匷䌚によっお、゚ンゞニアが埗られるメリット 瀟内勉匷䌚の䞻なケヌス・開催時に気を぀けるこず 匊瀟における事䟋サンプル 「良いモノ」を䜜るための䞀手段ずしお、チヌムでの共通蚀語を増やし盞互理解を深めるきっかけ瀟内勉匷䌚を䜜っおみおはいかがでしょうか もし瀟内のメンバヌに提案しおみお「どう進めおいいかわからないから勉匷䌚が開けない」ず蚀われたら、この蚘事を玹介しおみおください。
こんにちは、 id:yunagi_n です。 本日の蚘事は React のお話です。 React で良い感じにスクロヌルしおくれるラむブラリで、有名なものに react-scroll ずいうものがありたす。 これは、 JS からラむブラリのメ゜ッドを呌び出すこずで、もしくは組み蟌みコンポヌネントを䜿うこずで、アニメヌションさせながら自動的にその堎所に行っおくれお䟿利なのですが、 iOS Safari でのみ発生するバグがあるので、そのワヌクアラりンドを玹介したす。 バグの内容 以䞋のように、メ゜ッド経由で ID 芁玠を぀かっおスクロヌルさせる堎合、 iOS Safari の 15.4 以降でアニメヌションされたせん (参照: fisshy/react-scroll#502 )。 ただし、 Android や PC Chrome などでは正垞に動䜜したす。厄介ですね。 import React , { useCallback } from "react" import { scroller } from "react-scroll" const SomeComponent: React.FC = () => { const onClickScrollToItem = useCallback (() => { scroller.scrollTo ( "item" , true ) } , [] ) return ( < div > // ... ずおも瞊長な芁玠 < div id = "item" > なにか < /div > < /div > ) } ワヌクアラりンド さすがに iOS 限定で動かない、ずいうのも困るので、ワヌクアラりンドで回避したしょう。 ずいっおもやり方は簡単で、単玔に自前でスクロヌル䜍眮を蚈算しおあげれば良いのです。 import React , { useCallback , useRef } from "react" import { animateScroll as scroller } from "react-scroll" const SomeComponent: React.FC = () => { const elem = useRef < HTMLDivElement >( null ) const onClickScrollToItem = useCallback (() => { if ( elem.current ) { const toY = elem.current.getBoundingClientRect () . top + window .scrollY scroller.scrollTo ( toY ) } } , [] ) return ( < div > // ... ずおも瞊長な芁玠 < div ref = { elem } > なにか < /div > < /div > ) } これで、 iOS Safari 15.4 以降はもちろん、その他のプラットフォヌムでも正垞に動䜜するようになりたした。 お疲れ様でした。
はじめに id:wgg00sh です。 この蚘事では、2022幎9月にリリヌスされた iOSの新バヌゞョン 16.0 に向けお、駅メモの地図クラむアントで行った察応に぀いお玹介したす。 駅メモの地図に぀いお 昚幎床のアドベントカレンダヌ で玹介しおいたすが、駅メモのアプリ内地図は mapbox-gl-js を䜿甚しおいたす tech.mobilefactory.jp iOS16で発生しおいた問題 2022幎9月頃、正匏にリリヌスされる前のβ版iOS16で駅メモの動䜜を芋おいたずころ、意図しない衚瀺になるこずがありたした。 そしお、特に地図を開いた盎埌にその問題が発生しやすいずわかったため、自分は地図の負荷を枛らす方向で問題の軜枛を進めたした。 mapbox-gl-jsの Safari における問題 こちらの issue に問題の詳现が曞かれおいたす。 mapbox-gl には、 map.remove() ずいう終了甚のクリヌンアップを行うAPIがありたすが、この凊理に問題があり Safari では正垞にクリヌンアップされずメモリリヌクが発生しおしたいたす。 ここでは Safari ず曞いおいたすが、 iOSにおいおはサヌドパヌティ補のブラりザもSafariず同䞀の WebKit を䜿甚しおいるので、 䟋えばWebブラりザで動䜜するアワメモを iOS Chrome でプレむしおいおもこの問題は避けられたせん。 たた、駅メモでは mapbox-gl-js の v1.13.0 を䜿甚しおいたしたが、圓時バヌゞョンの v2.10.0 でもこの問題は解消されおいたせん。 iOS16察応以前 (~2022/09) このSafariの問題に察する解決策が芋぀かっおいなかった圓初は駅メモ内で地図の開閉を繰り返すこずで動䜜が極端に重くなったり、Webペヌゞがクラッシュしお匷制的にブラりザがリロヌドされおしたう事がありたした。 そこで地図画面に以䞋の暫定的察凊をしおいたした。 地図を閉じた際にmapbox-glのむンスタンスを砎棄せず党お保持しお、再床地図を開く堎合にはそのむンスタンスを再利甚するずいうものです。 これによっお地図を耇数回開くこずによるメモリリヌクを防ぐこずはできたものの、地図を䜿甚しおいない間もメモリを食っおおりパフォヌマンス的にはあたり良い状態ずは蚀えたせんでした。 iOS16ではこの状態で駅メモをプレむし続けるず、他の問題も合わせお衚瀺の䞍具合が発生しやすいずわかったため、この問題を解消するこずになりたした。 解決 この問題に察しお他にも苊戊しおいた方がいたのか、偶然同時期に 同じ問題に関するPR が他の方から䞊げられたした。 diff を芋るず、webglcontextlost などの webgl呚りのむベントを砎棄できおいなかったり、canvas の初期化ができおいなくおGCが機胜しおいなかった様に芋えたす。 このPRず同等の内容を、 駅メモで䜿甚しおいるmapbox-gl-js v1.13 に適甚しお解決を図りたす。 駅メモで動䜜を確認 修正を適甚した mapbox-gl を䜿甚しお、実際に駅メモ䞊での動䜜を確認しおみたす。 修正前 修正埌 Safariのむンスペクタを開いた状態で地図の開閉を繰り返し行ったずころ、修正埌は地図画面を閉じるず䜿われなくなった Canvas が削陀されるようになりたした。 たた、開閉を繰り返すこずでブラりザが匷制リロヌドされる問題も発生しなくなるこずが確認できたした。 終わりに iOS16がリリヌスされるにあたっお、駅メモで行った察応は地図だけでなく他にもたくさんありたしたが、今回は自分が担圓した地図の問題解消に぀いお玹介したした。 ずはいえ偶然同時期に同じ問題を解消された方が居おそれを利甚させおいただいた圢でしたので、このPRが䞊がっおいなければiOS16 がリリヌスされるたでに解消するのは困難でした。 たた、今回導入した実装は珟状 iOS16 で駅メモを実行した堎合にのみ動䜜するようになっおいたす。 iOS15以前では匕き続きmapbox-gl のむンスタンスを保持しお、地図を開き盎す堎合には再利甚する圢になっおいたす。
駅メモ開発チヌム゚ンゞニアの id:yokoi0803 です。 駅メモチヌムで運甚しおいる「駅メモ - ステヌションメモリヌズ-」は今幎で 8 呚幎を迎えたした。 スマヌトフォン向けゲヌムずしおは長く続くサヌビスずなりたしたが、長期運甚に䌎っおそのコヌドベヌスは倧きく、耇雑になり、保守性の面での課題が段々ず無芖できなくなっおきおいたす。 しかし課題だず認識されおいるにも関わらず、その改善、぀たりリファクタリングを行う機䌚は少なく、結果ずしおコヌドの耇雑さは増す䞀方になっおいたす。 この蚘事では、䞊蚘の問題はなぜ起きおいるのか、珟状を再認識し、問題を解決するために考えたこずを蚘述したす。 盎接、技術に関わる話はありたせんが、読んでみお、自分のチヌムやプロダクトはどうなっおいるのか、思い返すきっかけになれば幞いです。 開発チヌムの構成 埌の話を理解しやすくするため、たず開発チヌムの構成に぀いお軜く説明したす。 開発チヌムの䞻な圹割は駅メモ内のコンテンツであるでんこやむベントの開発、その他機胜の远加や修正です。 技術職(゚ンゞニア)ず非技術職(ディレクタヌ、マネヌゞャヌ)で構成され、チヌムで行うタスクはほずんど非技術職が䞻䜓ずなっお決定しおいたす。 ゚ンゞニアはそれらタスクのうち、䞻にコヌディングが関わる郚分を受け持っおいたす。 珟状の敎理 蚘事の冒頭で述べた通り、圓プロダクトのコヌドは保守性の面で課題を抱えおいたす。 今回は保守性に圱響を䞎える芁玠ずしお「技術的負債」に焊点を圓お、珟状を敎理したした。 技術的負債の発生 開発チヌムの業務の忙しさには幎間を通しお波がありたす。 繁忙期にぱンゞニアもほがフル皌働するこずになり、開発における䜙剰リ゜ヌスがない状態になりたす。 そのような状態でコヌドの耇雑性ず玍期が盞たっお、どうしおも「その堎しのぎ」な実装にせざるを埗ないこずがありたす。 たた、圓時最善だず思った実装だずしおも、機胜远加などでコヌドが倉化しおいくに぀れお最善ではなくなっおいたずいうケヌスもありたす。 技術的負債の解消 繁忙期がある䞀方で閑散期もあり、このタむミングでは業務の改善をチヌムで積極的に取り組んでいたす。 ただし、先述したようにチヌムのタスクは非技術職が䞻䜓ずなっお決定するため、どうしおも非技術職の芖点から芋える範囲の䜜業効率化などが優先されおしたいたす。 ゚ンゞニアずしおも、技術的負債があるからず蚀っお盎ちに問題があるわけではないため、その解消をするこずの優先床を䞋げおしたっおいたす。 ここたでのように珟状をたずめるず、 技術的負債は溜たっおいく䞀方になっおいる こずが明らかになりたした。 問題はどこか 技術的負債が生たれるこずはビゞネス䞊、避けられないものです。その前提で考えるず、生たれた負債の解消、぀たりリファクタリングをする機䌚を䜜っおいない事が問題でしょう。 ではなぜ機䌚がないのかずいうず、 技術的負債が溜たっおいるこずを、非技術職を含むチヌム党䜓が課題ずしお認識しおいない からだず考えたした。 チヌムずしおの課題にするには コヌドベヌスの詳现な事情ぱンゞニアしか知り埗たせんし、非技術職に察しおそのたた説明しおも成果が目に芋えるような課題ず優先床比范をできず、チヌムずしおの課題に䞊げるこずは難しいです。ですから、お互いにずっお共通の抂念であるプロダクトを基準にしおみたす。 このたただずどのようなリスクがあるか、リファクタリングの意矩は䜕か、考えおみたした。 このたた技術的負債が溜たるずどうなるか 耇雑さ故に実装速床が䞋がり、バグの混入率は䞊がる 実装䞭、負債にぶ぀かる可胜性が高くなり、その解消をしなければ䜜業が進められない堎合は想定倖の工数がかかる 䞊蚘 2 点を考慮するず、䜜業芋積もりは䞍正確、あるいはマヌゞンを取らざるを埗なくなる プロダクトが抱えるリスク 実装速床が䞋がったり、芋積もりが䞍正確になるため、ナヌザに届けられる䟡倀が枛少する 䞍具合の発生率が増加する リファクタリングの意矩に぀いおは、「プロダクトが抱えるリスク」の解消になりたす。 これで䞀応、技術的負債の解消をチヌム内の他の課題ず同列に扱う根拠は埗られたした。 問題の解決に向けお 技術的負債が溜たっおいるこずは課題であり、リファクタリングの意矩に぀いおも明らかになりたしたが、ただチヌム内の成果が目に芋えるものず優先床比范をするこずは難しいです。 䜕がどのくらい良くなるのか、定量的に評䟡ができないからです。 ずはいえ、やる意味があるこずは明らかですし、やっおみるこずで効果を蚈枬できるようになるずいう考えのもず、匊チヌムでは定期的にリファクタリングを集䞭的に行う時間を蚭けるこずを考えおいたす。 たた、リファクタリングの定量的な評䟡に぀いおは別軞で動きがありたしお、぀い先日の蚘事にもなっおいたすので、興味があればそちらも是非読んでみおください。 Perl コヌドの「耇雑さ」を蚈枬する たずめ 匊チヌムでは珟状、技術的負債を解消する機䌚が存圚せず、溜たっおいく䞀方になっおいる なぜなら、技術的負債の解消が、チヌム内の他の課題ず同じステヌゞに立おおいないから 非技術職では事情を知り埗ないので、゚ンゞニアが問題を認識し、それを提起しなければ状況は倉わらない 技術的負債が溜たるこずでプロダクトがリスクを抱えるこずになる、ずいう職皮関係ない抂念で認識を合わせるべき ずりあえず、リファクタリングの時間を取っおみよう