第3章 パラダイムの概要
著者見解
第3章では、プログラミングの代表的な3つのパラダイム――構造化プログラミング、オブジェクト指向プログラミング、関数型プログラミング――を取り上げ、それぞれが「何を使うべきか」ではなく「何を避けるべきか」を教える仕組みであることを紹介します。
まず構造化プログラミングは、1968年にエドガー・W・ダイクストラが「無制限のジャンプ(goto文)はプログラムを複雑にし、バグを生みやすい」という問題を指摘したことに始まります。以来、gotoを乱用する代わりに if/then/else や while/do といった制御構造を組み合わせることで、プログラムの流れを明確にし、可読性や保守性を高める手法として広まりました。ここでは「直接的なジャンプを乱用してはならない」というルールが、コードの品質を保つ大きな鍵となっています。
次にオブジェクト指向プログラミングは、1966年にオーレ・ダールとクリステン・ニガードが ALGOL 言語において関数のローカル変数をヒープ上に残し続けられる仕組みを発見したことから発展しました。この発見により、関数をクラスのコンストラクタ、ローカル変数をインスタンス変数、ネストした関数をメソッドとして捉え直すことで、オブジェクトという単位でデータと手続きをまとめられるようになりました。さらに関数ポインタを応用して多態性(ポリモーフィズム)を実現し、大規模開発での拡張性や再利用性を高める土台を築きました。ここでは「直接呼び出しの連鎖ではなく、オブジェクトを介した間接的なやり取りを厳密に守る」ことが要となります。
最後に関数型プログラミングは、ラムダ計算を発明したアロンゾ・チャーチ(1936年)の理論を基盤とし、変数への再代入を禁止する「不変性(イミュータビリティ)」を徹底します。副作用を排除した純粋な関数の組み合わせによってプログラムを構築するため、状態変化に伴うバグを避けやすく、並列処理やテストのしやすさといったメリットを得られます。ここでは「変数を書き換えてはいけない」という制約こそが、コードの予測可能性を飛躍的に高めるポイントです。
これら三つのパラダイムはいずれも、プログラマの自由度を制限することで逆説的に生産性や安全性を高めるという共通点があります。何をすべきかを示すのではなく、何をしてはいけないかを明確にすることで、より健全で保守しやすいプログラムを書くための「規律」を提供しているのです。
筆者所感
プログラミングの流行を振り返ると、パラダイムは関数型とオブジェクト指向の間を往復してきたように見えます。例えば関数型は1958年のLisp(ジョン・マッカーシー)に始まり、1970年代のMLやScheme、1990年代に整理されたHaskellといった研究言語群で発展しました。一方でオブジェクト指向は1970年代のSmalltalk(アラン・ケイら)を源流に、1985年のC++、1995年のJavaの登場で企業システムを中心に広く普及し、1990〜2000年代に主流になりました。近年はマルチコア化や並行処理の要求、関数型の「不変性」「副作用排除」が有利に働く場面が増えたため、Scala(2004)やErlangの利用拡大、言語側でのラムダ導入(C# 3.0/2007、Java 8/2014、JavaScriptのES6以降)などを通じて、再び関数型的な考え方が注目されています。
重要なのは「流行だからこれが正義だ」と決めつけることではなく、それぞれのパラダイムが得意とする場面と苦手な場面を理解して使い分けることです。オブジェクト指向ではデータ(状態)とその操作(メソッド)を同じオブジェクトにまとめるため、状態を内部に持たせつつ振る舞いを追加・変更しやすく、継承やポリモーフィズムで既存の振る舞いを拡張できるのが利点です。一方で、グローバルな状態や密な依存関係が複雑さを招きやすいという欠点もあります。関数型は並列性やテスト容易性に強いが、純粋な関数型では副作用を避けるため、状態やI/Oを明示的に扱う(例えば状態を明示的に渡すなど)必要があります。そのため、単純な入出力や外部状態を扱うケースではラップやボイラープレートが増え、設計上の負担が高く感じられることがあります。状況に応じてパラダイムの利点を取り入れ、実務的に折り合いをつけることが肝心です。
第4章 構造化プログラミング
著者見解
構造化プログラミングは、「誤りを証明できる(反証可能)」小さな単位にプログラムを分割し、テストや検証を通じてバグを発見しやすくする手法です。無制限な goto 文を禁止し、順次・選択・反復といった明示的な制御構造だけを用いることで、プログラムの制御フローをシンプルに保ち、誤りの検証を容易にします。さらに各関数やモジュールは「テストで誤りが検出されない限り十分正しい」とみなせるサイズに抑え、部分の正しさを組み合わせて全体の正しさを担保します。このアプローチにより、可読性や保守性が向上し、バグの早期発見・修正がしやすくなるという大きなメリットが得られます。
筆者所感
設計の基本は「小さな部品に分ける」ことです。私の経験では、伝統的なMVCをそのまま採用するとControllerが肥大化しがちで、その結果ユニットテストで品質を担保するのが非常に難しくなりました。ちょっとした変更でも手動での動作確認に頼らざるを得なかったため、依存関係が適切に管理されていない箇所では思わぬデグレが起きやすく、動作確認の範囲もどんどん広がって手戻りが増えていきます。受託開発では納品で終わることが多く当時は影響が限定的に見えましたが、振り返ると「保守したくない」コードが積み上がっていたと感じます。現在は自社SaaSでDDDやクリーンアーキテクチャを導入し、依存関係を整理して単位を小さくすることでユニットテストが書けるようになり、品質を安定して確保できています。両方を経験したからこそ、分割して単位を小さくする設計が保守性に直結することを強く実感しています。なお、goto文については実務で触れたことがないためここでは触れません。
第5章 オブジェクト指向プログラミング
著者見解
オブジェクト指向プログラミング(OOP)とは、システムを役割ごとに分割し、後から部品を入れ替えたり追加したりしやすくする設計手法です。特にポリモーフィズム(多態性)を用いると、上位レベルのビジネスロジックと下位レベルの詳細実装(たとえばデータベースアクセスやユーザーインターフェース)との依存関係を徹底的に制御できるため、まるでプラグインのように新機能を差し替えたり、実装を切り替えたりするのが非常に容易になります。
オブジェクト指向言語がしばしば「カプセル化」「継承」「ポリモーフィズム」の三大要素を掲げるのは、いずれもモジュール分割と再利用性を高めるためです。カプセル化はデータと処理をひとまとめにして外部からの直接操作を制限する仕組みですが、C言語でも関数と構造体で似たことは可能であり、OOP固有とは言い切れません。継承は既存のクラスを拡張して新たな機能を付加する手段で、手動で構造体と関数ポインタを組み合わせる時代から便利さが飛躍的に向上しました。そしてポリモーフィズムは同一インターフェースで内部処理を差し替えられる仕組みで、もともとは関数ポインタの応用に過ぎませんが、OOP言語では型安全かつ宣言的に扱えるようになっています。
この中でももっとも強力なのがポリモーフィズムを通じた「依存性の逆転(Dependency Inversion)」の実現です。ビジネスルールがインターフェースに依存し、具体的なデータベース接続や画面描画などの詳細は外側に委ねることで、UI や DB の変更がビジネスロジックに一切影響を与えなくなります。同時にビジネスロジックだけを独立してテスト・デプロイできるため、開発の効率と品質管理のしやすさが飛躍的に向上します。これこそがオブジェクト指向プログラミングの真骨頂であり、プラグインアーキテクチャを実現する鍵なのです。
筆者所感
インターフェイスを置くだけで疎結合な境界を簡単に作れるので、まずはそれで十分なことが多いです。実際、Controller→UseCase→Domain という境界はインターフェイスで分離すれば、テストの差し替えや実装入れ替えが容易になり、デプロイや運用の複雑さを増やさずに設計上の独立性を得られます。対して、サービス分割や別デプロイにするとネットワークや運用監視、デプロイ頻度の調整、トランザクション管理などコストが一気に増えます。変更の理由(責務)が同じものは一緒にしておく方が扱いやすく、理由が異なる場合に初めてサービス分割やデプロイ分割を検討すべきだと考えています。
第6章 関数型プログラミング
著者見解
関数型言語における不変性(イミュータビリティ)とは、一度初期化された変数の値が以降まったく変化しないという性質を指します。たとえば Haskell や Clojure のような関数型言語では、不変な変数を前提としてプログラムが組み立てられるため、値の上書きや副作用を避けながら安全に計算を進めることができます。一方、Java や C# などの命令型言語ではループ変数やフィールドが繰り返し上書きされるのが一般的で、変数の可変性に起因する問題が数多く発生します。
アーキテクチャの視点から見ると、可変変数は競合状態やデッドロックといった並行処理の難しさを生み出す主要因になります。複数のスレッドやプロセスが同じ変数を同時に読み書きしようとして整合性を失ったり、あるいは状態変化のタイミングを巡って相互にロックを待ちあうデッドロックが発生したりするためです。こうしたリスクを減らすためには、なるべくプログラム内の処理を不変コンポーネントとして設計し、可変な部分を最小化するアプローチが有効です。
実際には、システム全体を「不変コンポーネント」と「可変コンポーネント」に分割し、可変部分にはトランザクショナルメモリを適用するのがひとつの妥協策として広く用いられています。トランザクショナルメモリは、データベースのトランザクションと同じようにメモリ上の変数更新を原子性・一貫性・隔離性を保ったまま扱える仕組みです。これにより、複数の更新操作が衝突したときには自動的に検知してロールバックし、整合性を担保してくれます。したがって、可変コンポーネントを安心して狭い範囲に閉じ込めつつ、安全に並行更新を行うことができるわけです。
さらに一歩進んだ方法としてイベントソーシングという考え方があります。銀行口座の残高を例にとると、口座の現在残高を直接保存するのではなく、「入金」「出金」といった取引イベントをすべてログとして時系列に記録し、残高が必要なときには過去のイベントを累積して再構築します。この方式では、イベントは追記のみで更新や削除が発生しないため、データストア自体が完全に不変になります。並行更新の衝突も起きにくく、履歴を遡って状態を巻き戻したり検証したりするのも容易です。ただし、イベントが増え続けると再構築コストが高くなるため、定期的にスナップショットを取得して処理を高速化するなどの工夫が求められます。
まとめると、アプリケーション設計においてはまず不変性を第一原則とし、可能な限りロジックを不変コンポーネントで実装します。一方、どうしても必要な可変部分だけを切り出し、トランザクショナルメモリやイベントソーシングの技術を使って安全に扱うことで、並行処理の課題を最小限に抑えつつ高い可用性と整合性を実現できるのです。
筆者所感
可変変数はできるだけ避けるべきです。最近はハードウェアが高速でメモリも潤沢なので、値を上書きせず新しい変数を作るコストを過度に心配する必要はありません。新しい変数を宣言すると命名によって意図が明確になり、可読性や保守性も上がります。可変変数はループのインデックスなどごく限られた場面に留め、通常の処理は不変データや純粋関数で書くと安全です。
イベントソーシングを採用する場合の現実的な注意点も挙げておきます。イベントは追記で増え続けるため、履歴からその都度状態を再構築すると取得コストが増大します。そこで定期的なスナップショットや、夜間バッチで作るサマリーテーブルにより過去の集計を保持し、リアルタイム性と性能のバランスをとる設計が必要です。特に口座数やユーザ数が増えると累積レコードは指数的に増え得るので、最初から集約戦略決めておくことをおすすめします。