『Head Firstデザインパターン』を読んだ
気候が最高だったので、代々木公園で本を読んでいた。単に最高だった。
Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本
- 作者: Eric Freeman,Elisabeth Freeman,Kathy Sierra,Bert Bates,佐藤直生,木下哲也,有限会社福龍興業
- 出版社/メーカー: オライリージャパン
- 発売日: 2005/12/02
- メディア: 大型本
- 購入: 14人 クリック: 362回
- この商品を含むブログ (98件) を見る
どんな本か
初めて学ぶ方、過去に挫折した経験のある方、知識を確固たるものにしたい方を対象に、イラストや写真を使ってやさしく楽しく解説する人気のHead Firstシリーズのデザインパターン編。*1
巷に溢れるデザインパターン本の中でも、読みやすさを重視した、ちょっと変わった本。サンプルコードはJava。
- 「Head Firstデザインパターン」が設計力を底上げしてくれる良書だった - 雀巽の日記帳
- HeadFirstデザインパターンを読んで / fujimisakari blog
- Head Firstデザインパターン―頭とからだで覚えるデザインパターンの基本 - kagamihogeの日記
なぜ読んだか
ソフトウェアエンジニアなら幼稚園の段階で習得しておくべきデザインパターンを一切学ばないままここまで来てしまった感があったため。なぜお給料をいただけているのかまったく意味がわからない。都度ググったりしながら個々には知っていたり知らなかったりという状態だったが、どこかのタイミングで体系的に知っておきたいと思っていた。
感想
全体
だいたい休日2日間ほどかけて読んだ。もちろん、頭には入っていない。
前評判どおり、イラストや例え話、物語に大部分を割いて、各パターンを丁寧に説明している。自分は日本語に苦手意識があり、認知において視覚優位なので、わかりやすかった。これまでにチラ読みしてきたWebの記事や本の中では、格段に理解しやすかった。とにかく読者に飽きさせない、絶対に理解させてやるという気概を感じる。「デザインパターン同士の特別対談」みたいなページもあるのだが、だいたい険悪な空気になっており、笑える。
訳がちょっとしんどい部分があり、そこが改善されれば自分にとってベストな形式の本かもしれない。Head Firstシリーズなるものを初めて読んだが、他も読んでみたい。
デザインパターンの学習
各デザインパターンを読んでみて、初めて知るというよりは、「あ、これはこういうコードはデザインパターンのひとつで、こういう名前で呼ぶのか」という再発見が多くあった。実際のサービスで動いているコードをイメージできることは、本書の理解にかなり助力になったと思う。
一方で、先んじてデザインパターン単体を学習することにも一定の意味があると思った。自分のようなポンコツだと、これまでの経験上、コードから設計意図を読み解くのはかなり大変だったし、先んじてデザインパターンの知識があれば楽できた場面は無数にある。命名ひとつとっても、その設計の前提知識があれば、クラス名から得られる情報量がぜんぜん違う。
実コードで自分で「発見」することが理想だが、それが難しくとも、本書などで学習して実コードに適用する(あるいは仮想のサービスでも設計する)ことも意味がある。結論、好きにすればいい。ごめん。
技術書の読み方
印象的だったのは、序章で本の読み方についての記述があり、これは本書のみならず技術書を読む上で参考になりそうだった。自分は本を読むのが苦手なので、文字を目だけで追っている状態になったり、そんな状態でも休憩せずにぶっ続けで読みがち。あと、じっくり読むのが足りない。
今後
デザインパターンについては、今後コードを読むのが少しは楽になる気がする。また、自分で設計する際の材料にもしたい。初心者はデザインパターン使いたいマンになりがちだそうなので、気をつける。
前回、今回とOOPや設計の本が続いたので、次は久しぶりにデータベース系の何かしらを読みたい。
メモ
例によって無駄にクソ長い。サンプルコードはJavaだが、勉強がてらC#で書いてみた。一個も動かしてないのであってるかは知らん。
序章
- どれほど望んでも、脳は退屈なことを記憶しない
- 学習とは習得と記憶であり、そのためには文字以外に視覚情報、思考、集中、感情が必要となる
- 学習したいことが「重要である」と脳に思わせるには
- 全編に渡って「人の気配がするように」
- この本を読むときに気をつけること
- じっくり読む:理解すればするほど、記憶することは少なくなる
- 問題を解く
- Q&Aもじっくり読む
- 適度に動きながら読む
- 読後から寝るまで、何も「難しいこと」をしない:長期記憶への転送を阻害しない
- 水をたくさん飲む
- 大きな声を出す:聴覚からもインプット
- 脳が疲れたら休む:内容を表面的にしか理解できない、すぐ忘れる
- 自分なりの感想を持つ:何かを感じたときに記憶する、できるだけ感情移入する
- 自分で設計してみる
1章 デザインパターン入門
- 設計原則
- アプリケーション内の変化する部分と不変な部分と分離する
- すべてのパターンは「システムのある部分をその他のすべての部分と独立して変更できるようにする」方法を提供する
- 実装に対してではなく、インターフェースに対してプログラミングする
振る舞いをあらわすクラス群を作成する
// 実装に対するプログラミング Dog d = new Dog(); d.Bark(); // インターフェース/スーパータイプに対するプログラミング Animal a = new Dog(); a.MakeSound(); // animal型の参照を多態的に使用 // さらに、実行時に具象実装オブジェクトを代入 a = getAnimal(); a.MakeSound(); // Animalのサブタイプに関知しないが、それがmakeSound()を知っている
新たにAnimalを継承してCatを作成すれば、makeSound()を再利用し、meow()を実装できる
- meow()に触れずに、bark()の中身を変更できる
- 継承よりも構成(composition)を好む
- has-aはis-aより優れていることもある
- アプリケーション内の変化する部分と不変な部分と分離する
- Starategyパターン
- 定義:一連のアルゴリズムを定義し、それぞれをカプセル化して交換可能にする
- 効果:アルゴリズムを使用するクライアントとは独立して、アルゴリズムを変更できる
- 鴨の種類と鳴き方を動的に設定したい
public abstract class Duck { IQuackBehavior quackBehavior; // Duckオブジェクトは「鳴く」という振る舞い自体を処理しない // その振る舞いをquackBehaviorで参照されるオブジェクトに「委譲」する public void PerformQuack() => quackBehavior.Quack(); // 鳴く振る舞いを動的に変更するためのメソッド public void SetQuackBehavior(IQuackBehavior qb) => quackBehavior = qb; } public class MallardDuck : Duck { public MallardDuck() => quackBehavior = new Quack(); } public interface IQuackBehavior { void Quack(); } public class Quack : IQuackBehavior { public void Quack() => /* ガーガー鳴く */; } public class Squak : IQuackBehavior { public void Quack() => /* キューキュー鳴く */; } public class MiniDuckSimulator { public static void Main(string[] args) { var mallard = new MallardDuck(); mallard.PerformQuack(); // ガーガー鳴く mallard.SetQuackBehavior(new Squak()); mallard.PerformQuack(); // キューキュー鳴く } }
2章 Observerパターン:オブジェクトを事情通に
- Observerパターン
- 定義:オブジェクト間の1対多の依存関係を定義し、あるオブジェクトの状態が変化すると、それに依存しているすべてのオブジェクトが自動的に通知され更新されるようにする
public interface ISubject { public void RegisterObserver(Observer observer); public void RemoveObserver(Observer observer); public void NotifyObservers(); } public interface IObserver { void Update(float temp); } // 気温の変化を通知するサブジェクトを実装する public class WheatherData : ISubject { private List<IObserver> obsevers; private float temp; public WheatherData() => observers = new List<IObserver>(); public void RegisterObserver(Observer observer) => observers.Add(observer); public void RemoveObserver(Observer observer) { var index = observers.indexOf(o); if(index >= 0) { observers.remove(index); } } public void NotifyObservers() { foreach(var observer in observers) { observer.Update(temp); } } // 気温の変化を取得する public void SetTemperature(float temp) { this.temp = temp; NotifyObservers(); } } // 気温の変化の通知を受け取るオブザーバを実装する public class TemperatureDisplay : Observer { private float templature; public TemperatureDisplay(ISubject wheatherData) => wheatherData.RegisterObserver(this); public void Update(float templature) { this.templature = templature; Display(); } private void Display() => Console.WriteLine($"現在の気温:{templature}"); }
設計原則の利用
- 変化する部分は「サブジェクトの状態」と「オブザーバの数と種類」
- サブジェクトとオブザーバはどちらもインターフェースを利用する
- サブジェクトと任意の数のオブザーバを構成するが、これは継承階層構造では構築できない
追加の設計原則
- 相互にやり取りするオブジェクト間は疎結合にする
3章 Decoratorパターン:オブジェクトの装飾
- Decoratorパターン
- 定義:オブジェクトに付加的な責務を動的に付与する
- 効果:継承の代替となる、柔軟な機能拡張手段を提供する
// 抽象コンポーネント public abstract class Beverage { var description = "不明な飲み物"; public string getDescription() => return description; public abstract double cost(); } // 抽象デコレータ public abstract class CondimentDecorator : Beverage { public abstract string GetDescription(); } // 具象コンポーネント1 public class Espresso : Beverage { public Espresso() => description = "エスプレッソ"; public double Cost() => return 1.99; } // 具象コンポーネント2 public class HouseBlend : Beverage { public HouseBlend() => description = "ハウスブレンド"; public double Cost() => return 0.89; } // 具象デコレータ public class Mocha : CondimentDecorator { Beverage beverage; public Mocha(Beverage beverage) => this.beverage = beverage; public string GetDescription() return beverage.GetDescription() + ", モカ"; public double Cost() = 0.20 + beverage.Cost(); } // テスト public class StarbuzzCoffee { public void Main(string args[]) { var beverage = new Espresso(); Console.Writeline($"ご注文は {beverage.Getdescription()}、お値段は \${beverage.Cost()}"); // => ご注文はエスプレッソ、お値段は $1.99 // エスプレッソにモカをトッピング beverage = new Mocha(beverage); Console.Writeline($"ご注文は {beverage.Getdescription()}、お値段は \${beverage.Cost()}"); // => ご注文はエスプレッソ、モカ、お値段は $2.19 // 別のクラスであるハウスブレンドにモカをトッピング var beverage2 = new Mocha(new Houseblend()); Console.Writeline($"ご注文は {beverage2.Getdescription()}、お値段は \${beverage2.Cost()}"); // => ご注文はハウスブレンド、モカ、お値段は $1.09 } }
- 追加の設計原則
- クラスは拡張に対しては開かれた状態、変更に対しては閉じた状態であるべきである
4章 Factoryパターン:OOの利点を活用した構築
- Factory Methodパターン
- 定義:オブジェクト作成のためのインターフェースを定義するが、度のクラスをインスタンス化するかはサブクラスが決定する
- 効果:クラスはサブクラスににインスタンス化を先送りできる
// 抽象作成者(Creator)クラス // ファクトリメソッドを除くすべての製品操作メソッドの実装を含む public abstract class PizzaStore { public Pizza OrderPizza(string type) { // どの具象ピザが作られたかは知らない var pizza = CreatePizza(type); pizza.Prepare(); pizza.Bake(); pizza.Cut(); pizza.Box(); return pizza; } // Pizzaをインスタンス化する責務は、サブクラスのメソッド(=ファクトリメソッド)に移行する protected abstract Pizza CreatePizza(string type); } // 具象作成者(Creator)クラス public class NYPizzaStore : PizzaStore { // ファクトリメソッドを実装する public Pizza CreatePizza(string item) { if(item == "チーズ") return new NYStyleCheesePizza(); else if(item == "野菜") return new NYStyleVeggiePizza(); else return null; } } // 抽象製品(Product)クラス public abstract class Pizza { private string Name { get; set; }; public void Prepate() => ;// 省略 public void Bake() => ; // 省略 public void Cut() => ; // 省略 public void Box() => ; // 省略 } // 具象製品(Product)1クラス public class NYStyleCheesePizza : Pizza { public NYStyleCheesePizza() { Name = "ニューヨークスタイルのソース&チーズピザ"; } } // 具象製品(Product)2クラス public class ChicagoStyleCheesePizza : Pizza { public NYStyleCheesePizza() { Name = "シカゴスタイルのディープディッシュチーズピザ"; } } public class PizzaTestDrive { public static void Main(string[] args) { var nyStore = new NYPizzaStore(); var nyPizza = nyStore.OrderPizza("チーズ"); Console.WriteLine($"ご注文は、{nyPizza.Name()}"); // => ご注文は、ニューヨークスタイルのソース&チーズピザ var chicagoStore = new ChicagoPizzaStore(); var chicagoPizza = chicagoStore.OrderPizza("チーズ"); Console.WriteLine($"ご注文は、{chicagoPizza.Name()}"); // => ご注文は、シカゴスタイルのディープディッシュチーズピザ } }
追加の設計原則
- 設計原則(5): 具象クラスに依存せず、抽象に依存する(依存性反転の原則)
- 依存性反転の原則に従うための指針
- 具象クラスへの参照を保持する変数を持たない
- 具象クラスからクラスを継承しない
- 実装済みのメソッドをオーバーライドしない
Abstract Factoryパターン
- 定義:具象クラスを指定せず、一連の関連・依存オブジェクトを作成するインターフェースを提供する
- 効果:クライアントが具象プロダクトを知らずに一連のプロダクトを作成できる
// 食材ファクトリ public interface IPizzaIngredientFactory { public Sauce CreateSauce(); public Cheese CreateCheese(); public Veggies[] CreateVeggies(); } // ニューヨークの食材ファクトリ public class NYPizzaIntegredientFactory : IPizzaIntgredientFactory { public Sauce CreateSauce() => return new MarinaraSauce(); public Cheese CreateCheese() => return new ReggianoCheese(); public Veggies[] CreateVeggies() => return new Veggies { new Garlic(), new Onion(), new Mushroom() }; } public abstract class Pizza { public string Name { get; set; }; private Sauce sauce; private Cheese cheese; private Veggies veggies[]; // 食材ファクトリを利用して、ピザに必要な食材を集める public abstract void Prepare(); // ↓ Prepare以外のメソッドはFactory Methodパターンのときと変更なし public void Bake() => ; // => 省略 public void Cut() => ; // 省略 public void Box() => ; // 省略 } public class CheesePizza : Pizza { PizzaIngredientFactory ingredientFactory; // 食材を提供するファクトリを取得する public CheesePizza(PizzaIngredientFactory ingredientFactory) => this.ingredientFactory = ingredientFactory; void Prepare() { sauce = ingredientFactory.CreateSauce(); cheese = ingredientFactory.CreateCheese(); } } public class NYPizzaStore : PizzaStore { protected Pizza CreatePizza(string item) { Pizza pizza = null; // ニューヨーク店では、ニューヨークの食材ファクトリを利用する var ingredientFactory = new NYPizzaIntegredientFactory(); if(item == "チーズ") { // ニューヨーク店で使う食材ファクトリを渡す pizza = new CheesePizza(ingredientFactory); pizza.Name = "ニューヨークスタイルチーズピザ"; } else if(item == "野菜") { pizza = new VeggiePizza(ingredientFactory); pizza.Name = "NYスタイル野菜ピザ"; } return pizza; } }
- Factory MethodパターンとAbstract Factoryパターンの比較
- どちらも特定の実装(具象型)からアプリケーションを分離するのが得意だが、そのやり方が異なる
Factory Method Abstract Factory オブジェクトの作成 継承を使う オブジェクトコンポジションを使う 製品の追加 変更は必要ない インターフェースの変更が必要 製品のスコープ 1つの製品をつくだけのため、1つのメソッドで十分 一連の製品全体を作成するため、大きなインターフェースが必要 製品の実装 サブクラスが作成する具体的な方を利用するコードを、抽象作成者で実装する 具象ファクトリは、ファクトリメソッドを実装して製品を作成する 使いどころ 必要となるすべての具象クラスが事前にわかっている場合 作成する必要のある一連の製品があり、クライアントがグループをなす製品を使用するようにしたい場合
5章 Singletonパターン:唯一のオブジェクト
- Singletonパターン
- 定義:1つのクラスがただひとつのインスタンスを持つことを保証し、インスタンスにアクセスするグローバルポイントを提供sルウ
- 効果:スレッドプール、キャッシュ、レジストリの設定など、インスタンスを1つに限定したい場合
public class ChocolateBoiler { private boolean empty; private boolean boiled; private static ChocolateBoiler uniqueInstance; // privateなコンストラクタ private ChocolateBoiler() { empty = true; boiled = false; } // 唯一のインスタンスを返す public static ChocolateBoiler GetInstance() { if(uniqueInstance == null) uniqueInstance = new ChocolateBoiler(); return uniqueInstance; } }
- マルチプロセスで実行される場合、複数のインスタンスが生成される可能性がある
-
GetInstance()
を同期化することで解決する - それによってパフォーマンス劣化が問題となる場合、以下の対策がある
- 先行インスタンス化
- 二重チェックロッキング
-
6章 Commandパターン:呼び出しのカプセル化
- Commandパターン
- 定義:リクエストをオブジェクトとしてカプセル化する
- 効果:クライアントの異なるリクエスト、キュー、またはログリクエストでパラメータ化でき、アンドゥ可能な操作もサポートする - 「リクエストを行うオブジェクト」と「そのリクエストの実行方法を知っているオブジェクト」を分離したい場合に適用する
- サンプルコード
- 「ON/OFFボタンが2つあり、それぞれに電灯、ステレオなどを接続してON/OFF操作ができるようにするリモコン」を実装する
- 例として、「電灯のON」と「ステレオのOFF」についてサンプルコードを書いてみる
public interface Command { void Execute(); } // リモコンを実装する public class RemoteControl { Command[] onCommands; Command[] offCommands; // コンストラタではONとOFFの配列のインスタンス化と初期化のみ public RemoteControl() { onCommands = new Command[2]; offCommands = new Command[2]; // コマンドがロードされているか確かめるために、(ON|OFF)ButtonWasPushedでnullチェックをする必要がある // それを避けるために、初期化時点で「何もしないコマンド」をロードしておく var noCommand = new NoCommand(); for(var i = 0; i < 2; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } public void SetCommand(int slot, Command onCommand, Command offCommand) { onCommand[slot] = onCommand; offCommand[slot] = offCommand; } // リモコンのボタンを押すと呼ばれるメソッドたち public void OnButtonWasPushed(int slot) => onCommands[slot].Execute(); public void OffButtonWasPushed(int slot) => offCommands[slot].Execute(); } // コマンドを実装する(電灯をOFFするコマンド) public class LightOffCommand : ICommand { Light light; public LightOffCommand(Light light) => this.light = light; public void Execute() => light.off(); } // コマンドを実装する(ステレオをCDでONするコマンド) public class StereoOnWithCDCommand : ICommand { Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) => this.stereo =stereo; // 複数のメソッドを呼ぶことも可能 public void Execute() { stereo.on(); stereo.SetCD(); stereo.SetVolume(11); } } // リモコンを稼働させる public class RemoteLoader { public static void Main(string[] args) { RemoteControl remoteControl = new RemoteControl(); Light livingRoomLight = new Light(); Stereo stereo = new Stereo() LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight); StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo); remoteControl.SetCommand(0, livingRoomLightOn /* 本例では省略 */, livingRoomLightOff); remoteControl.SetCommand(1, stereoOnWithCD, stereoOff /* 本例では省略 */); } }
- アンドゥボタン:「一つ前に実行した処理」を取り消す機能も実装もできる
-
Command
インターフェイスにundo()
を追加し、各コマンドで実装する -
RemoteControl
クラスにundoCommand
ローカル変数を追加し、これに実行したコマンドを保存しておく
-
- Commandパターンのその他の使用法
- リクエストのキューイング
- リクエストのロギング
7章 AdaptorパターンとFacadeパターン:適合可能にする
- Adapterパターン
- 定義:クラスのインターフェースを、クライアントが期待する別のインターフェースに変換する
- 効果:互換性のないインターフェースを持つクライアントを使用できる
public interface IDick { void Quack(); void Fly(); } public class MallardDuck : IDuck { public void Quack() => /* ガーガー */ ; public void Fly() => /* 飛んでいます */ ; } public interface ITurkey { void Gobble(); void Fly(); } public class WildTurkey : ITurkey { public void Gobble() => /* ゴロゴロ */ ; public void Fly() => /* 短い距離を飛んでいます */ ; } // Duckオブジェクトの代わりにTurkeyオブジェクトを使いたい => アダプタを実装する // 適合させるインターフェースを実装する public class TurkeyAdapter : IDuck { ITurkey turkey; // 適合させるオブジェクトへの参照を取得する public TurkeyAdapter(ITurkey turkey) => this.turkey = turkey; // インターフェースの全メソッドを実装する public void Quack() => turkey.Gobble(); public void Fly() { // 七面鳥は短い距離しか飛べないので、鴨と同様の振る舞いを実現するために、5回飛ぶ for(var i=0; i < 5; i++) { turkey.Fly(); } } }
オブジェクトアダプタ | クラスアダプタ | |
---|---|---|
アダプティへの適合手段 | コンポジションを使ってリクエストをアダプティに渡す | ターゲットとアダプティをサブクラス化する(継承) |
追加するコード | アダプタとそのインターフェース | アダプタのみ |
利点 | 柔軟性を保てる | シンプルに保てる |
- Facadeパターン
- 定義:サブシステムの一連のインターフェースに対する、統合されたインターフェースを提供する
- 効果:クライアントは複雑なサブシステムへの参照を意識しなくてよくなる
public class HomeTheaterFacade { Amplifier amplifer; Tuner tuner; DvdPlayer dvd; CdPlayer cd; Projector projector; TheaterLights lights; Screen screen; PopcornPopper popper; public HomeTheaterFacade(Amplifier amplifier, Tuner tuner, // 以下略 ) { this.amplifer = amplifer; this.tuner = tuner; // 以下略 } public void watchMovie(string movie) { popper.on(); popper.pop(); lights.dim(10); screen.down(); projector.on(); // 以下略 } } public class HomeTheaterTestDrive { public string void Main(string[] args) { // 各要素をインスタンス化(略) var homeTheater = new HomeTheaterFacade(amplifier, tuner, /* 以降略 */); homeTheater.watchMovie("君の名は。"); } }
- 追加の設計原則
- 設計原則(6): 友達とだけやりとりをする(最小知識の原則)
- 「友達とだけやりとりをする」を実践するためのルール(メソッドの呼び出しを範囲内に収める)
public class Car { Engine engine; public Car() => /* エンジンなどを初期化 */; public void Start(Key key) { var doors = new Doors(); // 引数として渡されたオブジェクトのメソッドは呼び出してOK var authorized = key.Turns(); if (authorized) { // このオブジェクトのコンポーネントのメソッドを呼び出すのはOK engine.Start(); // このオブジェクト内のローカルメソッドは呼び出してOK UpdateDashboardDisplay(); // このオブジェクト内で作成またはインスタンス化されたオブジェクトのメソッドは呼び出してOK doors.Lock(); } } public void UpdateDashboardDisplay() => /* ?? */; }
パターン | 目的 |
---|---|
Decorator | インターフェースを変更せずに、責務を追加する |
Adapter | あるインターフェースを別のインターフェースに変換する |
Facade | インターフェースをより簡潔にする |
8章 Template Methodパターン:アルゴリズムのカプセル化
- Template Methodパターン
- 定義:メソッドにおけるアルゴリズムの骨組みを定義し、1つ以上の手順をサブクラスに先送りする
- 効果:アルゴリズムの構造を変えずに、サブクラスが実装の一部を提供できる
- コーヒーと紅茶を作る手順はほぼ同じなので、共通化したい
public abstract class CaffeineBeverage { // テンプレートメソッド public void PrepareRecipe() { boilWater(); Brew(); PourInCup(); AddCondiments(); } // コーヒーと紅茶で異なる処理をするので、抽象メソッドとして宣言しておき、実装はサブクラスへ先送りする public abstract void Brew(); public abstract void AddCondiments(); // コーヒーと紅茶で共通する処理 public void BoilWater() => /* お湯を沸かす */; public void PourInCup() => /* カップに注ぐ */; } public class Tea : CaffeineBeverage { public void Brew() => /* 紅茶を浸す */; public void AddCondiments() => /* レモンを追加する */; } public class Coffee : CaffeineBeverage { public void Brew() => /* フィルタでコーヒーをドリップする */; public void AddCondiments() => /* 砂糖とミルクを追加する */; }
フック: 抽象クラスに空の実装を用意しておき、継承クラスでは任意に実装する
- 実装してもしなくてもいい処理がある場合に使用する
追加の設計原則
- 設計原則(7): こちらを呼び出さないでください、こちらから呼び出します(ハリウッド原則)
パターン | 説明 |
---|---|
Template Method | サブクラスがアルゴリズムの手順の実装方法を決める |
Strategy | 交換可能な振る舞いをカプセル化し、委譲を使ってどの振る舞いを使うべきかを決める |
Factory Method | サブクラスがどの具象クラスをインスタンス化するかを決める |
9章 IteratorパターンとCompositeパターン:適切に管理されたコレクション
Iteratorパターン
- 定義:内部表現を公開することなくアグリゲートオブジェクトの要素に順次アクセスする方法を提供する
- 効果:アグリゲートインターフェースと実装が簡潔になり、本来の責務に集中できる
実装方法の異なる食堂とパンケーキハウスのメニューリストを統合し、ウェイトレスが透過的に扱えるようにしたい
public interface IIterator { boolean HasNext(); Object Next(); } // メニューリストを配列としてもつ食堂のメニューに対して、具象イテレータを実装する public class DinerMenuIterator : IIterator { MenuItem[] items; int position = 0; public DinerMenuIterator(MenuItem[] items) => this.items = items; public Object Next() => items[position++]; // すべての要素をみたかどうか、及び、次の項目がないか(配列の最大サイズがあるため)をチェックする public boolean HasNext() => !(position >= items.length || items[position] == null); } // メニューリストをArrayListとしてもつパンケーキハウスのメニューに対して、具象イテレータを実装する(省略) // ウェイトレスがメニュー内の項目のイテレータを取得できるようにするインターフェース public interface IMenu { public Iterator CreateIterator(); } // 食堂のメニューの具象クラスを実装する public class DinerMenu : IMenu { const int _maxItems = 8; int numberOfItems = 0; MenuItem[] menuItems; // コンストラクタ、addItemは省略 public Iterator CreateIterator() => new DinerMenuIterator(menuItems); // 他のメニューメソッドは省略 } // パンケーキハウスのメニューの具象クラスを実装する(省略) public class Waitress { // 実装ではなくインターフェースに対するプログラミング // ArrayList menus; public Waitress(ArrayList menus) => this.menus = mensu; public void PrintMenu() { var menuIterator = menus.iterator(); while(menuIterator.HasNext()){ var menu = (Menu)menuIterator.Next(); PrintMenu(menu.CreateIterator()); } } private void PrintMenu(Iterator iterator) { while(iterator.HasNext()) { var menuItem = (MenuItem)iterator.Next(); // メニュー項目をプリント } } }
追加の設計原則
- 設計原則(8): クラスは、変更される理由を1つだけ持つべきである(単一責務の原則)
Compositeパターン
- 定義:部分-全体階層を表現するために、オブジェクトをツリー構造に構成する
- 効果:クライアントは個別のオブジェクトとオブジェクトのコンポジションを同様に扱うことができる
さらに、「食堂のメニューでは、デザートサブメニューを選択できるようにする」場合など、階層構造を表現したい
// リーフノードとコンポジットノードのインターフェースを提供する、コンポーネントの抽象クラスを実装する public abstract class MenuComponent { // すべてのメソッドのデフォルト実装を提供する // 以下はコンポジットメソッド(追加、削除、取得) public void Add(MenuComponent menuComponent) => throw new UnsupportedOperationException(); public void Remove(MenuComponent menuComponent) => throw new UnsupportedOperationException(); public MenuConponent GetChild(int i) => throw new UnsupportedOperationException(); // 以下は操作メソッド // MenuItemが使うメソッド public string GetName() => throw new UnsupportedOperationException(); public string GetDescription() => throw new UnsupportedOperationException(); public string GetPrice() => throw new UnsupportedOperationException(); // MenuとMenuItemの両方が使うメソッド public string Print() => throw new UnsupportedOperationException(); } // メニューコンポーネントを実装する(リーフ) public class MenuItem : MenuComponent { string name; string description; double price; public MenuItem(string name, /* 略 */) => /* 略 */; public string GetName() => name; public string GetDescription() => description; public string GetPrice() => price; public void Print() => "お値段は" + GetPrice() + /* 略 */; } // コンポジットメニューを実装する(ノード) public class Menu : MenuComponent { ArrayList menuComponents = new ArrayList(); string name; string description; public Menu(string name, string description) { this.name = name; this.description = description; } public void Add(MenuComponent menuComponent) => menuComponents.Add(menuComponent); public void Remove(MenuComponent menuComponent) => menuComponent.Remove(menuComponent); public MenuComponent GetChild(int i) => (MenuComponent)menuComponents.get(i); public string GetName() => name; public string GetDescription() => description; // GetPrice()は無意味なのでオーバーライドしない public void Print() => "メニュー名は" + GetName() + /* 略 */; } public class Waitress { MenuComponent allMenus; public Waitress(MenuComponent allMenus) => this.allMenus = allMenus; public void PrintMenu() => allMenus.Print(); }
- Compositeパターンは「階層構造の管理」と「メニューの操作」の責務を持ち、単一責務の原則に反している
- 透過性(WaitressはMenuとMenuItemを区別しない)とのトレードオフ
パターン | 説明 |
---|---|
Strategy | 交換可能な振る舞いをカプセル化し、委譲を使って使用すべき振る舞いを決定する |
Adapter | 1つ以上のクラスのインターフェースを変換する |
Iterator | コレクションの実装を公開することなくオブジェクトのコレクションをトラバースする方法を提供する |
Facade | 一連のクラスのインターフェースを簡素化する |
Composite | クライアントはオブジェクトのコレクションと個別のオブジェクトを同じように扱うようにする |
Observer | 何らかの状態が変化した際に一連のオブジェクトに通知できる |
10章 Stateパターン:物事の状態
Stateパターン
- 定義:オブジェクトの内部状態が変化した際にその振る舞いを変更できる
- 効果:同上
25セントを入れたらクランクが回ってガムボールを販売するマシンを実装したい
public interface IState { void InsertQuarter(); void EjectQuarter(); void TurnCrank(); void Dispense(); } // 25セント未受領の状態オブジェクトを実装する public class NoQuarterState : IState { GumballMachine gumballMachine; public NoQuarterState(GumballlMachine gumballMachine) => this.gumballMachine = gumballMachine; public void InsertQuarter() => gumballMachine.SetState(gumballMachine.GetHasQuarterMachine()); public void EjectQuarter() => /* 25セントを投入していません */; public void TurnCrank() => /* クランクを回しましたが、25セントを投入していません */; public void Dispense() => /* まず支払いをする必要があります */; } // 同様に、売り切れ状態、25セント受領状態、販売状態のオブジェクトをそれぞれ実装する(省略) // ガムボールマシンのコンテキストオブジェクトを実装する public class GumballlMachine { IStete soldOutStete; IState noQuarterState; IState hasQuarterState; IState soldState; IState state = soldOutState; int count = 0; public GumballlMachine(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; } // アクションのためのメソッドの実装は、現在の状態に委譲するだけ public void InsertQuarter() => state.InsertQuarter(); public void EjectQuarter() => state.EjectQuarter(); // Dispense()は内部アクションであり、ユーザが直接呼び出すことはない public void TurnCrank() { state.TurnCrank(); state.Dispense(); } // 状態間の遷移のためのメソッド void SetState(State state) => this.state => state; void ReleaseBall() { if(count != 0) count--; } }
- Strategyパターンとクラス図は一致するが目的が異なる
パターン | 目的 |
---|---|
Strategy | サブクラス化に対する柔軟性のある代替手段 |
State | コンテキスト内で多数の条件分を使うことに対する代替手段 |
11章 Compoundパターン:パターンのパターン
- Compoundパターン
- 定義:2つ以上のパターンを組み合わせる
- 効果:繰り返し発生する問題や汎用的な問題の解決策となる
- MVCやモデル2はこれに該当する
12章 パターンの有効利用:実世界でのパターン
- パターン:コンテキストにおける問題の解決策
- コンテキスト:パターンが適用される状況
- 問題:このコンテキストの中で達成したい目標及び制約
- 解決策:一連の制約の中で目標を達成するための、一般的な設計
- パターンの目的に基づく分類
- 作成:クライアントがインスタンス化するオブジェクトから、クライアントを分離する方法を提供する
- Abstract Factory,Builder,Factory Method,Prototype,Singleton
- 振る舞い:クラスとオブジェクトの相互作用、責務の分配に関与する
- Chain of Responsibility,Comamand,Interpreter,Iterator,Mediator,Memento,State,Strategy,Template Method,Observer,Visitor
- 構造:クラスやオブジェクトをより大きな構造に組み込む
- Adapter,Bridge,Composite,Decorator,Facade,Flyweight,Proxy
- 作成:クライアントがインスタンス化するオブジェクトから、クライアントを分離する方法を提供する
- パターンが扱う対象に基づく分類
- クラスパターン:継承を介してクラス間の関係を定義する(コンパイル時に確立)
- オブジェクトパターン:コンポジションを介してオブジェクト間の関係を定義する(実行時に確立)
- パターンで考えるためのクイックガイド
- 簡潔にする(KISS)
- 特効薬でない
- 必要なケースを知る
- リファクタリングはよい機会
- 不要なものは(それがパターンだとしても)取り除く
- いま必要ないことはしない