🙆

Head First デザインパターン 第2版 デザインパターンへようこそ

2024/10/16に公開

1 デザインパターンへようこそ

誰かが、すでにあなたの問題を解決してしまっています。

パターンを使うには

  • パターンを頭の中にロードする
  • そのパターンを適用できる箇所を認識する
  • パターンでは、コードを再利用するのではなく、経験を再利用する

最初の状態

Goで実装しようとすると、オーバーライドがないので、すでに先取りして今回対処したい問題が発生してしまっている。※一部の具象オブジェクトだけ振る舞いが違うため、すべてのオブジェクトに対して個別に実装する必要がある(大部分は重複コード)

// オーバーライドができないので、インターフェースを定義する
type Duck interface {
	Quack()
	Display()
	Swim()
}

// 本当に共通する部分だけをベースとなる構造体に実装
type DuckBase struct {}

func (db DuckBase) Swim() {
	fmt.Println("All ducks float, even decoys!")
}

// 鳴き声が違うもの(RubberDuckがいるので、Quack()も個別実装になってしまう)
type RedHeadDuck struct {
	DuckBase
}

// ここが重複コードになる
func (r RedHeadDuck) Quack() {
	fmt.Println("Quack")
}

func (r RedHeadDuck) Display() {
	fmt.Println("I'm a real Red Headed duck")
}

次の状態

  • カモを飛ばせる必要が出た
  • Duckクラスにflyを追加
  • RubberDuck(ゴムのアヒル)が「飛ぶ」というクレームを受ける
  • 問題点:何か振る舞いの追加や変更がある度に、すべてのDcukの振る舞いを確認する必要がある
    • RubberDuck.flyはNOPなど

設計原則

  • アプリケーション内の変化する部分を特定し、不変な部分と分離する
    • 変化する部分を取り出して「カプセル化」する
    • そうすることで、コードの他の部分に影響を与えなくなる
    • その結果、コード変更による予期せぬ結果が少なくなり、システムの柔軟性が向上する

設計原則

  • 実装に対してでなく、インターフェースに対してプログラミングする
    • Duckの振る舞いは別クラスに配置する
    • この方法を取ることで、Duckのサブクラスは振る舞いの実装の詳細を知る必要がない
  • 「インターフェースに対するプログラミング」とは、実際は「スーパータイプに対するプログラミング」を意味する
    • 宣言する変数の型をスーパータイプ(通常は抽象クラスかインターフェース)にして、変数に代入されるオブジェクトがスーパータイプの任意の具象実装となるようにすべき
    • 変数を宣言するクラスは、実際のオブジェクトの型について知る必要がなくなる

素朴な疑問

  • 必ず実装してから変化点を抽出するのか?
    • 設計段階で変化する部分を予想して柔軟性を持たせる方法こともよくある
  • Duckもインターフェースにすべき?
    • この場合はそうすべきではない。(共通部分の実装が楽)
    • ※Goの場合には、最初からインターフェースが必要

以下、最終的なソースコード

flyBehavior.go
// 羽を使って飛ぶ
type FlyWithWings  struct{}

func (f FlyWithWings) Fly() {
	fmt.Println("I'm flying!!")
}

// 飛べない
type FlyNoWay struct{}

func (f FlyNoWay) Fly() {
	fmt.Println("I can't fly")
}

// ロケットで飛ぶ
type FlyRocketPowered struct{}

func (f FlyRocketPowered) Fly() {
	fmt.Println("I'm flying with a rocket")
}
quack.go
// 偽の鳴き声
type FakeQuack struct{}

func (q FakeQuack) Quack() {
	fmt.Println("Qwak")
}

// 鳴かない
type MuteQuack struct{}

func (q MuteQuack) Quack() {
	fmt.Println("<< Silence >>")
}

// 本物の鳴き声
type Quack struct{}

func (q Quack) Quack() {
	fmt.Println("Quack")
}

FlyBehaviorQuackBehaviorは、DuckBaseが使うのでここで定義する。
NewXxxでは、特に必要がなかったので、ポインタでなく値で返している。

