読書メモ:Head Firstデザインパターン(第2版)
July 23, 2023
- Head First デザインパターン(第2版)
- https://www.oreilly.co.jp/books/9784873119762/
上記を読了。内容をメモしておく。
オブジェクト指向プログラミングにおいて、先人たちの知恵と経験の結晶である「デザインパターン」を学ぶことで、頻繁に起こる問題とその対応策を体得し、優れた設計に基づいた信頼性の高いコードを効率的に開発することが可能となります。GoF の真髄を理解するため、教育心理学に基づきビジュアルを重視したレイアウト、文体、クイズやパズルを随所に盛り込み、飽きることなく読み進められる工夫を凝らしています。
Head First デザインパターン(第2版)
内容は上記書籍説明の引用通りで、デザインパターンを学んでいくもの。ただし、単にデザインパターンを紹介するというよりも、まずはあるオブジェクト指向設計の原則を取り上げたのち、その原則に従って特定の設計課題を解決するための解決策、つまり一般的に用いられているデザインパターンを紹介するという流れが繰り返される。デザインパターンと同時にオブジェクト指向設計の考え方についても学んでいける内容となっている。「Head First」シリーズは、教育心理学に基づいて脳が記憶しやすい形式になっている本のようだ。一般的な技術書と比較すると独特なデザインになっているため、かえって読みづらいと感じてしまうこともあった。これは単に好みの問題かもしれない。
なお、本書に掲載されているコードは以下の GitHub リポジトリでも確認できる。
- GitHub | Code for Head First Design Patterns book (2020)
- https://github.com/bethrobson/Head-First-Design-Patterns
メタ認知:思考について考える #
学習の秘訣は、学んでいる新しい物事が「本当に重要」だと脳に思わせることです。
脳を思いどおりにさせるためにできること
- 時間をかけて読もう。理解すればするほど、覚えなければならないことは少なくなる。
- ただ読むだけでなく、ときどき読むのを止めて考えましょう。本の中で問題が出されても、すぐに答えを見ないでください。誰かから本当に質問されていると思いましょう。脳に深く考えさせればさせるほど、学習や記憶の効果が高まるのです。
- この本を読んだ後は寝るまで他の本を読まないようにしよう。少なくとも、難しいものは読まないように。
- 学習の一部、特に学習内容の長期記憶への転送は、本を閉じたあとに行われます。脳が次の処理を実行するには時間が必要です。この処理中に新たに別のことを学習すると、前に学習したことが一部失われてしまいます。
- 水をたくさん飲もう。
- 脳は十分な水分がある状態で最もよく働きます。脱水状態(のどが渇いたと感じる前に起こります)になると認知機能は衰えます。
- 内容をはっきりと声に出してみよう
- 反すことは脳の別の部分を活性化します。何かを理解したい場合やあとで思い出しやすくしたい場合には、はっきりと声に出してください。さらにいいのは、それを誰かに明確に説明してみることです。
- 脳に耳を傾けよう
- 脳に負担をかけすぎないように注意しましょう。内容を表面的にしか理解できなくなったり、読んだばかりのことを忘れるようになったりしたら休憩してください。ある限界を超えると、それ以上詰め込もうとしても学習の効率は上がらず、かえって学習を妨げることもあります。
デザインパターン入門 #
設計原則
デザインパターンの前に、一般的なオブジェクト指向の設計原則を知っておきましょう。
- アプリケーション内の変化する部分を特定し、不変な部分と分離する。
- 実装に対してではなく、インタフェースに対してプログラミングする。
- 継承よりコンポジションのほうが好ましい。
- 相互にやり取りを行うオブジェクトの間には、疎結合設計を使う。
- クラスを拡張に対しては開かれた状態にするべきだが、変更に対しては閉じた状態にする。(解放閉鎖原則)
- 抽象に依存する。具象クラスに依存してはいけない。(依存関係反転の原則)
- オブジェクト間のやり取りを少数の身近な友達だけに限定する。(最小知識の原則/デメテルの法則)
- こちらを呼び出すな。こちらから呼び出す。(ハリウッドの原則)
- クラスを変更する理由は1つだけにする。(単一責務の原則)
共有パターンボキャブラリの威力
デザインパターンは、他の開発者との共有ボキャブラリを与えてくれます。ボキャブラリはより少ない言葉でより多くのことを言い表せます。他の開発者はあなたの考えている設計を迅速かつ正確に理解できます。このボキャブラリを習得すれば、他の開発者とより簡単にやり取りができ、また、パターンを知らない開発者にパターンの学習を始める気を起こさせることができます。
1章:Strategy パターン #
Strategy パターンは、一連のアルゴリズムを定義してカプセル化し、交換できるようにする。Strategy パターンを使うと、アルゴリズムを利用するクライアントとは独立してアルゴリズムを変更できる。
public interface FlyBehavior {
public void fly();
}
public interface QuackBehavior {
public void quack();
}
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
}
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("飛んでいます!!");
}
}
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("飛べません");
}
}
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("ガーガー");
}
}
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<<沈黙>>");
}
}
// 👇 main
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
}
public class Main {
public static void main (String[] args) {
Duck modelDuck = new ModelDuck();
modelDuck.performFly();
modelDuck.performQuack();
modelDuck.setFlyBehavior(new FlyWithWings());
modelDuck.performFly();
}
}
2章:Observer パターン #
Observer パターン
Observer パターンは、オブジェクト間の1対多の依存関係を定義し、あるオブジェクトの状態が変化すると、そのオブジェクトに依存しているすべてのオブジェクトに自動的に通知され更新されるようにする。
- サブジェクトは、共通インタフェースを使用してオブザーバを更新する。
- 任意の具象型のオブザーバは、Observer インタフェースを実装している限り Observer パターンに参加できる。
- サブジェクトはオブザーバが Observer インタフェースを実装していること以外はオブザーバに関して何も知らないので、オブザーバは疎結合である。
- Observer パターンを使うと、サブジェクトからデータを取り出したり(プッシュ)取り出したり(プル)できる。プルの方が柔軟だと考えられている。
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<Observer>();
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
}
上記の設計では、Subject の Observer がそのすべての値を必要としていなくても3つのデータ全てを update() メソッドに送り出して(プッシュして)います。これでも問題はありませんが、あとでプッシュ内容に風速などの別データを追加した場合はどうなるでしょうか?この場合、ほとんどの Observer がそのデータを必要としていなくても、すべての Observer の update()メソッドを変更しなければいけません。その代わりに Subject にプッシュさせるのではなく、Observer にデータを取り出させる(プルさせる)方法もあります。Subject がデータのゲッターメソッドを用意し、Observer がそのメソッドを使って必要に応じたデータを取り出すようにするわけです。
public class WeatherData implements Subject {
// ...
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
public interface Observer {
public void update();
}
public class SomeWeatherDataObserver implements Observer {
public void update() {
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
// ...
}
}
3章:Decorator パターン #
Decorator パターン
Decorator パターンは、オブジェクトに追加の責務を動的に付与する。デコレータは、サブクラス化の代替となる、柔軟な機能拡張手段を備える。
- Decorator パターンには、具象コンポーネントのラップに使われる一連のデコレータクラスがある。
- デコレータクラスは、装飾するコンポーネントの型と同じ型である。
- デコレータは、コンポーネントへのメソッド呼び出しの前後どちらかまたは両方に新しい機能を追加することで、コンポーネントの振る舞いを変更する。
- コンポーネントを任意の数のデコレータでラップできる。
- 一般に、デコレータはコンポーネントのクライアントに対して透過的である。つまり、クライアントがコンポーネントの具象型に依存していない限りは透過的である。
public abstract class Beverage {
String description = "不明な飲み物";
public String getDescription() {
return description;
}
public abstract double dost();
}
public abstract class CondimentDecorator extends Beverage {
Beverage beverage;
public abstract String getDescription();
}
public class Espresso extends Beverage {
public Espresso() {
description = "エスプレッソ";
}
public double cost() {
return 1.99;
}
}
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "ハウスブレンドコーヒー";
}
public double cost() {
return 0.99;
}
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + "、モカ";
}
public double cost() {
return beverage.cost() + 0.20;
}
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + "、モカ";
}
public double cost() {
return beverage.cost() + 0.20;
}
}
// 👇 main
public class CoffeeShop {
public static void main(String[] args) {
Beverage beverage1 = new Espresso();
System.out.println(beverage1.getDescription() + " $" + beverage1.cost());
// -> エスプレッソ $1.99
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2); // モカ
beverage2 = new Mocha(beverage2); // モカもう一丁(ダブルモカ)
beverage2 = new Whip(beverage2); // ホイップ
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
// -> ダークローストコーヒー、モカ、モカ、ホイップ $1.49
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3); // 豆乳
beverage3 = new Mocha(beverage3); // モカ
beverage3 = new Whip(beverage3); // ホイップ
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
// -> ハウスブレンドコーヒー、豆乳、モカ、ホイップ $1.34
}
}
4章:Factory パターン #
Simple Factory
Simple Factory は、正確には GoF デザインパターンではありませんが、クライアントを具象クラスから分離するための簡単な方法です。Simple Factory は一般的に使われているので、開発者の中にはこれを「Factory パターン」と勘違いしている人もいるくらいです。
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if (type.equals("チーズ")) {
pizza = new CheesePizza();
} else if (type.equals("ペパロニ")) {
pizza = new PepperoniPizza();
} else if (type.equals("アサリ")) {
pizza = new ClamPizza();
} else if (type.equals("野菜")) {
pizza = new VeggiePizza();
}
return pizza;
}
}
public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
SimplePizzaFactory factory = new SimplePizzaFactory();
PizzaStore store = new PizzaStore(factory);
Pizza pizza = store.orderPizza("チーズ");
}
}
Factory Method
Factory Method パターンは、オブジェクトを作成するためのインタフェースを定義するが、どのクラスをインスタンス化するかについてはサブクラスに決定させる。Factory Method により、クラスはサブクラスにインスタンス化を委ねることができる。
// 👇 abstract
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
// 👇 Add
abstract Pizza createPizza(String type);
}
public class NYPizzaStore extends PizzaStore {
Pizza createPizza(String type) {
if (type.equals("チーズ")) {
return new NYStyleCheesePizza();
} else if (type.equals("ペパロニ")) {
return new NYStylePepperoniPizza();
} else if (type.equals("アサリ")) {
return new NYStyleClamPizza();
} else if (type.equals("野菜")) {
return new NYStyleVeggiePizza();
}
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
PizzaStore nyPizzaStore = new NYPizzaStore();
PizzaStore chicagoPizzaStore = new ChicagoPizzaStore();
Pizza nyStorePizza = nyPizzaStore.orderPizza("チーズ");
Pizza chicagoStorePizza = chicagoPizzaStore.orderPizza("チーズ");
}
}
Simple Factory と Factory Method を比べると、Simple Factory はそうでないのに比較して Factory Method は使用する実装をサブクラスに決定させています。Simple Factory ではオブジェクト作成をカプセル化することはできますが、作成する製品を変更できないので、Factory Method のような柔軟性はありません。
Abstract Factory
Abstract Factory パターンは、具象クラスを指定せずに、一連の関連オブジェクトや依存オブジェクトを作成するためのインタフェースを提供する。
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() {
return new ThinCrustDough();
}
public Sauce createSauce() {
return new MarinaraSauce();
}
public Cheese createCheese() {
return new ReggianoCheese();
}
public Veggies[] createVeggies() {
return { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
}
public Pepperoni createPepperoni() {
return new SlicedPepperoni();
}
public Clams createClam() {
return new FreshClams();
}
}
public abstract class Pizza {
String name;
Dough dough;
Sauce sauce;
Veggies[] veggies;
Cheese cheese;
Pepperoni pepperoni;
Clams clam;
abstract void prepare();
void bake() {
System.out.println("180度で25分間焼く");
}
void cut() {
System.out.println("ピザを扇形にカットする");
}
void box() {
System.out.println("PizzaStoreの箱にピザを入れる");
}
void setName(String name) {
this.name = name;
}
String getName() {
return name;
}
}
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
System.out.println(name + "を下準備");
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
public class ClamPizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
public ClamPizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
System.out.println(name + "を下準備");
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
clam = ingredientFactory.createClam();
}
}
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
if (item.equals("チーズ")) {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("ニューヨークスタイルチーズピザ");
} else if (item.equals("ペパロニ")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("ニューヨークスタイルペパロニピザ");
} else if (item.equals("アサリ")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("ニューヨークスタイルアサリピザ");
} else if (item.equals("野菜")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("ニューヨークスタイル野菜ピザ");
}
return pizza;
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
PizzaStore nyPizzaStore = new NYPizzaStore();
Pizza nyStorePizza = nyPizzaStore.orderPizza("チーズ");
// 👇
// orderPizza() メソッドのなかで createPizza() メソッドが呼び出される
// createPizza() メソッドの中で食材ファクトリが実行される
// -> PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
// -> pizza = new CheesePizza(ingredientFactory);
// -> pizza.prepare();
// -> -> dough = ingredientFactory.createDough();
// -> -> sauce = ingredientFactory.createSauce();
// -> -> cheese = ingredientFactory.createCheese();
}
}
Factory Method と Abstract Factory の違い
Factory Method の要点は継承によるサブクラスを使ってオブジェクトを作成すること。Abstract Factory はコンポジションを使ってそれを実現する。
使い分けを考える場合、ただ一つの製品を作成するのであれば Factory Method を用いる(createPizza()
)。一連の製品群を作成する必要があれば Abstract Factory を用いる(createDough()
、createSauce()
、createCheese()
)。
Factory Method
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
pizza = createPizza(type);
// ...
return pizza;
}
abstract Pizza createPizza(String type);
}
public class NYPizzaStore extends PizzaStore {
// 👇(PizzaStoreから見て)サブクラスである NYPizzaStore にオブジェクト作成用のメソッドを委ねる
Pizza createPizza(String type) {
if (type.equals("チーズ")) {
return new NYStyleCheesePizza();
} else if (/* ... */) {
// ...
}
}
}
Abstract Factory
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
// ...
}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() {
return new ThinCrustDough();
}
public Sauce createSauce() {
return new MarinaraSauce();
}
public Cheese createCheese() {
return new ReggianoCheese();
}
// ...
}
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
// 👇コンポジションの形でファクトリを受け取る
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
// ...
}
}
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
if (type.equals("チーズ")) {
pizza = new CheesePizza(ingredientFactory);
} else if (/* ... */) {
// ...
}
return pizza;
}
}
5章:Singleton パターン #
Singleton パターンは、クラスがインスタンスを1つしか持たないことを保証し、そのインスタンスにアクセスするグローバルポイントを提供する。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
上記コードはマルチスレッドの場合に問題が発生し得る。次のとおり、各スレッドの処理が運悪く同時期に重なった場合、複数のインスタンスが作成される恐れがある。
- スレッド1:
getInstance() {
- スレッド2:
getInstance() {
- スレッド1:
if (uniqueInstance == null) {
- スレッド2:
if (uniqueInstance == null) {
- スレッド1:
uniqueInstance = new Singleton()
- スレッド1:
return uniqueInstance
- スレッド2:
uniqueInstance = new Singleton()
- スレッド2:
return uniqueInstance
解決策1:同期メソッドにする
デメリット:getInstance() の並列処理ができなくなることでのパフォーマンスが低下する。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
// 👇 synchronized キーワードを追加すると、すべてのスレッドはこのスレッドに入る順番を待つ。つまり2つのスレッドが同時にこのメソッドには入れなくなる。
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
解決策2:遅延インスタンス作成から先行インスタンス作成に変える
デメリット:遅延インスタンス作成の恩恵に預かれなくなる。
public class Singleton {
// クラスをロードするときにインスタンスを作成する。
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
解決策3:二重チェックロッキングを用いる
デメリット:特になし。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
// 👇 まずインスタンスが作成済みか調べたあと、未作成であればそのときは同期的に作成処理を行う。これによりパフォーマンスの懸念も不要になる。
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
6章:Command パターン #
Command パターンは、リクエストをオブジェクトとしてカプセル化する。リクエストを行うオブジェクトとそのリクエストの実行方法を知っているオブジェクトを分離したい場合に用いる。
- この分離で中心となるのはコマンドオブジェクト。レシーバをアクションでカプセル化する。
- インボーカは、コマンドオブジェクトの execute() メソッドを呼び出してリクエストを実行する。execute() メソッドはレシーバのアクションを起動する。
public interface Command {
public void execute();
}
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
public class SimpleRemoteControl {
Command slot;
public SimpleRemoteControl() {}
public void setCommand(Command command) {
slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonWasPressed();
}
}
7章:Adapter パターンと Facade パターン #
Adapter パターン
Adapter パターンは、クラスのインタフェースをクライアントが要求する別のインタフェースに変換する。アダプタは、インタフェースの互換性がないためにそのままでは連携できないクラスを連携させる。
public interface Duck {
public void quack();
public void fly();
}
public class MallardDuck implements Duck {
public void quack() {
System.out.println("ガーガー");
}
public void fly() {
System.out.println("飛んでいます");
}
}
// 👇 新しい鳥が登場!
public interface Turkey {
public void gobble(); // 七面鳥はガーガーとは鳴かず、ゴロゴロ(gobble)と鳴くものです。
public void fly();
}
public class WildTurkey implements Turkey {
public void gobble() {
System.out.println("ゴロゴロ");
}
public void fly() {
System.out.println("飛んでいます");
}
}
// 👇 Turkey を Duck と同じインタフェースにしたい
public class Turkey Adapter implements Duck {
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
public void quack() {
turkey.gobble();
}
public void fly() {
turkey.fly();
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
Duck duck = new MallardDuck();
testDuck(duck);
Turkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
Decorator パターンと Adapter パターンの違い
どちらもあるオブジェクトをラップするものだが、Decorator はラップするオブジェクトのインタフェースを変更せずに「新しい振る舞い」を追加しようとするものであり、Adapter はラップするオブジェクトの「インタフェースを変更する」ことを目的とする。
Facade パターン
Facade パターンは、サブシステムの一連のインタフェースに対する、統合されたインタフェースを提供する。ファサードは、サブシステムを使いやすくする高水準インタフェースを定義する。ファサードはインタフェースを単純化するだけでなく、クライアントを構成要素のサブシステムから分離する。
// 👇 ホームシアターで映画を見る準備
public class Main {
public static void main (String[] args) {
// (必要な要素をインスタンス化)
// ポップコーンメーカのスイッチを入れ、作り始める
popper.on();
popper.pop();
// 照明を暗くする
lights.dim(10);
// スクリーンを下ろす
screen.down();
// プロジェクターの準備
projector.on();
projector.setInput(player);
projector.wideScreenMode();
// アンプの準備
amp.on();
amp.setStreamingPlayer(player);
amp.setSurroundSound();
amp.setVolume(5);
// プレイヤーを操作して上映開始
player.on();
player.play(movie);
// ... 映画を終了する処理
}
}
// 👇 ファサードで単純化されたインタフェースを提供する。
public class HomeTheaterFacade {
Amplifier amp;
Tuner tuner;
StreamingPlayer player;
Projector projector;
TheaterLights lights;
Screen screen;
PopcornPopper popper;
public HomeTheaterFacade(
Amplifier amp,
Tuner tuner,
StreamingPlayer player,
Projector projector,
TheaterLights lights,
Screen screen,
PopcornPopper popper
) {
this.amp = amp;
this.tuner = tuner;
this.player = player;
this.projector = projector;
this.screen = screen;
this.lights = lights;
this.popper = popper;
}
public void watchMovie(String movie) {
popper.on();
popper.pop();
lights.dim(10);
screen.down();
projector.on();
projector.setInput(player);
projector.wideScreenMode();
amp.on();
amp.setStreamingPlayer(player);
amp.setSurroundSound();
amp.setVolume(5);
player.on();
player.play(movie);
}
public void endMovie() {
popper.off();
lights.on();
screen.up();
projector.off();
amp.off();
player.stop();
player.off();
}
}
public class Main {
public static void main (String[] args) {
// (必要な要素をインスタンス化)
HomeTheaterFacade homeTheater = new HomeTheaterFacade(
amp,
tuner,
player,
projector,
screen,
lights,
popper
);
homeTheater.watchMovie("レイダース/失われたアーク");
homeTheater.endMovie();
}
}
8章:Template Method パターン #
Template Method パターンは、メソッド内でアルゴリズムの骨組みを定義し、一部の手順をサブクラスに委ねる。テンプレートは、アルゴリズムを一連の手順として定義するメソッド。Template Method は、アルゴリズムの構造を変えることなく、アルゴリズムのある手順をサブクラスに再定義させる。
public abstract class Beverage {
// テンプレートメソッドはサブクラスにオーバーライドされたくないため final。
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// 飲み物によって入れ方がことなるので、これはサブクラスに委ねる。
abstract void brew();
abstract void addCondiments();
// 飲み物によらないのでここで定義。ただし必要ならばオーバーライドできる。
void boilWater() {
// ...
}
void pourInCup() {
// ...
}
}
public class Tea extends Beverage {
public void brew() {
// ...
}
public void addCondiments() {
// ...
}
}
public class Coffee extends Beverage {
public void brew() {
// ...
}
public void addCondiments() {
// ...
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
Tea myTea = new Tea();
myTea.prepareRecipe();
Coffee myCoffee = new Coffee();
myCoffee.prepareRecipe();
}
}
フックを使う
public abstract class Beverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
// 後処理としてサブクラスの任意の処理を実行する機会を与えたい場合はフックを使う
hook();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
// ...
}
void pourInCup() {
// ...
}
// サブクラスでオーバーライドされない限り何もしない
void hook() {}
}
9章:Iterator パターンと Composite パターン #
Iterator パターン
Iterator パターンは、内部表現を公開せずに、アグリゲートオブジェクトの要素に順次アクセスする方法を提供する。
*アグリゲート(aggregate)は、コレクションと同意。単なるオブジェクトの集合を指す。リスト、配列、ハッシュマップなど。
/**
* Iterator パターン導入前
*/
public class PancakeHouseMenu {
List<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<MenuItem>();
menuItems.add(new MenuItem(/* ... */));
menuItems.add(new MenuItem(/* ... */));
menuItems.add(new MenuItem(/* ... */));
}
public ArrayList<MenuItem> getMenuItems() {
return menuItems;
}
}
public class DinerMenu {
static final int MAX_ITEMS = 3;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
menuItems[0] = new MenuItem(/* ... */);
menuItems[1] = new MenuItem(/* ... */);
menuItems[2] = new MenuItem(/* ... */);
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();
DinerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenu.getMenuItems();
// オブジェクト内部のデータ形式として、前者はリスト、後者は配列で管理しており、
// getMenuItems() はそのデータ形式のまま返している。
// そのため両者で取り扱い方が微妙に異なり扱いづらい。
// 👇
for (int i = 0; i < breakFastItems.size(); i++) {
MenuItem menuItem = breakfastItems.get(i);
// ...
}
for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem= lunchItems[i];
// ...
}
}
}
/**
* Iterator パターン導入後
*/
public interface Iterator {
boolean hasNext();
MenuItem next();
}
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
public MenuItem next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
public class DinerMenu {
static final int MAX_ITEMS = 3;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
menuItems[0] = new MenuItem(/* ... */);
menuItems[1] = new MenuItem(/* ... */);
menuItems[2] = new MenuItem(/* ... */);
}
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
// (PancakeHouseMenu も同様に createIterator() で Iterator を返すようにしておく)
// 👇 main
public class Main {
public static void main (String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
DinerMenu dinerMenu = new DinerMenu();
Iterator dinerIterator = dinerMenu.createIterator();
printMenu(pancakeIterator);
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
// ...
}
}
}
Iterator パターンは、走査処理をアグリゲート(例:DinerMenu)ではなく、イテレータ(例:DinerMenuIterator)に任せる。アグリゲートから感服処理の責務を取り除き、コレクション管理の責務だけをまっとうさせる。
Composite パターン
Composite パターンでは、オブジェクトをツリー構造に構成して部分と全体階層を表現できる。Composite パターンを使うと、クライアントは個々のオブジェクトとオブジェクトのコンポジションを統一的に扱うことができる。
/**
* Iterator パターンの最終形コードの要改善点
*/
public class Main {
public static void main (String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
DinerMenu dinerMenu = new DinerMenu();
Iterator dinerIterator = dinerMenu.createIterator();
// 👇 メニューを1つずつ個別に管理していることで、新しいメニューが追加されるたびにここのコードを増やす必要がある。
printMenu(pancakeIterator);
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
// ...
}
}
}
/**
* 👇
* 「すべてのメニュー」があり、その配下に「サブメニュー(PancakeHouseMenu や DinerMenu)」があるようなツリー形式のデータ構造にしたい。
* すべてのメニューに対して操作を行うことができれば、上記のコードは改善される。
*/
// すべてのコンポーネント(ツリーの中のノードとリーフ)は、MenuComponent を実装しなければいけない。
// ただし、ノードとリーフでは異なる役割を持っていることから、どちらかにとっては必要のないメソッドも存在する。
// そのため、デフォルト実装は例外を投げるようにしておく。
// サブクラス(ノードとリーフ)は、サポートする必要のないメソッドはデフォルト実装のままに放っておけばよい。
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
// リーフに相当するクラス。(メニューに載っている商品1つ1つ)
public class MenuItem extends MenuComponent {
String name;
String description;
double price;
public MenuItem(/* ... */) {
// ...
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getPrice() {
return price;
}
public void print() {
System.out.println(getName());
System.out.println(getDescription());
System.out.println(getPrice());
}
}
// ノードに相当するクラス。(すべてのメニューやサブメニュー)
public class Menu extends MenuComponent {
List<MenuComponent> menuComponent = new ArrayList<MenuComponent>();
String name;
String description;
public Menu(/* ... */) {
// ...
}
public void add(MenuComponent menuComponent) {
menuComponent.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponent.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponent.get(i);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public void print() {
System.out.println(getName());
System.out.println(getDescription());
System.out.println("--------------");
// 配下のノードの print を再起的に呼び出す。エレガント!
for (MenuComponent menuComponent : menuComponents) {
menuComponent.print();
}
}
}
// 👇 main
public class Main {
public static void main (String[] args) {
MenuComponent allMenus = new Menu("すべてのメニュー");
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
// ツリーの最上位のメニューで print() を呼ぶだけですべてのメニューが表示される
allMenus.print();
}
}
コンポジットオブジェクト(ノード)とリーフオブジェクト(リーフ)どちらも Component インタフェースに従う必要があるため、クライアントからみるとこれらの違いは透過的に(見えなく)なる。これにより操作がしやすくなる一方で、不適切だったり意味のない操作が行われる可能性もあるため、安全性が少し損なわれている。安全性を重視するのであれば両者を別のインタフェースに分離することもできる。ただしその場合は透過性が失われ、条件文や instanceof 演算子をコードで使う必要性が出てくるため、操作のしやすさが損なわれることになる。これらは設計上のトレードオフである。
10章:State パターン #
State パターンでは、オブジェクトの内部状態が変化した際にオブジェクトが振る舞いを変更できる。クライアントから見ると、オブジェクトはそのクラスを変更したように見える。
- Context は、Context を構成している現在の状態オブジェクトに振る舞いを委譲してその振る舞いを実行する。
- 状態をクラスにカプセル化することで、変更が必要になったときに、その変更を局所化できる。
/**
* State パターン導入前
*
* 問題点:
* 変更要求があり、新たな状態が増えた場合にすべてのメソッドに手をいれる必要があり、保守が難しい。
*/
public class GumballMachine {
// QUARTER は 25セント硬貨
final static int SOLD_OUT = 0; // 売り切れ状態
final static int NO_QUARTER = 1; // 硬貨投入前状態
final static int HAS_QUARTER = 2; // 硬貨投入後状態
final static int SOLD = 3; // 販売処理中状態
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
// 客が25セントを投入しようとします。
public void insertQuarter() {
if (state == SOLD_OUT) {
System.out.println("25セントを投入することはできません。売り切れです。");
} else if (state == NO_QUARTER) {
System.out.println("25セントを投入しました。");
state = HAS_QUARTER;
} else if (state == HAS_QUARTER) {
System.out.println("もう1度25セントを投入することはできません。");
} else if (state == SOLD) {
System.out.println("お待ちください。ガムボールを出す準備をしています。");
}
}
// 客が25セントを取り出そうとします。
public void ejectQuarter() {
if (state == SOLD_OUT) {
System.out.println("返金できません。まだ25セントを投入していません。");
} else if (state == NO_QUARTER) {
System.out.println("25セントを投入していません。");
} else if (state == HAS_QUARTER) {
System.out.println("25セントを返却しました。");
state = NO_QUARTER;
} else if (state == SOLD) {
System.out.println("申し訳ありません。すでにハンドルを回しています。");
}
}
// 客がハンドルを回そうとします。
public void turnCrank() {
if (state == SOLD_OUT) {
System.out.println("ハンドルを回しましたが、ガムボールがありません。");
} else if (state == NO_QUARTER) {
System.out.println("ハンドルを回しましたが、25セントを投入していません。");
} else if (state == HAS_QUARTER) {
System.out.println("ハンドルを回しました。");
state = SOLD;
} else if (state == SOLD) {
System.out.println("2回回してもガムボールをもう1つ手にいれることはできません!");
}
}
// ガムボールを販売します。
public void dispense() {
if (state == SOLD_OUT) {
System.out.println("販売するガムボールはありません。");
} else if (state == NO_QUARTER) {
System.out.println("まずお金を払う必要があります。");
} else if (state == HAS_QUARTER) {
System.out.println("ハンドルを回す必要があります。");
} else if (state == SOLD) {
System.out.println("ガムボールがスロットから出てきます!");
count = count - 1;
if (count == 0) {
System.out.println("おっと、ガムボールがなくなりました!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
}
}
}
/**
* State パターン導入後
*
* - 状態の振る舞いをそれぞれのクラスに局所化した
* - 厄介な if 文をすべてなくした
* - 状態は修正に対して閉じているが、新しい状態クラスを追加することでガムボールマシンは拡張に対して開いている
*/
public interface State {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void insertQuarter() {
System.out.println("25セントを投入しました。");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
public void ejectQuarter() {
System.out.println("25セントを投入していません。");
}
public void turnCrank() {
System.out.println("ハンドルを回しましたが、25セントを投入していません。");
}
public void dispense() {
System.out.println("まずお金を払う必要があります。");
}
}
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State state;
int count = 0;
public GumballMachine(int numberGumballs) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
} else {
state = soldOutState;
}
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
}
void setState(State state) {
this.state = state;
}
void releaseBall() {
System.out.println("ガムボールがスロットから出てきます。");
if (count != 0) {
count = count - 1;
}
}
}
上記のコードでは、状態クラス(各 State)が次にどの状態に移行するかを決めているが、代わりにコンテキスト(GumballMachine)に遷移の流れを決定させることもできる。一般的な指針としては、状態遷移が固定されている場合にはコンテキストで遷移を管理させるのが適切で、遷移がより動的な場合には、遷移を状態クラス自体に配置する。状態クラスに状態遷移を配置することのデメリットは、状態クラス間に依存関係を作ってしまうこと。
11章:Proxy パターン #
Proxy パターンは、別のオブジェクトの代理(プレースホルダ)を提供し、そのオブジェクトへのアクセスを制御する。
- リモートプロキシは、クライアントとリモートオブジェクト間のやりとりを管理する。
- 仮想プロキシは、インスタンス化にコストがかかるオブジェクトへのアクセスを制御する。
- 保護プロキシは、呼び出し元に基づいてオブジェクトのメソッドへのアクセスを制御する。
- Proxy パターンには、他にもキャッシングプロキシ、同期プロキシ、ファイアウォールプロキシ、書き込み時コピープロキシなどの多くの変異形がある。
Proxy パターンと Decorator パターン
Proxy は Decorator と構造的に似ているが、この2つは目的が異なる。Decorator パターンはオブジェクトに振る舞いを追加するのに対し、Proxy パターンはアクセスを制御する。
12章:Compound パターン #
Compound パターンは2つ以上のパターンを組み合わせ、繰り返し発生する問題や一般的な問題を解決する解決策となる。
13章:パターンの有効利用 #
警告:
デザインパターンの使いすぎは、過剰設計のコードとなる恐れがあります。機能する最もシンプルな解決策を常に採用し、必要が生じた場合にパターンを導入するようにしましょう。
設計上の解決策でデザインパターンを使うということは、多くの開発者が時間をかけてテストした解決策を使えるというメリットがあります。共有ボキャブラリですから、他の開発者も認識できるような解決策を使っていることにもなります。
しかし、デザインパターンを使った場合、デメリットもあります。デザインパターンはクラスやオブジェクトを追加することも多いため、設計が複雑になります。デザインパターンを使うと、さらに階層を増やすことになり、複雑になるだけではなく非効率になることもあります。
これまで登場したパターンとその説明のまとめ
- Strategy: 交換可能な振る舞いをカプセル化し、委譲を使って使用すべき振る舞いを決定する。
- Observer: 状態が変化すると、オブジェクトに通知する。
- Decorator: オブジェクトをラップして新しい振る舞いを提供する。
- Factory Method: サブクラスが、作成する具象クラスを決定する。
- Abstract Factory: クライアントが具象クラスを特定せずに一連のオブジェクトを作成できるようにする。
- Singleton: ただ1つだけのオブジェクトが作成されることを保証する。
- Command: リクエストをオブジェクトとしてカプセル化する。
- Adapter: オブジェクトをラップし、別のインタフェースを提供する。
- Facade: 一連のクラスのインタフェースをシンプルにする。
- Template Method: サブクラスが、アルゴリズムにおける手順をどのように実装するか決定する。
- Iterator: コレクションの実装を後悔せずに、オブジェクトのコレクションを走査することができる。
- Composite: クライアントが、オブジェクトのコレクションと個々のオブジェクトを統一的に扱う。
- State: 状態ベースの振る舞いをカプセル化する。振る舞いの切り替えには異常を使用する。
- Proxy: オブジェクトをラップしてオブジェクトへのアクセスを制御する。
14章:残りのパターン #
これまでに GoF の全 23 のデザインパターンの中でも、特に利用頻度の高いものを取り扱ってきました。最後に、いままでのパターンほど頻繁には使われない残りのパターンを紹介します。
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Flyweight パターン
- Interpreter パターン
- Mediator パターン
- Memento パターン
- Prototype パターン
- Visitor パターン