#事の起こり
C++のトレーニングついでに、デザインパターンを実装していくという事を最近やっています!!(デザインパターン多すぎて挫折しそう) 本日はアダプター編です。(本記事では、Javaで学ぶデザインパターン入門(著 結城 浩,2001年発行,第3版)を参考にしています。ですので、実装もJavaライクなものとなっています)
#デザインパターンについて
デザインパターンについては前記事C++でデザインパターンを実装する:イテレータ編で説明しているので良かったら見てやってください
#アダプターとは ?
Adapter(アダプター)パターンは、「すでに提供されているもの」を「現在、必要とされるもの」に手直しする為のデザインパターンです。まあ、要は提供APIなんかを自分の環境に合わせて手直しする為のデザインパターンという訳です。
よく使われるプログラマーを煽る言葉として、**「未熟なエンジニアほど車輪の再発明をする」**という言葉があります。要は提供されているものを活用しろ!!再実装なんてするな!!ということです。
この言葉にドキリとしたプログラマー(私も含む)はアダプターを学んで、一つ上の新米エンジニアを目指しましょう!!
#すでに提供されているもの
今回は**「すでに提供されるもの」として次のようなクラスHelloを用意しました。このクラスは"Hello str"と挨拶するメンバ関数Hello_strを持っています。(※あくまで提供物です。これがないとアダプターパターンが作れないわけではありませんよ**)
class Hello{
private:
string str;
public:
//コンストラクタ
explicit Hello(const string& newStr):str(newStr){}
//デストラクタ
~Hello(){}
void Hello_str(void);//提供関数1
};
メンバ関数の実装は次の通りです。
void Hello::hello_str(void){
cout << "Hello" << str <<endl;
}
#提供物をそのまま使う事の危険性
例え話で申し訳ありませんが。次の物語を読んでください
A社提供のHelloクラスを使って、「Hello佐藤さん」を言いまくる商品を作るから、Aくんよろしく。新米プログラマーのA君は上司から言われて、下のような感じでそのままHelloクラスのhello_str()を使いまくった商品を作りました。そして、そしてその商品を売り出したところ、大ヒット!!! 全正解、10000万ダウンロードを記録した歴史的、商品となりました!!!
//何かしらのプログラムの内部
Hello h("佐藤さん");
h.hello_str();
なんかの処理
h.hello_str();
なんかの処理
h.hello_str();
//何かしらのプログラムの内部
。。。と喜んでいるのもつかの間、なんと、Helloクラスのhello_str()にセキュリティ上の重大な欠陥が見つかりました。そこで、A社はセキュリティ対応版のHelloクラスを提供し、hello_str()の代わりにhello_str_safe()を使うように強要!!!
class Hello{
private:
string str;
public:
//コンストラクタ
explicit Hello(const string& newStr):str(newStr){}
//デストラクタ
~Hello(){}
void hello_str(void);//セキュリティに問題が・・・
void hello_str_safe(void);//セキュリティ強化版を実装
};
A君は下記のような過酷な修正作業に追われるのでした。
//何かしらのプログラムの内部
Hello h("佐藤さん");
h.hello_str(); //ここも修正! hello_str_safeに書き換え
なんかの処理
h.hello_str(); //ここも修正!!
なんかの処理
h.hello_str(); //ここも修正!!!
//何かしらのプログラムの内部
このようなことを、未然に防ぐためにどうすればよかったのでしょうか。そう、アダプターパターンを使えばよかったのです
#アダプターを用いたサンプル:①クラス構成
では、アダプターパターン実装していきます。
アダプターは次の3つのクラスで構成されます。
① 提供物 (Helloクラス)
そのままの意味、提供物、A君を苦しめた。
② アダプター (Adapterクラス)
提供物を応用したクラス。
③ インターフェース (Greetクラス)
アダプターを提供するインターフェース
簡単ですね。提供物を応用したものがアダプターなので、Adapterクラスは提供物であるHelloクラスを継承しています。GreetクラスはHelloを応用したAdapterクラスを一般化した抽象クラスです。(少しややこしい)
#アダプターを用いたサンプル:②実装
###Greetクラス(インターフェース)の実装
実際に上のクラス構成に則り、アダプターを実装していきます。まずは、インターフェースであるGreetクラスからです。Greetクラスは抽象クラスなので、仮想関数の定義のみに止めて、実際のgeet_strの実装は行いません。(仮想デストラクタを忘れないようにしましょう。でないと、メモリリークの原因になります)
class Greet{
public:
virtual void greet_str(void) = 0; //仮想関数
virtual ~Greet(){}//仮想デストラクタ
};
###Adaptorクラス(アダプター)の実装
続いて、今回の主役のAdapterクラスを実装します。AdaptorクラスはHelloクラスとGreetクラスを多重継承したものになります。(多重継承するメリットはあまりありません。むしろ、多重継承は危険視されていて、C#などはC++の発展形の言語では多重継承が廃止されていたりします。したがって、Helloクラスだけ継承すれば良いとは思いますが、今回は参考書に則っているので悪しからず。)
class Adaptor:public Hello,public Greet{
public:
explicit Adaptor(const string& newStr):Hello(newStr){}
~Adaptor(){}
void greet_str(void);//提供物を応用した関数
};
メンバ関数、greet_strの実装は次のようになります。見たら分かることですが、greet_strでは提供物であるHelloクラスのメンバ関数hello_strを応用して実装されています。
void Adaptor::greet_str(void){
hello_str();//提供物を応用!!!
}
#アダプターを用いたサンプル:③実際に利用してみる
実際に今回のアダプターを仕様してみます。こんな感じで、提供物であるHelloクラスを機能拡張したGreetクラスが実装できています。
#include <iostream>
#include "myClass.hpp"
int main(int argc, const char * argv[]) {
//Helloクラスをそのまま利用
Hello h("加藤さん");
h.hello_str();//Hello加藤さん と表示される
//アダプターを活用
Greet *a = new Adaptor("加藤さん");
a->greet_str(); //Hello加藤さん
delete(a);
}
#ifndef myClass_hpp
#define myClass_hpp
#include <stdio.h>
#include <iostream>
using namespace std;
class Hello;
class Greet;
class Hello{
private:
string str;
public:
//コンストラクタ
explicit Hello(const string& newStr):str(newStr){}
//デストラクタ
~Hello(){}
void hello_str(void);//提供関数1
};
class Greet{
public:
virtual void greet_str(void) = 0; //仮想関数
virtual ~Greet(){}//仮想デストラクタ
};
class Adaptor:public Hello,public Greet{
public:
explicit Adaptor(const string& newStr):Hello(newStr){}
~Adaptor(){}
void greet_str(void);//提供物を応用した関数
};
#endif /* myClass_hpp */
#include "myClass.hpp"
using namespace std;
void Hello::hello_str(void){
cout << "Hello" << str << endl;
}
void Adaptor::greet_str(void){
hello_str();//提供物を応用!!!
}
#アダプターパターンのメリット
もし仮に、A君がHelloクラスをそのまま使わずにアダプターをしっかり作っていたとします。
そして、A社のHelloクラスのメンバ関数hello_strにセキュリティ上の問題があり、Helloクラスのメンバ関数に新たにhello_str_safeが追加されたとします。A社はhello_str_safeを使うように強要!!!
class Hello{
private:
string str;
public:
//コンストラクタ
explicit Hello(const string& newStr):str(newStr){}
//デストラクタ
~Hello(){}
void hello_str(void);//セキュリティに問題が・・・
void hello_str_safe(void);//セキュリティ強化版を実装
};
しかし、A君はアダプターを使って次のようにプログラムを書いていました。
//何かしらのプログラムの内部
Greet *a = new Adaptor("加藤さん");
a->greet_str();
なんかの処理
a->greet_str();
なんかの処理
a->greet_str();
//何かしらのプログラムの内部
このように書いておけば、a->greet_str()の修正は全て不要になります。なぜならば、Adaptorのhello_strをhello_str_safe()に書き換えるだけで、greet_str()に仕様箇所全てが修正できるからです!!!
void Adaptor::greet_str(void){
hello_str_safe();//ここだけの修正で済む!!!
}
//何かしらのプログラムの内部
Greet *a = new Adaptor("加藤さん");
a->greet_str(); //ここも修正不要!
なんかの処理
a->greet_str(); //ここも修正不要!!
なんかの処理
a->greet_str(); //ここも修正不要!!!!
//何かしらのプログラムの内部
こんな感じで、少しの手間を手間で修正に強く。。。そう考えると、アダプターパターンはすごく便利です。お手軽に使えて変更に強くなるので是非、皆さんにも使ってみて欲しいです。
#まとめ
今回の例は極端な例ですが、実際の現場では提供APIなどは頻繁に変わってきます。特にAPIを提供してくれる企業が超巨大企業だった場合、鶴の一声でAPIの仕様を簡単に変えてしまい、プログラマーが修正にてんやわんやになってしまうこともあるようです(私の先輩談)。こんな時に、面倒くさがらずアダプターを作っておけば、修正の手間を省けるわけですね!!
このように、アダプターパターンは提供APIなどの急な変更に強いプロダクトを作成できるというメリットがあります。これを機に、アダプターパターンを使いまくって、新米エンジニアを脱却しましょう。(ただし、アダプターのアダプターのアダプターみたいな実装はしないように注意しましょう)