13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Angular Advent Calendar 2025 2日目の記事です。プログラミングの考え方を紹介した記事です。

Angular Advent Calendar 2025

私がプログラマーという職業に就いた当初、設計なんて概念はまったく存在せずif文やfor文をひたすら積み上げていくスタイルが主流でした。私のいた会社では。できあがったプログラムは先頭から末尾まで連続した処理で、一連の命令すべてが詰まった単一のシナリオをなしていました。

そして20年以上が過ぎ時代は変わりました。DDD、レイヤードアーキテクチャ、SOLIDの原則、参照透過性...。いろいろな言葉が頭を駆け抜けていき、コードごっちゃりしたなと思っては言葉を漁り、もうちょっとすっきり書けないものかと頭を悩ませながらプログラミングしています。

命令を積み上げた単一のシナリオはモジュールや責務で分離されたいくつものシナリオに置き換わりました。システムを長く育てていくには、どんなシナリオを組み立ててどのように挙動や仕様を説明するのか、その作業がとても重要ではないかと思っています。

命令型のシナリオ

命令を積み上げるとはどういうことなのか、商品購入の擬似コードで考えてみます。

function purchase(items: Item[]): void {
    const discounts = items.map(item => {
        if (item.isSpecialDiscount) {
            return item.price * 0.5;
        }
        return item.price;
    });

    const subTotal = discounts.reduce((sum, price) => sum + price);
    const tax = subTotal * 0.1;
    const total = subTotal + tax;
    
    apiService.post('/purchase', { items, subTotal, tax, total });
}

上記のコードでは、割引、小計、税額、と計算処理が続きます。結果に向かって「XXに変換して、YYに加工して、ZZを計算する」といった具合に命令が連続していますね。謎の isSpecialDiscount はいったん置いといてください。

この調子で数十行続くとします。長いよね、分割しよう。

// 修正Ver1
function purchase(items: Item[]): void {
    const discounts = calculateDiscount(items); // スッキリ!
    const subTotal = discounts.reduce((sum, price) => sum + price);
    const tax = subTotal * 0.1;
    const total = subTotal + tax;
    
    apiService.post('/purchase', { items, subTotal, tax, total });
}

function calculateDiscount(items: Item[]): number[] { ... }

できたできた。俗に言う「メソッドの抽出」だよね、割引計算を calculateDiscount に分離して、うんスッキリした。

しかし修正後のコードは依然として命令型です。
割引計算が切り出されて別の場所に移動しただけで、次々と変数に放り込むスタイルに変わりはありません。むしろ calculateDiscount はなぜ配列を返すのかよく分からない関数になってしまいました。

メンタルモデル

物事を把握する時、頭の中にあるメンタルモデルが使われます。メンタルモデルはプログラミングにおいても重要な要素です。

道を歩いていて目の前の信号機が赤く点灯していたら立ち止まりますよね。赤は停止のサインだし反対側から車両が来るかもしれないと思うからです。私たちの脳に記憶されているメンタルモデルです。

信号機のライトが三角形で水色だったら何を思うでしょう。三角だから警告?いやまてよ緑のライトが劣化して水色なのかも?これもまたメンタルモデルです。三角形が警告で使われることやライトの劣化で変色することを記憶していて、目の前の出来事が何なのか推測しているのです。

プログラミングでもレビュアーなど読み手のメンタルモデルを意識すると、ものごとの把握が容易になります。

抽象化

プログラミングの抽象化とメンタルモデルの関係を考えてみましょう。

抽象化の作業では、ある概念を取り出して形にします。言葉の意味は理解できても自分のコードに向き合うと何を抽象化して良いのやら難しいですね。

私が最近思いついたヒントは人に説明することです。

// 元のコード再掲
function purchase(items: Item[]): void {
    const discounts = items.map(item => {
        if (item.isSpecialDiscount) {
            return item.price * 0.5;
        }
        return item.price;
    });

    const subTotal = discounts.reduce((sum, price) => sum + price);
    const tax = subTotal * 0.1;
    const total = subTotal + tax;
    
    apiService.post('/purchase', { items, subTotal, tax, total });
}

新しくジョインした架空のメンバーに説明するつもりでつぶやいてみます。

  • 自分)カートに入れた商品をこのページで購入します。
  • 自分)訳あり商品は50%オフです。
  • 自分)ここで計算している金額は購入前確認ページで表示します。

isSpecialDiscount って訳あり商品だったんかーい!と自分で気づきます。ここがポイントで、仕様とコードがつながっていないのです。この部分をきちんと表現できると説明力の高いコードになりそうです。

試しにラベリングで分岐させてみます。

