design pattern - sinyamada/note/comp/design_pattern.pdf ·...

44
Design pattern Masanori Yamada

Upload: vuongdien

Post on 16-Apr-2018

218 views

Category:

Documents


2 download

TRANSCRIPT

Design patternMasanori Yamada

デザインパターンとは?

ソフトウェア開発におけるデザインパターンとは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

まぁ知ってると得するテクニック集みたいなもの (ただし概念なので、実装は言語に依存する。こだわり過ぎも良くない)

余談:ある問題に対する、不適切な解決策を分類したものである アンチパターンというものも存在する

有名なGOFの23パターン

有名なGOFの23パターン

再利用性と可読性があがるような効果的なやつから ピックアップしていく

Template Patternデザインパターンの基本

Template method patternプログラムの流れを抽象的に基底クラスで作り、 具体的な処理は派生クラスを使い実装する

基底 派生

派生でオーバーライド (ここでは仮想関数) ※必ずprotected

Template method pattern

メリット

デメリット

• 再利用性の向上 • シンプル(流れが見やすい)

• 継承は関連が強すぎる(基底クラスの変更は全ての派生クラスに影響する)

実装例 c++ (あまりいい例ではないかも・・・)

#include <iostream> !void init(int* ary, int size) { for (int i = 0; i < size; ++i) ary[i] = i; } !void mul10(int* ary, int size) { for (int i = 0; i < size; ++i) ary[i] *= 10; } !void dispAry(int* ary, int size) { for (int i = 0; i < size; ++i) std::cout << ary[i] << std::endl; } !!int main(void) { const int LENGTH = 10; int array[LENGTH]; ! init(array, LENGTH); dispAry(array, LENGTH); mul10(array, LENGTH); dispAry(array, LENGTH); !}

#include <iostream> !class ArrayProcessor { public: void doProcess(int* ary, int size) { for (int i = 0; i < size; ++i) proc(ary[i], i); } protected: // この関数をオーバーライドすることで派生クラスによって処理を切り替えることができる virtual void proc(int& element, int index) = 0; }; !!class ArrayInitializer : public ArrayProcessor { protected: void proc(int& element, int index) { element = index; } }; !class ArrayMultiplier : public ArrayProcessor { protected: void proc(int& element, int index) { element *= 10; } }; !class ArrayDisplay : public ArrayProcessor { protected: void proc(int& element, int index) { std::cout << element << std::endl; } }; !//派生クラスの型に合わせてdoProcessの処理が変化 void proc(ArrayProcessor& proc, int* ary, int size) { proc.doProcess(ary, size); } !// main int main(void) { const int LENGTH = 10; int array[LENGTH]; !! ArrayInitializer init; ArrayDisplay dis; ArrayMultiplier multi; ArrayDetailDisplay del; ! proc(init, array, LENGTH); proc(dis, array, LENGTH); proc(multi, array, LENGTH); proc(del, array, LENGTH); ! return 0; } !!

テンプレートパターンを使ってない テンプレートパターンを使う

いくつかクラスを作ったら、同じような処理が出てきたので、共通化して、基底クラスにまとめた。これは良くある話だが、やり方によっては、Template Methodパターンになる場合と、まったく逆になる場合がある。 !Template Methodパターンは、基底クラスに処理の流れを書き、基底クラスから派生クラスのメソッドを呼び出す。だが時には、これとは逆に、派生クラスに処理の流れを書いて、派生クラスから基底クラスのメソッドを呼び出してしまっている例も見られる。 Template Methodパターンでは、基底クラスは、基本となる処理の流れを規定するもの、として、役割が明確である。一方、これが逆になってしまった例では、親クラスはたいがい、単なるいろいろな処理の寄せ集めに過ぎず、継承関係に意味がなくなっている。 !派生クラスから基底クラスのメソッドを呼び出している時は、設計がおかしくないか、疑ってみると良い。

逆は危険(読み飛ばして下さい)

Tempete Method Patternの応用

Strategy Patternアルゴリズムをmain文だけから動的に切り替える (愚直に書くよりmainがすっきり切り替えも楽)

Strategy patternTemplate method patternにcontext(文脈)を加えることで、 mainでの再利用が見やすくなる

基底

派生

派生でオーバーライド (ここでは仮想関数) ※必ずprotected

Template method patternにContextがくっついた

templete method patternを委譲で呼び出すようにしたもの

Strategy pattern

メリット

デメリット

• Template methodより高い再利用性 • main文がかなり見やすい

• クラスの数が多くて複雑

Template patternとStrategy patternの違い シーケンス図

Template pattern Strategy pattern