duck.go
type FlyBehavior interface {
	Fly()
}
type QuackBehavior interface {
	Quack()
}

// アヒルのベース
type DuckBase struct {
	flyBehavior   FlyBehavior
	quackBehavior QuackBehavior
}

func (b DuckBase) Fly() {
	b.flyBehavior.Fly()
}

func (b DuckBase) Quack() {
	b.quackBehavior.Quack()
}

func (b DuckBase) Swim() {
	fmt.Println("All ducks float, even decoys!")
}

// マガモ
func NewMallardDuck() MallardDuck {
	d := MallardDuck{}
	d.flyBehavior = FlyWithWings{}
	d.quackBehavior = Quack{}
	return d
}

type MallardDuck struct {
	DuckBase
}

func (m MallardDuck) Display() {
	fmt.Println("I'm a real Mallard duck")
}

// アメリカホシハジロ
func NewRedHeadDuck() RedHeadDuck {
	d := RedHeadDuck{}
	d.flyBehavior = FlyWithWings{}
	d.quackBehavior = Quack{}
	return d
}

type RedHeadDuck struct {
	DuckBase
}

func (r RedHeadDuck) Display() {
	fmt.Println("I'm a real Red Headed duck")
}

// ゴムのアヒル
func NewRubberDuck() RubberDuck {
	d := RubberDuck{}
	d.flyBehavior = FlyNoWay{}
	d.quackBehavior = FakeQuack{}
	return d
}

type RubberDuck struct {
	DuckBase
}

func (r RubberDuck) Display() {
	fmt.Println("I'm a rubber duckie")
}

// 木製のアヒル
func NewDecoyDuck() DecoyDuck {
	d := DecoyDuck{}
	d.flyBehavior = FlyNoWay{}
	d.quackBehavior = MuteQuack{}
	return d
}

type DecoyDuck struct {
	DuckBase
}

func (d DecoyDuck) Display() {
	fmt.Println("I'm a duck Decoy")
}

Duckインターフェースは、main関数で使うのでココで定義する。

main.go
func main() {
    // アメリカホシハジロ
	var d Duck = NewRedHeadDuck()
	d.Display()
	d.Fly()
	d.Quack()
	d.Swim()
    // 木製のアヒル
	d = NewDecoyDuck()
	d.Display()
	d.Fly()
	d.Quack()
	d.Swim()
}

type Duck interface {
	Quack()
	Fly()
	Display()
	Swim()
}

地元のレストランでふと耳にしたこと

  • 細部まで細かく正確に「長い」言葉で伝えるよりも、隠語を使った方が早くて、正確
  • ただし、伝える相手もその言葉を理解している必要がある

共有パターンボキャブラリの威力

  • パターンを使うと、より少ない言葉でより多くのことを言い表せる
    • 他の開発者に迅速かつ正確に伝えることができる
  • パターンレベルで話をすると、会話を「設計レベル」に長く留めることができる
    • オブジェクトやクラスレベルの詳細にとらわれず、設計にフォーカスできる。
  • 共有ボキャブラリは開発チームの質を向上させることができる
    • 全員で知識を共有することで、全体の質を向上させる
  • 共有ボキャブラリは、初級開発者の上達を促す
    • 初級開発者が中級以上の開発者の真似をしてパターンを学習するようになる

なぜデザインパターンなのか?

「疑い深い開発者」と「親切なパターンの師匠」の一連の対話があるが、基本的には、「疑い深い開発者」が言っていることは間違っていない気がする(SOLIDなどの設計原則を理解していれば、パターンを知らずとも再発明することができる)。が、自分では思いつかなかったけど、使えるパターンがあったり、自分で再発明すると他の人に「簡単に」伝えることができなかったりするので、パターンを学ぶことが重要ということだと思う。

さらに、

  • コストの節約:ゼロから再発明するのは時間も労力もかかる
  • 「なぜ」こういう設計にするのか、を考えることでどういう設計が優れた設計なのかを知ることができる

というメリットなどもあると思う。

Discussion