どうも、マイクです。おはようございます。
九月十三日、日曜日の朝七時をまわりました。「zenncast」今日もゆるっと始めていきましょう。
この番組では、いまZennで話題になっているトレンドの記事を、ラジオ感覚で一緒に味わっていく番組です。エンジニアの朝活のおともに、コーヒー片手に聞いてもらえたらうれしいです。
今日はお便りコーナーはお休みして、そのぶんガッツリ記事を紹介していきます。
今日ご紹介する記事は、ぜんぶで五本です。アーキテクチャ、AI開発フロー、データベースのパフォーマンス、テスト戦略、そして折りたたみ端末 iPhone Duo 対応の実践Tipsまで、かなり幅広く攻めていきますよ。
それでは、一本目からいきましょう。
まず最初は、多層アーキテクチャでよく話題になる「ドメインモデル貧血症」について、かなり踏み込んだ角度から解説している記事です。
この記事がおもしろいのは、「貧血症になるのは、エンジニアが怠けてるからじゃないよ」と。そうじゃなくて、「純粋性」「性能」「ビジネスロジックの集約」という三つを同時に満たすのが、構造的にめちゃくちゃ難しいんだ、というジレンマとしてちゃんと説明しているところなんですね。
たとえば、ドメイン層からリポジトリを直接呼びたくなる場面ってありますよね。でもそうすると、コンテキストとかエラーとか、副作用まわりの情報がドメインに入り込んできてしまって、ドメインのコードが「純粋な値だけ扱う」っていう理想から離れていきます。
かといって、「じゃあ必要なデータは先に全部取得しておこう」とすると、今度は不要なクエリが増えて、性能が落ちる。
さらに、「呼び出し側で条件分岐して必要なときだけクエリしよう」とすると、本来ドメインに閉じ込めたいビジネスロジックが、アプリケーション層やユースケース層に散らばっていってしまう。
この三択から抜け出せなくて、結果として貧血症っぽい設計になりがちだ、という話をしてくれています。
そこで筆者が提案しているのが、「Decisionパターン」と呼んでいるアプローチです。
ポイントは、判定関数が単純な真偽値を返さないこと。
たとえば、「許可」「拒否」みたいな結果だけじゃなくて、「まだ判定できないから、こういう追加情報がほしい」という状態まで含めた型を返すようにします。
ユースケース層では、この「状態オブジェクト」を見て、「あ、まだ足りない情報があるなら、そのときだけリポジトリを叩いてデータを取りに行こう」というふうに動く。
そうすることで、ドメイン側はあくまで値だけを扱う純粋なコードのままロジックを集中させつつ、必要になったタイミングでだけクエリを発行できるようになる、というアイデアです。
もちろん良いことばかりではなくて、Decisionパターン特有の注意点も挙げられています。
状態が増えていくと、ユースケース側でその状態ごとに `switch` 的な分岐を書くことになりますよね。ここで分岐漏れがあると、「まだ判定できない」状態をぐるぐる回してしまって、最悪無限ループになりかねない。
なので、Decisionが返す型を「直和型」、いわゆるサムタイプとして定義しておいて、静的解析ツールで「この型に含まれる全ケースをちゃんと処理していますか?」という網羅性チェックをかけるのが前提だよ、という話もしています。
面白いのは、Decisionパターンをきちんと型で表現しておくと、逆にメリットも出てくるところです。
ロジックが「状態ごとの小さなパーツ」に分かれていく代わりに、その情報から静的解析で「決定木の図」を自動生成しやすくなる。
つまり、ビジネスルールそのものをフローチャートとして可視化できるようになるんですね。
非エンジニアのステークホルダーに仕様を説明するときなんかにも、「このDecisionツリーがそのまま業務フローです」と見せられるようになるかもしれない、というのはかなりワクワクするポイントだと思いました。
。。,,。。
続いて二本目は、「AIにほぼ全任せで二十五万行のコードを運用している」という、すごい実践例の記事です。
題材になっているのは、ひとりで開発している社内向けのHTML共有サービス「Artifact Share」。
筆者は、このサービスの開発で「自分はコードを読まない」と決めてしまって、ほぼ全部AIに任せているんですね。
ただ、いきなり全部任せると怖いので、「事故が起きてからAIに対策を考えさせて、そのガードを足していく」という方針で、だんだん仕組みを育てていきました。
試行錯誤の結果たどり着いたのが、「仕様」と「実装」の二箇所でAIレビューを回す、「二重レビューループ」という最小限の仕組みです。
人間がやることは、かなり絞り込まれています。
まず最初に、AIと対話しながらIssueと受け入れ条件、つまり「こうなっていたらOKだよ」という条件を決める。
そして最後の最後に、プルリクエストのタイトルと、CIの結果だけを見てマージする。
この間にある「仕様書を書く」「仕様をレビューする」「コードを書く」「コードをレビューする」といった一連のプロセスは、すべてAI同士のループで自動的に回してしまう、というスタイルです。
AIレビューにも、ちゃんと「終わりを作る」ためのルールが決められています。
たとえば、「実害のあるバグだけをブロッカーとして直す」「防ぐのは『うっかりミス』だけに絞る」「二周目以降のレビューは差分だけを見る」といった感じですね。
また、レビューの中で出てくる「ここ改善したいな」という案についても、CLI経由で必ず「Issueとして残す」か「ここで破棄する」かを選ばせるようにしていて、中途半端な『TODOの山』を溜め込まない工夫がされています。
最初は、ルールや検査項目をどんどん盛っていった結果、「レビューの仕組みの方が、本体のサービスより重い」という状態に陥ったそうなんですが、そこから一度立ち止まって、三十七行くらいのシンプルな運用ルールへ削り込んだ、という振り返りもされています。
このへん、「自動化の仕組み自体が複雑になりすぎる問題」、身に覚えのある方も多いんじゃないでしょうか。
最後に筆者は、チーム開発でいきなり「人間のコードレビューをやめよう」とするのではなくて、この二重レビューループを「人間レビューの前段」に挟むのがいい、と提案しています。
AI同士に仕様と実装を先にチェックさせておけば、人間は「本当に人間の目じゃないと気づけないところ」に集中できる。
つまり、一番のボトルネックである「人間の注意力」を節約できるんじゃないか、という話ですね。
AIを「レビューの前処理」として使う、という実務的な落としどころが見えてくる記事でした。
。。,,。。
三本目は、データベースのJOINで起きる「デカルト積」の罠と、メモリへの影響について、かなり丁寧に計測した記事です。
テーマは、「親テーブルにぶら下がる二つの子テーブルを同時にJOINすると、何が起こるか?」という話。
イメージとしては、`users` に対して `posts` と `files` が兄弟関係でぶら下がっているようなケースですね。
この「兄弟関係JOIN」が曲者で、同じユーザーに紐づく投稿とファイルが、すべての組み合わせで掛け合わされてしまって、結果としてデカルト積が発生します。
そのユーザーに投稿が十件、ファイルが五件あれば、単純計算で五十行になってしまう。
`users → posts → comments` みたいな一直線の親子孫構造よりも、`posts ← users → files` のような兄弟関係のJOINの方が危ないよ、という説明がされています。
記事では、Doctrine ORMとPHPを使って、親子孫パターンと兄弟パターンを実際に比較計測しています。
生のクエリ結果の行数や、MySQLのバッファサイズだけを見ると、やはり兄弟パターンの方が大きく増えるんですが、ここで面白いのが、「最終的にオブジェクト化されたエンティティのメモリ使用量」までちゃんと測っているところです。
その結果、アプリケーション側のメモリは「行数そのもの」よりも、「エンティティの数」に強く依存している、ということがわかった。
つまり、行はたくさん返ってきても、ORMがうまく同一エンティティをまとめてくれていれば、そこまで爆発的には増えない場合もある、ということですね。
とはいえ、安心しきっていいかというと、そうでもない。
記事では、「一行あたりのデータが重い場合」の危険性も強調されています。
たとえば、一行に十キロバイトクラスのテキスト列が含まれているようなテーブル同士をデカルト積でJOINしてしまうと、行数の増加に比例して、データベースから受け取る生データの量が一気に膨れ上がります。
数ギガバイト単位のメモリを一気に食いにいって、アプリケーションが落ちる、なんてことも現実的にありえるよ、という指摘です。
まとめとして、筆者が「ここだけは意識しよう」と挙げているのは二点です。
ひとつめが、「重いカラムを持つテーブル同士で、デカルト積JOINを起こさないこと」。
もうひとつが、「ORMが保持するエンティティ数を意識して、オブジェクト化後のメモリ消費もちゃんと監視すること」。
結局のところ、「なんとなく怖いから避ける」ではなくて、計測して挙動を確かめながら設計していこう、と。
ORMを使っていると、ついSQLの実態を見落としがちですが、「どこでデカルト積になりうるか」を改めてチェックしたくなる記事でした。
。。,,。。
四本目は、テストとCIまわりのワークフローに関するアイデア、「Red-Green Stacked PR」という手法を紹介している記事です。
テーマは、不具合修正のプルリクエストで、「追加したテストが本当に元のバグを再現していたのか?」が、あとからだとCIの結果だけではわからない、という問題です。
よくあるのは、修正コードと、そのバグを再現するテストコードを同じPRで出すパターンですよね。
この場合、レビュー時には「修正を外してテストを走らせる」といった手作業をレビュアーがローカルでやって、「ああ、たしかにテストは失敗するし、修正を入れると成功するね」と確認することになります。
でも、この確認作業ってCIの記録には残らないし、毎回手作業でやるのはかなりコストが高い。
そこで筆者が提案しているのが、「テストが落ちること自体を、まずPRとしてCIに証明させてしまおう」というやり方です。
具体的には、Vitestの `test.fails` みたいな、「失敗したら成功とみなすテスト」を使います。
まず最初のPR、これを「Red」と呼んでいますが、ここでは実装コードは一切変えずに、「バグを再現するテストだけ」を追加します。
このテストには `test.fails` を付けておいて、「現在のコードだと、このテストは失敗するはずだよね」という状態を、CIでちゃんと確認させるんですね。
このRed PRがマージされると、「いまのmainブランチには、失敗するのが正しいテストが存在している」という状態になります。
次に、「Green」のPRを重ねます。
ここでは、先ほど追加したテストから `.fails` を外して、「成功するのが正しいテスト」に変え、そのうえでバグ修正の実装を入れます。
このPRのCIが通れば、「同じテストが、修正前は落ちていて、修正後は通るようになった」という因果関係が、RedとGreenそれぞれのPRのCI結果として、きれいに記録に残る、という仕組みです。
「コミットを分ければいいんじゃないの?」と思うかもしれませんが、多くのCIは中間コミットを全部チェックしてくれるわけではなくて、最終状態だけを見てしまいますし、squashマージをすると履歴も一つにまとまってしまいます。
それに対して、PR単位でRedとGreenを分けると、「RedのときのCIログ」と「GreenのときのCIログ」が、mainの履歴とも紐づいた形で長く残るのが大きな違いです。
さらに、GitHub CLIの `gh stack` を使うと、こうしたPRの積み上げや、順序付きのマージを一括で扱えるので、「Redの上にGreenを積んでいく」という運用もそこまでつらくないよ、という話もされています。
筆者は、特に重要で複雑なバグ修正のときに、このスタック型のPRを使うことで、「どの変更がどんなテスト結果につながったのか」という挙動の因果関係を、CIの記録として明確に残せる、と強調しています。
テスト駆動開発の「Red-Green」に、GitHubのPRスタックを組み合わせた、実務寄りのテクニックですね。
。。,,。。
そして最後、五本目は、折りたたみ式の新端末「iPhone Duo」にアプリを対応させるための実装・設計Tipsを、Apple Tech Talks六本とHIGをもとに、一次情報レベルでまとめた、かなり濃い技術記事です。
「具体的にどのAPIをどう使えばいいのか?」というところまで整理されていて、これからDuo対応する人にとっては実質チェックリストみたいな内容になっています。
まずレイアウト面の話から。
ここが大事なんですが、「画面サイズ」や「向き」ではなくて、「サイズクラス」と「セーフエリア」を基準にレイアウトを組もう、というのが基本方針として強調されています。
iPhone Duoだと、ヒンジ部分やカメラ周りに「reserved regions」と呼ばれる、コンテンツを避けたい領域があったり、左右でセーフエリアが非対称になったりします。
こういった「非対称なセーフエリア」や「ヒンジ・カメラ領域」を前提にして、柔軟に伸び縮みできるレイアウトを組もうね、という話ですね。
具体的なAPIとしては、`ArrangementView` と `UIArrangementViewController` を使って、画面をきれいに分割する「split」レイアウトや、一方を重ねる「overlay」レイアウトを作る方法が紹介されています。
さらに、`reservedRegions(kind:)` というAPIで、「ここは折り目だよ」「ここはカメラだよ」という領域を取得して、その部分を避けるようにビューを配置する、といったやり方も触れられています。
次にバーUI。
横長の画面を最大限に活かすために、ツールバーやタブバー、ステータスバーといったバー系UIを「縦方向に再配置する」仕組みが新たに入っています。
SwiftUIでもUIKitでも、`axisBehavior` という挙動の設定や、要素ごとの「可視性の優先度」、それからボタンが多いときに自動的にあふれた分をまとめてくれる `ToolbarOverflowMenu` のようなオーバーフロー統合を使って、
「アイコン中心で縦に並ぶバー」と「テキスト中心で横に並ぶバー」を、状況に応じて自動で切り替える、といった設計が推奨されています。
「縦バーはアイコン寄り、横バーはテキスト寄り」といった役割分担まで書かれているので、UIデザインの参考にもなりそうです。
マルチディスプレイ周りでは、ヒンジの状態を教えてくれる `onHingeChange` や `UIHingeInteraction`、外側ディスプレイや外部ディスプレイに補助的なUIを出すための「scene accessories」、たとえば `sceneAccessory` や `CameraCaptureAccessory` といった仕組みが紹介されています。
これらを組み合わせて、「開いてるのか、閉じてるのか、半分折れてるのか」「いまどのディスプレイがアクティブなのか」「ウインドウは何枚出ているのか」といった状況変化を、基本的にはサイズクラスとシーン情報だけで扱おう、というメッセージが繰り返し出てきます。
個別端末の画面サイズにベタ書きで対応するのではなく、「コンテナとシーンをベースに作る」方向に寄せていく感じですね。
カメラ周りは、Duoならではの新しいAPIがいくつか出てきます。
前面カメラが二つあって、それらをまとめて扱える「Virtual Front Camera」。
ビューの観点から「ユーザー側」「反対側」を教えてくれる「direction coordinator」。
回転時の補正をフレームワーク側に任せられる「rotation coordinator」。
こういった新しいコンポーネントが用意されています。
深度情報や高フレームレートなど、端末ごとのリッチな機能をフルに使いたい場合は「個別カメラ」を選び、
逆に、いろんな状況でもそこそこ安定して動いてほしい、という汎用性重視のときは「Virtual Front Camera」を使う、といった設計方針も整理されていました。
記事の後半には、「既存アプリを iOS二十七・一のSDKとiPhone Duoに移行するためのチェックリスト」も載っています。
ここでは、たとえば「画面サイズ依存のロジックをやめる」「標準のナビゲーションコンテナやバーコンテナへ移行する」「Split Viewのマルチタスキングに対応する」「カメラ機能は新しいAPIに寄せていく」といった、段階的に進められる改修タスクが整理されています。
いきなり全部載せ替えではなく、「この順番で変えていけば、Duo対応に近づいていけるよ」というロードマップになっていて、現場で手を動かす人にはすごくありがたい構成だなと思いました。
。。,,。。
さて、駆け足で五本ご紹介してきました。最後にさらっとおさらいしておきましょう。
一つ目は、多層アーキテクチャでドメインモデルが貧血症になりがちな理由を、「純粋性」「性能」「ロジックの集約」という三つ巴のジレンマとして整理し、「Decisionパターン」でうまくバランスを取ろう、という記事。
二つ目は、社内サービス「Artifact Share」を題材に、「コードを読まない」ことを前提にしたAI主導の開発フロー、仕様と実装の二重レビューループで、人間の注意力のボトルネックを減らしている実践例でした。
三つ目は、兄弟テーブル同士のJOINで起こるデカルト積の罠と、特に「重いカラム」を含むときのメモリ爆発の危険性を、Doctrine ORMの計測を通して解説した記事。
四つ目は、Vitestの `test.fails` を使って、「テストが落ちる状態」と「直して通る状態」をそれぞれ別PRとして残す「Red-Green Stacked PR」というワークフローの提案。
そして五つ目が、iPhone Duo対応のためのレイアウト、バーUI、マルチディスプレイ、カメラAPI、それから移行チェックリストまで網羅した、実践的な技術まとめでした。
気になった記事があれば、このあとショーノートに元の記事へのリンクやキーワードをまとめておきますので、通勤時間のおともにでも、ぜひ原文もじっくり読んでみてください。
この番組「zenncast」では、みなさんからの感想やご意見もお待ちしています。
「このテーマもっと深掘りしてほしい」とか、「こんな現場の悩みも取り上げてほしい」とか、なんでも大歓迎です。
ぜひラジオ感覚で、ゆるく参加してもらえたらうれしいです。
それでは、そろそろお別れの時間です。
今日も良い一日をお過ごしください。お相手はマイクでした。
また次回の「zenncast」でお会いしましょう。