Strategy
Rubyによるデザインパターン生活2日目。
今日はStrategyパターンを学びます。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
アルゴリズムの一部を変化したい場合の対処法としてTemplete methodを習いました。
Templete methodではベースとなるアルゴリズムを実装したスーパークラスを継承しなくてはなりません。
Rubyは単一継承しかできないので、継承を頻繁に利用することは好ましくありません。
可能であれば、何事も継承より委譲で実装すべきです。
そこで、Strategyパターンの出番ですよ、と。
Strategyパターンは委譲によりアルゴリズムを切り替えることができます。
それでは、コードを見ていきます。
登場するクラス
- Reportクラス
- レポート出力するクラス。出力処理の詳細アルゴリズムを委譲する。
- HTMLReportFormatクラス
- HTMLレポート出力アルゴリズムを実装したクラス。
- PlainReportFormatクラス
- プレーンレポート出力アルゴリズムを実装したクラス。
まずはReportクラス。
class Report def initialize(format) @title = "日記" @text = ["今日は大阪に行ってきました。", "天気は晴れでした。"] @format = format end def output_report @format.print(@title, @text) end end
コンストラクタに出力フォーマット用のオブジェクトを受け取ります。
output_reportメソッドではフォーマット用のオブジェクトに処理を委譲しています。
次にHTMLReportFormatクラスとPlainReportFormatクラスです。
class HtmlReportFormat < Format def initialize() end def print(title, text) puts "<html>" puts "<head><title>#{@title}</title></head>" puts "<body>" text.each { |line| puts "<p>" + line + "</p>"} puts "</body>" puts "</html>" end end
class PlainReportFormat < Format def initialize end def print(title, text) puts "***** #{title} *****" text.each { |line| puts line } end end
実行するためのプログラムです。
puts "HTML REPORT--------" report = Report.new(HtmlReportFormat.new) report.output_report puts "PLAIN REPORT--------" report = Report.new(PlainReportFormat.new) report.output_report
そんで実行結果です。
HTML REPORT-------- <html> <head><title></title></head> <body> <p>今日は大阪に行ってきました。</p> <p>天気は晴れでした。</p> </body> </html> PLAIN REPORT-------- ***** 日記 ***** 今日は大阪に行ってきました。 天気は晴れでした。
とまぁ実行結果は前回と同じです。ちょっとソースを解説してみます。
RubyはJavaと違って厳密な型定義は必要としません。
そのため、HTMLReportFormatクラス、PlainReportFormatクラスにスーパークラスを定義する必要がありません。*1
Strategyパターンでは、アルゴリズムを委譲する側のクラス(Reportクラス)をContextと呼び、
アルゴリズムを実装する側のクラス(HTMLReportFormatクラス、PlainReportFormatクラス)をStrategyと呼ぶようです。
また、今回はCotextからStrategyへパラメータを受け渡す際に、
def output_report @format.print(@title, @text) end
のように2つの引数を渡しています。
これは次のように置き換えても構いません。*2
def output_report @format.print(self) end
これはStrategyの引数に自身を示すオブジェクトを渡すことで、
引数をまとめちゃうテクニックです。
この場合のトレードオフとして、ContextとStrategy間の結合度が強くなってしまいます。
で、RubyのStrategyパターンのお話はここまでで前半戦終了です。
JavaのエンジニアならここまででOK!な気持ちになりますが、
RubyだとこんなふうにStrategyをコードブロックで渡すこともできます。
Reportクラスを次のように変更します。
class Report attr_reader :title, :text def initialize(&format) @title = "日記" @text = ["今日は大阪に行ってきました。", "天気は晴れでした。"] @format = format end def output_report @format.call(self) end end
コンストラクタにコードブロックを引数にとるように修正しました。
すると、呼び出しもとでStrategyを作成することができます。
呼び出しもとプログラム
require "report" #format HTML_FORMATTER = lambda do |context| puts "<html>" puts "<head><title>#{context.title}</title></head>" puts "<body>" context.text.each { |line| puts "<p>" + line + "</p>"} puts "</body>" puts "</html>" end PLAIN_FORMATTER = lambda do |context| puts "***** #{context.title} *****" context.text.each { |line| puts line } end puts "HTML REPORT--------" report = Report.new &HTML_FORMATTER report.output_report puts "PLAIN REPORT--------" report = Report.new &PLAIN_FORMATTER report.output_report
なんとなくRubyっぽいプログラムになりました。
実行結果はさきほどと同じになります。
Rubyの場合は実行時にStrategyアルゴリズムを動的に作ることもできそうです。
勉強になりました。
円運動とFlickrと
この週末は移動時間がけっこうあったので、楽しくコーデイングしてました。
今回やってみたのは、
ほんで結果はこんなの。
PV3Dも雰囲気がわかってきたので、
これまでActionScript3.0アニメーションで習った知識を試してみました。
#sin、cosを使って円運動させてみただけ。
Flickr APIも初めて使ったけど、そんなに難しくなかったです。
api_keyをゲットすればあとはFlickrが返してくれるXMLを解析していけばOKでした。
ちなみにASにはFlickr用のライブラリもあるみたいだけど、まずは力技でやってみました。
ASはXMLと相性がいいから、ライブラリ使わなくてもどうにかなりました。
package { import flash.display.Bitmap; import flash.display.Loader; import flash.events.Event; import flash.net.URLLoader; import flash.net.URLRequest; import flash.system.Security; import org.papervision3d.materials.BitmapMaterial; import org.papervision3d.objects.primitives.Plane; import org.papervision3d.view.BasicView; public class FlickrPlane extends BasicView { private var pictures:Array = []; public function FlickrPlane() { Security.loadPolicyFile("http://farm1.static.flickr.com/crossdomain.xml"); Security.loadPolicyFile("http://farm2.static.flickr.com/crossdomain.xml"); Security.loadPolicyFile("http://farm3.static.flickr.com/crossdomain.xml"); Security.loadPolicyFile("http://farm4.static.flickr.com/crossdomain.xml"); camera.y = 100; camera.z = -1500; // APIキーは公開しても良い? var req:URLRequest = new URLRequest("http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=自分のAPIキー&user_id=36325810@N02"); var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, onComplete_loader); loader.load(req); startRendering(); } private var picCount:int = 0; private var currentPicNo:int = 0; private function onComplete_loader(evt:Event):void { var xml:XML = XML(evt.target.data); var xmlList:XMLList = xml.photos.photo; picCount = xmlList.length(); for each(var photo:XML in xmlList){ // この辺、てきとう。 var urlObj:Object = { farm:photo.@farm, server:photo.@server, id:photo.@id, secret:photo.@secret, create:function():String{ return "http://farm" + this.farm + ".static.flickr.com/" + this.server + "/" + this.id + "_" + this.secret + "_m.jpg" } } var url:String = urlObj.create(); var req:URLRequest = new URLRequest(url); var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, photoLoad); loader.load(req); } } // 半径とか private var radius:Number = 500; private var centerX:Number = 0; private var centerY:Number = 0; private var centerZ:Number = 0; private function photoLoad(evt:Event):void { var bitmap:Bitmap = evt.target.content as Bitmap var bitmapMaterial:BitmapMaterial = new BitmapMaterial(bitmap.bitmapData); bitmapMaterial.doubleSided = true; var plane:Plane = new Plane(bitmapMaterial); // 初期配置 var angle:Number = 360 / picCount * currentPicNo * Math.PI / 180; currentPicNo++; plane.x = centerX + Math.cos(angle) * radius; plane.z = centerZ + Math.sin(angle) * radius; scene.addChild(plane); pictures.push(plane) } // 回転速度 private var vr:Number = 0.01; private var cos:Number = Math.cos(vr); private var sin:Number = Math.sin(vr); override protected function onRenderTick(event:Event = null):void { for(var i:int; i < pictures.length; i++){ var plane:Plane = pictures[i]; var x1:Number = plane.x - centerX; var z1:Number = plane.z - centerZ; var x2:Number = cos * x1 - sin * z1; var z2:Number = cos * z1 + sin * x1; plane.x = centerX + x2; plane.z = centerZ + z2; } super.onRenderTick(event); } } }
週末プログラミングはたのしい。
Templete Method
新幹線が退屈なのでたまにはRubyのコーディングでも勉強してみます。
もうすぐ新横浜。
ちょっと前に買ってほったらかしにしてたこの本で勉強してみます。
Rubyによるデザインパターン。
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 12人 クリック: 193回
- この商品を含むブログ (58件) を見る
なんとなく「でざいんぱたーん」の響きにやられて衝動買いしてしまった一品。
なんちゃって「デザインパターン」の知識しかない僕には良い復習になるんじゃないかと。
「はっはーん、ふふーん、はいはい、こまんどぱたーんね。」と曖昧な返事しかできないようじゃ厳しいかと。
そんで、第1回はTemplete Method。
プログラムのおおまかな流れをスーパークラスで定義しておいて、
プログラムの変化する部分をサブクラスでオーバーライド実装します。
これは問題ないのでコードだけ。
それはそうと、Rubyの場合、抽象メソッドを定義できないから、
こんなふうに例外を返すメソッドを定義して抽象メソッドとするみたいね。
def output_head raise "Called Abstract method : output_head" end
登場するクラス
- Report
- レポート出力するクラス。メソッドのテンプレートを定義するクラス。変化する実装はサブクラスでオーバーライドする。
- HTMLReport
- HTMLレポートを出力するクラス。Reportのサブクラス。
- PlainReport
- プレーンなレポートを出力するクラス。Reportのサブクラス。
report.rb
class Report def initialize @title = "日記" @text = ["今日は大阪に行ってきました。", "天気は晴れでした。"] end def output_report output_start output_head output_body_start output_body output_body_end output_end end def output_start end def output_head raise "Called Abstract method : output_head" end def output_body_start end def output_body @text.each { |line| output_line line } end def output_line(line) raise "Called Abstract method : output_line" end def output_body_end end def output_end end end
html_report.rb
require "report" class HTMLReport < Report def initialize super end def output_start puts "<html>" end def output_head puts "<head>" puts "<title>#{@title}</title>" puts "</head>" end def output_body_start puts "<body>" end def output_line(line) puts "<p>#{line}</p>" end def output_body_end puts "</body>" end def output_end puts "</html>" end end
plain_report.rb
class PlainReport < Report def initialize super end def output_head puts "*****#{@title}*****" end def output_line(line) puts line end end
- 実行プログラム(main.rb)
require 'html_Report' require "plain_report" puts "HTML REPORT--------" report = HTMLReport.new report.output_report puts "PLAIN REPORT--------" report = PlainReport.new report.output_report
実行結果
HTML REPORT-------- <html> <head> <title>日記</title> </head> <body> <p>今日は大阪に行ってきました。</p> <p>天気は晴れでした。</p> </body> </html> PLAIN REPORT-------- *****日記***** 今日は大阪に行ってきました。 天気は晴れでした。
アレンジしてみた。
同じようなネタが続きます。小出しにしていけばよかったかも。
いじってたらいろいろわかってきました。
- camera.target
- カメラはx、y、zで位置を変えることができるんだけど、ターゲットの指定を変えないと思ったとおりに動いてくれなかった。
camera.target = cubeList[4];
とすればカメラが中央のCubeに向く。
あと、なんとなくTweenerの使い方がわかってきた。たのしー。
参考にさせて頂いたサイト。#鮭さんスゲー。
Papervision3Dをやってみた4: 鮭とプログラムとか
ソース。
package { import caurina.transitions.Tweener; import flash.events.Event; import flash.events.MouseEvent; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.materials.utils.MaterialsList; import org.papervision3d.objects.primitives.Cube; import org.papervision3d.view.BasicView; public class Cube33 extends BasicView { private var cubeList:Array = [] private var cubeSize:Number = 100; public function Cube33() { camera.y = 800; camera.x = cubeSize; for(var i:int = 0; i < 9; i++){ var cube:Cube = createCube(cubeSize); cube.x = cubeSize * (i % 3) cube.z = cubeSize * int(i / 3) cubeList.push(cube); scene.addChild(cube); } // ターゲットを決める camera.target = cubeList[4]; addEventListener(MouseEvent.CLICK, onClick); startRendering(); } private function createCube(size:Number = 200):Cube{ var tileNames:Array = ["front", "back", "top", "bottom", "right", "left"]; var tileColors:Array = [0x0000FF, 0x00FF00, 0xFF0000, 0x00FFFF, 0xFFFF00, 0xFF00FF]; var materialsList:MaterialsList = new MaterialsList(); for(var i:int = 0; i < tileNames.length; i++){ var colorMaterial:ColorMaterial = new ColorMaterial(tileColors[i], 0.7); materialsList.addMaterial(colorMaterial, tileNames[i]); } return new Cube(materialsList, size, size, size); } override protected function onRenderTick(event:Event = null):void { super.onRenderTick(event); } private var moveSpace:Number = 200; private function onClick(evt:MouseEvent):void { for(var i:int = 0; i < cubeList.length; i++){ var moveX:Number = 0; var moveZ:Number = 0; if(i % 3 == 2){ moveX = +moveSpace; }else if(i % 3 == 0){ moveX = -moveSpace; } if(int(i / 3) == 2){ moveZ = +moveSpace; }else if(int(i / 3) == 0){ moveZ = -moveSpace; } var cube:Cube = cubeList[i]; Tweener.addTween(cube, { x:cube.x + moveX, z:cube.z + moveZ, time:1, transition:"easeOutBounce"}); Tweener.addTween(cube, { rotationX:cube.rotationX + 90, time:1, delay:1, transition:"easeOutBounce"}); Tweener.addTween(cube, { rotationY:cube.rotationY + 90, time:1, delay:2, transition:"easeOutBounce"}); Tweener.addTween(cube, { rotationZ:cube.rotationZ + -90, time:1, delay:3, transition:"easeOutBounce"}); Tweener.addTween(cube, { x:cube.x, z:cube.z, time:1, delay:4, transition:"easeOutBounce"}); } } } }
Tweenerのディレイとバウンドで
つづいてドミノっぽいのを作ってみました。
気づきとしては、
- Tweenerのバウンドが面白い。
Tweener.addTween(cubeList[i], {y:cubeList[i].y - size, time:1, delay:i * 0.2, transition:"easeOutBounce"});
これだけでバウンドしてくれる。
ソース。
package { import caurina.transitions.Tweener; import flash.events.Event; import flash.events.MouseEvent; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.materials.WireframeMaterial; import org.papervision3d.materials.special.CompositeMaterial; import org.papervision3d.materials.utils.MaterialsList; import org.papervision3d.objects.primitives.Cube; import org.papervision3d.view.BasicView; [SWF(backgroundColor=0x000000)] public class CubeSort extends BasicView { private var cubeList:Array = []; private var size:Number = 80; private var count:int = 10; public function CubeSort() { camera.y=500; for(var i:int = 0; i < count * count; i++){ var compositMaterial:CompositeMaterial = new CompositeMaterial(); compositMaterial.addMaterial(new ColorMaterial(0xFFFFFF * Math.random())); compositMaterial.addMaterial(new WireframeMaterial(0xFFFFFF * Math.random())); var materialsList:MaterialsList = new MaterialsList(); materialsList.addMaterial(compositMaterial, "all"); var cube:Cube = new Cube(materialsList, size, size, size); cube.x = size * (i % count) - (size * count / 2); cube.z = size * int((i / count)); scene.addChild(cube); cubeList.push(cube); } startRendering() addEventListener(MouseEvent.CLICK, onClick); } private function onClick(evt:MouseEvent):void { for(var i:int = 0; i < count * count; i++){ Tweener.addTween(cubeList[i], {y:cubeList[i].y - size, time:1, delay:i * 0.2, transition:"easeOutBounce"}); } } private var cameraSpeed:Number = 10; private var cameraRange:Number = 200; override protected function onRenderTick(event:Event=null):void { if(camera.x > cameraRange || camera.x < -cameraRange){ cameraSpeed *= -1; } camera.x += cameraSpeed; super.onRenderTick(event); } } }
迫ってくるかんじ
仕事が落ち着いてきたので、引き続きPV3Dをさわってみます。
いろいろと発見があって楽しい。
本日の成果はこちら。
今回気づいたのは、、
- CompositeMaterial
- 複数のマテリアルを集約できちゃうみたい。ColorMaterialだけだと物足りないので、WireframeMaterialも混ぜるとキレイに見える。
- MaterialsList
- "all"とするとCubeを作るのが楽。6面作らないでいいから楽。
- 数が多いとそれっぽく見える。
- for文書くだけで雰囲気は偉い違う。実は何もスゴいことしてない。
以下、ソース。
package { import flash.events.Event; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.materials.WireframeMaterial; import org.papervision3d.materials.special.CompositeMaterial; import org.papervision3d.materials.special.ParticleMaterial; import org.papervision3d.materials.utils.MaterialsList; import org.papervision3d.objects.primitives.Cube; import org.papervision3d.objects.special.ParticleField; import org.papervision3d.view.BasicView; [SWF(backgroundColor=0x000000)] public class CubeSpace extends BasicView { private var space:Number = 4000; private var cubeList:Array = []; public function CubeSpace() { for(var i:int = 0; i < 30; i++){ var compositMaterial:CompositeMaterial = new CompositeMaterial(); compositMaterial.addMaterial(new ColorMaterial(0xFFFFFF * Math.random(), 1)) compositMaterial.addMaterial(new WireframeMaterial(0xFFFFFF * Math.random())) var materialsList:MaterialsList = new MaterialsList(); materialsList.addMaterial(compositMaterial, "all"); var cube:Cube = new Cube(materialsList); cube.x = space * Math.random() - space / 2; cube.y = space * Math.random() - space / 2; cube.z = space * 2 * Math.random() + space * 2; // なんとなく cube.rotationX += Math.random() * 360; cube.rotationY += Math.random() * 360; cube.rotationZ += Math.random() * 360; cubeList.push(cube); scene.addChild(cube); } startRendering(); } override protected function onRenderTick(event:Event=null):void { for(var i:int; i < cubeList.length; i++){ var cube:Cube = cubeList[i]; cube.rotationX += 1; cube.rotationY += 1; cube.rotationZ += 1; cube.z -= 20 } super.onRenderTick(event); } } }
プログラミングの勉強方法について。
休みの日にプログラミングを勉強する場合、
目的と制限時間を決めて勉強するのが良いと思います。
例えば、日曜日暇だとして、
「プログラミングの勉強でもするかな」と思ったとします。
なんとなく興味のあることを勉強する場合、
僕の場合はFlashとかの勉強がブームなんで、
「とりあえずActionScriptでもやるかな」とスタートします。
これが良くない勉強の仕方。ナンセンスなやり方です。
このやり方だと、最初の10分で何をやろうかWebを検索してるうちに、
やる気がなくなってインターネットして終了になることが多いです。
なんで、プログラミングの勉強をする場合、
なるべく具体的な目的と制限時間を決めて勉強することにします。
「とりあえずActionScriptでもやるかな」とスタートするんじゃなくて、
「PV3Dを使って3Dなメニュー画面を作ってみる!」みたいに、まず目的を具体的にします。
目的を具体的にすると、そこに至るまでに何を学べばよいか(調べればよいか)が明確になるので勉強が捗ります。
また、目的に至るまでの流れにそって学習するため、知識が体系化するため脳に定着しやすいように思います。*1
そんで、制限時間を決めます。
その日の都合にもよるけど、だいたい1、2時間くらいがいいかな、と。
あるいは、Macのバッテリーが切れるまで、とかにする。
制限時間があるのとないのでは、集中力が違います。
話がそれるけど、
スタバとか行ってコーディングすると集中できるのは、
Macのバッテリーと、コーヒーがなくなるまでの制限時間があるからのような気がします。
あと、制限時間内で学んだことは、
ブログなどでアウトプットすることも大事ですね。
復習の意味も込めて。
貴重な休みの時間は大事に使おうと思う、今日この頃でした。
*1:脳とか言ってる時点で説得力ないし。