function purchase(items: Item[]): void {
    const discounts = items.map(item => {
        // if (item.isSpecialDiscount) {
        if (item.discountType === "wakeari") { // NEW!
            return item.price * 0.5;
        }
        return item.price;
    });
    ...

ローマ字を使えという事ではないです、念のため。良いネーミングは後で考えるとして、謎の変数ではなくラベリングで伝える工夫ができそうです。

さて次は、どんどん生み出される変数をどうにかしましょう。

const discounts = ...;
const subTotal = ...;
const tax = ...;
const total = ...;

必要だから生み出しているのであって、これらが示す意図があるはずです。意図に名前を持たせて抽出しましょう。

そういえば少し前の自分から「購入前確認ページで表示」という説明がありましたね。

purchase だとユーザーの行動のように思えるし billing receipt だと支払いが確定した印象があります。detail は商品明細なのか請求明細なのか曖昧さがあります。 estimate order あたりがしっくりくるでしょうか。

よし決めた order(注文) として抽象化しましょう。

// 修正Ver2
function purchase(items: Item[]): void {
    const order = createOrder(items); // NEW!
    apiService.post('/purchase', order);
}

function createOrder(items: Item[]): Order {
    const discounts = items.map(item => {
        if (item.discountType === "wakeari") {
            return item.price * 0.5;
        }
        return item.price;
    });
    ...
        
    return { items, subTotal,tax, total };
}

最初の修正と比べてみましょう。

// 修正Ver1
function purchase(items: Item[]): void {
    const discounts = calculateDiscount(items);
    const subTotal = discounts.reduce((sum, price) => sum + price);
    const tax = subTotal * 0.1;
    const total = subTotal + tax;
    
    apiService.post('/purchase', { items, subTotal, tax, total });
}

function calculateDiscount(items: Item[]): number[] { ... }

1回目と2回目の修正で異なるのは、抽象化による「概要」と「詳細」の区別です。

1回目では discounts(割引) subTotal(小計) といった「詳細」が列挙されます。「概要」は登場しないため読み手が想像することになります。

2回目は order(注文) という「概要」の提示からスタートします。注文に関することが格納されるのだろうなと予想した後に、割引、小計といった「詳細」が続きます。wakeari(訳あり) はWebサイトを見ていれば該当する商品が思い浮かび、それに続いてコードを読み進めます。

orderwakeari は抽象化であると同時にメンタルモデルを示しています。概念を提示することで、読み手は何を表すものか見当が付けられるのです。

広すぎる抽象化

isSpecialDiscount は「訳あり商品」を抽象表現した変数といえます。ただ、この変数名からは意味するものが具体的に思い浮かびません。

customer user data などの名詞、convert apply parse などの動詞はそれっぽいが故に読み手が想像する範囲が広がります。

parse はXMLをJSONに変換するようなシーンでは具体的な処理内容を想起できるでしょう。ですが parse(userData) とすると中で何が行われるのかよく分からなくなってしまいます。ありがちな単語を雑に使うと目的が読み手に伝わりません。

安易にそれっぽい単語を選んでいませんか?言葉とシステム上の概念がマッチしますか? customer を採用する前に visitorowner などもっと具体性のある単語を探しましたか?

それっぽい言葉は想像の範囲が広く、コードを読まないと実体がつかめないのです。

狭すぎる抽象化

「XXに変換して、YYに加工して、ZZを計算する」のブロック切り出しは、それぞれの処理の抽象化といえるかもしれません。ただ、常に同じ1箇所から呼び出されるならそれは単なる移動です。

コードの読み手はメンテナーとしての書き手でもあります。バグ修正のためのコード探索やデータフローの把握など目的を持ってコードリーディングしています。何をするのかはっきりしない関数に出会うと、自分の目的と関連があるのか分からず中に入ってコードを読みます。そして処理内容と結果を理解してから、また次の関数の中へ入っていきます。結果として「XXに変換して、YYに加工して、ZZを計算する」をひと通り理解することは変わりません。

プロジェクトのあちこちにロジックが分散して行ったり来たり、結局ぜんぶ読まないと理解できないってこと、ありませんか?

単に移動したたけのブロック切り出しは扱う対象が限定的で、コードを追いかけないと挙動が分からないのです。

メンタルモデルとのマッチング

メンタルモデルとのマッチングが高くなると、コードを効率的に読むことができます。

items.map(() => {...}) というコードを見て Array#map の内部実装を見に行こうとは思わないですよね。自分で書いたことがあり挙動が分かるからです。isLoggedIn というフラグに出会った時、ログイン画面の実装を見に行ってどのタイミングで切り替わっているか確認しないですよね。ログイン後の状態を示していると信頼するからです。

信号機が赤なら立ち止まるように Array#mapisLoggedIn も概念として理解できると、詳細を追いかける手間が省けます。

反対に曖昧な名付けや処理結果を次々と変数に放り込むコードは、メンタルモデルとのマッチングが低い状況です。書き手の頭の中の「ああして、こうして」を投影したコードは、違う考えを持った読み手にストレートに伝わりません。これは何だろう結果はどうなるんだろうとひとつずつ読み解いていくのにエネルギーを費やします。

有名な理論に「マジカルナンバー」というものがあります。短期記憶に留めておける情報量は個人差はあれど限りがあるというものです。私はこの理論を「変化する状態を思考するための脳内スロット数」だと思っています。とある変数は convertFoo() が変換した値を保持していて、それを buildBar() に渡す、さらに calcHoge() で計算した値が最終的に使われる、これでスロットひとつが消費されます。その結果と連動して変化する値が存在するとまたひとつスロットが消費されます。これが続くとアップアップになって、さっきの処理の結果は何だっけなとコードを読み直します。ひとことでまとめるなら「つらい」のです。

読み手のメンタルモデルにマッチする概念を使うこと、私はそこにプログラミングというシナリオを説明的にする鍵があるのではないかと考えています。

httpResource by Angular

抽象化されたメンタルモデルの例としてAngularの httpResource を紹介します。(2025/12時点でExperimentalです)

// コンポーネント
userId = input.required<string>();
user = httpResource(() => `/api/user/${userId()}`);
<!-- テンプレート -->
@if(user.hasValue()) {
  <user-details [user]="user.value()">
} @else if (user.error()) {
  <div>Could not load user information</div>
} @else if (user.isLoading()) {
  <div>Loading user info...</div>
}

httpResource はHTTP通信を担う機能です。

HTTPの通信結果は正常系(200 / 300)と異常系(400 / 500)に分類できます。フロントエンドではこの2値に加えてレスポンス待ち状態でレンダリングを切り替えることが頻繁にあり、HTTPリクエストの多くの箇所で状態判定をしています。

HTTP通信結果と画面の状態という質の異なる値を連動させるには少し工夫が必要です。フラグで状態判定しながらエラー値を格納するようなボイラープレート化したコードをよく書いていました。モデルとして表現できないかなとチャレンジしたこともありますが、非同期処理がシンプルに扱えず断念しました。なので httpResource が登場した時、欲しかったもの来たぁぁ!!!とひとり盛り上がっていました。

httpResource には value error loading の材料が揃っていて、HTTP通信コンポーネントを扱う開発者のメンタルモデルをリソース表現したものと言えます。ロジックやフラグを読み解かなくても、UIがどのように切り替わるのか知りたければすぐにテンプレートにたどり着きます。誰なんでしょう、こんな画期的なリソースを思いついたのは!

開発者の共通言語

フレームワークが関与しないドメインのロジックでは、開発者の共通言語が活躍します。

慣習的な要素

例えば UseCase DataSource だとレイヤーを表すことができます。Constant Handler なら扱うものの性質を表します。なんでも Utils にぶちこまずに Validator Helper など役割を与えるのも良いですね。

Repository という名前からは外部通信するステートレスなサービスを想像しますよね。こういった慣習的な要素は、単語そのものが説明力を備えています。

名付け

それっぽい名前を安易に使わず、できるだけこれだと思う表現にたどり着くまで探しましょう。

previousSavedData より cache のほうが再利用の用途が明確で、 removeFromManager より revokegrant のほうが対象がはっきりします。first last も良いですが head tail ならファイルや文字列処理を連想しやすくなるでしょう。

ドメイン表現

抽象化してドメインを示しましょう。抽象化とは対象をぼかしたり共通化することではなく、ものごとの輪郭をはっきりさせる効果があります。

データのまとまりだけではなく、データをどう扱うか、状態変化に対してどうふるまうか、そういったことも表現対象です。モデルの導出、処理の集約、関数の汎化、型表現、いろいろな手段があります。

対象がぼんやりとしか見えないのに加えて手法の選択肢が多いのが抽象化の難しいところですが、開発者としての腕の見せどころでもあります。

共通言語の効果

「商品フィードをフェッチしてサムネイル表示、SSRで高速化してポーリングでリアルタイムにタイムセール商品を差し替えます。あ、商品10万点あるんでページングか無限スクロールはマストです。」

ステークホルダにこんな説明したら何いってんだと思われますが、開発者間では伝わりますよね。会話でもプログラミングでも、共通言語は理解を助ける働きをします。

読み手を意識して書いたコードは、JSONパースや画像取得など処理の羅列ではなく「商品フィード」や「タイムセール」の概念が提示されているでしょう。そして「1分後に再実行」みたいな命令ではなく開発者のメンタルモデルにマッチした表現によって「これポーリングだな」と読み手がスッと気付くはずです。

プログラミングでシナリオを作るということ

私の仕事は、プログラミングという手段で目的達成のシナリオを作ることです。期待通りのアウトプットは大事な成果物ですが、そこに至る工程そのものも開発者にとっての成果物です。

読み手に伝える説明力を意識することで良いシナリオライターになれると期待しています。

明日は ksakae1216 さんです!

13
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?