基礎●●○○○

オブジェクト指向

Object-Oriented Programmingオブジェクトしこう

データとそれを操作する手続きをオブジェクトにまとめて設計する考え方。カプセル化・継承・ポリモーフィズムが柱。

概要

オブジェクト指向プログラミング(OOP: Object-Oriented Programming)は、データと、そのデータを操作する手続きをひとまとまりの「オブジェクト」として束ね、オブジェクト同士のやり取りとしてプログラムを組み立てる設計の考え方です。たとえば「銀行口座」というオブジェクトは、残高というデータと、入金・出金という操作をセットで持ちます。残高を書き換えたければ必ず入金・出金という窓口を通る — この「窓口を通さないとデータに触れない」という境界づくりが、オブジェクト指向の出発点です。

Java・C#・Ruby・Python・Swift など、現在広く使われている言語の多くはオブジェクト指向を主要な様式として採用しており、クラス・インスタンス・メソッドといった語彙はプログラミングの共通語になっています。フレームワークの設計、デザインパターンDDD(ドメイン駆動設計)といった上位の設計論も、オブジェクト指向の語彙の上に築かれています。

柱として挙げられるのが「カプセル化」「継承」「ポリモーフィズム」の3つです。ただし本質は個々の機能ではなく、変更の影響が及ぶ範囲を境界で区切り、大きなプログラムを「独立して理解できる小さな部品の集まり」に分解することにあります。

なぜ生まれたか

オブジェクト指向以前の主流だった手続き型プログラミングでは、プログラムは「手続き(関数)の列」と「それらが読み書きするデータ」に分かれていました。小さいうちは問題ありませんが、プログラムが大きくなると、あるデータ(たとえばグローバルな設定や共有のデータ構造)を何十もの手続きがあちこちから読み書きするようになります。すると「この変数はどこから書き換えられ得るのか」が誰にも追えなくなり、1か所の修正が思わぬ場所を壊す — データと手続きが散らばっていることが、規模の拡大とともに致命傷になっていきました。

オブジェクト指向はこの問題に「データと、それを触ってよい手続きを同じ場所に置き、外からはその手続き経由でしか触れなくする」という答えを出しました。データを書き換え得るコードがオブジェクトの内側に閉じ込められるため、追跡すべき範囲が一気に狭まります。1960年代の Simula がクラスとオブジェクトの原型を生み、1970年代の Smalltalk が「すべてはオブジェクトであり、メッセージを送り合う」という思想として完成させ、その後 C++ や Java を通じて産業の主流になりました。

詳細

カプセル化 — 境界を作り、内部を隠す

カプセル化は、データ(フィールド)と操作(メソッド)を1つのオブジェクトにまとめ、内部の表現を外から直接触れないよう隠すことです。銀行口座の例なら、残高フィールドを外部から直接代入できてしまうと「残高がマイナスにならない」という不変条件を全利用箇所で守らせる必要がありますが、出金メソッドの中でだけ検査すれば、守るべき場所は1か所で済みます。関心を1か所に凝集させ、モジュール間の結び付きを弱く保つ — 結合度と凝集度の理想を、言語機能として実現する仕組みと言えます。

手続き型: データと手続きが散在オブジェクト指向: 境界の内側に凝集共有データ誰でも直接書き換え可手続きA手続きB手続きC手続きD書き換え箇所が追えない口座オブジェクト残高 — 外から不可視入金する出金する明細オブジェクト履歴 — 外から不可視記録する一覧を返す利用側のコードメソッドという窓口を通してのみ触れる
手続き型で散在するデータと手続き(左)と、オブジェクトに凝集した構造(右)の比較

継承とポリモーフィズム — 同じ呼び方で違う振る舞い

継承は、既存のクラス(親)の性質を引き継いだ新しいクラス(子)を定義する仕組みです。「口座」を継承した「普通口座」「定期口座」は、共通の性質を親から受け継ぎつつ、出金ルールなど自分固有の部分だけを上書き(オーバーライド)できます。

そして継承以上に重要なのがポリモーフィズム(多態性)です。呼び出す側は「口座」という抽象だけを知っていて「出金する」と依頼し、実際にどのルールで処理されるかは、実行時にそのオブジェクトの実体(普通口座か定期口座か)が決めます。呼び出し側のコードを一切変えずに新しい種類を追加できる — これが、条件分岐の羅列でプログラムを書き分けていた手続き型との決定的な違いです。GUI ツールキットの「描画する」、例外処理の階層、ORM のモデルクラスなど、フレームワークの拡張点はほぼすべてポリモーフィズムでできています。

利用側のコード「描画して」と依頼図形 — 抽象描画する — 中身は未定円として描く四角形四角形として描く三角形三角形として描くどれに振り分けるかは実行時にオブジェクトの実体が決める
ポリモーフィズム — 呼び出し側は抽象だけを知り、実体が振る舞いを決める

継承の濫用と「合成を優先せよ」

継承は強力ですが、コード再利用の手段として濫用すると親子が密結合になります。子は親の内部実装に依存するため、親の変更が全子孫に波及し、階層が深くなるほど「どの振る舞いがどの階層で決まるのか」を追うのが困難になります。「ペンギンは鳥だが飛べない」に代表される、is-a 関係のほころびも古典的な問題です。この経験から、デザインパターンの GoF 本は「クラス継承よりオブジェクト合成を優先せよ」という原則を掲げました。飛ぶ・泳ぐといった振る舞いを部品として持たせ(has-a)、組み合わせで多様性を表現する方が、変更に強い構造になります。SOLID 原則のリスコフの置換原則や依存性逆転の原則も、継承と抽象の正しい使い方を示す指針です。現代の言語設計(Go や Rust)がクラス継承そのものを持たず、インターフェイス(トレイト)と合成だけを提供しているのは、この教訓の反映と言えます。

メッセージングとしてのオブジェクト指向

Smalltalk を設計したアラン・ケイは後年、「オブジェクト指向の本質はクラスや継承ではなく、メッセージングだ」と述べています。オブジェクトを「内部状態を隠し持ち、メッセージを受け取って自律的に振る舞う小さなコンピュータ」と見なし、システム全体をその協調として設計する — この見方に立つと、オブジェクト指向は言語機能の話ではなく「責務をどう分割し、誰に何を依頼するか」という設計思想になります。実際、マイクロサービスがサービス間をメッセージキューや API でつなぐ構図は、このメッセージング観をプロセスやネットワークの規模で再演したものと見ることができます。

実務では、オブジェクト指向は関数型プログラミングと対立するものではなく、併用されるのが普通です。ドメインの概念と不変条件はオブジェクトに閉じ込め、データ変換は純粋な関数で書く。境界の設計に興味が向いたら、DDDリファクタリングの語彙へ進むと、オブジェクト指向を「動くコードの書き方」から「変更に強い設計の道具」として捉え直せます。