クラスの数は多いが役割分担がはっきりしてきりしてる

Template patternとStrategy patternの違い クラス図

Template pattern

Strategy pattern

ここを再利用

実装例 c++ (template pattern)

#include <iostream> !class Prop { public: void propcalc(int* ary, int size) { for (int i = 0 ; i<size ; i++){ calc(ary[i], i); } } protected: // この関数をオーバーライドすることで派生クラスによって処理を切り替えることができる virtual void calc(int& element, int index) = 0; }; !!//ここから派生クラスで実装して使用するときは基底クラスから呼び出す class PropInitializer : public Prop { protected: void calc(int& element, int index) { element = index; } }; !class PointSource : public Prop { protected: void calc(int& element, int index) { element *= 10; } }; !class WallSource : public Prop { protected: void calc(int& element, int index) { element *= 100; } }; !class PropGet : public Prop { protected: void calc(int& element, int index) { std::cout << element << std::endl; } }; !!!//派生クラスの型に合わせてpropcalcの処理が変化 void propagetor(Prop& obj_prop, int* ary, int size) { obj_prop.propcalc(ary, size); }

// main int main(void) { const int LENGTH = 10; int array[LENGTH]; !! PropInitializer init; PropGet get; WallSource wall; PointSource point; !! propagetor(init, array, LENGTH); propagetor(wall, array, LENGTH); propagetor(get, array, LENGTH); ! propagetor(init, array, LENGTH); propagetor(point, array, LENGTH); propagetor(get, array, LENGTH); ! return 0; }

実装例 c++ (strategy pattern)

void init() { for (int i =0 ; i< size_ ;i ++){ array_[i] = i; } } !void PropGet() { for(int i =0; i<size_ ;i ++){ std::cout << array_[i] << std::endl; } } !//派生クラスの型に合わせてpropcalcの処理が変化 ! void calc(){ obj_prop_->propcalc(array_,size_); } !!private: Prop* obj_prop_; int* array_; int size_; !}; !!!// main int main(void) { const int LENGTH = 10; ! Context* obj1 = new Context(new WallSource(),LENGTH); Context* obj2 = new Context(new PointSource(),LENGTH); obj1->init(); obj1->calc(); ! obj2->init(); obj2->calc(); ! obj1->PropGet(); obj2->PropGet(); ! return 0; }

#include <iostream> !class Prop { public: void propcalc(int* ary, int size) { for (int i = 0 ; i<size ; i++){ calc_src(ary[i], i); } } protected: // この関数をオーバーライドすることで派生クラスによって処理を切り替えることができる virtual void calc_src(int& element, int index) = 0; }; !!//ここから派生クラスで実装して使用するときは基底クラスから呼び出す class PropInitializer : public Prop { !}; !class PointSource : public Prop { protected: void calc_src(int& element, int index) { std::cout<<"calc point src" << std::endl; element *= 10; } }; !class WallSource : public Prop { protected: void calc_src(int& element, int index) { std::cout<<"calc wall src" << std::endl; element *= 100; } }; ! // context classによってインターフェイス(Prop)にアクセス class Context{ public: //コンストラクタ \ Context(Prop* obj_prop, int size){ obj_prop_ = obj_prop; size_ = size; array_ = new int[size_](); } //デストラクタ \ ~Context(){ delete [] array_; free (obj_prop_;) }

感想

• strategyはクラスがごちゃるが、mainは非常に見やすくなる

• のちのち拡張を考えてプログラミングするならstrategyを使った方が良さそう(複雑になるので非推奨と書いてある文献もあった)

Factory Method Pattern

Factory Method Pattern基底クラスで処理の骨組みを作り、派生クラスで実装するパターンである Template method patternをインスタンス生成に応用したもの

基底

派生

派生でオーバーライド (ここでは仮想関数) ※必ずprotected

インスタンスの生成をするメソッドをオーバーライド

Tempete Method Pattern の応用 終わり

Bridge Pattern

2種類の派生の橋渡し

継承するメリットのおさらい

基底クラス

派生クラス

骨組み(機能の規定)

具体的な中身(機能の実装)

派生クラス 機能の追加

この2つの派生は区別されるべき物 骨組みと中身をつなぐ派生(機能クラスの階層) 機能をさらに追加するための派生(実装クラスの階層)

一般的に深い階層構造は複雑なのでよくない

2タイプの異なる派生の分離と橋渡し

Bridge Pattern

2タイプの派生を分離する(ただ分離すると関係が分からなくなるのでBridge Patternで橋渡しする)

基底クラスで抽象メソッドによってインタフェース(API)を規定

