Murayama blog.

プログラミング教育なブログ

Strategy

Rubyによるデザインパターン生活2日目。
今日はStrategyパターンを学びます。

Rubyによるデザインパターン

Rubyによるデザインパターン



アルゴリズムの一部を変化したい場合の対処法として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--------
***** 日記 *****
今日は大阪に行ってきました。
天気は晴れでした。


とまぁ実行結果は前回と同じです。ちょっとソースを解説してみます。


RubyJavaと違って厳密な型定義は必要としません。
そのため、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アルゴリズムを動的に作ることもできそうです。


勉強になりました。

*1:ダックタイピングというやつです。

*2:ぼくはこっちの方をよく使います。

円運動とFlickrと

この週末は移動時間がけっこうあったので、楽しくコーデイングしてました。


今回やってみたのは、

  • Z軸を使った円運動
  • Flickr APIから画像データの取得


ほんで結果はこんなの。


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によるデザインパターン

Rubyによるデザインパターン

Rubyによるデザインパターン


なんとなく「でざいんぱたーん」の響きにやられて衝動買いしてしまった一品。
なんちゃって「デザインパターン」の知識しかない僕には良い復習になるんじゃないかと。
「はっはーん、ふふーん、はいはい、こまんどぱたーんね。」と曖昧な返事しかできないようじゃ厳しいかと。


そんで、第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:脳とか言ってる時点で説得力ないし。