【Goで理解するオブジェクト指向設計 #1】─ カプセル化・ポリモーフィズム
はじめに ~「優れたシステム」の具体的な作り方とは?~
前回の振り返り
第1章, 第2章を通して、クリーンアーキテクチャの概念を「依存性のルール」を守る上で欠かせないものさしである「凝集度・結合度・安定度」の3つを引き合いに紹介し、その上で、変更に強いシステムの「設計図」(クリーンアーキテクチャのレイヤー構造)を学んできました。
ここで新たな問いが生まれます。
「どれだけ素晴らしい設計思想があっても、それを実現するにはどうしたらいいの?」
例えば、「どうすれば、あのレイヤー間の境界線を守れるのか?」「どうすれば、依存の矢印を内側に向かせられるのか?」といった調子です。
OOPとは? ~複雑な世界を「モノ」として捉える技術~
オブジェクト指向(OOP)の定義には諸説あり、その本質がイメージしづらい、と感じる方も多いのではないでしょうか。
「クラス構文で書くこと」がOOPなのではありません。OOPとは、文法ではなく 複雑な現実世界を、人間が理解しやすい形に整理するための「考え方」 そのものです。
世界を「視点」で切り取る
OOPの第一歩は、全てをありのままに捉えるのではなく、ある特定の「視点」から世界を切り取り、モデル化することです。
例えば、「自動車」という一つの概念も、視点によってその姿は全く異なります。
カーディーラーの視点: 「価格」「色」「在庫数」といったデータを持つ 「商品」 として見える。
自動車工場の視点: 「エンジン」「タイヤ」といった無数の 「部品の集合体」 として見える。
優れた設計とは、アプリケーションの目的に応じて、この 「どの視点でモデル化するか」 を適切に決定することから始まります。

「データ」と「振る舞い」を一つの「モノ」にまとめる
そして、ここからがOOPの真骨頂です。
切り取ったモデルを、私たちは 「オブジェクト(モノ)」として扱います。このオブジェクトは、必ず2つの側面 を持っています。
データ(状態): そのモノが 「何であるか」 を表す情報。(例:カーディーラーの「商品」オブジェクトは、「価格」や「在庫数」というデータを持つ)
振る舞い: そのモノが 「何ができるか」 を表す操作。(例:「商品」オブジェクトは、「販売する」「展示する」という振る舞いを持つ)
OOP以前の世界では、この「データ」と、データを操作する「手続き(振る舞い)」は、バラバラに管理されていました。OOPは、関連するデータと振る舞いを一つの「オブジェクト」というカプセルに閉じ込めたのです。
この 「データと振る舞いの合体」 こそが、私たちがこれから学ぶカプセル化、ポリモーフィズム、継承といった強力なメリットを生み出す、全ての源泉となります。
今回は、このオブジェクト指向という考え方をプログラミングに取り入れることで得られる、3つの具体的な「建築技法」を詳しく見ていきましょう。
カプセル化 ~システムの「境界」を築く技術~
オブジェクト指向とは 「関連するデータ(状態)と振る舞いを一つの『モノ』にまとめること」 だと学びました。
この「まとめる」という行為こそが、 カプセル化 です。
では、なぜわざわざ「カプセル」に閉じ込める必要があるのでしょうか?
それは、 変更の影響範囲を限定し、システムの他の部分に波及させないため です。
第一章で学んだ「高凝集」と「疎結合」をコードレベルで実現するための、最も強力な武器、それがカプセル化なのです。
WHAT:カプセル化とは何か? ~車の運転に学ぶ~
カプセル化の本質は 複雑な内部(How)を隠蔽し、シンプルな外部(What)だけを見せる ことです。
実は、私たちは皆、日常的にカプセル化の恩恵を受けています。
例えば、「車」です。

