Head First デザパタ復習 メモ(1)
序文
- OOPとそのデザインパターンについての感覚が薄れていて、復習を始めました。多分3,4個くらいで終わりそうです。
本文
1章
OOP(Object Oriented Programming)とは?
- データと行動を持つオブジェクト間のやり取りを中心としてプログラミングするパラダイム。
- オブジェクトのテンプレートをクラス(Class)と呼び、そしてそのクラスがメモリ領域に一つの存在として化したのはインスタンスと呼ぶ。
- OOPのクラスに関しての大分類として3つの特徴としては、
- 抽象化:一連の演算を持ったあるロジックかデータを一つにまとめてモジュール化すること。
- 継承(多形性・ポリモーフィズム):ベースとしたタイプを元にして様々なサブクラス(子クラス)タイプが出来ること。子クラスは親クラスの挙動を拡張する。
- カプセル化:メソッドとデータを一つにまとめてモジュールかすることを指す。これによってカプセル化した任意タイプは外からは機能だけを提供する。
Strategyパターン
テンプレート:クライアントにコンポジションとして入って機能のポリモーフィズム実装を行う。
class Strategy { public: virtual void Process() = 0; };
テンプレートの実装
class DoA final : public Strategy { public: void Process() override final { std::printf("A!\n"); } }; class DoB final : public Strategy { public: void Process() override final { std::printf("B!\n"); } };
クライアント
class Object final { public: Object(Strategy& strategy) : mPtrDo{&strategy} {}; void Do() { cassert(this->mPtrDo != nullptr); this->mPtrDo->Process(); } private: Strategy* mPtrDo = nullptr; };
- 一連のアルゴリズムまたは機能をテンプレートをカプセル化する。
- そしてこのテンプレートを継承したタイプを持つようにする。しかし、使いからは必ず交換できるようにしなければならない。
それにポインタなどで指すタイプは一連のカプセル化したもののベースとなるインタフェースであるべき。- これにより使いのクライアントは実装に独立的になる。
2章
OOPの原則
- Mutable(変化できる)な部分をカプセル化する。
- 継承よりはコンポジションを好む。
- インタフェースに対して実装するようにする。即ち、メソッドが提供する機能を中点として実装を行う。
- 相互にやり取りするオブジェクト間には疎結合(Loosed Coupling)を仕様する。柔軟に変更するからだ。
- 疎結合:密結合(Tightened Coupling)よりは交換性や拡張性、そしてそれぞれの役割の分担には優れる。しかし要素間の連携に密結合よりは時間が掛かる可能性がある。
Observerパターン
Observer
:データが変わることによってObservable
からデータを受け取る観察者タイプ。
class Observer { public: Observer(Observable& observable) : mObserverable{&observable} { this->mObserverable->Register(static_cast<Observer&>(*this)); } virtual ~Observer() { this->mObserverable->Remove(static_cast<Observer&>(*this); } virtual void Update(int32_t i) = 0; protected: Observable* mObserverable = nullptr; };
Observable
:アウトサイトからデータやりとりを行って、そしてObserver
にデータを渡すようにするタイプ。またはSubject
でも呼ばれる。
class Observable { public: virtual void Register(Observer& observer) = 0; virtual void Remove(Observer& observer) = 0; virtual void Notify() = 0; protected: std::vector<Observer*> mObserver = {}; };
WeatherData
:Subject
の実装体。Obsevable
の関数3つを実装している。
class WeatherData : public Observable { public: void Register(Observer& observer) override final { this->mObserver.emplace_back(&observer);} void Remove(Observer& observer) override final { auto it = std::find(this->mObserver.begin(), this->mObserver.end(), &observer); if (it == this->mObserver.end()) { this->mObserver.erase(it); } } void Notify() override final { for (auto& observer : mObserver) { observer->Update(this->mI); } } private: int32_t mI = 114514; };
DerivedA
DerivedB
:Observer
の実装体。
アップデートはDerived
ではなくObservable
を継承したタイプのインスタンスの値が変わると
自動的に「伝播」するようなる。
class DerivedA : public Observer { public: DerivedA(Observable& observable) : Observer{observable} { } void Update(int32_t i) override final { std::printf("From DerivedA : %d\n", i * 2); } }; class DerivedB : public Observer { public: DerivedB(Observable& observable) : Observer{observable} { } void Update(int32_t i) override final { std::printf("From DerivedB : %d\n", i * 65536); } };
- オブジェクト間の1対多の依存関係を形成する。
- 「1」とは「多」で共通的に重なる部分である、「値」「多のインスタンスの数」「継承したものの種類」を取り出したものだ。
- しかし多はデータを実際に持てず、「1」のデータが変わると「多」に通知され「多」のデータまたはロジックが実行するようになる。
- 「1」対「多」の関係はいつも「疎結合」(インタフェース)を維持する。そして「多」は異なるタイプの
Observable
に登録することが出来る。
- 「1」対「多」の関係はいつも「疎結合」(インタフェース)を維持する。そして「多」は異なるタイプの
3章
OOPの原則
- 開放・閉鎖原則(Open-Closed Principle):クラスは拡張に対して開かれている状態を維持し、そして変更には閉じた状態を維持するように設計しなければならない。
- SOLID原則の「O」に該当する。
- 「S」はSingle-Principle原則。
- SOLID原則の「O」に該当する。
- 開放・閉鎖原則に従うには拡張手段を提供するような設計をしなければならない。
しかし一般ではすべてのタイプにOCPをやるのは出来ない。 - この本でOCPを従っているパターンはObserverパターンなどになる。
- しかし実際には限界があって、MVCパターンがもっと使わているそう?
Decoratorパターン
Component
:Decoratorパターンのすべてのベースとなるタイプ。
struct Component { Component(int32_t value) : mValue {value} {}; virtual ~Component() = default; virtual int32_t Add() = 0; protected: int32_t mValue = 0; };
Decorator
:Component
から派生した、他のComponent
(複数も可能)を持つようにするタイプ。
名前通りに具象タイプの「装飾」をする。
struct Decorator : public Component { Decorator(Component& component, int32_t value) : Component{value} { this->mComponent = &component; }; virtual ~Decorator() { if (this->mComponent != nullptr) { delete this->mComponent; } } protected: Component* mComponent = nullptr; };
実装例:
struct Component { Component(int32_t value) : mValue {value} {}; virtual ~Component() = default; virtual int32_t Add() = 0; protected: int32_t mValue = 0; }; struct ConcreteComponentA final : public Component { ConcreteComponentA(int32_t value) : Component{value} {}; int32_t Add() override final { return this->mValue; } }; struct ConcreteComponentB final : public Component { ConcreteComponentB(int32_t value) : Component{value} {}; int32_t Add() override final { return this->mValue << 2; } }; struct Decorator : public Component { Decorator(Component& component, int32_t value) : Component{value} { this->mComponent = &component; }; virtual ~Decorator() { if (this->mComponent != nullptr) { delete this->mComponent; } } protected: Component* mComponent = nullptr; }; struct ConcreteDecorator final : public Decorator { ConcreteDecorator(Component& component, int32_t value) : Decorator{component, value} {}; int32_t Add() override final { return this->mComponent->Add() + this->mValue; } }; int main() { Component* instance = new ConcreteComponentA(10); instance = new ConcreteDecorator(*instance, 5); instance = new ConcreteDecorator(*instance, 7); instance = new ConcreteDecorator(*instance, 3); instance = new ConcreteDecorator(*instance, 6); std::printf("Value = %d\n", instance->Add()); // Will be 31. delete instance; }
- このように、Decoratorパターンは追加的な責務を動的に付与する。
- 不必要なサブクラス化を防ぐ。そして実装方式をSum Typeではなく、Product Typeとして扱うようにする。
Component
をインスタンス化するために必要となるコードの複雑さが増加する問題も抱えている。これに対し、Factory
Builder
パターンが役に立つ。すべてのベースとなる
Component
、最終方となるConcreteComponent
タイプら、そしてデコレータになるDecorator
、Decorator
を継承したConcreteDecorator
らこの4つのタイプがパターンを成している。Decorator
タイプは必ずComponent
インタフェースを持つ。こうしてDecorator
に多階層でComponent
をつけることが出来る。- しかし
Decorator
ではないタイプはComponent
参照変数などを持っていない。他のタイプのインスタンスは最終階層となる。 - 実使用例としては、
Java
プログラミング言語のI/O
クラスがこうなっているそうだ。
4章
OOPの原則
- 抽象に依存する。具象くらすに依存しては行けない。
Factoryパターン
Factory
:引数によってインタフェースをもとにしたサブクラスを生成し、返すようにする。まさに「工場」。
template <typename TType, typename TEnum> struct IFactory { virtual std::unique_ptr<TType> Produce(TEnum value) = 0; }; struct PizzaFactory : public IFactory<DPizza, EPizza> { std::unique_ptr<DPizza> Produce(EPizza value) override final { switch (value) { case EPizza::Cheese: return std::make_unique<CheesePizza>(); case EPizza::Combination: return std::make_unique<CombinationPizza>(); case EPizza::Potato: return std::make_unique<PotatoPizza>(); case EPizza::Vagetable: return std::make_unique<VagetablePizza>(); } return nullptr; } };
- PizzaFactoryは4つの🍕を返すようにする。そうするためにはEnumとベースクラスとなる
DPizza
を継承したサブクラスを実装しなければならない。 - 4つのピザはベースクラスに基づきそれぞれ違う挙動をするように実装している。
enum class EPizza { Cheese, Combination, Potato, Vagetable }; struct DPizza { virtual void Print() = 0; }; struct CheesePizza : public DPizza { void Print() override { puts("CheesePizza"); } }; struct CombinationPizza : public DPizza { void Print() override { puts("CombinationPizza"); } }; struct PotatoPizza : public DPizza { void Print() override { puts("PotatoPizza"); } }; struct VagetablePizza : public DPizza { void Print() override { puts("VagetablePizza"); }};
- 実行コード
int main() { PizzaFactory factory{}; factory.Produce(EPizza::Cheese)->Print(); factory.Produce(EPizza::Combination)->Print(); factory.Produce(EPizza::Potato)->Print(); factory.Produce(EPizza::Vagetable)->Print(); return 0; }
CheesePizza CombinationPizza PotatoPizza VagetablePizza
- Factoryパターンは上記の
Produce
メソッドのように、いくつかの引数を受け取ってサブクラス化したインスタンスを生成するようにする。 Produce
メソッドはFactory Method
と呼ぶ。- Factoryパターンの利点は、すべての作成コードを一つのオブジェクトやメソッドに配置することで、重複を避けて保守を1か所に集約する。
- 複雑度のよって依存性が高くなる可能性がある。
Abstract Factoryパターン
- 普通の
Factory
パターンとは違って、Client
が持っている具象FactoryインスタンスをProduct
に渡して実装するようにする。 - または
Factory
から抽象メソッドを実装するようにして、多様な具象Factoryを持つようにすることもアリ。 多くの場合には上のFactory Methodを使ってAbstract Factoryを実装しているそうだ。
Abstract Factoryのやることはインスタンス化するために必要となるインタフェースを定義することである。
そしてそのインタフェースで書かれた各メソッドはインスタンスを生成するのに必要となる役割を担う。- HeadFirst本では店舗ごとに成分が違うピザメニューを生成するために、Factoryタイプをメニューのコンストラクタに入れて成分を構成するようにしている。
Factory MethodとAbstract Factoryの違い?
Factory Method | Abstract Factory | |
---|---|---|
実装方法 | 継承 | コンポジション |
仕様方法 | 独立的に存在し、中で直接生成するようにする。 | ファクトリをインスタンス化して、抽象型に対して書かれたコードに渡して生成する。 |
クライアントからは | クライアントを具象型から分離させる。 | Factory Methodと同じ。 |
具象型の成分が 追加したら |
インタフェースを変更することはない。 | 成分を補うためにインタフェースを変更する。 |
インタフェースのサイズ | 小さめ | 大きめ |
- わからなかったらHeadFirstデザパタ本の160p~161pを見よう。
結論
(2)で5章、6章、7章、8章、9章をまとめたいと思います。
5章はSingletonパターンですが、簡単すぎますので省略します。