TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

226

こんにちは、エンジニアの id:mp0liiu です。 自分が所属しているチームでは現在もPerl製のプロダクトを運用しており、VSCode で Perl のコードを書いたり触ったりする機会が多いです。 Perl は開発環境が貧弱で他の言語と比べるとあまり開発体験はよくありませんが、それでも少しずつ便利な拡張機能が充実していってるので、この記事では自分が利用している便利な VSCode の Perl 向け拡張機能を紹介します。 Perl Navigator marketplace.visualstudio.com 今年話題になった Languager Server を利用した拡張機能です。 他にも Perl の Languager Server を利用した拡張機能はいくつか種類がありますが、以前から存在する拡張機能と比べると自動補完やコードジャンプがちゃんとできたり、 Perl::Critic 、 Perl::Tidy 、 perlimports をまとめて扱ってくれる点が優れています。 環境にもよりますが、インストールするだけで基本的な機能は動作してくれます。 Perl::Critic と perlimports はバンドルされていないので別途インストールする必要があります。 使える機能はだいたい次のような感じです 文法チェック Perl::Critic によるコードの静的検査 サブルーチンの返り値を return で返しているかどうかをチェックする Subroutines::RequireFinalReturn を有効にしてチェックしたときの様子です 自動補完 コードジャンプ Perl::Tidy によるコードフォーマット perlimports による use 周りのコードのクリーンアップ Object::Pad, Moose など DSL や keyword プラグイン系の構文のシンタックスハイライトへの対応 Perl(the96.vscode-perl) marketplace.visualstudio.com ctags によるコードジャンプおよび自動補完と、Perl::Tidy によるコードフォーマットができる拡張機能です。 利用するには事前に ctags をインストールしておく必要があります。 機能的には Perl Navigator と重複しているのですが、 ctags を利用してコードジャンプや自動補完をしているので精度が悪くなるかわりに動的にモジュールをロードしている場合や型を推測しにくい変数からもコードジャンプや補完が効くといったメリットがあるため、 Perl Navigator と併用しつつ邪魔と感じたら無効化したりしています。 引数をポップアップで表示してくれるという点も少し嬉しいです。 (名前付き引数や引数のバリデーターには対応していませんが・・・) Perl insert package marketplace.visualstudio.com 開いているファイル名と対応したパッケージ名を入力してくれるプラグインです。 コマンドパレットから実行するか、自動補完もオプションで有効にできます。 巨大なプロジェクトだと名前空間が深くなっていちいちファイル名と対応したパッケージ名を手動で入力するのは大変だし typo すると気づきにくくて大変なので重宝しています。 Perl Rename Symbol marketplace.visualstudio.com App::PRT や App::EditorTools を利用して変数、メソッド名、パッケージ名など識別子を正確に rename してくれる拡張機能です。 リファクタリングをするときなどに重宝しています。 別途 App::PRT と App::EditorTools のインストールが必要です。 perl-auto-use marketplace.visualstudio.com まだ use していないモジュールがある場合はファイルの先頭の方で use $module を挿入してくれる拡張機能です。 コマンドパレットから使います。 外部モジュールの関数を使っていてまだ use していない、といった場合も場合も use $module qw( $function ); というようにモジュールを use しつつ利用している関数だけインポートしてくれますが、 同名の関数をもつモジュールが複数あったりすると期待したモジュールが挿入されるとは限らないので注意してください。 Better Perl Syntax marketplace.visualstudio.com デフォルトの Perl の シンタックスハイライトから更に以下の字句に異なった色付けをしてくれるようになり、コードが見やすくなります。 数値 演算子 関数呼び出し 正規表現の文字クラス ^ が先頭につく特殊変数、 $^V など 関数ブラケット デフォルトのカラーテーマはこれらの字句の色付けに対応していないので、Material Theme などのカラーテーマと併用する必要があります。 また、Perl Navigator で有効になる DSL や keyword プラグイン系の構文のシンタックスハイライトにはシンタックスハイライトが効かなくなるので、それらのモジュール使っているときは無効にしています。 シンタックスハイライトにはいろいろ好みがあると思いますが、見やすくなると思うので一度試してみてはいかがでしょうか。 まとめ 以上、自分が利用している VSCode の Perl 向け拡張機能を紹介させていただきました。 ちゃんと型をもっている言語と比べるとどうしても劣ってしまいますが、これらの拡張機能を揃えるだけでもかなり開発体験はよくなるのでぜひ試してみてはいかがでしょうか!
駅メモ!チームでエンジニアをしている id:stakHash です。 弊社の主力プロダクトの 1 つである駅メモ!は、今年で 8 周年を迎えました 🎉 スマートフォンゲームとしては息の長いサービスですが、現在でも日々様々な新機能の開発が進んでいます。 今後も今以上の速度でユーザの皆様に価値提供をしていくためには、分かりやすく変更しやすいコードベースを維持・改善していくことが必要です。 しかし、「分かりやすさ」「複雑さ」という主観的でぼんやりとした感覚値は、長いライブサービスでは、人員の入れ替わりもあって判断が困難になっていました。 そこで、 「複雑さ」 を定量的に計測する方法を探ってみました。 「複雑さ」とは 今回は、Microsoft 社が主に Visual Studio 内で利用している 保守容易性指数 (Maintainability Index) を扱ってみます。 MAX( 0 , ( 171 - 5.2 * ln( Halstead ボリューム ) - 0.23 * ( 循環的複雑度 ) - 16.2 * ln( コード行数 )) * 100 / 171 ) 式の通り、これは 0~100 の範囲の値になります。低いほどそのコードが複雑で、保守しにくいことを表します。 Visual Studio では、次のように警告する範囲を決定しているようです。 保守容易性指数 警告色 0~9 赤 10~19 黄 20~100 緑 20 が 1 つの基準値となるでしょうか。 さて、2 つの別のメトリクスが出てきましたので、簡単に説明します。 Halstead ボリューム 1 つ目は Halstead ボリューム (Halstead Volume) で、1977 年に Halstead 博士が導入した Halstead complexity measures という一連のメトリクスのうちの 1 つです。 詳しい説明は Wikipedia に任せて、式を見ていきます。 Volume = (オペレータの数 + オペランドの数) * log2(オペレータの種類 + オペランドの種類) 式を見れば分かる通り、コード中の 語彙の複雑さ に注目していることが分かりますね。 Perl においては、 Perl::Metrics::Halstead というパッケージが存在します (駅メモ!のサーバサイドは Perl で実装されています)。 循環的複雑度 2 つ目は 循環的複雑度 (Cyclomatic Complexity) です。 同じく詳しい説明は Wikipedia 先生にお任せしますが、 線形的に独立した経路の数 = 構造的な複雑さ を表します。 Perl においては Perl::Metrics::Simple で計測できます。 計測してみる 本題です。 今回計測したいのは、「複雑さ」という非常に抽象度の高い指標でした。 上記で紹介した「保守容易性指数」は「語彙的な複雑さ」と「構造的な複雑さ」の両面を考慮しており、「複雑さ」の計測に適した指標の 1 つであると考えられます。 これを計測する Perl モジュールが見つからなかったので、上述した 2 つのモジュールを参考に Perl::Metrics::Maintainability を実装しました。 このリポジトリ自体を計測してみると、全て 20 以上をマークしていました。 極端に複雑化していない事が確認できます。 MI LoC cc volume path -------------------------------------------------------------------------------- 39.67 48 14 1287.07 ./lib/Perl/Metrics/Maintainability/Result.pm 39.89 47 11 1460.16 ./bin/perlmi 39.95 49 14 1100.45 ./lib/Perl/Metrics/Maintainability/File/Result.pm 40.56 47 5 1526.19 ./lib/Perl/Metrics/Maintainability/File.pm 46.19 33 5 720.46 ./lib/Perl/Metrics/Maintainability.pm では、駅メモ!の実装を計測してみると、全体の約 2% に当たるファイルが 10 を下回っていました。 改善のし甲斐がありそうですね! (ファイルパスは機密保持の観点から削除しています) MI LoC cc volume path -------------------------------------------------------------------------------- 0.00 489 140 19917.78 0.00 437 208 23967.10 0.00 693 198 33429.84 0.00 216 199 15096.23 0.00 699 58 42585.91 0.00 1259 208 51969.83 0.00 653 116 31531.67 0.00 538 87 24446.56 0.00 866 109 43274.66 0.00 603 104 30685.96 0.00 417 156 22806.22 ... まとめ 今回の計測により、今まで個々人が漠然と「ここは複雑そうだな」と思っていたものが、数値化・順位づけされて見えるようになりました。 実際にどこを改善していくかは、コードを精査する必要がありますが、コードベースの「複雑さ」を抑えていくための目安としては有効に使えそうです。
駅メモ!チームエンジニアの id:yumlonne です。 この記事ではスーパープロジェクト(サブモジュールが登録されている親プロジェクト)側で git checkout や git pull を実行したときに、自動で git submodule update 相当の処理を実行してくれる便利な設定を紹介します。 git submodule については ドキュメント を参照してください。 記事中の各種動作は git version 2.38.1 で確認しています。 背景 私は最近サブモジュールが存在するプロジェクトを触り始めました。 しかし、git 操作をするときにサブモジュールが存在することを意識していないと、サブモジュールの参照を意図せず書き換えてしまうことがありました。 $ cd MyProject $ git checkout topic-A # サブモジュールをtopic-Aブランチが持っているコミットに向ける $ git submodule update $ git checkout topic-B # ここで git submodule update を忘れると、サブモジュールはtopic-Aの状態から更新されない! $ git add . # 気づかずにコミットすると、topic-Bのサブモジュールの向き先がtopic-Aと同じになってしまう $ git commit -m "hoge" もちろんコミット前の確認やコードレビューがあるので気がつくことはできますが、pull や checkout をするたびに submodule update を打つのも面倒です。 そこで git config を調べたところ、 submodule.recurse というフラグで実現できそうということが分かりました。 このフラグは様々な git コマンドの --recurse-submodules オプションを制御するため、他のよく使いそうな git コマンドに与える影響も調べてみました。 各コマンドへの影響 以下の各コマンドは git config --global submodule.recurse true を設定した上で検証しています。 詳細な影響は man git config や man git ${command} で--recurse-submodules オプションの説明を参照してください。 switch ブランチを切り替えたときにサブモジュールも自動で追従します。 $ git submodule status 5b8930e2a251ead82076cf17cab13b95f0ec392d SubProject (5b8930e) $ git switch topic-A Switched to branch 'topic-A' $ git submodule status 89c87486bd15a4ebc84a7166a46977806ecfaced SubProject (heads/main-1-g89c8748) restore サブモジュールのファイルも復元されるようになります。 $ ls README.md SubProject $ ls SubProject/ README.md $ echo "hoge" >> README.md $ echo "fuga" >> SubProject/README.md $ git status On branch main Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) (commit or discard the untracked or modified content in submodules) modified: README.md modified: SubProject (modified content) $ git restore . $ git status On branch main nothing to commit, working tree clean checkout checkout でブランチを切り替えた場合は switch 相当、ファイルを復元した場合は restore 相当の動作になります。 pull サブモジュールの新しいコミットも取得し、必要があれば switch と同様に自動で追従してくれます。 $ git submodule status 61b0cf596df0b2c68617be4a31f0feeca270d3f1 SubProject (61b0cf5) $ git pull origin main From github.com:yumlonne/MyProject * branch main -> FETCH_HEAD Fetching submodule SubProject Updating ae9b6ab..edcc372 Fast-forward SubProject | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated detached HEAD. Submodule path 'SubProject': rebased into '5b8930e2a251ead82076cf17cab13b95f0ec392d' $ git submodule status 5b8930e2a251ead82076cf17cab13b95f0ec392d SubProject (5b8930e) submodule.recurse true の設定により、 pull のたびにサブモジュールのフェッチが実行されます。 以下のように fetch の config として on-demand を指定することで、変更されたサブモジュールのみフェッチされるようになります。 git config --global fetch.recurseSubmodules on-demand push スーパープロジェクトで push を実行したとき、サブモジュール側のコミットも一緒に push してくれます。 $ cd SubProject/ $ echo "hoge" >> README.md $ git add . $ git commit -m "update README.md" $ cd ../ $ git add . $ git push origin main Pushing submodule 'SubProject' Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 2 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (3/3), 322 bytes | 322.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To github.com:yumlonne/SubProject.git 47cd1b1..be119ef main -> main Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Delta compression using up to 2 threads Compressing objects: 100% (2/2), done. Writing objects: 100% (2/2), 237 bytes | 237.00 KiB/s, done. Total 2 (delta 1), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:yumlonne/MyProject.git 43ddc9d..9030778 main -> main 他にも、config で push.recurseSubmodules を設定するか、push 時にオプションを渡すことで挙動をカスタマイズできます。(長くなるので省略します) grep サブモジュールのファイルも grep できるようになります。 $ git grep -n hoge SubProject/README.md:2:hoge まとめ submodule.recurse true を設定することで、色々なコマンドがサブモジュールを意識して動いてくれるようになりました。 ここでは紹介していないコマンドやオプションもあるので、使う際はお手元の git のバージョンに対応したドキュメントを読むことをおすすめします。
🎄モバイルファクトリー Advent Calendar 2022!毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします! こんにちは。モバファクでマネージャーをしている ゆっぴぃ です。 タイトルにもある通り、エンタメ企業の社員である私が、どのようにエンタメを楽しんでいるのかをブログ記事として書いてみました。 これを書こうと思った背景 社内でチームメンバーからこんな質問を受けました。 「ゆっぴぃさんって、いつこんなにアニメを見たりゲームをやったりしてるんですか?」 メンバーがこの質問をした背景には、普段の業務における会話の中で、私がアニメの話だったりゲームの話をする印象があるからかな(?)と思います。 ただ、その私に対する印象はインプットしている量が多いからではなく、私のアニメ等に関する発言(アウトプット)頻度が多いことが影響しているのではないかなと思っています。 他のメンバーのほうが、たくさんのゲームやアニメを知っているなと私自身は感じています。 ちなみに、「いつこんなにアニメ見たりゲームやったりしてるんですか?」の回答は平凡になるので割愛させてください。 私が実践するエンタメの楽しみ方 「楽しい」のアウトプット とある別のエンタメ企業の方がこんなツイートをしていらっしゃいました。 「エンタメ企業で働くものとして、「自身の思う楽しい」が多くの人に伝わるように言語化することが大事である。」と。 私自身、これは働いている上で実感することが多々あります。 チームメンバーを巻き込み、みんなが「楽しい」と思えるものを作るには、企画者自身が「それがどうして楽しいのか」を話せないといけません。 企画者以外でも「我々自身が作っているものはどうすればさらに良くなるのか」を考えて行動することが、よりよいモノを作るうえでは必要です。 そのため、日常的にエンタメの面白さを言語化することが、よいモノづくりをするうえで必要な訓練であると考えます。 ゲームやアニメはもちろん、遊園地やアウトドアアクティビティなどなど、「楽しい」経験をしたら言語化すること。 これが私が考え実践している、エンタメ企業で働く人のエンタメの楽しみ方です。 実際にどうやっているのか エンタメに触れる時に大切にしていること これまで「アウトプットは大事だ!!」と語ってしまいましたが、私自身、一番大事にしていることは「全力で楽しむこと」です。 個人的には「勉強の一環だ!」と思ってエンタメに触れると(個人的には)心から楽しむことができないです。 アウトプットをすることなど忘れて、目の前のエンタメに全力で向き合っています。 アウトプットのことは楽しんだ後に考えましょう。 そもそも楽しまないと、そのあとのアウトプットも苦労しますよね……? アウトプット時に大切にしていること アウトプットの方法は、あまり真面目に考えないのが吉だと思います。 まず一番簡単なのは、友達など身近な人と話すこと。 映画などに一緒に行った場合、そのあとにカフェやファミレスで感想を話しあうなんてことを経験したことがある人は多いと思います。 これもアウトプットの一つの形です。私もよくやります。 それ以外に私がやっているのは、いわゆるレビューサイト的なところに書くことです。 世の中には便利なサービスがたくさんあって、アニメの感想を書いたり、飲食店の感想を書いたり、エンタメの種類に合わせて自分の記録を残すことができるようになっています。 意外にも、そういうサイトに感想を投稿するとほんの少しばかりですが、知らない人から反応をいただけることもあったりするものです。 私は初めてそういうサイトに投稿したときに反応があるとは予想していなかったので、嬉しい・楽しい気持ちが湧いてきました。 アウトプットの手段は様々です。アウトプット自体も「楽しい」と思えるような方法を地道に見つけるのが個人的にはおすすめです。そのほうが継続できるからです。 その他のPOINT 「楽しい」をさらに深堀るために 「楽しい」を言語化していく上で、どのように深堀りをしていくのかを意識したほうが学びは大きいと思います。 深堀りの方法について簡単に書くと以下の通りです。 ※ちなみにSNSで拾った知識だったりしますので、エンタメ業界の公式なお話ではありません 横に広げていく深堀り:「□□は他の○○と、こういうところが共通していて面白い」 縦に広がていく深堀り:「××が面白いと感じる理由は、▲▲なところにある。そもそも▲だとなぜ面白いかというと~」 私はどちらかというと横に広げる掘り方が得意なタイプです。だからこそ、心持ちとしては、いろんなエンタメに触れようとしています。 他のチームメンバーと話していると、縦に深堀りをするのが上手だと感じる人ももちろんいます。そういう方は一つのエンタメに対しての情熱が人一倍あり、そのエンタメについて話をしている様子を見るだけで聞き手もワクワクしてしまうものです。 最後に いかがでしたでしょうか。エンタメを作ることを仕事にしている社会人のエンタメの楽しみ方でした。 ただ「楽しい」だけで済まさずに、言語化していく姿勢はすごく大事です。 私自身もまだまだ「楽しい」の言語化が得意とは言えないですし、エンタメは日々変化し進化していくので継続して取り組んでいきます。 また、私の所属するチームでは、「楽しい」を共有をする会議があります。厳密には「楽しい」に限定した話ではなく、身の回りにあったことを話す会議です。 その会議には、毎週新しくプレイしたゲームを話してくれる人がいたり、おいしい食べ物の写真を載せてくれるメンバーもいます。 このように「楽しい」の言語化をするタイミングも設けて、チームみんなで良いモノづくりができるよう励んでいます。 ぜひ皆さんも「楽しい」のアウトプットを意識してみてくださいね!それでは!
こんにちは。駅奪取チームエンジニアの id:dorapon2000 です。 私達のチームでは、4月〜7月にプロダクトの負荷対策に注力しました。その結果、通信量の削減やDB負荷の低減、それに伴うインフラコストの削減などに繋がりました。負荷対策の方法は手探りながら多くのことをしたのですが、その中で今回は不要なDBのロック待ちを改善した部分に注目して、どのような方法でロック待ちを改善したかについてサンプルコードを交えてお話していきます。 ロック待ちの改善にバリエーションがあることについて持ち帰っていただけると幸いです。 環境 Amazon Aurora MySQL version 2 (MySQL 5.7) データベースエンジンはInnoDB トランザクション分離レベルはREPEATABLE READ ロックとロック待ち 詳細は他の記事にお譲りします。ここでは簡単な説明をば。 ロックをわかりやすく言うと、他の人に自分の作業領域への割り込み作業をさせない仕組みです。 もう少し具体的に言うと、DB内の指定範囲を別のトランザクションからの参照・更新を一時的に不可にさせる仕組みです。指定範囲はテーブル全体だったり、1行だったり、複数行だったりします。 特定のレコードをロックすると、他のトランザクションはロックが解除されるまで待たなくてはいけません。これをロック待ちと言います。 私のチームで起きていたロック待ちの問題 ロック待ちが瞬間的に連鎖的に発生し、そのうち大半のトランザクションがロックを獲得する前にタイムアウトによってエラーになっていました。 大量のトランザクションがタイムアウトまで負荷をかけ続ける状態です。 負荷がかかる様子はAWS RDSのPerformance Insightsで確認できます。 ロック待ちを見つける RDSのPerformance Insightsでは負荷の原因となった発行クエリは見られても、ソースコード内の場所までは特定できません。 そのため、私達は以下の方法でロック待ちが多発している箇所を特定しました。 発行クエリからソースコードの該当箇所を予想する 負荷がかかりそうな処理のソースコードをコードリーディングする クエリの発行箇所のファイル名と行数をコメントとして発行クエリに付随させる仕組みを自作する 特に3つ目の方法により、Performance Insights 上でクエリの呼び出し元が明確になりました。 修正の方針 問題のロック待ちが多発する箇所を特定したら、次は修正です。 本当にそのロックは必要か 条件で絞り込み、ロックを取る回数を減らせないか ロックを取る前に早期returnができないか そもそも、その処理は必要か 修正例① 利用されないuserロック 最も素朴な修正例です。 db->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; return if 9 割Trueになる条件; 処理A 処理B # 実はuserレコードをロックしている 処理C # userレコードのロックが必要 }); # COMMIT 以下のようにすることで無駄なロックを削除できました。 db->txn_do(sub { - $user->lock - return if 9割Trueになる条件; 処理A 処理B 処理C }); 解説 利用されないロックは削除すればいいです。しかし、言うは易く行なうは難し。 実際に利用されていないことを証明するために、userロック以降のすべての処理を目で追いかけました。 その結果、処理Bで実は重複してuserロックをしていることがわかり、それ以前でuserロックを利用する処理がないことがわかりました。 トランザクション先頭のuserロックを削除することで、10割userロックしていたコードは1割しかロックしないコードになりました。 修正例② 早期return 美しくないですが簡単で大きな効果があった修正例です。 db->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; return if $user->has_active_license ; # ほぼここで早期returnする ライセンスが切れている場合の処理 }); # COMMIT 以下のようにすることでロックの回数を減らす事ができました。 db->txn_do(sub { + return if $user->has_active_license; $user->lock return if $user->has_active_license; ライセンスが切れている場合の処理 }); 解説 修正前の問題点 has_active_licenseは、ライセンスの有効期限が切れていればFalseを、切れていなければTrueを返すメソッドです。ほとんどの場合で切れていないためTrueを返し、ソースコード上は早期returnします。 修正前のコードの問題点は、ほとんどの場合で早期returnして何もしないにも関わらず、必ずuserロックが取られてしまうことです。 ifの前にロックを取る理由 has_active_licenseの前でロックを取る理由は、最新のユーザ情報を取得したいからです。MySQLでは、SELECT FOR UPDATEをすることで、トランザクション中に別トランザクションでレコード更新が発生しても、更新後の値を読み取ることができます。つまり最新のレコード情報を取得できます。Locking Readと呼ぶようです。詳しくは以下の記事が詳しいです。 漢(オトコ)のコンピュータ道: InnoDBのREPEATABLE READにおけるLocking Readについての注意点 早期returnの重ねがけ さて、本題である早期returnを重ねて書くことの効果について説明します。第一にコードが冗長以外の副作用がないことは明らかです。第二にロックの回数を減らす事ができます。説明すると言ってもこれだけですが、非常に嬉しいわけです。すべてのコードを目で追いかける必要がありません。 Locking Readをしない1回目の早期returnはユーザ情報が古く、ライセンスが切れているのに切れていないと判定される可能性があります。それをLocking Readする2個目の早期returnで拾ってあげます。その逆であるパターンはロジック上存在しないため考慮しません。 修正例③ キャッシュを使う まずは修正前のコードから。 db_master->txn_do( sub { # BEGIN $user->lock # SELECT * FROM user WHERE id = :user_id FOR UPDATE; ログイン処理A if 1 日 1 回だけ ログイン処理B if イベント参加後に 1 日 1 回だけ ログイン処理C if API経由のアクセスを含めて 1 日 1 回だけ ログイン処理D if など }); # COMMIT ログイン情報をキャッシュに保存して、キャッシュがないときに限りトランザクションの処理を実行するようにしました。 + my $cache_key = $class->generate_key($user->id、日付、イベント参加してるかのフラグ、API経由かどうかのフラグ); + my $is_already_logged_in = cache->get($cache_key); + return if $is_already_logged_in; db_master->txn_do( sub { $user->lock ログイン処理A if 1日1回だけ ログイン処理B if イベント参加後に1日1回だけ ログイン処理C if API経由のアクセスを含めて1日1回だけ ログイン処理D if など }); + cache->set($cache_key => 1); 解説 修正前の問題点 ほとんどの場合で処理が何もされないにも関わらず、userロックを取っていることです。 userロックを取る理由 前述と同様に最新の情報がほしいためでもありますし、ログイン処理の中で並列に実行されると不具合・不整合が起きる箇所が多くあるためでもあります。 キャッシュ利用によるロック回避 修正例③の解決方法は修正例②と思想は同じです。ロックを取る前に条件に合致しないときだけ早期returnして、合致したときは処理を実行するようにしています。 今回はその条件をキャッシュから取得できるにしています。 ポイントはキャッシュに使うキー(鍵)です。ログイン処理A/B/C/Dのいずれかを実行する必要があるとき、キャッシュキーが存在していなければいいわけです。例で示します。 その日1回もアクセスしたことがない キャッシュキーはないため、早期returnされない ログイン処理ABCDが実行される login_1_20221216_false_false のような値をキーとしてキャッシュが作成される その日2回目のアクセス キャッシュキーは login_1_20221216_false_false ですでに存在し、早期returnされる ログイン処理は実行されない その日3回目のアクセス時にイベントにも参加していた キャッシュキーは login_1_20221216_true_false で存在せず、早期returnされない ログイン処理Bのみ実行される login_1_20221216_true_false のような値をキーとしてキャッシュが作成される キャッシュを使う方法は、ログイン処理全体をリファクタリングせずにロックを削減できる点が嬉しいです。 さいごに 並列実行を回避するためにuserレコードをロックしたくなりますが、ロックを剥がす労力は地道で相当だということが大きな学びでした。より影響が小さいレコードでロックできないか、そもそもロックを回避できないか。軽い気持ちでuserロックをすると将来痛い目に合うかもしれません。
エンジニアの id:toricor です。今年の初めまではサーバサイド(Perl)のタスクを中心に仕事をしていましたが、その後Android & iOS開発を担当するようになりもうすぐ1年になります。 今日はAndroidの位置情報ライブラリを題材に、インターフェースを活用してテスト用に位置情報のデータソースを差し替えやすくするAndroidのテスト例を紹介します。 play-services-location の21系ではFusedLocationProviderClientがクラスからインターフェースに変わった 位置情報取得の中心を担うライブラリ play-services-location の最新のリリースのうち、今回はFusedLocationProviderClientの変更に焦点をあてます。 アプリケーション開発者はFusedLocationProviderClientを介して位置情報を利用します。FusedLocationProviderClientは、Android端末がGPSやWifiなどから取得した位置情報について、まとめて管理して適切な位置情報を返してくれます。 さて、2022年10~11月リリースの play-services-location の21系のリリースノートによるとFusedLocationProviderClientがクラスからインターフェースになったとのことです。 21.0.1のリリースノート 21.0.0のリリースノート FusedLocationProviderClient, ActivityRecognitionClient, GeofencingClient and SettingsClient are now interfaces instead of classes, which helps enforce correct usage and improves testability. developers.google.com インターフェースになったことで improves testability テスタビリティ(テスト容易性)が向上したということです。 Androidのテストではインターフェースが共通のテスト用の偽の実装と差し替えるパターンが推奨されています が、実際にテストが容易になったのかをテストを書き実感したいと思います。 現在地を取得するgetCurrentLocationメソッドが正しい位置オブジェクトを返すかテストしたい 単純な現在地取得実装を用意しました GeoLocationRepository内でFusedLocationProviderClientの現在地取得(getCurrentLocation)メソッドを呼び出します GeoLocationRepositoryのコンストラクタはFusedLocationProviderClientを受け取り差し替え可能にします getCurrentLocationが成功すればaddOnSuccessListener、失敗すればaddOnFailureListenerで追加されたリスナーが呼ばれます // 一部省略 class GeoLocationRepository( private val locationProvider: FusedLocationProviderClient) { private val currentLocationRequest = CurrentLocationRequest.Builder().apply { setPriority(Priority.PRIORITY_HIGH_ACCURACY) setDurationMillis( 1000L ) setMaxUpdateAgeMillis( 30000L ) }.build() // テスト対象のメソッド // 取得したLocationの各値を、自前で用意したLocationPayloadに詰め替える @SuppressLint ( "MissingPermission" ) suspend fun getCurrentLocation(): LocationPayload { val def = CompletableDeferred<LocationPayload>() val cancellationTokenSource = CancellationTokenSource() val locationTask: Task<Location> = locationProvider.getCurrentLocation( currentLocationRequest, cancellationTokenSource.token ) locationTask.addOnSuccessListener { location: Location? -> def.complete( if (location == null ) { getEmptyLocationPayload() } else { buildLocationPayload(location) } ) } locationTask.addOnFailureListener { Log.d( "GeoLocationRepository" , "FailureListener @@@@@@" ) def.complete(getEmptyLocationPayload()) } return try { def.await() } finally { cancellationTokenSource.cancel() } } 本物のFusedLocationProviderClientを使うテストはセットアップと結果の制御が難しい まず本物のFusedLocationProviderClientクラスをそのまま使うテストを考えます。 FusedLocationProviderClientにはmockモードがあり、任意の位置情報を返すようにセットすることができます。 getCurrentLocationを呼び出したときにセットしておいた位置情報が取れたかどうかを確かめるテストを書けます。 しかし事前準備は少々手間がかかります。 debug/AndroidManifest.xmlに <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" tools:ignore="ProtectedPermissions" /> を与えます 「設定」->「開発者向けオプション」-> 「仮の現在地情報アプリを選択(Select mock location app)」から対象アプリを指定しておきます (または Uiautomator を利用し adb shell appops を使う方法 があります ) // setMockMode=trueの場合のテスト例 class GeoLocationRepositoryTest { private lateinit var client: FusedLocationProviderClient private val location = Location( "mock" ).apply { latitude = 35.6812362 longitude = 139.7671248 speed = 42.0F accuracy = 0.68f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } @Before fun setUp() { val context = InstrumentationRegistry.getInstrumentation().targetContext client = LocationServices.getFusedLocationProviderClient(context) client.setMockMode( true ).addOnFailureListener { throw it } } @After fun tearDown() { client.setMockMode( false ).addOnFailureListener { throw it } } @Test fun latitudeIsCorrect() { client.setMockLocation(location).addOnFailureListener { throw it } runTest { val acquiredLocation = GeoLocationRepository(client).getCurrentLocation() assertEquals( 35.6812 , acquiredLocation.latitude, 0.001 ) } } } 本物のFusedLocationProviderClientが提供するsetMockModeをtrueにすることで、getCurrentLocationが成功した場合の本物のレスポンスに近しいテストが可能です getCurrentLocationを意図的に失敗させ任意の例外を発生させるようなテストはできません FakeのFusedLocationProviderClientを使う場合はセットアップと返り値の改変が容易になる play-services-location の21系ではFusedLocationProviderClientがインターフェースとなりました。 この結果、本物のFusedLocationProviderClientの代わりに、FusedLocationProviderClientインターフェースを実装する偽のFakeFusedLocationProviderClientをGeoLocationRepositoryに渡すことができるようになりました。 // 偽のFusedLocationProviderClient // 一部省略 class FakeFusedLocationProviderClient : FusedLocationProviderClient { // テストケースごとに返り値を変化させるためのフラグ var shouldFail = false private val location = Location( "mock" ).apply { latitude = 35.6812362 longitude = 139.7671248 speed = 42.0F accuracy = 0.68f time = System.currentTimeMillis() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() } override fun getCurrentLocation( p0: CurrentLocationRequest, p1: CancellationToken? ): Task<Location> { // https://developers.google.com/android/reference/com/google/android/gms/tasks/Tasks return if (shouldFail) { Tasks.forException( Exception ()) } else { Tasks.forResult(location) } } // インターフェースのメンバーを省略 override fun setMockMode(p0: Boolean ): Task<Void> { TODO( "Not yet implemented" ) } } @OptIn (ExperimentalCoroutinesApi :: class ) class GeoLocationRepositoryWithFakeClientTest { private lateinit var fakeClient: FakeFusedLocationProviderClient @Before fun setupClient() { fakeClient = FakeFusedLocationProviderClient() } @After fun tearDown() { fakeClient.shouldFail = false } @Test fun latitudeIsCorrect() { runTest { // FakeのClientを渡す! val acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals( 35.6812362 , acquiredLocation.latitude, 0.0001 ) } } @Test fun zeroLatitudeIsAcquiredWhenFail() { runTest { fakeClient.shouldFail = true val acquiredLocation = GeoLocationRepository(fakeClient).getCurrentLocation() assertEquals( 0.0 , acquiredLocation.latitude, 0.0001 ) } } } ここまでで、次の2点でテスタビリティ向上を確認できました。 テスト用の偽のFusedLocationProviderClientに置き換えることで、getCurrentLocationの返り値を自由に変更できるようになりました getCurrentLocation失敗時のFailureリスナーを呼び出しやすくなりました ACCESS_MOCK_LOCATION権限付与は不要になりセットアップが簡単になりました まとめ play-services-location の21系を使うテストでは、本物ではなく偽のFusedLocationProviderClientを使うことで、テスト対象メソッドの結果の制御がしやすくなったりセットアップが単純になったりすることでテストが容易になりました。 参考 Release Notes  |  Google Play services  |  Google Developers Use test doubles in Android  |  Android Developers Android端末の地理的位置を変更するJUnitテストルール | Y_SUZUKI's Android Log
はじめに サービスをデプロイするときはビルドしてテストしてから行うという手順はよくあります。 その時に、Google Cloud Platform (GCP) 上で CI/CD パイプラインを構築し、コードの変更をトリガーにしてビルド・テスト・デプロイが手軽にできる手法を紹介します。 使用するツール GCP Cloud Build App Engine GitHub 作成するもの Vue.js のプロジェクトで GitHub 上の main ブランチに push/merge されたら自動でビルド・テスト・デプロイを行う環境を構築します。 Cloud Build とは? 公式ドキュメント サーバーレス CI / CD プラットフォームでビルド、テスト、デプロイを行います。 構成を yaml ファイルで記述でき、実行するのはシェルスクリプトから独自で作成した Docker イメージなども活用できるので自由度の高いパイプラインを作成できます。 また、実行トリガーも GitHub 連携、Webhook など様々な場面で組み込みやすいものが用意されています。 App Engine とは? 公式ドキュメント モノリシックなサーバーサイドのレンダリングのウェブサイトを構築し、アジリティを維持します。App Engine は一般的な開発言語をサポートし、さまざまなデベロッパー ツールを提供しています。 こちらも構成を yaml ファイルで記述するだけで準備が整い、コンテンツの配信からサービスのスケーリングまでマネージメントしてくれます。 実際に作成する 上で紹介した 2 つのサービス + GitHub で実際に構築してみます。 Vue Application の作成 まずは、デプロイするサービスのセットアップです。 詳細は省きますが、今回は Vue.js Quick Start の Creating a Vue Application にそって、プロジェクトを作成しています。 基本的に例示されている設定と同じですが、Vitest の追加だけ Yes に変更し、ユニットテスト環境のセットアップを行っています。 App Engine の設定 あらかじめ、App Engine でデプロイするサービスを確認しておきます。 何も設定しない場合は default サービスにデプロイされますが、 default 以外が良い場合は app.yaml に設定が必要になります。 app.yaml の設定 デプロイするものや形式に合わせて適宜調整をしてください。 今回作成するのは Vue.js のプロジェクトでビルド成果物が配信できれば良いので、 runtime には nodejs16 ( php81 とかでも大丈夫です)を、 service にはデプロイする App Engine のサービス名を記述します。 runtime と service が記述できたら、 handler の項目でファイルと配信 URL を紐付けます。 Vue.js Quick Start の Creating a Vue Application から作成していれば、ビルド成果物が dist 以下に生成されるので、成果物を配信できるように記述します。 詳細な記述方法は こちら をご覧ください。 今回は以下のような yaml を記述しました。 service : your-service-name runtime : nodejs16 handlers : - url : / static_files : dist/index.html upload : dist/index.html secure : always - url : /(.*) static_files : dist/\1 upload : dist/(.*) secure : always App Engine で配信するものの構成を app.yaml としてリポジトリ内に置いておきます。 .gcloudignore の設定 続いて、App Engine でデプロイしないフォルダ類を指定します。 こちらもプロジェクトに合わせて適宜調整をしてください。 詳細な記述方法は こちら をご覧ください。 今回は以下のように記述して、ビルド成果物以外はデプロイしないようにしています。 * !dist/** これもリポジトリの一番上に .gcloudignore として置いておきます。 Cloud Build のトリガーを設定する 続いて、GitHub と Cloud Build の連携をします。 Cloud Build のダッシュボード から トリガー にすすみ、 トリガーを作成 を選択。連携したいソースを選択して認証、接続したいリポジトリを決めます。 リポジトリ決定後はトリガーの作成に移り、どのブランチにどのトリガーでビルドを開始するかを決めておきます。 今回は以下のようなトリガーを設定しました。 名前: TestTrigger リージョン: グローバル(非リージョン) イベント: ブランチにpushする リポジトリ: test-repo ブランチ: ^main$ 構成 形式: 自動検出 ブランチ名はサジェストが出ますが、正規表現で安全に指定しましょう。 cloudbuild.yaml の構成 最後に、Cloud Build で用いる yaml ファイルを作成します。 Vue.js のプロジェクトなので、npm でパッケージをインストールした後ビルド、テストを実行して App Engine にデプロイを行うコマンドまでを自動実行するように記述します。 yaml は、行いたいことをステップごとに書いていきます。 各ステップでは、 name で Docker イメージを指定します。指定できるのは 公式にサポートされているイメージ や、 Container Registry で管理されているイメージなどが指定できます。 node イメージは entrypoint が yarn と npm が設定できるようになっているので、必要に応じて使い分けましょう。今回は npm で設定しています。 あとは、コマンドラインで入力するような引数を args に渡してあげれば、ステップの記述は完了です。 詳細な記述方法は こちら をご覧ください。 今回は以下のような記述になりました。 steps : - name : node entrypoint : npm args : [ "install" ] - name : node entrypoint : npm args : [ "run" , "build" ] - name : node entrypoint : npm args : [ "run" , "test:unit" , "run" ] - name : "gcr.io/cloud-builders/gcloud" args : [ "app" , "deploy" , "app.yaml" , "--project" , "projectname" , "--quiet" ] このような yaml を記述し、 cloudbuild.yaml としてリポジトリ内に置いておきます。 確認 いよいよ main ブランチへ push ... の前に、ディレクトリ構造を確認しておきます。 ここまでのステップで以下のような構造になっていれば大丈夫です。 (重要じゃないところは省いています) project root L src L (Vue Application のソース) L (dist) L (なくてもOK。 手元でビルドをするとここに成果物が出てると思います。) L .gcloudignore L app.yaml L cloudbuild.yaml L package.json L package-lock.json 完成! ここまで設定をすれば、実際に GitHub で main ブランチへ push, merge を行うと、これらが自動で実行され、数分でデプロイまで完了しているのが確認できると思います。 お疲れ様でした。 まとめ Cloud Build + App Engine を使って、CI/CD パイプラインを構築しました。 これで、コードの変更だけに集中できますね! みなさんも良き CI/CD ライフを~
駅メモ!チームエンジニアの id:Eadaeda です。 みなさんシェルスクリプト書いてますか?私は時々書いています。12/2 の記事ではシェルスクリプトのテストを書いてみませんかという話を書きました。 tech.mobilefactory.jp 今回はテストではなく、linter の話です。 シェルの文法はなかなか難しいです。例えばダブルクォートで括るかどうかなどです。 # スクリプト a.sh があるとして $ cat ./a.sh #!/bin/bash echo " [ $1 ] " " [ $2 ] " " [ $3 ] " " [ $4 ] " " [ $5 ] " " [ $6 ] " # 例:引数のコマンド置換をダブルクォートで括るかどうかで動作が変わる $ ./a.bash $( date ) [ Wed ] [ Nov ] [ 30 ] [ 17:06:59 ] [ JST ] [ 2022 ] $ ./a.bash " $( date ) " [ Wed Nov 30 17:07:10 JST 2022 ] [] [] [] [] [] こういった慣れてないと陥りやすい罠はどんなものにもありますが、linter があれば先に気づくことができそうですね。 シェルスクリプトの linter 今回は ShellCheck を linter として使っていきます。例えば先の a.sh を実行するだけのスクリプト b.sh を以下のように書いたとします。 #!/bin/bash ./a.sh $( date ) これを shellckeck にかけると…。 $ shellcheck b.sh In b.bash line 3: ./a.sh $( date ) ^-----^ SC2046 ( warning ) : Quote this to prevent word splitting. For more information: https://www.shellcheck.net/wiki/SC2046 -- Quote this to prevent word splitt... こんな感じで警告してくれます。かんたんな対処方法が書かれていますね。より詳しい内容が知りたい場合は、同時に出力されている URL にアクセスするか、 SCxxxx で gg れば該当のページを探すことができます。 自分はそこそこチェックをするようにしています。意図しない罠を回避したいのもモチベですが、「ええっ!こんな罠が!?」と勉強にもなるのでハッピーです。 まとめ 今回は ShellCheck をかんたんに紹介しました。ぜひお手持ちのシェルスクリプトで試してみてくださいね。
こんにちは、エンジニアの id:yunagi_n です。 みなさんは JavaScript において、 URL をパースするとき、どの API を使用していますか? もっとも簡単なのは、 URL Interface を使用することだと思います。 今回は、その URL Interface が、 JavaScript の実行エンジンによって挙動が異なることについて書こうと思います。 事前情報 この記事の内容は、以下のバージョンにて確認を行っています。 macOS 12.5.1 Google Chrome 107.0.5304.121 Safari 15.6.1 Firefox 107.0.1 本題 URL Interface は、 各種ブラウザおよび Node.js 上で URL を扱うためのインターフェースです。適当な URL を渡すと、下のように各部分毎に分解してくれて、 URL を元に何かしたいときに便利なインターフェースです。 const url = new URL( "https://example.com" ) console.log(url.protocol) // => https: 今回は、そんな URL Interface について、ブラウザー実装毎による違いについてのお話です。 例えば、最もよく使われている Chromium 系列 (V8) の場合は、例として https://example.com/test?a=b#c のような URL を渡すと下記のような結果を返します。 const url = new URL( "https://example.com/test?a=b#c" ) console.dir(url) /* * URL { * hash: "#c", * host: "example.com", * hostname: "example.com", * href: "https://example.com/test?a=b#c", * origin: "https://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "https:" * search: "?a=b", * username: "" * } */ このような一般的な URL (ここではプロトコル部が HTTP および HTTPS であるものを指す) であれば、どのブラウザーでも同じ挙動をしてくれます。では、例えば FTP のセキュア版のプロトコルである FTPS を含んだ ftps://example.com/test を渡すとどうなるでしょうか? V8 の場合は以下のような結果を返します。 const url = new URL( "ftps://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "", * hostname: "", * href: "ftps://example.com/test", * origin: "null", * password: "", * pathname: "//example.com/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */ 通常の URL を渡した場合と挙動に違いがありますね。ちなみに Firefox (SpiderMonkey 系) でも同じ結果を返してくれます。 では WebKit 系列 (JavaScript Core) ではどうでしょう?答えは以下のようになります。 const url = new URL( "ftps://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftps://example.com/test", * origin: "ftps://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */ それぞれ、 host 部の扱いが異なっているのが特徴です。 V8 は host 部は無かったものとして扱い、 pathname にすべてを含めているのに対し、 JavaScript Core はおそらく私たちがイメージした結果と同じもの、つまりは host 部を example.com として返してくれています。 ではセキュアではない FTP 、つまりはプロトコル部が ftp: の URL を渡すとどうなるでしょうか?答えはすべてのブラウザーで次のような結果になります。 const url = new URL( "ftp://example.com/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftp://example.com/test", * origin: "ftp://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftp:" * search: "", * username: "" * } */ これから分かるように、 Chromium 系列と Firefox 系列は、プロトコル部によって、 host および hostname のパース結果が異なります。 では、挙動が異なるのがこれだけだというと、他にも異なる部分があります。 例えば、 hostname に大文字小文字の両方を含む文字列を渡した際の結果は、一般的なプロトコルの場合は以下のようになります。 const url = new URL( "http://ExAmple.COM/test" ) console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "http://example.com/test", * origin: "http://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "http:" * search: "", * username: "" * } */ しかしそうでないもの、ここでは Web3 の文脈でよく使われている IPFS プロトコルの URL を渡した場合、 Chromium 系や Safari では以下のようになり、 const url = new URL( "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe" ) console.dir(url) /* * URL { hash: "", host: "", hostname: "", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", origin: "null", password: "", pathname: "//QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", port: "", protocol: "ipfs:", search: "", username: "", * } */ SpiderMonkey 系 (Firefox) ではこうなります。 const url = new URL( "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe" ) console.dir(url) /* * URL { hash: "", host: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", hostname: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe/", origin: "ipfs://qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", password: "", pathname: "/", port: "", protocol: "ipfs:", search: "", username: "", * } */ 面白いのが、 href は大文字小文字を区別していますが、 host などその他は大文字小文字を区別せず、すべて小文字で表されています。 結論としては、図にまとめると以下のような挙動をします。 Parse Result \ URL Protocol http / https / ftp ftps ipfs hash すべてのブラウザで挙動は一致 - - host すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる hostname すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる href すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 Firefox で挙動が異なる origin すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる pathname すべてのブラウザで挙動は一致 Safari で挙動が異なる Firefox で挙動が異なる protocol すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 すべてのブラウザで挙動は一致 では、すべてのブラウザで挙動を揃えるにはどのようにすればよいのでしょうか? 答えは簡単で、 URL Interface を使用していないパーサーライブラリを使用します。 例としては url-parse などが使用できます。 ただし、こちらも内部で isSpecial という関数で一部プロトコルでのみ、 origin をセットしていたりするので、プロトコルが違えば、 origin に限っては挙動が異なります。 しかし、それ以外についてはどのブラウザ、プロトコルでも共通の動作をしているので、より多くのブラウザで同一の挙動を実現するには、十分ではないでしょうか。 ということで、今回は URL Interface の挙動の違いについて、解説しました。
こんにちは。モバイルファクトリーでエンジニアをしているまえけんです。 自分の居るチームではスクラムで開発をしていて、自分はスクラムマスターとしてチーム運用をしていました。 が、プロダクトオーナーの退職と組織編成によるチーム人数の増加などにより、スクラムマスターからプロダクトオーナーへ帽子を被り直すことになりました。 そこで、初めてプロダクトオーナーをやってみて何があったのか、気づいたこと、などをまとめようと思います。 突然の別れと新チーム 自分がスクラムマスターをしていた頃、チームは全部で5人居ました。しかし1ヶ月の間に、 新プロジェクトの立ち上げ プロダクトオーナーの退職 組織編成に伴いチームメンバーが5人から9人に増加 という怒涛の変化がありました。特に当時のプロダクトオーナーとは新プロジェクトについて立ち上げから協力して計画を建てていたので、その半ばでの退職ということで突然の別れとなってしましました。 人数も増え、チームとしてはほぼ新しくなり、プロダクトオーナーも不在という状況で、調整をした結果自分がプロダクトオーナーの役割を担うことになりました。 プロダクトオーナーを実践するのは初めてなので、まず何から手を付けるべきなのか迷う状況になりました。 プロダクトオーナーの役割 スクラムについて迷ったら何はともあれ スクラムガイド を見直すのが良いと思うので、スクラムマスターとプロダクトオーナーの役割の定義について見てみました スクラムマスターについて スクラムマスターは、スクラムガイドで定義されたスクラムを確⽴させることの結果に責任を持つ。スクラムマスターは、スクラムチームと組織において、スクラムの理論とプラクティスを全員に理解してもらえるよう⽀援することで、その責任を果たす。 プロダクトオーナーについて プロダクトオーナーは、スクラムチームから⽣み出されるプロダクトの価値を最⼤化することの結果に責任を持つ。 スクラムマスターは「スクラムの確立」に責任を持ち、プロダクトオーナーは「プロダクトの価値を最大化すること」に責任を持つとあります。言い換えると、スクラムマスターは「チームを観察する人」、プロダクトオーナーは「(開発している)プロダクトを観察する人」なのかなと思います。 スクラムマスターはチームを取り巻く問題を見つけて解決するとしたら、プロダクトオーナーはプロダクトを取り巻く問題を見つけて解決(判断)する人だと思いました 。 また、元スクラムマスターとして、もし今の状況の時にプロダクトオーナーにやって欲しい事はなんだろうということも考えてみました。ここは尊敬するマスター・センセイ(アジャイルサムライ)の助言も参考にしてみました アジャイルサムライ――達人開発者への道 作者: JonathanRasmusson , 西村直人 , 角谷信太郎 オーム社 Amazon プロジェクトが新しく始まった時点では、その成功について思い描く姿は人によって大きく異なるものだ。(中略) ここでの問題は、プロジェクトの開始時点で関係者の認識が揃ってないことじゃない(むしろそれは自然なことだ)。問題は、関係者全員でプロジェクトについて話し合うよりも前にプロジェクトを始めてしまうことにある。 チームメンバーが誰も居ないところで合意したことを前提にしているから、プロジェクトがだめになるんだ。 新メンバーが入ってきたという事もあり、プロダクトオーナーとして最初にすべきは「メンバーを集めて認識を揃える」事だと思い、まずインセプションデッキの整備から始めました。 プロダクトオーナーの単一障害点 インセプションデッキも概ね完成してチームとの合意も取れてきて、第1スプリントを始めるぞというタイミングで自分が新型コロナウイルスに罹ってしまい、約1週間チームから離れる事態になりました。 プロダクトオーナーとして判断をしたり、特に新メンバーからの質問に答えるのが途絶えたことで少なからずチーム内で混乱があったみたいです。プロダクトオーナーは単一障害点になりやすいという事を聞いたことがありますが、第1スプリントからそれを実感する事になりました。 認識を揃える難しさ 数スプリントが経過した頃、レトロスペクティブの場で「作っているプロダクトの最終形が想像できない」「今作っているものがどうなるのか不明」というのが開発メンバーから意見が出てきました。個人的な視点ではすでにインセプションデッキも作ってチームと合意もとっていたので、こういう事が出てくるのは驚きでした。 深堀りしてみると、インセプションデッキによってたしかにプロダクトの「Why(なぜ作るのか)」「How(どうやって作るのか)」は可視化できましたが、「What(何を作るのか)」がまだ可視化出来てないと気づきました。特に このプロダクトの楽しさはどこか というのが伝わってない事がわかりました。自分の頭の中にプロダクトの最終形が存在していても、それを示す事も重要である事に気づきました。 まとめ プロダクトオーナーに就任してまだ数スプリントだけですが、ここまでで大事だと感じた事をまとめます。 プロダクトオーナーだけで抱え込まない プロダクトオーナーは単一障害点になりやすい存在です。どうしてもプロダクトオーナーじゃないと判断できない、答えられない事は存在すると思いますが、それ以外については例えばスクラムマスターと協同出来ることを早いうちから始めるべきだったと思います。 今回で言うと、例えば新メンバーのオンボーディングも自分がやっていたのですが、開発に関するオンボーディングは自分から棚卸しできたかもしれないです。プロダクトオーナーじゃないと出来ない仕事以外はなるべく協力していきたいと思います。 プロダクトの方向性を示し続ける インセプションデッキをチームで作った当時は概ねプロダクトの方向性を共有することが出来ましたが、作って終わりでは良くないことがわかりました。 マスター・センセイも話している通り、認識が揃わないことは自然なことで、時間経過でそれは発生し続けると思いました。プロダクトオーナーとしての仕事の1つにプロダクトの方向性を示すということがありますが、常にチームと対話すること等で方向性を 示し続ける も必要だと思いました。 現在ではプロトタイプを作ることで、まずプロダクトの最終形をざっくり作って認識を揃える(対話を促す)、新しいアイディアを試す、ということをしようと思ってます。
🎄モバイルファクトリー Advent Calendar 2022!毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします! こんにちは。駅メモ!シリーズでデザイナーをしている19卒入社の @watagisan です。 アドベントカレンダー初投稿ということで、新卒デザイナーとして、「どのように行動すると入社して早い時期からいい感じに会社に貢献できるか」について個人的に意識していた「心構え」のまとめを書きました。 こころとからだのためにたいせつなこと 睡眠はたいせつ IT関係の企業では、どちらかというと頭を使うお仕事が多いのでしっかり休まないと日常業務に支障をきたします 作業時間(残業時間)ではなく成果を自慢しよう 自分の扱いがおかしいなと感じたらすぐに誰かに相談する 上司もチームももちろん僕もあなたも、人間なのでチームメンバーとの相性が悪かったりする場合がどうしてもあります 上司やチームからの扱いが何かおかしいなと感じたら、身近な人にすぐに相談 わからなければすぐ質問 「Aさんに聞きたいけど忙しいかも…?」→忙しければそう言ってくれると思うので、聞いたほうがいいです 「まず自分で考えるべきかも…?」→自分で考えるべき疑問であればそう促すと思うので、聞いたほうがいいです 知識がまだ乏しい段階で「自分で考えるべき問題か」「質問すべき問題か」を判断するのは難しいです 上司に質問されることに怯えなくてもよい 上司や先輩からの質問は詰問や叱責の意図を伴っていない なんとなく「怒られているのかな」と勘違いしないこと 例えば「ここのストライプのデザインはどういう意図をもってストライプにしたの?」と言われた時 × ストライプじゃ変だからマシなデザインにしろ! ○ 数多あるパターンからストライプを選んだ理由はなんだろう?ストライプは適切だろうか? アウトプットをしよう ここでいうアウトプットとは、自分が知り得た知見や技術などを他の人が利用できるように社内向けのドキュメントとしてまとめておくことです 「自分の持っている知識なんてみんな知ってるだろうしアウトプットしても意味ないかも…」というのは間違いです 「自分が思いつくようなことはほとんど誰かが先に思いついている」のは正しいです ですが「思いついたことを文字や図にアウトプットしている」かどうかは別 たとえ被っているところがあったとしても、一字一句同じになることはないのでアウトプットする意味はあります 「自分が新卒で経験したことなんてみんな知ってるしドキュメントにしてまとめておく必要なんかないか…」 →あなたの経験とみんなの経験は当然異なるので、あなたの経験や感想を書き留めておくことは今後の新卒の糧になります おしごとをする上での行動の心構え 組織の人間であることを意識して責任を持つ デザインに限らずですが、社会人として・組織の人間として恥ずかしくない立ち振る舞いが求められます なにか不祥事があると会社の名前に傷がつき、それは社員全員の社会活動に影響します あなたがデザインしたものは、「会社の制作物」として世の中に認識されます 制作を進める時に、それはリリースして問題ない表現かを考えておくとよいです あなたの行動によって起きた不利益は会社が被ります 主体性を持つ お仕事を任せる時、全然やる気がなさそうな人よりも実績のある人ややる気がありそうな人に任せたくなります なので自分から手を挙げて仕事を取りに行くような姿勢だと、お仕事も成長のチャンスも多くなります 社風にもよりますが、実績がなくても新人ならお仕事を任せてもらえる風土があれば主体的になりやすいです 常に積極的に手を挙げる姿勢が早いうちに身につくと、どんどんお仕事を任せてもらえて実績も増え、スキルもどんどん向上します 当事者意識を持つ 何か問題が起きた時に、問題解決のために自分から動こうとする姿勢を見せると、周囲からの信頼は厚くなります 「自分には関係ない事だから…」とか、「新卒の自分が出る幕じゃないな…」と思う人には会社としては問題解決を任せたくないです 何か自分にできることはないかとりあえず考えるようにしておくと、自分にもできることがあった時にすぐ動けるようになります スピード感を持って動く 例えば何か作業を頼まれた時、他に特別優先すべき事項がなければ、先にその頼まれた仕事を終わらせて報告した方が周囲からの評価は上がります 他の重要なタスクや業務時間を犠牲にしてまで先にやれというわけではないですが、もし順番を前後しても大丈夫なら先に終わらせてしまった方が評価されるし信頼もされやすくなるのでのちのち自分にとって得になると思います もちろん仕事のスピードとクオリティの両方を満たすのがベストなんですが、クオリティ面で未熟な新卒のうちにチームに貢献するにはスピード重視に振り切ってしまってもよいと思います すばやく作業していろんな人に見てもらって、より多くのフィードバックをもらって修正していく方が時短になります (なるべく) 早めに呼びかけに応じる (できれば) オフィスにいるときはデスクで仕事をしていることが見えますが、リモートワークだとそれが見えないのでチームメンバーがそれぞれ何をしているのか(忙しいのか暇そうなのかすら)わかりません なので返事が遅いと「ちゃんと仕事してるのかな…?」とか「無視されちゃったのかな…」とか心配になります (これは信用していないわけではなく、関係が浅い新卒のうちはどうしても不安に感じてしまうという話です) なので呼びかけがあった時などはなるべく早く応答するようにすると、周囲は「この人には安心して仕事を任せられるな」と感じます ミーティング中だったりとっても忙しいときは「あとで確認してお返事させていただきます!」とか「ミーティングの後にお返事します!」とかとりあえず見たよ!ということを伝えてあげると声をかけた人は安心できます 業務をする上での心構え 「会社、チーム、上司が自分に期待していること」が何かを考える (新卒に限らずですが) 自分が今周囲から何を期待されているのか考えてみる機会を設けるとよさそうです 何を期待されて採用されたのかを考え、その期待になるべく応えられるように動くと評価は上がりやすいと思います 逆に何を期待されているのかの認識を間違えて、期待されてないことを頑張ってしまうとせっかく頑張ったのに評価されなかったり、仕事ができない人だと思われてしまったりしてかなしいです 考えてもわかんないよ〜!というときは直接上司やメンターの人に聞いてしまってよいと思います ミスしたらすぐに正直に報告する ミスが後から発覚して大ごとになるより、ミスをすぐに共有してみんなでリカバリできた方が会社としての損害は少なくなります なので、やらかしたら気付き次第すぐに誰かに報告しましょう 例えば… 「すみません、Googleドライブ上の○○のデータを誤って削除してしまったのですが、どうすればよいでしょうか?削除してしまったデータは○○、削除してしまったデータがあったドライブのURLは○○です」などなど わからないことはすぐに聞く 仕様、納期、要件、デザインの方向性などなど 入りたての頃はわからないことが多いのは会社の人たちもみんな知っているので、忙しいかもしれないし…とか思わず遠慮せずに聞くとよさそうです 誰に聞けばいいかわからない…というときは、「@わかる方お願いします」とかで聞いたら誰かが適切な人に繋いでくれると思います わからないまま1人で考えて業務時間を浪費するよりも、頻繁に質問して仕事を少しでも進められる方が会社としては助かります 質問するときは回答しやすいように質問する 初めのうちは何もわからないので難しいかもですが、聞かれた側がすぐに返事しやすい質問だとお返事が早く返ってきやすいと思います 例えば…「質問するときに自分なりの答えを添えて質問する」 「お疲れ様です、 自分の手持ちの作業についてです。 指示されていた画像制作の作業が終わったのですが、次に作業した方が良い作業などありますか? 自分としてはこの作業(タスク情報のURLなど添えつつ)が納期近そうかつ自分でも対応できそうなので、次はこれを作業しようかなと思いますがどうでしょうか? 」 という感じで ①なにについての質問かわかるように前置きをする(相手が忙しかった場合に緊急で返事した方がいいか後でもいいかの指標になります) ②質問したい内容を具体的に書く(必要に応じてURLなどあるとなおよいです) ③質問に対する自分の意見を添える(意見が合っていれば「それでいいよ!」で返事が済みますし、間違っていれば相手も何が勘違いの原因になっているのか把握しやすいです) 例えば…「関連情報を提示して適切な回答を引き出す」 「お疲れ様です、 こちらの画像制作に関して、制作で使用する キャラクターの素材画像を探しているのですが、どこにあるか教えていただけませんでしょうか? Googleドライブ上で検索したのですが見当たりませんでした。 こちらの画像制作要件は以下のURLです。 」 という感じで ①なにについての質問かわかるように前置きをする ②質問したい内容を具体的に書く ③質問前にどう行動したかを添える(なぜ質問をするに至ったのかがわかるので、回答する側もどんな情報を提供すればいいか把握しやすいです) ④関連する情報を示す(もしそもそもの質問が間違っていた時に指摘できるので、自分が知り得る関連情報は全て提示しておくと認識の齟齬が少なくなります) さいごに 個人的な心構えなので誰しも参考になる内容ではないと思いますが、入社して困った時に役に立ったらうれしいです。
こんにちは。駅奪取エンジニアの id:dorapon2000 です。 コード差分の大きなプルリクエスト(以下、プルリク)をコードレビューした経験は多くの方があると思います。 プルリクは小さく・単位ごとに、とは頭でわかっていても、実装している内に想定よりも大きくなってしまったり、1つのプルリクにまとめなければコンテキストが伝わらなかったり、どうしてもということはあります。本当に申し訳ないと思いつつレビュー依頼を出したり、出されたり。 今回は、巨大なプルリクを前にして、自分がどうモチベーションを保つか、どう読み解いていくか、同じ状況を避けるためにレビューイと協力できることはなにか、について自分の場合を例に紹介していきます。 プルリクは小さく ここでは巨大なプルリクは避けるべきだという前提で記事を進めていきます。 つまり、プルリクは可能な範囲で小さい方がよいという前提です。 その理由は、コードレビューに関する優良な記事が多くあるため、そちらに譲ります。 また、どれほどで巨大かは主観で大丈夫です。レビュアーの心のソウルジェムが濁り始めたら、この記事が役に立つかもしれません。 リスペクトを忘れない 自分はレビューイに対するリスペクトがコードレビューで一番大切だと考えています。それは巨大なプルリクでも同様です。 悪意があってプルリクを大きくしたわけではありませんし、実装も大変だったはずです。 よりよいコードへするために協力するという気持ちとねぎらいを忘れないようにしています。 読み解く3つの選択肢 巨大なプルリクには様々なコンテキストが埋め込まれており、レビュアーはApproveする前にそれらを理解する必要があります。 理解にかかる時間と体力が、頭を重くする理由です。 モチベーションを保つためにも、理解しやすくするためにも、自分は3つの選択肢を考えます。 プルリクを分割してもらう 会話をしながらレビューイに説明してもらう 簡単なもの⇒メイン処理の順に読む 1. プルリクを分割してもらう プルリクが大きいならば、分割することで1つあたりの負担も小さくなります。 まず一番最初に考える選択肢ですが、同じことはレビューイも考えており、たいてい分割できません。 現状から分割するには余計に実装コストがかかったり、そもそも分割するほうがレビューが大変になる場合です。 2. 会話をしながらレビューイに説明してもらう 弊社は完全リモートなため、通話をしつつ画面共有してもらいながら、プルリクの各差分の意味を説明してもらいます。 会話のメリットは、なにより心理的な負担が低いことです。自分でコードを追わずとも、何をする箇所に、どういう変更を、どういった意図で入ったのかを理解できます。 プルリクが巨大なため、疑問点も多く出てくるでしょう。それも通話であれば即座に解決します。このスピード感は非同期でするレビューにはない魅力です。 もちろん、通話している分だけレビューイの時間も必要です。しかし、通話のあるなしで両者がレビューに掛ける合計時間の差はないように感じます。 会話へ入る前に、レビューイは事前解説コメントをプルリクエストに残したり、レビュアーはプルリク全体を眺めておくことで、よりスムーズな会話になります。 また会話後も、会話の内容をプルリクエスト内に記載しておくことで、実装意図を見返す際や他のチームメンバーへ共有する際に役立ちます。 3. 簡単なもの⇒メイン処理の順に読む 3つ目の選択肢は、プルリクを地道に読みます。 レビューイと都合がつかないなどの、会話の選択肢が取れない苦肉の策だと思っています。 自分の場合、小さいプルリクでは表示されているファイルの上から順に見ます。プルリクが小さいため、それでも内容を把握できます。大きなプルリクではそうはいきません。まず、独立した簡単なものから見ていきます。これは、メインの処理にある最も複雑なコンテキストを理解する際のノイズを最初に除去するイメージです。 CSSの簡単な変更だったり、単純な変数名の変更だったりです。 ノイズの除去後は、メイン処理をプログラムの実行順に上から読んでいきます。 過去に、実装するときは上から順に、レビューするときはバラバラ、という時期がありました。しかし、レビューするときも上から順に読むほうが理解しやすいです。 将来の巨大なプルリクエストを避ける ここまででレビューは終わりです。 最後に、巨大なプルリクエストを分割する方法があったかを、レビューを通して得られたプルリクの理解をもとに考えます。 何度も言いますが、大きなプルリクエストのレビューは大変です。その体験を通して、どうすれば分割できるのかノウハウが溜まっていくはずです。 ノウハウをレビューイにも共有して、適切な粒度のプルリクエストをチームの文化にしていきます。
こんにちは、駅奪取チームエンジニアの id:kebhr です。 駅奪取チームでは Pull-Request を本番環境に反映する前に Jenkins を用いてフルテストを実行しています。 手順としては Jenkins をキックするシェルスクリプトを使い、開発環境で次のようなコマンドを実行します。 cd $PROJECT_ROOT # カレントブランチのテストを実行する ./jenkins # or # 特定のブランチのテストを実行する ./jenkins ${branch_name} しかし、このスクリプトは push 忘れのチェックを行っておらず、 ブランチ・コミットを push し忘れた状態でフルテストを走らせてしまう という問題点がありました。 push 忘れに気付いたときに push し、フルテストを走らせれば問題ありませんが、フルテストを二度走らせることになるためリードタイムが長くなります。 そこで、ローカルに push していないコミットが存在する場合は、フルテストを実行せずにエラーを表示するようにスクリプトを変更しました。 全体像 #!/bin/zsh set -ue # 中略 # 引数またはカレントブランチを取得し $BRANCH にブランチ名を格納 if ! git show-branch remotes/origin/ $BRANCH > /dev/null 2 >& 1 ; then echo " このブランチは push されていません " exit 1 fi if ! git diff remotes/origin/ $BRANCH .. $BRANCH --quiet --exit-code ; then echo " push されていないコミットがあります " exit 1 fi # Jenkins API をキック ブランチの push 忘れを判定する if ! git show-branch remotes/origin/ $BRANCH > /dev/null 2 >& 1 ; then echo " このブランチは push されていません " exit 1 fi git show-branch <branch> は、引数に与えたブランチが存在すれば終了コード 0、存在しなければ 128 を返します。 ブランチの push 忘れを判定するためには、origin にそのブランチが存在することを確認すればよいので、引数に remotes/origin/$BRANCH を与えます。 なお、この実装では、fetch していないブランチのテストを実行しようとした際に「このブランチは push されていません」と表示されエラー終了しますが、チーム内でこのような状況には直面しないため許容しています。 コミットの push 忘れを判定する if ! git diff remotes/origin/ $BRANCH .. $BRANCH --quiet --exit-code ; then echo " push されていないコミットがあります " exit 1 fi git diff A..B --quiet --exit-code は A と B の間に差分が存在すれば終了コード 1、存在しなければ 0 を返します。 コミットの push 忘れを判定するためには、リモートとローカルの差分を確認すればよいので、引数に remotes/origin/$BRANCH..$BRANCH を与えます。 git diff は > /dev/null 2>&1 といったコマンドを用いずとも、 --quiet オプションによって出力を抑制できます。 --exit-code オプションは結果を終了コードでも返すオプションです。しかし --quiet オプションを指定すると暗黙的に --exit-code オプションが有効になります。よって --exit-code オプションは省略できますが、コードの理解を容易にするために記述しています。
id:nesh です。 今回の記事では駅メモ!エンジニアで定期的に開催している社内勉強会「Denco Tech Night」について紹介したいです。 Denco Tech Night について この勉強会は 2017 年から始めました。 社内勉強会が促進されている環境であるため、その社内勉強会の 1 つとして駅メモ!エンジニアチームによる勉強会です。 開催概要 参加者は駅メモ!エンジニアが参加必須です。 また、他チームのエンジニアも任意で参加できます。(現状は駅奪取チームのエンジニアも定期的に参加しています。) 発表する内容の絞りは特になく、業務で得た学びや個人的に勉強したこと、チームでの取り込みの共有などがあります。 駅メモ!チームの人数は多いため、この勉強会のタイミングで他の人が開発したツールや、業務上の Tips などが共有されたりします。 今まであった発表のタイトル例です。 Web Component 触ってみた AstroNvim で Neovim をはじめました アクセシビリティと WAI-ARIA について Android プロジェクトのビルドを理解して速くする Perl5.36 の変更点 開催頻度は隔週で行っています。 開催を始めた時に参加するメンバーを決めて、一通り全員が発表し終わったタイミングを一区切りにして、次の開催頻度や時間帯を検討したりします。 開催目的 Denco Tech Night を開催する目的は次のとおりです。 「人前で説明する」練習 ニーズの理解と解決の訓練 技術的知見の理解・共有 ブランディング それぞれの目的について補足します。 「人前で説明する」練習 社外勉強会に参加して発表するハードルを下げるために、社内でも発表できる機会を設けて、人前で説明することを慣れてもらう目的です。 ニーズの理解と解決の訓練 開催概要にも書いたように発表する内容の決まりはありません。 各々が発表する内容を検討するとき、今のチームにとってどういった発表の需要があるかを元に発表のネタを決めることもあります。 Denco Tech Night でそういった訓練をします。 技術的知見の理解・共有 言葉通りでチーム内の他の人との技術的知見の共有をしたり、発表を通してチーム内の理解度を同レベルに持っていくための場として使えます。 ブランディング 発表した内容によって、その人は何が得意かとかがわかります。 企業や会社のブランディングの意味に近いですが、勉強会を通じて、社内環境で自分自身のブランディングができます。 勉強会を継続するための工夫 2017 年から継続して Denco Tech Night が開催できたことに対して、どういった工夫があったかを言語化してみました。 発表準備の手間を減らす Denco Tech Night の発表形式は自由です。ほとんどの人は Docbase *1 に記事を書いて、発表時はその記事を画面共有しながら発表しました。 業務時間内に勉強会の発表資料を作成できるように 発表資料作成には時間が必要です。発表資料を作成するハードルを下げるために、業務時間内で Denco Tech Night 資料を作っても良いことにしました。 本来の開発業務に状況次第ではリスケ可能 業務上で忙しいタイミング時に発表順が来たり、緊急のトラブル発生がしたりのように、さまざまな原因で発表が難しくなることもあるので、リスケはできます。 他の人と発表順を交換してみて、ダメなら発表をスキップする形でリスケされます。 まとめ 以上が駅メモ!エンジニアによる社内勉強会「Denco Tech Night」 の紹介でした。 また、勉強会を継続できるための工夫をまとめてみました。 *1 : 社内で標準利用している情報共有ツールです。
こんにちは。駅奪取チームエンジニアの id:dorapon2000 です。 よくシェルスクリプトのIF文に >/dev/null 2>&1 を書いて条件文とすることはありませんか。実行結果の成否をIF条件として利用したいのであって実際に出力したいわけではないケースです。 実は身近なコマンドでもオプションで出力を抑制できます。3つ紹介します。 検証環境 grep --quiet git diff --quiet apt list -qq aptの警告について 検証環境 Ubuntu 16.04 bash 4.3.48 grep --quiet 特定の文字列を含むときにだけif文を実行します。 まず、 >/dev/null 2>&1 を使う書き方。grepに引っかかると0を返し(True)、grepに引っかからないと1を返します(False)。 if cat /var/log/syslog | grep " Error " > /dev/null 2 >& 1 ; then echo ' Found ' fi --quiet オプションを使うことで、出力を抑制してくれます。 if cat /var/log/syslog | grep --quiet " Error "; then echo ' Found ' fi # -q でも可 if cat /var/log/syslog | grep -q " Error "; then echo ' Found ' fi 以下のサイトでは、 --quiet を使うことで高速化もするという嬉しい情報もあります。 シェルスクリプトでファイルに特定の文字が含まれているかどうかを高速に判定する方法 | ゲンゾウ用ポストイット git diff --quiet git差分があるときにだけif文を実行します。 まず、 >/dev/null 2>&1 を使う書き方。 git diff は差分のありなしに関わらず正常終了の0を返すため、ifで使うには --exit-code オプションが必要です。差分があるときに1を(False)、差分がないときに0を返すようになります(True) # ① --exit-code を使う場合 if ! git diff --exit-code > /dev/null 2 >& 1 ; then echo ' Diff ' fi あるいは --exit-code を使わず以下のように書く人もいるでしょう。 # ② [を使う if [ `git diff | wc -l` -gt 0 ]; then echo ' Diff ' fi # ③ bash構文を使う if [[ `git diff | wc -l` > 0 ]] ; then echo ' Diff ' fi さて、git diffにも --quiet オプションがあります。さらに --quiet オプションは暗黙的に --exit-code オプションを利用しており、一石二鳥です。 if ! git diff --quiet ; then echo ' Diff '; fi 非常にスッキリしましたね! apt list -qq aptで特定のパッケージをインストールしていなかったらインストールしたいことは多いです。 まずは、前述した grep --quiet を使うやり方から。 if apt list --installed sl | grep -q sl ; then sudo apt install sl -y fi これでいいじゃん!ですが、aptの -qq オプションでも同じことができるため紹介します。 aptの -qq オプションは --quiet オプションをより強力にしたもので、進捗情報を消してくれます。ログ出力のためにあるオプションのようです。完全に出力を消すものではありません。 $ apt list --installed sl 一覧表示... 完了 # <-- これが消える sl/xenial,now 3 .03-17build1 amd64 [ インストール済み ] $ apt list -qq --installed sl sl/xenial,now 3 .03-17build1 amd64 [ インストール済み ] したがって、最初のif文は以下のように書き換える事ができます。 grep ^ でパイプで渡される出力が空かどうかを判定します。 if ! apt list -qq --installed sl | grep -q ^ ; then sudo apt install sl -y fi aptの警告について 上記例を実行すると警告が表示されます。 WARNING: apt does not have a stable CLI interface. Use with caution in scripts. 書かれているとおりで、aptは出力フォーマットが安定しているわけではないため、出力結果を加工するスクリプトはあまりよろしくないようです。 可能なら別のコマンドを利用したほうがよく、 apt list --installed に関しては dpkg -l で代替できます。 if ! dpkg -l | grep -q sl ; then sudo apt install sl -y fi
こんにちは、ブロックチェーンチームのエンジニア id:charines です。 この記事ではJavaScriptにおける unhandledrejection がどのような条件で発生するのかをクイズ形式でまとめています。 unhandledrejection とは unhandledrejection はエラーハンドリングされていない Promise が拒否されたときにグローバルスコープに送られるイベントです。 developer.mozilla.org unhandledrejection が発生したときの挙動は環境によって異なりますが、例えばNodeJSではプロセスが強制終了するため、気づかぬうちに発生させないよう注意が必要です。 では具体的にどのような状況で unhandledrejection が発生するのかをクイズ形式で見ていきます。 クイズ 次のコードを実行したとき unhandledrejection は発生するでしょうか。 (各問題のコードはNode v18.12.1にて検証しています) 問1 Promise.reject(); 解答・解説 発生する Promise.reject は拒否された Promise を返す関数です。エラーハンドラがないため unhandledrejection が発生します。 問2 Promise.reject() . catch (() => {} ); 解答・解説 発生しない Promise.prototype.catch はエラーハンドラを設定する一般的な方法です。今回は空の関数を渡しているため、式全体は undefined に解決します。 問3 Promise.reject() .then(v => v) .then(v => v, () => {} ); 解答・解説 発生しない Promise.prototype.then の第二引数は Promise.prototype.catch でエラーハンドラを設定するのと同じ働きをします。 また、ハンドラはPromiseチェーンを辿って呼び出されます。 問4 const f = async () => { const p = Promise.reject(); await sleep(); p. catch (() => {} ); } ; f(); ※ sleep は一定時間後に解決する Promise を返す関数で実装は以下です。 import { setTimeout } from "timers/promises" ; const sleep = () => setTimeout(50); 解答・解説 発生する 4行目でエラーハンドラを設定していますが、2行目の時点で処理が開始されるため、 p はエラーハンドラの設定より先に拒否されてしまいます。 ハンドラは Promise の作成直後に設定するべきです。 問5 const f = async () => { await Promise.reject(); } ; f(). catch (() => {} ); 解答・解説 発生しない await で待った Promise が拒否された場合、 await 式はその値で例外を発生させ、 f() が返す Promise は拒否されることになります。4行目で f() に対してハンドラを設定してるため、この場合 unhandledrejection は発生しません。 2行目の await がない場合は問1と同じ状況なので unhandledrejection が発生します。 問6 const f = async () => { const p = Promise.reject(). catch (e => { throw e } ); await sleep(); await p; } ; f(). catch (() => {} ); 解答・解説 発生する エラーハンドラの中で例外を投げた場合、 catch はさらに拒否される Promise を返します。 また、4行目で await をしていますが、それより前に p は拒否されてしまうので unhandledrejection が発生します。 問7 const f = async () => { const p = Promise.reject(). catch (e => e); await sleep(); throw await p; } ; f(). catch (() => {} ); 解答・解説 発生しない エラーハンドラによって p は拒否されずエラーの値に解決します。 非同期関数内で例外を投げる場合は、問5の await で待った Promise が拒否されるパターンと同じで、 f() の返す Promise が拒否されます。これは6行目でエラーハンドラが設定されているので unhandledrejection は発生しません。 最後に 7問の Promise を使った簡単な実装を用いて unhandledrejection が発生する条件をまとめてみました。 特に問6のように拒否され得る Promise を作成して後から await するというパターンで unhandlerejection が発生してしまうようなケースは注意すべきだと思います。 参考文献 PromiseのUnhandled Rejectionを完全に理解する 日本語の記事でECMAScriptの仕様を読み解いています
こんにちは、エンジニアの id:kaoru-k_0106 です。 CloudFront で gzip 圧縮を有効にしたところ転送量が減ったのはもちろんですが、予想外にリクエスト数も減ったため、理由が気になって調査して記事にしてみました。 背景 駅奪取シリーズは、2022 年現在もフィーチャーフォン(ガラケー)をサポートしており、非常に機能が限られたブラウザへの対応が必要となります。 そのため、gzip 圧縮や Accept-Encoding ヘッダに対応していない端末があったときのため、当初 CloudFront の gzip 圧縮を有効にしていませんでした。 後に検証して、問題ないことがわかったため有効にしたのですが、その際、転送量が減ったのはもちろんですが、リクエスト数もなぜか減少しました。 リクエスト数も課金の対象ですのでありがたいのですが、理由を考えたところ、gzip 圧縮されたままキャッシュすることで、ディスクキャッシュに保存できるファイル数が増えたのではないかと考えたため、検証してみました。 Chrome のディスクキャッシュはどこにある? 手元の Mac の場合以下のディレクトリにありました。 ~/Library/Caches/Google/Chrome/Default/Cache/Cache_Data なお、プロファイル(= Chrome のユーザ)ごとに分かれており、 Default の部分を Profile 1 などに変更することで各プロファイルごとのキャッシュを見られます。 ファイル名はハッシュのようで中身はわからないですが、このように大量のファイルが入っています。(念の為ファイル名にはモザイクをかけています) キャッシュされてるデータを見てみる この画像がディスクキャッシュされてるようなので、キャッシュから掘り起こしてみましょう。 探し方が分からなかったので、試しに URL で grep してみることにします。 $ find . -type f -print | xargs grep 'https://static.ekidash.com/v16378249952/img/portal/pc/description.png' Binary file ./xxxxxxxxxxxxxxxx_x matches Binary file ./xxxxxxxxxxxxxxxx_x matches それらしきファイルが 2 つヒットしましたが、2 つともバイナリファイルのようなので、バイナリエディタを使って開いてみます。 今回は VS Code のバイナリエディタ拡張機能である Hex Editor で開いてみました。 PNG のファイルヘッダが見えることから、レスポンスをそのままキャッシュしている可能性が高そうです。 参考: https://ja.wikipedia.org/wiki/Portable_Network_Graphics gzip 圧縮されたファイルのキャッシュを探す 続いて gzip 圧縮されたレスポンスを探してみることにしましょう。 js ファイルは CloudFront で gzip 圧縮されるので、portal.min.js をキャッシュから探してみましょう。 $ find . -type f -print | xargs grep 'https://static.ekidash.com/v16536443652/js/dist/production/portal.min.js' Binary file ./xxxxxxxxxxxxxxxx_x matches Binary file ./xxxxxxxxxxxxxxxx_x matches 見つかったファイルを開いたところ、ファイルヘッダが gzip になっています! よって、Chrome ではキャッシュが gzip のまま保存されることがわかりました。 参考: http://openlab.ring.gr.jp/tsuneo/soft/tar32_2/tar32_2/sdk/TAR_FMT.TXT Chrome のキャッシュの最大サイズは? 続いて、ディスクキャッシュの最大サイズを調べたところ、こちらの Q&A サイトに似たような質問がありました。 https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit Chromium(Chrome のベース)だと一般的に 10% of the available disk space (使用可能なディスク容量の 10%)とのことです。 このことから、機種依存でありますが 1 ファイルあたりのサイズが小さくなると、より多くのファイルがディスクキャッシュされるようになりそうです。 しっかり検証するのであれば比較実験をしたほうが良いのですが、今回の調査はいったんここまでとします。 Safari のキャッシュについて少し調べた ブラウザエンジンが異なる Safari だとキャッシュ周りの仕様が違いそうなので、こちらも少し調べてみました。 開発者ツールで確認したところ、リロードしたときはメモリキャッシュが使われましたが、再起動した場合はディスクキャッシュが使われました。 キャッシュの有効期限はどうなってる? 今回 Cache-Control ヘッダの設定をしていなかったのですが、キャッシュされたのはなぜだったのでしょうか? 調べたところ、以下のブログ記事でこう触れられていました。 一般的には Last-Modified ヘッダの日時と Date ヘッダの日時の差の 10%の値を有効期間として定めることが多いと RFC7234 に記載されています。 引用元: Cache-Control ヘッダがないときもブラウザがキャッシュする! そのため、とくに設定しなくてもキャッシュしてくれるようですが、Cache-Control でキャッシュする日数を明示したほうが良さそうですね。 なおキャッシュする場合は、キャッシュバスティングする仕組みを入れておきましょう。 まとめ 断言はできませんが、リクエストが減った理由として、gzip 圧縮によってファイルサイズが小さくなることでキャッシュされるファイル数が増えたからだと考えられます。 そして、バイナリエディタはこんなときにも役立ちますね。
こんにちは! モバファクで採用担当として働く @overallfactory です。 毎週土曜日は「良いモノ」を作る技術というテーマで、モバファクの非エンジニアが知見やTipsをお届けします。今回の記事では、「良いモノ」=「良い組織」と捉えてエンジニア採用について紹介します。 具体的に紹介していくのはカジュアル面談について。 私が面談時にどのような準備を行い、どのような時間の使い方をしているのかについて記事にまとめました。 近年では、現場で働くエンジニアもカジュアル面談に関わることが増えてきているかと思います。個人の考えではありますが、少しでも参考になれば、嬉しいです。 カジュアル面談とは? カジュアル面談とは、選考の前に求職者と企業の担当者が情報を交換し合う場のことです。 企業側としては、求人の詳細や企業情報についての説明を行い、求職者に興味を持ってもらうことを目的としています。 注意しないといけないのは、選考ではないということ。決して合否を判定する場にしてはいけません。 モバファクの採用では、基本的に面接前にカジュアル面談を実施しており、会社について理解していただいた上で選考に進んでいただいております。 カジュアル面談の心構え カジュアル面談を行うに当たって、特に大切にしている心構えについて3つ紹介します。 一見、非効率的と感じるかもしれませんが、5年以上の採用経験をする中で、効率的な採用において非常に重要な考え方だと思っています。 ①カジュアル面談では興味を獲得すべし! カジュアル面談では「候補者に魅力を伝え、選考に進んでいただく」ことを第一に考えています。 モバファクは上場企業とはいえども、名前を聞いたことがないという人はとても多いです。 だからこそ、最初の面談は超重要。1時間程度の面談で「面談/面接に何度でも足を運びたい!」と思ってもらわないといけません。 面談をする限りは色々と聞いてみたいことがあると思いますが、まずは全力で宣伝を行い、「とりあえず受けてみよう!」と求職者に思ってもらいましょう。話はそこからです! ②嘘や適当な発言はしない わからないことは「わからない」と回答をすることも大切です。 私はエンジニアではないので、込み入った技術の話には答えられません。 回答が難しい質問があれば、「わからない」と正直に答えた上で、「メールで返答する」「別途エンジニアと面談の機会を作る」など真摯に対応をすることを心がけています。 その場を誤魔化そうと、知ったかぶりをしても、信頼を失うだけです。 ③会社の魅力を理解すべし 自社のどんなカルチャー/事業が他社に比べて秀でているかを常に考えましょう。 例え優れた制度やカルチャーが自社にあったとしても、求職者が見ている企業でも同様に存在するものであれば、伝えてもあまり効果はありません。 せっかく魅力を伝えるのであれば、求職者の人に「おっ!今までになかった魅力的な企業だな!」と、思ってもらいたいものです。 短い時間で効率的に自社の魅力を伝えるためにも、常に他社の状況に目を向け、自社がどういう強みを持っているのかを客観的に知ることがとても大切です。 カジュアル面談前に準備すること 「面談/面接の質は事前準備で決まる」という考えを、モバファク採用チームでは大切にしています。 面談の質を高めるために、私の場合は大きく2つのことを事前に行っています。 ①求職者の情報を知る 事前に求職者の情報をもらっている場合は、必ず丁寧に目を通すようにしています。 面談に慣れているといっても、準備がないと面談がスムーズにいかないことが多いです。 面談をスムーズに回すためにも、事前にどんな質問をするかを10個以上は書き出すようにしています。 また、求職者にとっても企業が事前に情報を読み込んできてくれることに、悪い気はしないはずです! ②カジュアル面談のストーリーを考える どのような流れで、どんな話を伝えるのかを細かくまとめます。 事前に履歴書を見ている場合は、「技術的に成長ができる環境を求めているから、勉強会制度や学習支援制度の話をしてみよう」など、どうやって魅力を訴求するかも詳細に決めておきます。 大事なのは一本槍にならないこと。仮に勉強会制度の話に共感してもらえなかった場合、社員のキャリアモデルの話をするなど、一歩先のことも考えておくと面談が円滑に進みます。 求職者の情報が事前に見れないときは、どういった話の流れで就活の軸や将来像を聞き、回答によってどんな話をするか、場合分けをしておきます。 カジュアル面談の流れ では、実際にカジュアル面談でどのような話をどのような流れで行っているのかを紹介します! 例外もありますが、私の場合以下のような流れで進めることが多いです。 (カジュアル面談は基本的には60分で実施しています。) ①アイスブレイク カジュアル面談の目的には、ミスマッチを防ぐことも含まれます。 だからこそ、お互いに本音で話し合うことが理想的。 初対面ということもあるので限界はありますが、砕けた会話を冒頭ですることで、カジュアルな雰囲気づくりを意識しています。 事前に求職者から情報をいただいている場合は、出身地の話や趣味の話などに触れることが多いです。 ②目的/流れの説明 個人的には、目的の説明が最も大事だと思っています。 目的がふわっとした面談を防ぐためには、カジュアル面談が面接の場ではなく、求職者側が企業を選ぶ場であるということを明確にすることが何よりも大切です。 具体的には、「カジュアル面談の目的は何なのか?」「この面談で何を判断していただきたいのか?」について丁寧に説明をするようにしています。 目的と流れの説明を怠ると、認識の齟齬から「会社説明だと思ったら、質問攻めにされてしまった」など、不信感を求職者に抱かせてしまう場合があるのでご注意を! ③求職者へのヒアリング 会社の説明に移る前に、必ず簡単なヒアリングを行うようにしています。 求職者が就活において求めていることがわからないと、訴求すべき点も見えてきません。 だからこそ、何に興味を持っていて、どんな軸で就職活動をしているのか、そして将来的にはどんなスキルを身につけていきたいのかは、かなり具体的に聞くようにしています。 また、これまでの経歴、開発物についてもヒアリングを行っています。 モバファクが求める人材は「プログラミングが大好きな人」。社員やカルチャーとのマッチングを知るために、どのようなモチベーションで開発を行ってきたかなどを伺います。志向性等で気になるポイントがあれば、具体的にその旨をお伝えし、認識の齟齬がないようにしています。 ④会社の説明 ヒアリングで伺った就活の軸や将来像に対して、会社の魅力を訴求していきます。 求職者によって流れはまちまちで、勉強会などの文化を求めてる人には、会社のカルチャーについて。裁量を求める人には、会社の方針や1、2年目のキャリアモデルをメインに。 サービス面に興味が強い方には、サービスの詳細や各サービスのやりがいについて説明をしていきます。 合わせて、求職者の希望に応えられそうにない点についても丁寧に説明を行います。 不信感を抱かせないためにも、できないことは「できない」と正直に伝えるようにしています。 ⑤選考の案内 面談の終わりに「会社説明を受けて、選考へ進みたいと思ってくれたか?」と聞き、「進みたい」と言ってくれた方には、選考のご案内を行います。 注意点としては、冒頭の目的の説明時に、「選考に進むか否かを最後に判断して欲しい」という旨を伝えておくこと。 事前に伝えておかないと、求職者側に強制的な印象を与えてしまうかもしれません。 ミスマッチを防ぐためにも「No!」と言えるような雰囲気を作っておきましょう。 最後に カジュアル面談について記載をさせていただきました。 個人的に意識していることなので、正解ではないと思います。 ですが、これからカジュアル面談に関わるみなさまにとって、何かしらのヒントになっていれば、嬉しいです。 最後に宣伝です! 現在モバファクでは、中途エンジニア、新卒エンジニアを募集中です。 ご興味ある方は以下をご覧ください! https://recruit.mobilefactory.jp/recruit/
駅メモチームでエンジニアをしている id:Eadaeda です。シバンは #!/usr/bin/env を使う派です。 皆さんはシェルスクリプト書いてますか? 環境構築、開発、テスト、ビルド、デプロイなどなど、一連の作業を自動化するための手段として時々出番があるんじゃないでしょうか。 ところでそのシェルスクリプト、テスト書いてますか? シェルスクリプトのテスト 「シェルスクリプトのテスト〜?」って感じですよね。殆どの場合、一度書いてしまえばあんまり壊れることはないし別に…って感じですよね。わかります。実際開発環境のために docker compose up するだけのスクリプトなら雑でもいいですよね。 でも、重要な役割をもつスクリプトならどうでしょう。例えばアプリケーションのエントリーポイントや、リリースビルド・デプロイのためのスクリプトなどが思いつきます。 こういうのはテストである程度保証されていれば安心じゃないですか?どうですか?書きたくなってきましたか?とりあえず一回書いてみませんか? Bats/ShellSpecでシェルスクリプトのテストを書いてみよう シェルスクリプトにもテストフレームワークがあります。たとえば Bats や ShellSpec などです。 今回は上記2つについて少しだけ紹介しようと思います。テスト対象のスクリプトは以下のものとします。 #!/usr/bin/env zsh if [[ " ${1} " == " en " ]] ; then echo " Hello World " elif [[ " ${1} " == "" ]] ; then echo " こんにちは 世界 " fi 第一引数に en を渡せば Hello World を、何も渡さなければ こんにちは 世界 と出力するだけのスクリプトです。これを bin/hello-world.sh として保存しておきます。 Bats Bats は10年ぐらい前からあるそこそこ定番のテストフレームワークです。簡素に書けるのがいいところかなと思っています。 test/hello-world.bats として以下の内容を保存します。 #!/usr/bin/env bats # 出力を検査するシンプルなテスト。シェルの構文っぽく書く @ test " 引数がないとき、こんにちは 世界が返されるべき " { result = " $( ./bin/hello-world.sh ) " [ " $result " = "こんにちは 世界" ] } @ test " 引数がないとき、こんにちは 世界が返されるべき - 2 " { # run を使うと $output に出力が格納される run ./bin/hello-world.sh [ " $output " = "こんにちは 世界" ] } # bats-core/bats-supportとbats-core/bats-assertを ./test/helpers 以下にcloneして読み込めば # 様々なヘルパーが使えるようになって便利 load ' helpers/bats-support/load ' load ' helpers/bats-assert/load ' @ test " 引数がenのとき、Hello Worldが返されるべき " { run ./bin/hello-world.sh en # runの出力が一致しているかを見るヘルパー assert_output ' Hello World ' } 実行は bats コマンドに渡してやればよいです $ bats ./ test /hello-world.bats hello-world.bats ✓ 引数がないとき、こんにちは 世界が返されるべき ✓ 引数がないとき、こんにちは 世界が返されるべき - 2 ✓ 引数がenのとき、Hello Worldが返されるべき 3 tests, 0 failures setup/teardownも書くことができます #!/usr/bin/env bats setup() { # ヘルパーのロードをsetupでやっちゃう load ' helpers/bats-support/load ' load ' helpers/bats-assert/load ' } @ test " 引数がenのとき、Hello Worldが返されるべき " { run ./bin/hello-world.sh en # setupでロードしてるのでヘルパーが使えちゃう assert_output ' Hello World ' } ShellSpec ShellSpec はBDDな単体テストフレームワークです。RSpecとかJestみたいな書き味でテストを書いていくことができ、機能も豊富です。 まずはプロジェクトのセットアップです。最低限 .shellspec ファイルが必要となりますが、 shellspec --init で作成できるので、これを使うのが楽です。 $ shellspec --init create /path/to/ pwd /.shellspec create /path/to/ pwd /spec/spec_helper.sh あとは spec/ 以下にテストを書いていきます。今回、READMEの Typical directory structure にならってファイル名は spec/bin/hello-world_spec.sh としました。内容は以下の通りです。なんだかプログラムっていうか普通の文章みたいになりますね。 Describe " bin/hello-world.shについて " Context " 引数がないとき " It " こんにちは 世界が出力されるべき " When call ./bin/hello-world.sh The output should eq ' こんにちは 世界 ' End End Context " 引数がenのとき " It " Hello Worldが出力されるべき " When call ./bin/hello-world.sh en The output should eq ' Hello World ' End End End テストの実行は shellspec --init したディレクトリで shellspec を実行するだけです # --shell指定なしだと /bin/sh になる $ shellspec --shell zsh Running: /bin/zsh [ zsh 5 . 8 . 1 ] .. Finished in 0 . 27 seconds ( user 0 . 04 seconds, sys 0 . 04 seconds ) 2 examples, 0 failures まとめ 今回はシェルスクリプトのテストフレームワークであるBatsとShellSpecの触りだけご紹介しました。どちらを使うかはREADMEを読んでみて決めてみてくださいね。 以上です。
BC チームでエンジニアをしている id:d-kimuson です 11月にリリースされた TypeScript 4.9 から satisfies operator が追加されました。satisfies operator が追加されたことで 「React Router でのナビゲーションを型安全にする」がやりやすくなったのでやってみました この記事で紹介するコードは TS Playground で試すことができます React Router v6.4 からオブジェクト形式でルーティングをかけるようになり、ルーティング宣言から型を拾いやすくなった React Router v6.4 から createXXXRouter のAPIが追加され、コンポーネントではなく、プレーンオブジェクトでルーティングを書けるようになりました import { createBrowserRouter } from "react-router-dom" const router = createBrowserRouter ( [ { path: "/" , element: < HomePage / >, } , ] ) な形式でルーティングを宣言できます 以前からある <BrowserRouter> <Routes> <Route path="/" component={HomePage} /> </Routes> </BrowserRouter> なコンポーネント形式のルーティングでは難しかった、「宣言から型情報を読み取る」ことができるようになりました ルーティングの宣言に型の制約を課したいが、具体な型に解決させたい ルーティングの宣言から型情報を拾えるようになったので、良い感じに拾って型安全なナビゲーションを実現したいなと考えます しかし 宣言に型の制約を課しつつ 型自体は宣言から具体な型に解決させる はちょっと実現が面倒です ルーティングオブジェクトの宣言に RouteObject[] 型の制約を課すために形注釈をつけると import type { RouteObject } from "react-router-dom" const routes: RouteObject [] = [ { path: "/" , element: < HomePage / >, } , ] 制約は課すことができますが、routes は RouteObject[] 型に解決されてしまうので、具体的なルーティング( / ) を型情報から拾うことができません 宣言に合わせた型を拾いたいなら注釈をつけずに as const を使うのが有効です const routes = [ { path: "/" , element: < HomePage / >, } , ] as const ただし、今度は routes に RouteObject[] な制約をかけられていません 結果、補完が効かなくなったり宣言ではなく使用箇所での型エラーになってしまったりで望ましくありません satisfies operator この問題が satisfies operator で解決して、「制約を書けるが具体な型に解決させる」ができるようになりました satisfies operator は型の制約をかしますが、解決される型には影響を与えません したがって import type { ReadonlyDeep } from "type-fest" // as const すると readonly 化してしまうので type RoutesDef = ReadonlyArray < ReadonlyDeep < RouteObject >> const routes = [ { path: "/" , element: < HomePage / >, } , ] as const satisfies RoutesDef で宣言することで、 RouteObject[] な制約でルーティングを宣言しつつ、routes には宣言通りの型に解決させることができるようになりました 上の routes 変数は実際に const routes2: readonly [{ readonly path: "/" ; readonly element: JSX. Element ; }] 型に解決され、制約に違反すると型エラーが出ます ※ satisfies がないと実現できないというわけではなく、Vue 関連のエコシステムでよく使われている defineXXX のパターンでも一応同じことは達成できましたが、satiesfies operator で実現しやすくなりました 遷移に制約をつける routes を具体な型に解決させられるようになったので、型演算を通じて型安全なナビゲーションを実現できます サンプルとして、以下のルーティングの宣言を用意します const routes = [ { path: "/" , element: < HomePage / >, } , { path: "/nests" , element: ( < div > < h2 > Nests route < /h2 > < Nav / > < /div > ), children: [ { path: ":nestId" , element: ( < div > < h2 > nests 20 < /h2 > < Nav / > < /div > ), } , ] , } , ] as const satisfies RoutesDef typeof routes を扱いやすい型に整形する ルーティングのネストは children で書かれていて使いにくいので、まずは使いやすい型に変換していきます type RouteConfig < T extends RoutesDef , U = ToRouteUnion < T >> = AsObjectShape < U extends { path: string } ? U : { path: string } > /** * @desc children のネストを解決して Union にする * { * readonly path: "/"; * readonly element: JSX.Element; * } | { * readonly path: "/example"; * readonly element: JSX.Element; * } | { * readonly path: "/nests"; * readonly element: JSX.Element; * readonly children: readonly [...]; * } | { * path: "/nests/:nestId"; * } */ type ToRouteUnion < T extends RoutesDef > = T extends ReadonlyArray < infer I > ? MergeChild < I > : never /** * @desc 使いやすい Object 形式に整形 * { * "/example": { * path: "/example"; * }; * "/nests": { * path: "/nests"; * }; * "/": { * path: "/"; * }; * "/nests/:nestId": { * path: "/nests/:nestId"; * } & { * params: { * nestId: string; * }; * }; * } */ type AsObjectShape < T extends { path: string } > = { [ K in T [ "path" ]] : { path: K } & ( ParsePathParams < K > extends infer Params ? keyof Params extends never ? {} : { params: Params } : never ) } type MergeChild < T > = T extends { path: string children: ReadonlyArray < infer Children extends { path: string } > } ? | ( T extends { element: JSX. Element } ? T : never ) | MergeChild < { path: ` ${ T[ "path" ] } / ${ Children[ "path" ] } ` } & ( Children extends { children: any } ? { children: Children [ "children" ] } : {} ) > : T /** * @desc リテラルなルーティング文字列からパスパラメタを抽出する * @example ParsePathParams<'/nests/:nestId'> = { nestId: string } */ type ParsePathParams < T extends string > = [ T ] extends [ ` ${ string } : ${ infer I1 } ` ] ? I1 extends ` ${ infer Param } / ${ infer I2 } ` ? Required < { [ K in Param ] : string } & ParsePathParams < I2 >> : { [ K in I1 ] : string } : {} こういうパズルを組みます ここでは詳細な説明はしませんが children にネストしていたルーティングをマージして それぞれのパスからパスパラメタを抽出して 使いやすい型に整形 をしています typeof routes を渡してあげると export type RouteConf = RouteConfig <typeof routes > これは以下に解決されます type RouteConf = { "/" : { path: "/" ; } ; "/nests" : { path: "/nests" ; } ; "/nests/:nestId" : { path: "/nests/:nestId" ; } & { params: { nestId: string ; } ; } ; } 使いやすい型を抽出することができました 型安全に遷移先のリンクを生成する 使いやすい型が手に入ったので、これを使って型安全にパスを生成できる utility を作っていきます export const pagePath = < T extends keyof RouteConf >( path: T , ...args: RouteConf [ T ] extends { params: any } ? [ RouteConf [ T ][ 'params' ]] : [] ) : string => { const [ params ] = args as [ Record < string , string > | undefined ] return params === undefined ? path : Object .entries ( params ) .reduce ( ( s: string , [ key , value ] ) => s.replace ( `: ${ key } ` , value ), path ) } これで pagePath 関数を通すことで型安全に遷移先のルーティングを書くことができるようになりました Link タグの to には pagePath の関数でパスを設定します < ul > < li > < Link to= {pagePath('/')} > Home </ Link > </ li > < li > < Link to= {pagePath('/example')} > Example </ Link > </ li > < li > < Link to= {pagePath('/nests/:nestId', { nestId: '20' })} > Nests 20 </ Link > </ li > </ ul > useNavigate からの遷移でも同様に const navigate = useNavigate () const onClick = () => { navigate ( pagePath ( '/nests/:nestId' , { nestId: '20' } )) } とすることで、型安全なナビゲーションを実現することができました その他の型安全ルーティング ということで、React Router をそのまま使いながら型安全なナビゲーションを実現できましたが、ルーティングの宣言自体型を拾うことを前提に作られてないので対応するのがそこそこ大変でした また、クエリパラメタについても React Router のルーティング宣言にはクエリの型を書くようなインタフェースがないので拾うことができません したがって、本格的に型安全を目指したい場合は 型情報を拾うことを前提にしたインタフェースでルーティングを宣言し、React Router に渡せる routes を吐き出すようなアプローチ react-router-typesafe-routes 等 型情報を拾うことを前提にしたルーティングライブラリ Rocon 等 を使うのが良いと思います 一方、ルーティングのインタフェースを変えてしまうと React Router 等のメジャーなライブラリと比較してメンテナスが滞ったときや、バージョンアップ時のマイグレーションがつらくなる側面はあります 今回紹介した「React Router のインタフェースに乗っかりながら、可能な範囲で型安全性を保証するアプローチ」の場合、インタフェース変更時のマイグレーションも公式のマイグレーションに乗っかれば良いだけなので無難な選択肢にはなるのかなと思います できればアプリケーションコードにはこの辺りを入れたくないので、願わくば、公式や著名なところからライブラリとして出てくれると嬉しいんですが... まとめ satiesfies operator と React Router v6.4 のオブジェクト形式のルーティングで標準的な記法をそのまま使って型安全なナビゲーションを実現することができるようになりました また、実際に最低限の実装例を紹介しました それでは良い型安全ライフを!