運転手である私たちは、「ハンドルを回せば曲がる」「アクセルを踏めば進む」というシンプルなインターフェース(What)さえ知っていれば、車を運転できます。
エンジン内部で「どのプラグが点火し、どのピストンが動いているか」といった複雑な仕組み(How)を一切知る必要はありません。
この内部の仕組みが隠されているからこそ、私たちは安心して運転に集中できるのです。
もし、運転手がエンジンの空気量まで操作しなければならなかったら…?
それは、車とエンジンの関心の分離が失敗していることを意味します。
HOW:「良いカプセル」を壊さないためのルール
優れた設計とは、このように各関心事に強固な「境界」を築くことです。
しかし、せっかく築いた境界も、扱い方を間違えれば簡単に壊れます。
その境界を守るための有名なルールが 「デメテルの法則(知識最小の原則)」 です。
これは非常にシンプルで 「直接の友達とだけ話せ。友達の友達と話すな」 というルールです。
言い換えると オブジェクトの内部から、別のオブジェクトを取り出して、さらにその中身を操作するな ということです。
メソッドチェーンが壊す「境界」
この法則を破りがちなのが、便利な メソッドチェーン です。
// 悪い例:デメテルの法則違反
// 運転手が、車のエンジンを取り出して、さらにそのピストンを直接操作している
car.GetEngine().ManipulatePiston()
このコードは、carオブジェクトが隠蔽しているはずのEngineという内部の詳細を外部に漏らしてしまっています。これでは、エンジンが新しいモデルに交換されただけで、運転手のコードまで修正が必要になってしまいます。
理想的な姿
// 良い例:車に「お願い」する
// 運転手は、車のインターフェース(アクセルを踏む)を呼び出すだけ
car.Accelerate()
このコードでは、運転手は車の内部を知りません。ただ「加速してくれ」とお願いしているだけです。加速の具体的な方法は、全てcarオブジェクトが責任を持ちます。
結論:カプセル化は「責任」の設計である
このように、カプセル化とは、単に状態と振る舞いをまとめればOKというような、単純なテクニックではありません。
システムのどこに境界を引き、誰が何に責任を持つのかを明確にすることで、変更の波紋を食い止める。ソフトウェア全体の堅牢性を決める、極めて重要な設計思想なのです。
ポリモーフィズム(多態性)~システムの「安定」を築く技術~
多態性のマジック❶:膨大な条件式を「消滅」させる
ソフトウェアを脆くメンテナンスしにくくする最大の原因の一つが、膨大なif文やswitch文による条件分岐です。
例えば、動物の鳴き声を処理する以下のようなコードがあったとします。
func main(){
// いろいろな動物インスタンスを呼び出す
dog := animal.NewAnimal("Dog")
cat := animal.NewAnimal("Cat")
// オーナーインスタンスを呼び出す
owner := owner.NewOwner()
// オーナーインスタンスがtouchメソッドを実行する
fmt.Println(owner.Touch(*dog))
fmt.Println(owner.Touch(*cat))
}
type Animal struct{
Animal_type string
}
func NewAnimal(animal_type string) *Animal{
return &Animal{Animal_type: animal_type}
}
type Owner struct{
}
func NewOwner() *Owner{
return &Owner{}
}
func (o Owner) Touch(animal animal.Animal) string{
switch animal.Animal_type {
case "Dog":
return "ワン"
case "Cat":
return "ニャン"
// 新たな動物が追加されたら、ここに条件式を新たに追加する必要がある。
}
return "Error"
}
このコードは一見、明快に動きます。しかし、致命的な問題を抱えています。
もし、「インコ」や「カエル」が追加されたらどうなるでしょう?動物が増えるたびに、このswitch文のcaseを永遠に修正し続けなければなりませんね。
問題の分析:密結合
この問題の原因は、第一章で学んだ 「密結合」 に他なりません。
Ownerストラクトが、Animalストラクトの 内部実装の詳細(Animal_typeというフィールド) を直接知ってしまっています。
これは、Animalが本来隠すべき内部の都合を 隠蔽できていない(=カプセル化が失敗している) ことを意味し、結果としてOwnerとAnimalが癒着してしまっているのです。
「振る舞い」に注目し、立場を逆転させる
ここで、多態性の出番です。 「動物の種類(Type)」で分岐するのではなく、「全てのPetは 『鳴く』という振る舞い を持つべきだ」と考え方を変えてみましょう。
Go言語では、この「共通の振る舞い」をinterface(インターフェース)として定義します。
type Pet interface {
Speak()
}
この「契約」さえ守っていれば、つまりSpeak()メソッドさえ持っていれば、システムはそれを「鳴ける動物」として扱います。
上記のinterfaceを使って書き直した実装が以下になります。
func main() {
// いろいろな動物インスタンスを呼び出す
dog := animals.NewDog()
cat := animals.NewCat()
// オーナーインスタンスを呼び出す
owner := owner.NewOwner()
// オーナーインスタンスがtouchメソッドを実行する
owner.Touch(dog)
owner.Touch(cat)
}
package animals
// 各動物はdog.go, cat.goとして、animalsパッケージに内包する。
type Dog struct {
}
func NewDog() *Dog {
return &Dog{}
}
func (d *Dog) Speak() {
// Dog speaking logic
println("Dog barks")
}
package animals
type Cat struct{
}
func NewCat() *Cat {
return &Cat{}
}
func (c *Cat) Speak() {
// Cat speaking logic
println("Cat meows")
}
func NewOwner() *Owner {
return &Owner{}
}
func (pc *Owner) Touch(pet pet.Pet) {
pet.Speak()
// Additional touching logic can be added here
println("Touching the pet completed")
}
これによって膨れ上がっていたswitch文が、完全に消滅しました。
Ownerストラクトは、DogやCatといった具体的な動物の「種類」には一切関心がありません。
ただ、「Petインターフェースを満たすもの」のSpeakメソッドを呼び出しているだけです。
もし「インコ」を追加したくなっても、Parrot structにSpeak()メソッドを実装するだけ。このOwnerストラクトのメソッドTouch()を修正する必要がないのです。
これが、多態性がもたらす第一の魔法です。
【Go言語の補足】ダックタイピングとは?
ここで、Go言語の強力な特徴に触れておきましょう。 他の言語(JavaやPHPなど)では、DogクラスがPetインターフェースを「実装します(implements)」と、明示的に宣言する必要があります。
しかしGoでは、Dog structにSpeak()メソッドを定義しただけで、自動的にPetインターフェースを満たしていると見なされます。
If it walks like a duck and quacks like a duck, then it must be a duck. (アヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いない)
Goは、そのstructが「何者であるか(血統)」を問わず 「何ができるか(振る舞い)」 だけを見ます。
この「ダックタイピング」という性質が、Goのコードを非常に柔軟で、疎結合なものにしているのです。
多態性のマジック❷:アーキテクチャの依存関係を「逆転」させる」
さて、この『抽象に依存する』という強力な魔法を、もっと大きなスケールで見てみましょう
第一章で、私たちは 『安定依存の原則(SDP)』を学びました。『安定なモジュール(レシピ)』は『不安定なモジュール(フライパン)』に依存してはならない というルールです
しかし、そこで一つの大きな矛盾に直面していました。「レシピは、フライパンを使わなければ料理ができない。それなのに、どうやって依存しないようにするの?」と。
レシピが「A社のフライパン」という具体的な道具に依存すると、フライパンを買い替えただけで、レシピ(システムの核)まで修正が必要になる。これはSDPに違反した、脆い設計です。

この矛盾を解決する答えこそが、多態性です。
優れた設計では、レシピは「180度で均一に加熱できること」という 抽象的な『要求(インターフェース)」 を定義するだけです。
そして、「A社のフライパン」や「B社のIHコンロ」といった具体的な道具が、その要求に従います(インターフェースを実装します)
この結果、依存の矢印が逆転します。不安定な「フライパン」が、安定な「レシピの要求」に従う形となり、アーキテクチャの核であるレシピは完璧に守られます。

この「具体的な実装(フライパン)ではなく、抽象的なインターフェース(要求)に依存する」という設計原則。
これこそが、SOLID原則の一つ 「依存性逆転の原則(Dependency Inversion Principle: DIP)」 です。
そして、このDIPをコードレベルで実現可能にする具体的な技術こそが、多態性なのです(DIPの詳細は、今後のSOLIDの章でさらに詳しく解説します!)
このように、多態性はswitch文を消すといった局所的な改善だけでなく、アーキテクチャ全体の安定性を守る、システム設計において最も強力な技術の一つであることが分かります。
Discussion