派生クラスの具象メソッドによってインタフェース(API)を実装

機能の階層

実装の階層

派生クラスの具象メソッドによってインタフェース(API)を実装を拡張

機能の階層

実装の階層

Bridge Pattern2つの派生の仕方を分離して委譲で関係をつける

機能 実装委譲で関係をつける

基底クラス

派生クラス

骨組み(機能の規定)

具体的な中身(機能の実装)

派生クラス 機能の追加

Bridge Pattern の威力Before

After

実装例 c++ (Bridge pattern)#include <iostream> #include <string> !/* Implemented interface. */ class AbstractInterface { public: virtual void someFunctionality() = 0; }; !/* Interface for internal implementation that Bridge uses. */ class ImplementationInterface { public: virtual void anotherFunctionality() = 0; }; !/* The Bridge */ class Bridge : public AbstractInterface { protected: ImplementationInterface* implementation; public: Bridge(ImplementationInterface* backend) { implementation = backend; } }; !/* Different special cases of the interface. */ !class UseCase1 : public Bridge { public: UseCase1(ImplementationInterface* backend) : Bridge(backend) {} ! void someFunctionality() { std::cout << "UseCase1 on "; implementation->anotherFunctionality(); } }; !class UseCase2 : public Bridge { public: UseCase2(ImplementationInterface* backend) : Bridge(backend) {} ! void someFunctionality() { std::cout << "UseCase2 on "; implementation->anotherFunctionality(); } };

/* Different background implementations. */ !class Windows : public ImplementationInterface { public: void anotherFunctionality() { std::cout << "Windows :-!" << std::endl; } }; !class Linux : public ImplementationInterface { public: void anotherFunctionality() { std::cout << "Linux! :-)" << std::endl; } }; !int main() { AbstractInterface *useCase = 0; ImplementationInterface *osWindows = new Windows; ImplementationInterface *osLinux = new Linux; !! /* First case */ useCase = new UseCase1(osWindows); useCase->someFunctionality(); ! useCase = new UseCase1(osLinux); useCase->someFunctionality(); ! /* Second case */ useCase = new UseCase2(osWindows); useCase->someFunctionality(); ! useCase = new UseCase2(osLinux); useCase->someFunctionality(); ! return 0; } !

機能の階層

実装の階層

UseCase1 on Windows :-! UseCase1 on Linux! :-) UseCase2 on Windows :-! UseCase2 on Linux! :-)

UseCase1;実態useCase;API

Windows;追加の実態

Bridge Pattern機能 実装

#include <iostream> #include <string> !/* Implemented interface. */ class AbstractInterface { public: virtual void someFunctionality() = 0; }; !/* Interface for internal implementation that Bridge uses. */ class ImplementationInterface { public: virtual void anotherFunctionality() = 0; }; !/* The Bridge */ class Bridge : public AbstractInterface { protected: ImplementationInterface* implementation; public: Bridge(ImplementationInterface* backend) { implementation = backend; } }; !/* Different special cases of the interface. */ !class UseCase1 : public Bridge { public: UseCase1(ImplementationInterface* backend) : Bridge(backend) {} ! void someFunctionality() { std::cout << "UseCase1 on "; implementation->anotherFunctionality(); } }; !class UseCase2 : public Bridge { public: UseCase2(ImplementationInterface* backend) : Bridge(backend) {} ! void someFunctionality() { std::cout << "UseCase2 on "; implementation->anotherFunctionality(); } };

/* Different background implementations. */ !class Windows : public ImplementationInterface { public: void anotherFunctionality() { std::cout << "Windows :-!" << std::endl; } }; !class Linux : public ImplementationInterface { public: void anotherFunctionality() { std::cout << "Linux! :-)" << std::endl; } }; !int main() { AbstractInterface *useCase = 0; ImplementationInterface *osWindows = new Windows; ImplementationInterface *osLinux = new Linux; !! /* First case */ useCase = new UseCase1(osWindows); useCase->someFunctionality(); ! useCase = new UseCase1(osLinux); useCase->someFunctionality(); ! /* Second case */ useCase = new UseCase2(osWindows); useCase->someFunctionality(); ! useCase = new UseCase2(osLinux); useCase->someFunctionality(); ! return 0; } !

まとめ

• 拡張性の高さがポイント

• 「橋渡し」のクラスを用意することによって、クラスを複数の方向に拡張させることを目的とする。

Singleton Pattern

クラスに対してオブジェクトは1つのみ

オブジェクトは1つSingleton Pattern

クラスに対してオブジェクト1つにしたい時

使いどころ

