PHPの最近のブログ記事
observerとは「観察者」という意味。
Observerパターンは観察対象の状態が変化すると、観察者に
対して変化が通知される。
状態変化に応じた処理を記述するときに有効なパターン。
Observerパターンのクラス図
Observerパターンのサンプルプログラム
今回のサンプルプログラムは結城本のサンプルをPHPにした感じです。数を生成するオブジェクトを観察者が観察して、生成された数字を観察者
毎に異なる形式で表示します。
DigitObserverは数字で表示します。
GraphObserverは「*」の棒グラフで表示します。
・Observer側(Observer.php)
// interface Observer interface Observer { public function update($generator); public function getName(); } // DigitObserver Class // 数字で表示します。 class DigitObserver implements Observer { public function update($generator) { echo "DigitObserver:" . $generator->getNumber() . "\n"; } public function getName() { return 'DigitObserver'; } } // GraphObserver Class // 「*」で棒グラフみたいに出力します。 class GraphObserver implements Observer { public function update( $generator ) { echo " GraphObserver: "; for( $i = 0; $i < $generator->getNumber(); $i++ ) { echo "*"; } echo "\n"; } public function getName() { return 'GraphObserver'; } }
・Subject側(Generator.php)
// Abstract NumberGenerator // 抽象クラス abstract class NumberGenerator { private $observers = array(); public function addObserver( $observer ) { $this->observers[ $observer->getName() ] = $observer; } public function deleteObserver( $observer ) { unset( $this->observers[ $observer->getName() ] ); } public function notifyObservers() { foreach( $this->observers as $val ) { $val->update( $this ); } } abstract public function getNumber(); abstract public function execute(); } // RandomNumberGenerator Class // 具象クラス class RandomNumberGenerator extends NumberGenerator { private $number; public function __construct() { srand(time()); } public function getNumber() { return $this->number; } public function execute() { for ($i = 0; $i < 20; $i++) { $this->number = rand(0, 49); $this->notifyObservers(); } } }※Iteratorパターンは使ってません・・・。
・mainプログラム(main.php)
require "Observer.php"; require "Generator.php"; // 具象クラス $generator = new RandomNumberGenerator(); // 観察者1 $observer1 = new DigitObserver(); // 観察者2 $observer2 = new GraphObserver(); // 観察対象に観察者1を追加 $generator->addObserver($observer1); // 観察対象に観察者2を追加 $generator->addObserver($observer2); // 実行 $generator->execute();実行結果
$ php main.php DigitObserver:34 GraphObserver: ********************************** DigitObserver:1 GraphObserver: * DigitObserver:14 GraphObserver: ************** DigitObserver:9 GraphObserver: ********* DigitObserver:24 GraphObserver: ************************ DigitObserver:36 GraphObserver: ************************************ DigitObserver:18 GraphObserver: ****************** DigitObserver:42 GraphObserver: ****************************************** DigitObserver:12 GraphObserver: ************ DigitObserver:36 GraphObserver: ************************************ DigitObserver:35 GraphObserver: *********************************** DigitObserver:8 GraphObserver: ******** DigitObserver:13 GraphObserver: ************* DigitObserver:22 GraphObserver: ********************** DigitObserver:6 GraphObserver: ****** DigitObserver:36 GraphObserver: ************************************ DigitObserver:20 GraphObserver: ******************** DigitObserver:39 GraphObserver: *************************************** DigitObserver:33 GraphObserver: ********************************* DigitObserver:40 GraphObserver: ****************************************
解説
Observerインターフェースは「観察者」を表現するインターフェースで具体的な
観察者はこのインターフェースを実装します。
NumberGeneratorがupdateメソッドを呼び出します。
updateメソッドはNumberGeneratorが内容の変更をObserverに通知するための
メソッドです。
NumberGeneratorは数を生成する抽象クラスです。
実際の数の生成と数を取得する部分はサブクラスに実装してほしいので
抽象メソッドになってます。
observersプロパティはNumGeneratorを観察しているObserverたちを保存
しています。
addObserverはObserverを追加すメソッド、deleteObserverはObserverを削除
するメソッドです。
notifyObserversメソッドはObserver全員に更新を伝えるメソッドです。
※addObserverでobserversプロパティに登録されているObserver達に通知。
Observerパターンでの役割分担
・Subject(被験者)役
サンプルプログラムでのNumberGeneratorクラスに相当
・ConcreteSubject(具体的な被験者)役
サンプルプログラムでのRandomNumberGeneratorクラスに相当
・Observer(観察者)役
サンプルプログラムでのObserverインターフェースに相当
・ConcreteObserver(具体的な観察者)役
サンプルプログラムでのDigitObserverクラスやGraphObserverクラスに相当
メリット
・具体的な被験者クラスは現在自分を観察しているクラスについて気にする必要が無い。
・具体的な観察者クラスも自分の観察しているについて気にする必要が無い。
・通知したい観察者だけを被験者が登録すればいい。
・観察対象がイベントの監視を一括して行えるように監視義務を委譲できる。
・将来新たにObsesrverを追加する必要が出てきても、Subjectを変更する必要が無い。
デメリット
・updateメソッドが呼ばれる順番が変わっても問題が起きない実装にしなければならない
※Observerの行為がSubjectに影響を与える場合は互いに呼び出しが続く可能性がある
⇒Observer役に「現在Subject役から通知されている最中がどうか」をあらわすフラグを持たせるなどで回避
実装する時の順番
1.Observerの振る舞いを統一する
2.オブザーバに自らを登録させる
3.イベント発生の際、オブザーバに通知する
4.観察対象から追加情報を取得する
Observerパターンって名前がなんか・・・
Observerパターンの解説を読んでいつも気持ち悪いなと思うところ。。
「観察者」と説明しているのに観察対象者側がどの観察者に観察させるかを
選んでいるところ。
観察というよりは変更を教えてもらってる感が非常に強い。
ObserverパターンのObserverは能動的ではなくて受動的。
で、どうもObserverパターンをいう呼び名以外にPublish-Subscribeパターン
と呼ばれることもあるらしい。「Publish」発行と「subscribe」購読ということみたい。
こっちの呼び名のほうがしっくりくると思うんだけど、みんなはどうなんだろ?
MVCとか
Model/View/ControllerのModelとViewの関係はObserverパターンの
Subject役とObserver役の関係に対応しているみたい。
ViewがModelをどのようにみせるか?という役割をもっているのであれば
Modelが変わったんだからViewも変わるはずという意味では納得。
ギコ猫とObserverパターン
位置情報関連をまったく知らないのでWEB+DB PRESS vol.57を読んで現在の主流を
把握する。
以下メモ。
※自分がdocomoケータイしかもって無いのでサンプルプログラム
はdocomoでしか確認してない。。
※スマートフォンの取得方法は今回は書かない。だって試せないんだもん。
店舗やランドマークなど地理空間上に重ねるポイントのことを
POI(Point of Interest)という。
POIは経年変化が激しくデータベースの維持が大変。
食べログとかはCGM(Consumer Generated Media)でメンテナンスしてる。
データベースをAPIで提供しているところも多い。
位置情報サービスを支える技術
・位置情報取得部
・POIデータベース
・サーバインターフェース
・クライアントインターフェース
位置情報取得部
現在の主流
GPS
基地局測位
Wi-Fi測位
POIデータベース
POIデータベースはPOIを登録し、検索や入出力機能を提供するため
のものです。
サーバインターフェース
POIデータベースのデータ入出力をするためのサーバ側APIです
位置情報データのやり取りはOpenGISという標準規格に沿ったものが主流
らしいけど国内のサービスでOpenGISに準拠したものは多くない。
位置情報を表現する規格は沢山あり有名どころではXML形式のGMLやGMLを元に
つくられたGoogleのKML、簡易的な地理空間情報のみ表現可能なGeoRSS、
JSON形式のGeoJSONなどがある。
クライアントインターフェース
クライアントインターフェースは、地図などでPOIデータを表現する部分。
FOSS4G
Free and Open Source Software for Geospatial の略
フリーのオープンソースの地理空間情報ソフトウェアの総称。
PostGIS OpenLayersなど色々
緯度経度について
緯度(latitude)は赤道を0度として南北へそれぞれ90度で表現したものです。
数字で表現する場合北を+南を-とします。
経度(longitude)はグリニッジ天文台跡を南北へ通る子午線を0度とし、東西へ
それぞれ180度で表現したものです。
数字で表現する場合、東を+西を-とします。
緯度経度の一般的な表記として分、秒を60進で表現した度分秒(DMS)単位と、
全てを10進法で示したDegree単位とがつかわれます。
最近はDegreeが多いようです。
DMS単位
・東経139度43分0秒88、北緯35度38分50秒644
・東経139°43′0″88、北緯35°38′50″644
・+139.43.0.88、+35.38.50.644
Degree単位
・東経139.716911度、北緯35.647401度
・+139.716911、+35.647401
同じ場所でも利用する測地系(datum)によって緯度経度の値が変わる。
多くの測地系があるけど日本で気にする必要があるのは「日本測地系」と
「世界測地系(WGS84)」
日本では2002年4月1日まで日本測地系が使われてきた経緯がありまだサービス
として利用しているものもある。
日本測地系をそのまま世界測地系に利用してしまうと400メートル位ズレが
発生する。そのため利用するためには変換する必要がある。
これを測地系変換という。
GPS
見晴らしがよければかなり正確な位置が取得できるが、測位に時間がかかる。
屋内では測位出来ないこともある。純粋にGPS衛星のみ利用する場合と
基地局情報も利用して測位情報を補完するAGPS(Assisted GPS)の2種類ある。
ナビとかはGPSを使った正確な位置情報が必要になる。
基地局測位
GPSが搭載されていない端末でも、基地局情報を元にした三角測位によって
大体の場所を把握できる。
GPS搭載端末でも基地局測位のみを利用することもできる。
場所とか基地局の数とかで大幅に誤差が出ることもある。
その代わりに短時間で位置情報を取得できる。
ある程度広いエリアのタウン情報を表示するような位置情報サイトの場合は
これで十分かも。取得時間という面でユーザビリティの観点からあえてこちらを
採用することもある。
Wi-Fi測位
iPhoneやAndroidなどのスマートフォンで利用されている技術です。
Wi-FiのSSID(Service SetID)及び電波状況と経度緯度を組み合わせた
データベースを利用し位置を割り出しています。
GPSに比べて電池消費量や速度で有利です。
ガラケーでの位置情報取得
いずれのキャリアでも特定のフォーマットでGET/POSTアクセスを行うと、キャリア
のゲートウェイから緯度経度付きのクエリがサーバに送信される。
ドコモ
[GPS]
ドコモでGPSを利用して緯度経度を取得するには<a>タグやフォームに
「lcs」と記述する
サンプル - 例)
<a href="http://example.com/xxx" lcs>位置情報取得</a>
結果 - 例)
GET /xxx?lat=%2b35.00.35.6008&lon=%2B135.41.35.600
&geo=WGS84&x-acc=3
lat/lonが緯度/経度、geoが測地系(WGS84は世界測地系)、x-accが測位レベル
測位レベルは以下
3 ⇒ 水平誤差50m未満
2 ⇒ 水平誤差50m以上300m未満
1 ⇒ 水平誤差300m以上
詳細はドコモのページで。
[オープンiエリア(基地局測位)]
GPSを利用せずに基地局情報を元にした測位結果を返す。
GPSより高速かつすべての端末で利用できるが精度はかなり大雑把。
(周辺の基地局の密集度などによって精度が変わる)
<a>タグやフォームでの記述例
サンプル - 例)
<a href="http://w1m.docomo.ne.jp/cp/iarea?encode=OPENAREACODE&msn=OPENAREAKEY&posinfo=1&nl=http://example.com/xxx">
位置情報取得</a>
結果 - 例) nlで指定したURLに以下の位置情報が送信される
AREACODE:エリアコード
LAT:+dd.mm.ss.sss (緯度)
LON:+ddd.mm.ss.sss (経度)
GEO:wgs84(測地系)
XACC: 1 or 2 or 3 (測位レベル)
POSINFO:1(指定したposinfoの値)
arg1で指定した名前:arg1で指定した値
arg2で指定した名前:arg2で指定した値
au
[EZナビ(GPS)]
<a>タグやフォームでdevice:gpsone?url=URLと指定することで指定したURLに
位置情報が送信される。
サンプル - 例)
<a href="device:gpsone?url=http://example.com/xxx&ver=1&datum=0&unit=0">
位置情報取得</a>
varの値は固定らしい。
datumは測地系で0が世界測地系で1が日本測地系。
unitは緯度経度の単位で0がDMS単位、1がGegree単位。
結果 - 例)
GET /xxx?ver=1&datum=0&unit=0&lat=%2B35.12.34.56&lon=%2B135.12.34.56
&alt=50&time=20020806134715&smaj=100&smin=100&vert=100
&majaa=60&fm=0
altは海抜高度(メートル)、timeは位置情報を取得した時間(日本時間YYYYMMDDHHMMSS)、
smajは水平誤差(メートル)、fmは測位結果の精度に関する情報が0?5の値で
入ります。
smin、vert、majaaはそれぞれ短軸半径誤差、高度誤差、誤差楕円長軸角度らしいけど正確な値は入ってこないらしい・・・。
詳細はauのページで。
[簡易位置情報(基地局測位)]
サンプル - 例)
<a href="device:location?url=http://example.com/xxx">位置情報取得</a>
結果 - 例)
GET /xxx?datum=tokyo&unit=gms&lat=35.43.25.38&lon=135.43.25.38
datum(測地系)にtokyoと入ってくるが必ず世界測地系が返ってくるらしいので
無視しないといけないらしい・・・。
auの注意点は独自パラメータを引き渡せないところ。
EZ番号とかいうのをキーにして一旦DBとかにセッション情報を格納したり、URLを
動的にしてセッションIDを埋め込むとか対策が必要。
ソフトバンク
ソフトバンクはやり方が共通。
[GPS,基地局測位共通]
<a>タグやフォームでlocation:auto?url=URLのように指定すればいいらしい。
サンプル - 例)
<a href="location:auto?url=http://example.com/xxx">位置情報</a>
autoの部分をcellに変えると簡易位置情報のみ、gpsに変えるとGPSのみを利用する
ということみたい。
結果 - 例)
GET /xxx?pos=N35.40.53.00E139.45.57.97&geo=wgs84&x-acr=1
緯度経度はposにDMS単位で入ってくる。
詳細はソフトバンクのページで。
汎用ライブラリ
いままで見てきたようにキャリアごとのコードが違ってめんどくさい。
で、PHPにはPEAR::Net_UserAgent_Mobile_GPS、phpgeomobilejpなど
キャリアの差を吸収してくれるライブラリがあるらしい。
ここまで書いてみてGPS搭載端末じゃなくても位置情報取れるんだ!
ってことを学びました。
ではとりあえず素の実装で位置情報を取得するプログラムを書いてみる。
素のプログラム
index.php
<?php $HTTP_USER_AGENT = $_SERVER['HTTP_USER_AGENT']; function IsDoCoMo() { global $HTTP_USER_AGENT; if( ereg("DoCoMo", $HTTP_USER_AGENT) ) { return true; } else { return false; } } function IsEZ() { global $HTTP_USER_AGENT; if( ereg("KDDI", $HTTP_USER_AGENT) ) { return true; } else { return false; } } function IsSoftBank() { global $HTTP_USER_AGENT; if( ereg("Vodafone", $HTTP_USER_AGENT) ) { return true; } else if( ereg("SoftBank", $HTTP_USER_AGENT) ) { return true; } else { return false; } } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>位置情報サンプル</title> </head> <body> <p>携帯の位置情報を取得するよ!</p> <?php if( IsDoCoMo() ) {?> <form action="http://w1m.docomo.ne.jp/cp/iarea" method="post"> <input type="hidden" name="ecode" value="openareacode" /> <input type="hidden" name="msn" value="openareakey" /> <input type="hiddne" name="nl" value="http://xxxxx.com/mobile/
geo.php" /> <input type="hidden" name="arg1" value="param=hogehoge"/> <input type="hidden" name="arg2" value="guidlax=
<?php echo $HTTP_X_DCMGUID?>" /> <input type="hidden" name="posinfo" value="1" /> <input type="submit" name="ok" value="位置情報取得!" /> </form> <?php } elseif( IsEZ() ) {?> <a href="device:gpsone?url=http://xxxxx.com/mobile/geo.php&ver=1
&datum=0&unit=0">位置情報取得!</a> <?php } elseif( IsSoftBank() ) {?> <a href="location:auto?url=http://xxxxx.com/mobile/geo.php
¶m=hoge">
位置情報取得!</a> <?php } else {?> <p>携帯じゃないよ。</p> <?php echo $_SERVER['REQUEST_URI'] ?> <?php }?> </body> </html>
geo.php
<?php $HTTP_USER_AGENT = $_SERVER['HTTP_USER_AGENT']; function IsDoCoMo() { global $HTTP_USER_AGENT; if( ereg("DoCoMo", $HTTP_USER_AGENT) ) { return true; } else { return false; } } function IsEZ() { global $HTTP_USER_AGENT; if( ereg("KDDI", $HTTP_USER_AGENT) ) { return true; } else { return false; } } function IsSoftBank() { global $HTTP_USER_AGENT; if( ereg("Vodafone", $HTTP_USER_AGENT) ) { return true; } else if( ereg("SoftBank", $HTTP_USER_AGENT) ) { return true; } else { return false; } } $LAT = $_POST['LAT']; $LON = $_POST['LON']; if( IsDoCoMo() or IsEZ() ) { if( ($LAT) and ($LON) ) { $lat = str_replace( "+", "", trim($LAT) ); $lon = str_replace( "+", "", trim($LON) ); } $lat = trim( $lat ); $lon = trim( $lon ); $lat = str_replace( " ", "", $lat ); $lon = str_replace( " ", "", $lon ); } else if( IsSoftBank() ) { $pos = trim( $pos ); $pos_arr = explode( "E", $pos ); $lat = str_replace( "N", "", $pos_arr[0] ); $lon = $pos_arr[1]; } else { $flag = 1; } if( $flag != 1 ) { $lat_arr = explode( ".", $lat ); $lon_arr = explode( ".", $lon ); $lat_m = trim( $lat_arr[0] ) + trim( $lat_arr[1] ) / 60実行結果画面
+ ( trim( $lat_arr[2] ) + trim( $lat_arr[3] ) / 1000 ) / 3600; $lon_m = trim( $lon_arr[0] ) + trim( $lon_arr[1] ) / 60
+ ( trim( $lon_arr[2] ) + trim( $lon_arr[3] ) / 1000 ) / 3600; } else { $lat_m = '34.707785'; $lon_m = '137.73778'; } echo '緯度 -> ' . $lat_m . '<br />'; echo '経度 -> ' . $lon_m . '<br />'; echo sprintf( '<img src="http://maps.google.com/staticmap?center=%s,%s
&markers=%s,%s,redc&zoom=14&size=300x200&maptype=mobile&sensor=false
&key=key" width="300" height="200" class="imageframe"
alt="Google Static Maps" />', $lat_m, $lon_m, $lat_m, $lon_m ); ?>
基地局測位は誤差がでかいな・・・。
やってみた感想
実機で確認しないとわからないから作るの面倒だなと・・・。
でも逆に環境さえそろえてしまえば色々なアプリと組み合わせられる
からアイデアが広がりそうだなーといった感じ。
Mementoとは「記念品」、「形見」といった意味です。
Mementoパターンを利用すると以下が行えるようになります。
- undo(やり直し)
- redo(再実行)
- history(作業履歴の作成)
- snapshot(現在状態の保存)
保存しておく必要があります。だたし、保存しておくだけではダメで保存しておいた
情報からインスタンスを元の状態に戻せないといけません。
インスタンスを復元するにはインスタンス内部の情報に自由にアクセスする
必要があります。しかし不用意にアクセスを許してしまうと、そのクラスの内部構造
に依存したコードを書いてしまいカプセル化を破壊する結果になってしまいます。
インスタンスの状態を表す役割を導入することにより上記のようなカプセル化の破壊
をすることなく保存と復元を行えるのが今回学ぶMementoパターンです。
クラス図
サンプルコード
ファイル分けてみた。
Memento.php
// Memento クラス class Memento { private $money; protected function __construct($money) { $this->money = $money; } public function getMoney() { return $this->money; } }
Game.php
// Game パッケージ require_once 'Memento.php'; class Gamer extends Memento { private $money; public function __construct( $money ) { $this->money = $money; } public function getMoney() { return $this->money; } public function bet() { $dice = mt_rand(1,6); // サイコロ振る echo "サイコロの目は" . $dice . "\n"; if( $dice == 1 ) { // 所持金が100増える $this->money += 100; echo "所持金が100増えました。\n"; } else if( $dice == 2 ) { // 所持金が100減る $this->money -= 100; echo "所持金が100減りました。\n"; } else if( $dice == 4 ) { // 所持金が200増える $this->money += 200; echo "所持金が200増えました。\n"; } else if( $dice == 6 ) { // 所持金が半分になる $this->money = $this->money / 2; echo "所持金が半分になりました。\n"; } else { echo "何も起こりませんでした。\n"; } } public function createMemento() { $mem = new Memento( $this->money ); return $mem; } public function restoreMemento( $mem ) { $this->money = $mem->getMoney(); } }
main.php
// main require_once 'Game.php'; $gamer = new Gamer( 500 ); $mem = $gamer->createMemento(); for( $i = 0; $i < 100; $i++ ) { echo "==============> " . $i . "\n"; $gamer->bet(); echo "所持金は" . $gamer->getMoney() . "円になりました。\n"; if( $gamer->getMoney() > $mem->getMoney() ) { echo "だいぶ増えたので現在の状態を保存しておこう。\n"; $mem = $gamer->createMemento(); } else if( $gamer->getMoney() < ( $mem->getMoney() / 2 ) ) { echo "だいぶ減ったので以前の状態に戻そう。\n\n"; $gamer->restoreMemento($mem); echo "状態復元! 所持金は" . $gamer->getMoney() . "円になりました。\n"; } }
結果はこちら。(長いけどあえてさらす)
$ php main.php ==============> 0 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 ==============> 1 サイコロの目は6 所持金が半分になりました。 所持金は125円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は500円になりました。 ==============> 2 サイコロの目は5 何も起こりませんでした。 所持金は500円になりました。 ==============> 3 サイコロの目は3 何も起こりませんでした。 所持金は500円になりました。 ==============> 4 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 ==============> 5 サイコロの目は2 所持金が100減りました。 所持金は150円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は500円になりました。 ==============> 6 サイコロの目は2 所持金が100減りました。 所持金は400円になりました。 ==============> 7 サイコロの目は6 所持金が半分になりました。 所持金は200円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は500円になりました。 ==============> 8 サイコロの目は1 所持金が100増えました。 所持金は600円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 9 サイコロの目は2 所持金が100減りました。 所持金は500円になりました。 ==============> 10 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は600円になりました。 ==============> 11 サイコロの目は2 所持金が100減りました。 所持金は500円になりました。 ==============> 12 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は600円になりました。 ==============> 13 サイコロの目は5 何も起こりませんでした。 所持金は600円になりました。 ==============> 14 サイコロの目は1 所持金が100増えました。 所持金は700円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 15 サイコロの目は6 所持金が半分になりました。 所持金は350円になりました。 ==============> 16 サイコロの目は1 所持金が100増えました。 所持金は450円になりました。 ==============> 17 サイコロの目は2 所持金が100減りました。 所持金は350円になりました。 ==============> 18 サイコロの目は6 所持金が半分になりました。 所持金は175円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は700円になりました。 ==============> 19 サイコロの目は4 所持金が200増えました。 所持金は900円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 20 サイコロの目は1 所持金が100増えました。 所持金は1000円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 21 サイコロの目は2 所持金が100減りました。 所持金は900円になりました。 ==============> 22 サイコロの目は2 所持金が100減りました。 所持金は800円になりました。 ==============> 23 サイコロの目は6 所持金が半分になりました。 所持金は400円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 24 サイコロの目は6 所持金が半分になりました。 所持金は500円になりました。 ==============> 25 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 26 サイコロの目は5 何も起こりませんでした。 所持金は1000円になりました。 ==============> 27 サイコロの目は6 所持金が半分になりました。 所持金は500円になりました。 ==============> 28 サイコロの目は2 所持金が100減りました。 所持金は400円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 29 サイコロの目は6 所持金が半分になりました。 所持金は500円になりました。 ==============> 30 サイコロの目は5 何も起こりませんでした。 所持金は500円になりました。 ==============> 31 サイコロの目は2 所持金が100減りました。 所持金は400円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 32 サイコロの目は3 何も起こりませんでした。 所持金は1000円になりました。 ==============> 33 サイコロの目は6 所持金が半分になりました。 所持金は500円になりました。 ==============> 34 サイコロの目は1 所持金が100増えました。 所持金は600円になりました。 ==============> 35 サイコロの目は6 所持金が半分になりました。 所持金は300円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 36 サイコロの目は6 所持金が半分になりました。 所持金は500円になりました。 ==============> 37 サイコロの目は6 所持金が半分になりました。 所持金は250円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1000円になりました。 ==============> 38 サイコロの目は4 所持金が200増えました。 所持金は1200円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 39 サイコロの目は6 所持金が半分になりました。 所持金は600円になりました。 ==============> 40 サイコロの目は6 所持金が半分になりました。 所持金は300円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1200円になりました。 ==============> 41 サイコロの目は1 所持金が100増えました。 所持金は1300円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 42 サイコロの目は4 所持金が200増えました。 所持金は1500円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 43 サイコロの目は6 所持金が半分になりました。 所持金は750円になりました。 ==============> 44 サイコロの目は4 所持金が200増えました。 所持金は950円になりました。 ==============> 45 サイコロの目は1 所持金が100増えました。 所持金は1050円になりました。 ==============> 46 サイコロの目は1 所持金が100増えました。 所持金は1150円になりました。 ==============> 47 サイコロの目は4 所持金が200増えました。 所持金は1350円になりました。 ==============> 48 サイコロの目は5 何も起こりませんでした。 所持金は1350円になりました。 ==============> 49 サイコロの目は2 所持金が100減りました。 所持金は1250円になりました。 ==============> 50 サイコロの目は2 所持金が100減りました。 所持金は1150円になりました。 ==============> 51 サイコロの目は1 所持金が100増えました。 所持金は1250円になりました。 ==============> 52 サイコロの目は4 所持金が200増えました。 所持金は1450円になりました。 ==============> 53 サイコロの目は4 所持金が200増えました。 所持金は1650円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 54 サイコロの目は1 所持金が100増えました。 所持金は1750円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 55 サイコロの目は2 所持金が100減りました。 所持金は1650円になりました。 ==============> 56 サイコロの目は3 何も起こりませんでした。 所持金は1650円になりました。 ==============> 57 サイコロの目は6 所持金が半分になりました。 所持金は825円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は1750円になりました。 ==============> 58 サイコロの目は5 何も起こりませんでした。 所持金は1750円になりました。 ==============> 59 サイコロの目は1 所持金が100増えました。 所持金は1850円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 60 サイコロの目は3 何も起こりませんでした。 所持金は1850円になりました。 ==============> 61 サイコロの目は4 所持金が200増えました。 所持金は2050円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 62 サイコロの目は6 所持金が半分になりました。 所持金は1025円になりました。 ==============> 63 サイコロの目は2 所持金が100減りました。 所持金は925円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は2050円になりました。 ==============> 64 サイコロの目は3 何も起こりませんでした。 所持金は2050円になりました。 ==============> 65 サイコロの目は3 何も起こりませんでした。 所持金は2050円になりました。 ==============> 66 サイコロの目は3 何も起こりませんでした。 所持金は2050円になりました。 ==============> 67 サイコロの目は1 所持金が100増えました。 所持金は2150円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 68 サイコロの目は5 何も起こりませんでした。 所持金は2150円になりました。 ==============> 69 サイコロの目は5 何も起こりませんでした。 所持金は2150円になりました。 ==============> 70 サイコロの目は1 所持金が100増えました。 所持金は2250円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 71 サイコロの目は5 何も起こりませんでした。 所持金は2250円になりました。 ==============> 72 サイコロの目は3 何も起こりませんでした。 所持金は2250円になりました。 ==============> 73 サイコロの目は6 所持金が半分になりました。 所持金は1125円になりました。 ==============> 74 サイコロの目は6 所持金が半分になりました。 所持金は562.5円になりました。 だいぶ減ったので以前の状態に戻そう。 状態復元! 所持金は2250円になりました。 ==============> 75 サイコロの目は3 何も起こりませんでした。 所持金は2250円になりました。 ==============> 76 サイコロの目は5 何も起こりませんでした。 所持金は2250円になりました。 ==============> 77 サイコロの目は5 何も起こりませんでした。 所持金は2250円になりました。 ==============> 78 サイコロの目は2 所持金が100減りました。 所持金は2150円になりました。 ==============> 79 サイコロの目は1 所持金が100増えました。 所持金は2250円になりました。 ==============> 80 サイコロの目は2 所持金が100減りました。 所持金は2150円になりました。 ==============> 81 サイコロの目は3 何も起こりませんでした。 所持金は2150円になりました。 ==============> 82 サイコロの目は3 何も起こりませんでした。 所持金は2150円になりました。 ==============> 83 サイコロの目は5 何も起こりませんでした。 所持金は2150円になりました。 ==============> 84 サイコロの目は2 所持金が100減りました。 所持金は2050円になりました。 ==============> 85 サイコロの目は2 所持金が100減りました。 所持金は1950円になりました。 ==============> 86 サイコロの目は4 所持金が200増えました。 所持金は2150円になりました。 ==============> 87 サイコロの目は1 所持金が100増えました。 所持金は2250円になりました。 ==============> 88 サイコロの目は2 所持金が100減りました。 所持金は2150円になりました。 ==============> 89 サイコロの目は1 所持金が100増えました。 所持金は2250円になりました。 ==============> 90 サイコロの目は2 所持金が100減りました。 所持金は2150円になりました。 ==============> 91 サイコロの目は4 所持金が200増えました。 所持金は2350円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 92 サイコロの目は1 所持金が100増えました。 所持金は2450円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 93 サイコロの目は5 何も起こりませんでした。 所持金は2450円になりました。 ==============> 94 サイコロの目は1 所持金が100増えました。 所持金は2550円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 95 サイコロの目は2 所持金が100減りました。 所持金は2450円になりました。 ==============> 96 サイコロの目は1 所持金が100増えました。 所持金は2550円になりました。 ==============> 97 サイコロの目は4 所持金が200増えました。 所持金は2750円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 98 サイコロの目は4 所持金が200増えました。 所持金は2950円になりました。 だいぶ増えたので現在の状態を保存しておこう。 ==============> 99 サイコロの目は4 所持金が200増えました。 所持金は3150円になりました。 だいぶ増えたので現在の状態を保存しておこう。
Mementoの実装解説
Mementoを作る作成者役(Originator)がGamerクラスです。
MementoクラスはOriginatorの内部情報をまとめます。
MementoはOriginatorの内部情報を持っていますが誰にでも
情報を見せるわけではなく、2種類のインターフェースを持ってます。
MainはCaretaker役といわれ現在のOriginator役の状態を保存したい
ときにそのことをOriginatorに伝えます。するとOriginatorはMementoを
つくりCaretakerに渡します。
Mementoの持つ2つのインターフェース
・wide interface
広い?インターフェースOriginatorだけに内部状態をさらけ出している
Mementoクラスの__construct
・narrow interface
狭い?インターフェースCaretaker役に対して参照させるだけ
MementoクラスのgetMoney
メリット
・undo、redo、history、snapshotなどの実装に使える
・MementoクラスがOriginator以外からはブラックボックスとなり情報の隠蔽ができる
カプセル化を破壊しない
・CaretakerとOriginatorを分けて役割分担を行うことでOriginatorを変更する必要がなくなる
※Caretakerがいつスナップショットを取るかを指示し保持も行う
OriginatorはMementoを作る役と復元するだけ
デメリット
・大量のMementoを保持するとメモリを大量に消費する(差分や圧縮して保存しないと)
・必要がなくなったときにちゃんとMementoを処理しないと残ったままになる
使いどころ
undo、redo、history、snapshot
Commandパターンとともに利用しundo、redoを実装するなど
ギコ猫とMementoパターン
同じものを共有して無駄をなくすパターンです。
どこの無駄を無くすのかというとメモリの使用量です。
インスタンスをできるだけ共有して無駄にnewしないことでインスタンスを
沢山作らせない→メモリの使用量を減らすということです。
クラス図
サンプルコード
class BigChar { private $charName; // 文字の名前 private $fontdata; // 大きな文字を表す文字列 public function __construct($charName) { $this->charName = $charName; try { $fileName = "big" . $this->charName . ".txt"; if (!file_exists($fileName)) { throw new Exception(); } $this->fontdata = implode("", file($fileName)); } catch (Exception $e) { $this->fontdata = $this->charName . "?"; } } public function prints() { echo $this->fontdata; } } class BigCharFactory { private $pool = array(); private static $singleton; private function __construct(){} // 外からnewさせない public static function getInstance() { if (!is_object(BigCharFactory::$singleton)) { BigCharFactory::$singleton =& new BigCharFactory(); } return BigCharFactory::$singleton; } public function getBigChar($charName) { $bc = isset($this->pool[$charName]) ? $this->pool[$charName] : null; if ($bc == null) { $bc = new BigChar($charName); $this->pool[$charName] = $bc; } return $bc; } } class BigString { private $bigChars = array(); public function __construct($string) { $factory = BigCharFactory::getInstance(); for ($i = 0; $i < strlen($string); $i++) { $this->bigChars[$i] = $factory->getBigChar(substr($string, $i, 1)); } } public function prints() { for ($i = 0; $i < count($this->bigChars); $i++) { $this->bigChars[$i]->prints(); } } } if ($argc < 2) { echo "Usage: php flyweight.php digits\n"; echo "Example: php flyweight.php 1212123\n"; exit; } $bs = new BigString($argv[1]); $bs->prints();実行結果
$ php self_sample.php 123 ......##........ ..######........ ......##........ ......##........ ......##........ ......##........ ..##########.... ................ ....######...... ..##......##.... ..........##.... ......####...... ....##.......... ..##............ ..##########.... ................ ....######...... ..##......##.... ..........##.... ......####...... ..........##.... ..##......##.... ....######...... ................
利用者は Flyweight
クラスにあたるインスタンスを取得する場合に、直接その
クラスのコンストラクタを呼び出す代わりに BigCharFactory#
getBigChar()
にアクセスする。 一方、呼び出された
BigChar
Factory
オブジェクトは、状況に
応じて振る舞いを変える。
その時点で対象のインスタンスが生成されていない場合- 対象のインスタンスを新たに生成する。
- 生成したインスタンスをプールする(言い換えると、メンバのコンテナオブジェクトに格納する)。
- 生成されたインスタンスを返す。
- 対象のインスタンスをプールから呼び出す。
- 対象のインスタンスを返す。
このように BigChar
Factory
では状況によって処理は違うけれど、利用者側が
得る結果は全く同じなので、利用者は BigChar
Factory
の内部構造を意識せず
に使うことが出来る。
メリット
オブジェクトを生成する処理をクライアント側から隠蔽することができます。
インスタンスを使いまわすのでメモリ消費量が抑えられる。
newの回数が減らせるのでプログラムのパフォーマンスもあげられる。
デメリット
Flyweight Objectはどこから呼ばれるかわからないので状態を持ってはいけない。
システム全体で、使いまわすインスタンスの把握が必要。
一度
FlyweightFactory
に保存されたインスタンスは、たとえ不要になった場合でもガベージコレクションされることがないため、場合によっては明示的に
FlyweightFactory
から削除する必要がある。
使いどころ
Flyweight パターンを採用すべき典型的な例は、不変なクラスを扱う場合。
不変なクラスとはインスタンスが生成された後にそのインスタンスの状態が変化しないようなクラス。
対象が不変でないクラスの場合は、その状態が変更されている場合に再利用できないのでこのパターンに適しません。
ギコ猫とFlyweightパターン
とりあえず今回は社内勉強用資料として。
自分の担当はAbstractFactoryパターン。
AbstractFactory = 「抽象的な工場」・・・。
名前だけでは一体何がうれしいのかまったくわかりませんね。
デザパタ本での解説は関連する部品を組み合わせて製品を作るとなっています。
その他では・・・
インスタンスの生成を専門に行うクラスを用意することで、
整合性を必要とされる一連のオブジェクト群を間違いなく生成するパターン
「関連、または依存しあうオブジェクトのファミリを、その具象クラスを指定すること無しに生成する
インターフェースを提供する」
とか言われています。
イメージ図はこんな感じ。
// ペットボトル工場の抽象クラス abstract class abstractPlasticBottleFactory { abstract public function makeContent(); abstract public function makeBottle(); } // オレンジ工場(具象クラス) class orangeFactory extends abstractPlasticBottleFactory { public function makeContent() { return new orangeContent(); } public function makeBottle() { return new orangeBottle(); } } // コーラ工場(具象クラス) class colaFactory extends abstractPlasticBottleFactory { public function makeContent() { return new colaContent(); } public function makeBottle() { return new colaBottle(); } } // 中身の抽象クラス abstract class Content { abstract public function createContent(); } class orangeContent extends Content { public function createContent() { return 'Create Orange Juice'; } } class colaContent extends Content { public function createContent() { return 'Create Cola'; } } // ボトルの抽象クラス abstract class bottle { abstract public function createBottle(); } class orangeBottle extends bottle { public function createBottle() { return 'Create Orange Bottle'; } } class colaBottle extends bottle { public function createBottle() { return 'Create Cola Bottle'; } } // クライアントクラス class createFactory { private $content=null; private $bottle=null; function __construct( abstractPlasticBottleFactory $factory ) { $this->content = $factory->makeContent(); $this->bottle = $factory->makeBottle(); } public function create() { echo $this->content->createContent() . "\n"; echo $this->bottle->createBottle() . "\n"; } } // オレンジジュース $orange_factory = new createFactory( new orangeFactory ); $orange_factory->create(); // コーラ $cola_factory = new createFactory( new colaFactory ); $cola_factory->create();
実行すると・・・。
Create Orange Juice Create Orange Bottle Create Cola Create Cola Bottleサンプルの例がなんかおかしい気もしますが。。
今回のサンプルでは「abstractPlasticBottleFactory」が工場の抽象クラスとなり、
「abstractPlasticBottleFactory」を継承する工場の具象クラスの実装すべきメソッドを定義しています。
工場の具象クラスでは利用すべきオブジェクト(材料)を用意し独自に実装を行えます。
工場の具象クラスで利用する各材料に関しても抽象クラスを定義して
おくことによりクライアントプログラム対してどの工場を利用するのかだけ指定すれば
一連のオブジェクト群を間違いなく生成することができます。
今回の例は「Orange」と「Cola」の2つだけですが、具象クラスを同様に増やすことで
工場のバリエーションを増やすことができます。
利用する側はどんな工場が存在するのかさえ知っていればどの工場でも
同じ呼び出し方法で利用可能です。
(※工場内で何が起きているかは知らなくても使える)
メリット
・具象クラスに処理をまとめることができる
・利用する材料の組み合わせを各工場で変更できる
・クライアントクラスが再利用可能
デメリット
・抽象クラスの工場で新たにメソッドを定義した場合に抽象クラスの工場を継承する全ての具象工場で
変更が必要になる。
で、↑のデメリットに対して抽象クラスの工場でデフォルトの指定をしておけばいいじゃないか?
という意見もあります。。
確かにそうすれば全ての具象クラスで処理を追加する必要も無くなる。
でもそれって工場の種類が増えた場合に、具象クラスでメソッドの実装を忘れたら
意図しない結果が返ってくる可能性もあるのではなかろうか?
材料の組み合わせを各具象クラスが責任を持って指定していないのだから。。
そのときの要件によってどっちがいいのか変わるようなきがする・・・。