例:仮にintクラスみたいなのを作ったときに、int x int yは自動的に必ず等しいものになるようにする(globalな変数が作れる、故に危険でもある)

Singleton Pattern• 同じ型のインスタンスが private なクラス変数として定義されている。 • コンストラクタの可視性が private である。 • 同じ型のインスタンスを返す getInstance() がクラス関数として定義されている

Singleton pattern

メリット

デメリット

• インスタンスを1つに限定できる。 • 無駄にインスタンスを作って実行効率が落ちるのを防げる

• 常に同じインスタンスにアクセスできるので、グローバル変数と同じ扱いになり、危険

実装例 c++ (Singleton pattern)

#include <iostream> class Singleton{ public: // 唯一のアクセス経路 static Singleton* GetInstance() { static Singleton instance; // 唯一のインスタンス return &instance; } ! // あとは、このクラス自身が必要なメンバを定義する !private: // 生成やコピーを禁止する Singleton(){} Singleton(const Singleton& rhs); Singleton& operator=(const Singleton& rhs); }; !!!int main(){ ! Singleton* obj1 = obj1->GetInstance(); Singleton* obj2 = obj2->GetInstance(); !! if(obj1==obj2){ std::cout << "same instance"<<std::endl; } return 0; }

newで作ってもいいが、deleteするタイミングが分かんなくなるので、static生成する方が多い

Adapter Pattern

変換コネクタで利用しやすく

Adapter Pattern

他人が作ったプログラムを使うとき、自分のプログラムの呼び出し方に合わせたい時(他人のプログラムから必要なとこだけが見えるようにしてシンプルに)

使いどころ

例:

Adapter patternAdapterクラスを挟んで変換コネクタ替わりにして 呼び出すクラスを変更せずにインターフェイスだけを変更する

継承を利用 委譲を利用2パターンの実装がある(どっちもあんまかわんない)

利用したいクラス(Adaptee)の派生クラス(Adapter)を作成し、そのAdapterに対してインタフェース(Target)を実装することで実現される。

利用したいクラス(Adaptee)のインスタンスを生成し、そのインスタンスを他クラスから利用することで実現される。

呼び出し

呼び出される

呼び出し

呼び出される

Adapter pattern

メリット

デメリット

• もともとあるクラス(Adaptee)に変更入れないので、気軽に再利用しやすい

• インターフェイスの数を減らせる

• 特に無いが、継承で実装すると多重継承になるのがちょっと嫌かも

実装例 c++ (Adapter pattern 委譲)#include <cstring> #include <iostream> using namespace std; !//Adaptee class Banner { public : Banner(string str){ this->string_ = str; } void ShowWithParen(){ cout << string_.c_str() << endl; } void ShowWithAster(){ cout << "*" << string_.c_str() << "*" << endl; } private : string string_; }; //Target(interface) class Print { public : virtual ~Print(){}; virtual void PrintWeak() = 0; virtual void PrintStrong() = 0; }; //Adapter class PrintBanner : public Print { public : ~PrintBanner(){ delete banner_; } PrintBanner(string str) { banner_ = new Banner(str); } void PrintWeak(){ banner_->ShowWithParen(); } void PrintStrong(){ banner_->ShowWithAster(); } private : Banner * banner_; };

int main() { Print *p = new PrintBanner("Hello"); p->PrintWeak(); p->PrintStrong(); delete p; getchar(); return 0; } !

利用したいクラスのインスタンスを作成

自分の好きなインターフェイスを仮想で作る(こうするとAdapterの中身を知らなくても使える)

変換を実装

Mediator Pattern

複雑なクラス関係を整理する

クラスのやりとりを1つに集中させて可読性をあげたい クラス間の結びつきを緩くしたい

使いどころ

Mediatorクラス 仲介者のクラスです。Colleagueクラスからの通知を受け取るメソッドを定義します。 ConcreteMediatorクラス Mediatorクラスのサブクラスで、Mediatorクラスで定義されたメソッドを実装します。・Colleagueクラス Mediatorクラスに通知をおこなうクラスです。「colleague」とは「同僚」「仲間」という意味です。Mediatorクラスからの通知を受け取るメソッドを定義する場合もあります。 ConcreteColleagueクラス Colleagueクラスのサブクラスです。内部にMediatorオブジェクトを保持しており、このMediatorオブジェクトを通じて他のConcreteColleagueクラスとやりとりをおこないます。

Mediator patternクラス間のやりとりは必ずMediatorを通すことでMediator(仲介者)に 交通整理をやらせる

あんまクラス図の読み方にこだわるのは、よくないので矢印の意味は継承かそうでないかくらい覚えとけばいい

継承

呼び出し

継承