— boreal-kiss.com

Archive
Tag "DesignPatterns"

イントロダクション

Grand Central Dispatch (GCD)にはforループをマシンのコア数に応じて並列処理してくれる便利な関数 dispatch_apply(size_t, dispatch_queue_t, void) が存在する。

void dispatch_apply(
	size_t iterations,
	dispatch_queue_t queue,
	void (^block)(size_t));

引数は前から順番に、forループのイテレーション回数、タスク送信先のキュー、各イテレーションで行われるタスク、である。この関数はforループの各イテレーションごとにキューにタスクを送っている。したがって、もしタスクの処理時間がキューにタスクを送信するためにかかる時間(オーバーヘッド)より小さくなるような場合には注意が必要となる。普通にforループをまわす時よりも処理が遅くなる可能性があるからだ。具体例を見てみよう。

実行環境

Mac OS X 2.4 GHz Intel Core 2 Duo (CPU2個)

forループ vs. GCD その1

以下のような「何もしない」blockをforループでまわして処理にかかった時間を計測してみる。

void (^block)(int i) = ^(int i){
 
};

比較するのは普通のforループ処理を行う関数 loop_normal(int, void)

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

とGCDを用いた場合の関数 loop_normal_gcd(int, void) である。

static void loop_normal_gcd(int count, void (^block)(int)){
	void (^block_gcd)(size_t i) = ^(size_t i){
		block(i);
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(count, queue, block_gcd);
}

比較した結果が下図になる(横軸はイテレーション回数、縦軸が関数の処理にかかった時間)。これを見るとGCDを用いた場合の方が(格段に)遅くなっていることがわかるだろう。この原因は「何もしない」タスクを完了するための時間に比べてキュー送信のオーバーヘッドが大きいことによる。

for loop vs GCD

それではこのオーバーヘッドが無視できるような構造にするにはどうすればよいだろうか。ひとつの方法はループにストライドを導入することである。

ストライドの導入

この方式はストライドで定義される整数値分の回数のイテレーションを一回のイテレーションに押し込んでしまう方法だ。これによりforループのイテレーション回数が減り、かわりに一回のイテレーションにおけるタスク量を増やすことができる。具体的に見てみよう。まず普通のforループを行う関数 loop_normal(int, void) がある。これはcount回のイテレーションを行う。

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

次に上記関数と全く同じ処理を行う関数が以下の loop_stride(int, int, void) である。ただし整数 stride (stride < count) を導入することで関数内部のループ回数が (count / stride) になっていることに注目してもらいたい。イテレーション回数が減った分イテレーション一回のタスク量が増えていることもわかるだろう。

static void loop_stride(int count, int stride, void (^block)(int)){
	int iMax = count / stride;
	int remainder = count % stride;
 
	void (^block_stride)(int i) = ^(int i){
		int j = i * stride;
		int jMax = j + stride;
 
		if (i == iMax - 1){
			jMax += remainder;
		}
 
		while (j < jMax) {
			block(j);
			j++;
		}
	};
 
	for (int i=0; i<iMax; i++){
		block_stride(i);
	}
}

ストライドを導入すればイテレーションの一回のタスク量を増やせることがわかった。それでは実際にGCDに放り込んで結果を見てみよう。

forループ vs. GCD その2

比較したのは普通のforループを行う関数 loop_normal(int, void)

static void loop_normal(int count, void (^block)(int)){
	for (int i=0; i<count; i++){
		block(i);
	}
}

とストライドを導入して一回のタスク量を増やした関数 loop_stride_gcd(int, int, void) である。

static void loop_stride_gcd(int count, int stride, void (^block)(int)){
	int iMax = count / stride;
	int remainder = count % stride;
 
	void (^block_gcd)(size_t i) = ^(size_t i){
		int j = i * stride;
		int jMax = j + stride;
 
		if (i == iMax - 1){
			jMax += remainder;
		}
 
		while (j < jMax) {
			block(j);
			j++;
		}
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(iMax, queue, block_gcd);
}

そして両者の引数に「何もしない」blockを渡し実行時間を比較した。

void (^block)(int i) = ^(int i){
 
};

ここではイテレーションの回数は100,000回に固定し、ストライドの値を変化させて両者を比較した。結果は以下の図になる(横軸はストライドの値、縦軸は関数の処理にかかった時間)。ストライドの値が大きい場合はGCDを用いた場合の方が処理が早くなっていることがわかる。これらはつまりキューにタスクを送る際のオーバーヘッドが全体として無視できる程度になっていることを意味する。

for loop vs GCD (stride)

まとめ

GCDの恩恵を受けるためにはタスクは適度に重い(処理に時間がかかる)ものである必要があることがわかった。タスクが軽すぎる場合、GCDを利用することでかえって普通のforループよりも処理が遅くなる場合がある。これはGCDのオーバーヘッドによる。このような場合にはforループにストライドを導入することでイテレーション回数を減らし一回のタスクを重くすることで解決できる。

Read More

Grand Central Dispatch

OSX 10.6から導入されたGrand Central Dispatch (GCD)は複数タスクを処理するための便利な仕組みのことで、これを使うとマルチコアの恩恵を簡単に受けることができる。複数同時処理といった仕組みとしてスレッドがあるが、GCDの場合(スレッドを作成する時・制御する時のような)面倒くさい手順を一切踏まなくてよくなる。さらにシステム側でCPUを適切に利用する工夫がされているので(CPU1個なら1個、2個なら2個、8個なら8個勝手に使ってくれる)実行環境を気にしなくてよい、という利点が挙げられる。詳細は以下のドキュメントが非常に参考になる。

さて今回、CPUが複数あるマシンでGCDの恩恵を受けているかどうかを簡単な計算で確認したので以下に結果を載せておく。GCDにはforループを簡単に並列化する仕組みがあるのでそれを利用した。なお計算を行ったマシンは2.4 GHz Intel Core 2 Duo。

forループの並列化

以下のようなforループがあるとする。

for (int i=0; i<count; i++){
	//Do some work.
}

ループ内のi回目の処理が他と独立したタスクである場合GCDのdispatch queueを用いて並列処理が可能になる。以下は上記forループと同じ仕事を行う。

//GCD way.
void (^block)(size_t i) = ^(size_t i){
	//Do some work.
};
 
dispatch_queue_t queue 
	= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(count, queue, block);

ここで

void (^block)(size_t i)

はblockと呼ばれるApple独自のC言語の拡張である。これは簡単に言うと一連の作業をまとめてオブジェクト化したものであり1、GCDとは親密な関係にある。block自体も簡単で便利な仕組みなので、興味があるひとは以下のドキュメントを参考にしてほしい。

さて上記の場合、blockに記述された内容がforループ内の処理に相当し、これをGCDのdispatch_apply()の引数として渡してシステム側で処理してもらっている。記述が簡単な点にも注目してもらいたい。

計算結果

普通のforループとdispatch queueを用いた場合を比較したものを載せておく。forループ内の1回の処理時間(t_task)を横軸、ループ終了までにかかった時間を縦軸にとってある。ここでループの回数(count)はt_taskの値ごとに異なっており

t_task x count = 10 sec.

となるように設定してある。例えば図の一番左のバーの場合、ループ内の一回の処理時間を0.00001秒、ループの総数を1000000回としてある(全処理時間は0.00001 x 1000000 = 10秒)2。試行範囲に限って言えばdispatch queueを用いた方が2倍ほど早いことがわかる。2倍という数字はCPU2個使った並列化、ということだろう。設定によっては普通のforループの方が早くなるようなことがあるかと思われたがそのようなこともなかったので、並列化が可能であればGCDはどんどん使った方が良さそうだ。

確認用プログラム

上記計算に使用したプログラム。ループ内の処理時間の変更は指定時間スリープさせることで実現させてある。gettimeofday_sec()はマイクロ秒測定用の関数でC言語: 実行時間測定の方法より拝借した。

#import <Foundation/Foundation.h>
#include <sys/time.h>
 
double gettimeofday_sec();
static void test_normal_loop(int count, NSTimeInterval sleep);
static void test_dispatch_que(int count, NSTimeInterval sleep);
 
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
	const NSTimeInterval TEN_SEC = 10.0;
 
	for (int count = 10; count < 1e7; count*=10){
		NSTimeInterval sleep = TEN_SEC / count;
		test_normal_loop(count, sleep);
		test_dispatch_que(count, sleep);
	}
 
    [pool drain];
    return 0;
}
 
double gettimeofday_sec(){
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + (double)tv.tv_usec*1e-6;
}
 
static void test_normal_loop(int count, NSTimeInterval sleep){
	double t1 = gettimeofday_sec();
 
	for (int i=0; i<count; i++){
		[NSThread sleepForTimeInterval:sleep];
	}
 
	double t2 = gettimeofday_sec();
 
	printf("%f\n", t2 - t1);
}
 
static void test_dispatch_que(int count, NSTimeInterval sleep){
	double t1 = gettimeofday_sec();
 
	void (^block)(size_t i) = ^(size_t i){
		[NSThread sleepForTimeInterval:sleep];
	};
 
	dispatch_queue_t queue 
		= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_apply(count, queue, block);
 
	double t2 = gettimeofday_sec();
 
	printf("%f\n", t2 - t1);
}

Footnotes

  1. 仕組みとしてはNSInvocationに似ているがblockは特定のメソッドだけに固執しておらず、記述された内容をprocedualに実行できる []
  2. 縦軸のループ処理時間が10秒以上になっているものがあるのはループを行う際のオーバーヘッドの累積によるもの。 []
Read More

イントロダクション

Objective-Cのカテゴリはサブクラスを作ることなくメソッドの拡張(またはメソッドごとの分類)が行える点で強力だ。しかしメソッドの定義はできてもインスタンス変数の追加ができない。そこでCocoaのKey Value Coding (KVC)の機能を用いてカテゴリに擬似的にインスタンス変数を生成させる。これにより元クラスを手直しすることなくインスタンス変数を追加することができる。なおこの手法は、よりフレキシブルという理由でNSMapTableを用いるのが一般的なようだが、ここではNSMutableDictionaryを代用した。理由は使い慣れているからだ。NSMapTableを用いた方法はBuck and Yacktman – Cocoa Design Patternsが詳しい。

具体例

簡単な例としてボタンを押すとアラートを表示するアプリケーションを考える。ボタンプッシュ後の動作はAppControllerクラスで以下のように定義されているとする。

//AppController.m
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
}
 
@end

ここで「一度アラート表示された場合は再度アラートを表示させない」仕様に変更するとしよう。AppControllerにBOOL値を格納するインスタンス変数を追加して制御するのが手っ取り早いが、ここではAppControllerのカテゴリ(ShowAtOnce)を新規に定義して制御することにする。カテゴリ作成の大まかな手順は以下のようになる。

  1. 疑似インスタンス変数格納用にNSMutableDictionary変数をカテゴリに定義
  2. 疑似インスタンス変数としたいオブジェクトを上記NSMutableDictionary変数に格納。その際のキーストリングはAppControllerインスタンスに固有のものを用いる
  3. 格納したオブジェクトのAccesors (Setters/Getters)をカテゴリに定義
  4. インスタンスの解放時にNSMutableDictionaryからオブジェクトを消去するメソッドをカテゴリに定義

以下順にみていこう。

1. 疑似インスタンス変数格納用変数の定義

疑似インスタンス変数にしても情報を格納する場所は必要になる。そこで疑似インスタンス変数を格納する場所としてNSMutableDictionaryオブジェクトをカテゴリ(ShowAtOnce)に定義する。さらにこのオブジェクトのAccesorsをクラスメソッドとして定義する。つまりこの格納用のNSMutableDictionaryオブジェクトはAppControllerインスタンスすべてで使い回されることになる。

//AppController-ShowAtOnce.m
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
@end

2. インスタンス固有のキーストリング

上記例のアプリケーションの仕様を実現するためには、一度アラートが表示されたかどうかを調べるBOOL値を(NSMutableDictionary *)_simulatedIVarsに格納できればよさそうだ。しかし今は疑似インスタンス変数を作りたいのでAppControllerインスタンスごとに異なる値を格納できるようにする必要がある。そこでまず「AppControllerインスタンスに固有なNSStringオブジェクト」を生成するメソッドを定義する。このメソッドは-[AppController self]のストリング表現を返す。

//AppController-ShowAtOnce.m
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}

さらに「疑似インスタンス変数名に相当するNSStringオブジェクト」を用意する。これはクラス全体で使い回しができるようにすればよい。ここではAlertHasEverBeenShownKeyStringという名前で定義しておこう。

//AppController-ShowAtOnce.m
 
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";

あとは「AppControllerインスタンスに固有なNSStringオブジェクト」と「疑似インスタンス変数名に相当するNSStringオブジェクト」を適当に繋げて「あるAppControllerインスタンスの疑似インスタンス変数に固有なNSStringオブジェクト」を作成するメソッドを定義する。このメソッドの返り値が(NSMutableDictionary *)_simulatedIVarsへの格納用キーストリングとして利用されるわけだ。

//AppController-ShowAtOnce.m
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}

現在までのAppController (ShowAtOnce)の中身をまとめると以下のようになっているはずだ。

//AppController-ShowAtOnce.m
 
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
#pragma mark -
#pragma mark Utilities
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}
 
@end

3. 疑似インスタンス変数のAccesors

上記メソッド群を利用してカテゴリ(ShowAtOnce)に定義する。Settersは-[AppController _setAlertHasEverBeenShown:]、Gettersは-[AppController _alertHasEverBeenShown]としよう。ここはstraightforwardだと思う。

//AppController-ShowAtOnce.m
 
-(BOOL)_alertHasEverBeenShown{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
	if (object == nil){
		return NO;
	}
 
	return [object boolValue];
}
 
-(void)_setAlertHasEverBeenShown:(BOOL)yn{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	[[[self class] _simulatedIVars] 
		setObject:[NSNumber numberWithBool:yn] forKey:aKey];
}

あとはAppController内でこのメソッドを使ってやればよい。具体的には以下のようになるだろう。

//AppController.m
 
#import "AppController-ShowAtOnce.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	if (![self _alertHasEverBeenShown]){
		NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
		[self _setAlertHasEverBeenShown:YES];
	}
}
 
@end

4. 疑似インスタンス変数を解放

AppControllerインスタンスごとに(NSMutableDictionary *)_simulatedIVarsに登録されるのだからAppControllerインスタンスが不要になったら疑似インスタンス変数も解放する必要がある。そのために以下のようなメソッドをAppController(ShowAtOnce)に定義しておく。このメソッドは呼ばれれば不要になった疑似インスタンス変数を解放する。

//AppController-ShowAtOnce.m
 
-(void)_removeSimulatedIVars{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
 
	if (object){
		[[[self class] _simulatedIVars] removeObjectForKey:aKey];
	}
}

さてこれまでAppControllerそのものを一切変更しないように実装すると言ってきたが実は嘘で、AppControllerそのものを手直しする必要がでてくる。というのも疑似インスタンス変数の解放を呼ぶメソッドをカテゴリに書くことは危険だからだ。例えば-[AppController dealloc]をカテゴリ(ShowAtOnce)に再定義することは可能だが、もし他に疑似インスタンス変数を持つ別カテゴリが同様の-[AppController dealloc]を実装した場合どちらの-[AppController dealloc]が呼び出されるかわからなくなるからだ(結果どちらかのカテゴリの疑似インスタンス変数は解放メソッドを呼ばれないがために生き残ってしまう)。したがって疑似インスタンス変数の解放メソッドを呼び出すのはAppControllerとするのがよいだろう。

//AppController.m
 
-(void)dealloc{
	[self _removeSimulatedIVars];
	[super dealloc];
}

全ソースコード

最終的なコードは以下のようになっている。

//AppController.h
 
#import <Cocoa/Cocoa.h>
 
@interface AppController : NSObject {
 
}
 
-(IBAction)buttonPressed:(id)sender;
@end
//AppController.m
 
#import "AppController.h"
#import "AppController-ShowAtOnce.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	if (![self _alertHasEverBeenShown]){
		NSRunAlertPanel(@"Alert", @"This is an alert.", @"OK", nil, nil);
		[self _setAlertHasEverBeenShown:YES];
	}
}
 
-(void)dealloc{
	[self _removeSimulatedIVars];
	[super dealloc];
}
 
@end
//AppController-ShowAtOnce.h
 
#import <Cocoa/Cocoa.h>
#import "AppController.h"
 
@interface AppController (ShowAtOnce)
 
+(NSMutableDictionary *)_simulatedIVars;
-(void)_removeSimulatedIVars;
-(BOOL)_alertHasEverBeenShown;
-(void)_setAlertHasEverBeenShown:(BOOL)yn;
-(NSString *)_instanceID;
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString;
@end
//AppController-ShowAtOnce.m
 
#import "AppController-ShowAtOnce.h"
 
//Private
static NSString * const AlertHasEverBeenShownKeyString = @"alertHasEverBeenShown";
 
@implementation AppController (ShowAtOnce)
 
static NSMutableDictionary *_simulatedIVars = nil;
 
+(NSMutableDictionary *)_simulatedIVars{
	if (_simulatedIVars == nil){
		_simulatedIVars = [[NSMutableDictionary alloc] init];
	}
	return _simulatedIVars;
}
 
-(void)_removeSimulatedIVars{
	NSLog(@"%s", __FUNCTION__);
 
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
 
	if (object){
		[[[self class] _simulatedIVars] removeObjectForKey:aKey];
	}
}
 
#pragma mark -
#pragma mark Setters Getters
 
-(BOOL)_alertHasEverBeenShown{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	id object = [[[self class] _simulatedIVars] objectForKey:aKey];
	if (object == nil){
		return NO;
	}
 
	return [object boolValue];
}
 
-(void)_setAlertHasEverBeenShown:(BOOL)yn{
	NSString *aKey = 
		[self _objectKeyForKeyString:AlertHasEverBeenShownKeyString];
	[[[self class] _simulatedIVars] 
		setObject:[NSNumber numberWithBool:yn] forKey:aKey];
}
 
#pragma mark -
#pragma mark Utilities
 
-(NSString *)_instanceID{
	return [NSString stringWithFormat:@"%@", self];
}
 
-(NSString *)_objectKeyForKeyString:(NSString *)aKeyString{
	return [NSString stringWithFormat:@"%@:%@", 
		[self _instanceID], aKeyString];
}
 
@end
Read More

イントロダクション

あるメソッドを遅延して実行したい場合、-[NSObject performSelector:withObject:afterDelay]を用いることが多々ある。しかし対象のメソッドが複数の引数を持つものであった場合、この方法は直接的には用いることができなくなってしまう。そこでNSInvocationクラスとNSProxyクラスを用いて複数引数を持つメソッドでも適用できるように工夫する。なおこの方法はHigher Order Messaging(メッセージを引数に持つメッセージという意味合い)と呼ばれる。

*******************************************************************
HOMについてまとめた記事はこちら。
Higher Order Messaging (HOM) – boreal-kiss.com
*******************************************************************

問題の一例

例えば以下のようなコントローラークラスを考える。このコントローラーではUI中のボタン(Show Alert)が押されるとアラートが表示される仕組みになっている。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openAlertWithTitle:@"Alert" message:@"This is an alert." 
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle 
				message:(NSString *)aMessage 
				defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
@end

このままだと、ボタンが押された途端にアラートが表示され、ボタンの描画状態がプッシュ中のままになることがわかる。気持ちわるい。

そこで-[NSObject performSelector:withObject:afterDelay:]を用いてアラートを遅延表示させるのが得策だろう。しかし-[NSObject performSelector:withObject:afterDelay:]では-[AppController showAlertWithTitle:message:defaultButtonTitle:]を正しく実行できない。引数が複数あるためだ。それではどうすればよいだろうか。解決策のひとつとして複数の引数をひとつにまとめてしまう方法が考えられる。

NSInvocation

NSInvocationはメソッドをオブジェクト化したクラスである。デザインパターンで言うところのCommandパターンに相当するもので、これを利用することで上記問題を解決することができる。具体的には以下のような手順を踏むことになるだろう。

  1. -[NSObject performSelector:withObject:afterDelay:]で本来実行したい複数引数を持つメソッドをNSInvocationオブジェクトとしてひとつにまとめてしまう(メソッドのオブジェクト化)
  2. 作成したNSInvocationオブジェクトを実行するための新たなメソッド(引数はNSInvocationオブジェクトひとつのみ)を定義、それを-[NSObject performSelector:withObject:afterDelay:]で実行する。単一引数なので問題なし
  3. NSInvocationオブジェクトの中身(本来実行したいメソッド)を実行する

以下では新しく-[AppController openDelayedAlertWithTitle:message:defaultButtonTitle:]としてアラートの遅延表示を実現している。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert."
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
//New
-(void)openDelayedAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
 
	NSMethodSignature *aSignature = [[self class] 
		instanceMethodSignatureForSelector:
		@selector(openAlertWithTitle:message:defaultButtonTitle:)];
	NSInvocation *anInvocation = [NSInvocation 
		invocationWithMethodSignature:aSignature];
	[anInvocation setTarget:self];
	[anInvocation setArgument:&aTitle atIndex:2];
	[anInvocation setArgument:&aMessage atIndex:3];
	[anInvocation setArgument:&aDefaultButtonTitle atIndex:4];
	[anInvocation setSelector:
		@selector(openAlertWithTitle:message:defaultButtonTitle:)];
	[self performSelector:@selector(performInvocation:)
		withObject:anInvocation afterDelay:0.0];
}
 
//New
-(void)performInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:self];
}
 
 
@end

実行結果。アラート表示が遅延されボタンがプッシュ状態でなくなっているのがわかる。気持ちいい。

上記例からメソッドをオブジェクト化するというNSInvocationクラスの有用性が実感できた。反面、NSInvocationオブジェクトは作成するまでの手順が面倒なこともわかったと思う(例えば引数が10個あるメソッドをNSInvocationオブジェクト化するためには10回も-[NSInvocation setArgument:atIndex:]を書かないといけない)。実は適当なメッセージを送るとメッセージ内容をNSInvocationオブジェクトにしてくれる便利クラスが存在する。NSProxyだ。

NSProxy

NSProxyはデザインパターンで言うところのProxyパターンを実現するものである。NSProxyは自分が理解できないメッセージをとりあえずNSInvocationオブジェクトにしてしまうという機能を持っているのでこれを利用する。NSProxyはメッセージを受け取ると以下のような手順でNSInvocationオブジェクトを作成し、NSInvocationオブジェクトをどのように扱うのか指示を待つ。

  1. -[NSProxy methodSignatureForSelector:]を呼び出しNSMthodSignatureオブジェクトを作成する。ただしNSMethodSignatureオブジェクトを作成するための必要な情報(ターゲット etc.)はメソッド内に適切に与えてやる必要がある
  2. NSMethodSignatureオブジェクトを元にNSInvocationオブジェクトを作成する
  3. -[NSProxy forwardInvocation:]を呼び出しメソッド内容の指示に従う

自動作成されたNSInvocationオブジェクトは-[NSProxy forwardInvocation:]の引数に渡されるので、あとはこのNSInvocationオブジェクトの扱い方を内部に記述してやればよい。以下ではNSProxyのサブクラスとしてTrampolineクラスを用いてNSInvocationオブジェクトを作成させている。AppControllerの中身の変更にも注目してもらいたい。

#import "Trampoline.h"
 
@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openDelayedAlertWithTitle:@"Alert" message:@"This is an alert."
		defaultButtonTitle:@"OK"];
}
 
-(void)openAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	NSRunAlertPanel(aTitle, aMessage, aDefaultButtonTitle, nil, nil);
}
 
//New
-(void)openDelayedAlertWithTitle:(NSString *)aTitle
	message:(NSString *)aMessage
	defaultButtonTitle:(NSString *)aDefaultButtonTitle{
	[[self trampoline] openAlertWithTitle:aTitle
		message:aMessage defaultButtonTitle:aDefaultButtonTitle];
}
 
-(void)performInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:self];
}
 
//New
-(Trampoline *)trampoline{
	return [Trampoline trampolineWithTarget:self
		selector:@selector(performInvocation:)];
}
 
 
 
@end
@interface Trampoline : NSProxy {
	id _target;
	SEL _selector;
}
 
-(id)initWithtarget:(id)aTarget selector:(SEL)aSelector;
+(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector;
 
@end
@implementation Trampoline
 
-(id)initWithtarget:(id)aTarget selector:(SEL)aSelector{
	_target = aTarget;
	_selector = aSelector;
	return self;
}
 
+(id)trampolineWithTarget:(id)aTarget selector:(SEL)aSelector{
	return [[[[self class] alloc] initWithtarget:aTarget selector:aSelector] 
		autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[_target performSelector:_selector withObject:anInvocation afterDelay:0.0];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

References

Read More

剣士装備・ガンナー装備

モンスターハンターの武器は大きくわけて二種類に分類できる。それは大剣やハンマーといった近接武器(剣士専用)と弓やボウガンなどの遠距離武器(ガンナー専用)である。面白いのは防具にも剣士専用のものとガンナー専用のものがあるところで、武器と防具の関係に以下のような制約が付けられているのが他のRPG(例えばドラクエ)と大きく異なる点であろう。

  • 剣士専用武器を装備している時は剣士専用防具しか装備できない。
  • ガンナー専用武器を装備している時はガンナー専用防具しか装備できない。

剣士・ガンナー武器の違いは予想できるとして、剣士・ガンナー防具の違いは何だろう。答えは防御力だ。ガンナー専用防具の防御力は剣士専用防具のそれに比べて著しく低い。これは妥当な設定だ。崖の上でスコープを構えて銃撃するハンターよりも、モンスターの足下すれすれで剣を振り回しているハンターの方がモンスターからダメージを受ける可能性が高いからであろう。ダメージを受ける頻度が異なる者同士をより平等に扱うためには防御力による差別化をはかったという風に考えれば、上記のような剣士・ガンナー防具の制約はとてもリーズナブルだ。

裸のハンター

さて剣士・ガンナー防具の制約、面白い仕組みなのだが、少しだけストレスになることがある。着替えをする時だ。モンハンをやったことがある人なら経験があると思うが、今例えば剣士専用装備(近接武器+近接武器用防具)をしているとしよう。この状態で武器をガンナー専用であるボウガンに替えたとするとどうなるか。答えは防具が全部外れて裸になる、である。なぜならボウガンを装備した時点でガンナー武器を装備していることになり、上記の剣士・ガンナー防具の制約から剣士防具が装備できなくなるからである。ボウガンだけを装備した裸のハンターはガンナー専用防具をイチから装備し直さないといけないのだ。そしてこの問題は剣士・ガンナー間の装備変更にはいつもつきまとうことになる。

便利な機能

そこでゲーム内に用意されているのが着替えを一括で行うマクロ機能だ。自分のお気に入りの武器と防具の組み合わせをあらかじめ登録しておくことで、次回からボタン一つで全身の装備コーディネートができてしまう。もちろん登録時の装備は自分が今現在装備しているものから選ばれているため、剣士・ガンナー防具の制約も必ず満たすことになる。登録スロットも20もあり(MHP2Gの場合)、好きなときに好きなスロットの装備にボタン一つで変身できるようになる。

本題

前置きが長くなったが、このモンハンの装備一括変更のマクロ機能、デザインパターンで言うところの何に相当するのか、ということで少し考えた。いくつか候補があったものの結局これに落ち着いた。Abstract Factoryパターンである。全く同じ操作でユーザーを煩わせることなく機能する、というところに重点を置いた結果である。Abstract Factoryパターンの解説は以下リンク先へどうぞ。簡単なActionScriptコード付き。

» Abstract Factory

Read More

Eric Gamma et al. – Desingn Patternsを全部Action Scriptで解説しようと思ったらなかなかハードで途中で力尽きてしまった。一応全体三部のうちのStructural Patterns(以下参照)については完成はしていたので公開することに。ただし注意。そもそもの動機が自分のための勉強だったため、単なる書物の和訳にならないように自分で本質だと感じた部分をメインに書いている。したがって内容に少しでも疑問を持った場合は原著をあたってください。

  • Adapter
  • Bridge
  • Composite
  • Decorator
  • Facade
  • Flyweight
  • Proxy

» Design Patterns for ActionScript

Read More

今日William Sanders – ActionScript 3.0 Design Patternsを読み直してて気づいた。ずっとViewは見た目オンリーで仕事は何もせずにControllerにまかせっきりだと思ってたけど、自分の衣装は自分で着るらしい。

一般的なMVC

いわゆる本に載ってるやつで意外にViewが働く。triadと呼ぶにふさわしく仕事がバランスよく分担されている。

  1. ユーザーがViewとインタラクション
  2. ViewがControllerにユーザーインタラクションを通達
  3. Controllerがインタラクション内容にそってModelを更新
  4. Modelが自身の変更をViewに通達
  5. Viewが更新されたModelを使ってView自身を更新

MVCオルタナティブ

僕がMVCだとおもってたやつでViewはほとんど仕事をしない。自分としてはModelとViewに全く関連性がないのはなかなか良さそうに思うんだけどどうなんだろう。Controllerが仕事しすぎ?

  1. ユーザーがViewとインタラクション
  2. ViewがControllerにユーザーインタラクションを通達
  3. Controllerがインタラクション内容にそってModelを更新
  4. Modelが自身の変更をControllerに通達
  5. Controllerが更新されたModelを使ってViewを更新
Read More

プログラミングテクニックのまとめ – プログラミング日記の「ifをポリモーフィズムによりなくせ」に相当するものをActionScriptで実践した。以下の例では分岐が高々2箇所なのでif文をなくす労力の方が無駄に思えるが、if文やswitch文の分岐箇所が増えるような場合ほど威力を発揮するはず(拡張が容易・メンテナンスが楽などの理由)。

a. if文が必要な場合

例えば線を描画したいとき、描画始点はGraphics.moveTo()、その他はGraphics.lineTo()を使う。そのため以下の例のようにforループでArrayデータの内容を読み込んで折れ線グラフを描画させるルーチン内ではif文を使う必要がある。

package {
    import flash.display.Sprite;
    import flash.display.Shape;

    public class Main extends Sprite{
        private var _drawer:Shape;
        private var _data:Array = [0,10,0,10,0,10,0,10,0,10];//Test data
        
        public function Main(){
            _drawer = new Shape();
            addChild(_drawer);
            draw();
        }
        
        private function draw():void{
            _drawer.graphics.lineStyle(1,0x0);
            for (var i:int=0; i<_data.length; i++){
                var x:Number = 20*i;
                var y:Number = 20*_data[i];
                
                if (i==0){
                    _drawer.graphics.moveTo(x,y);
                }
                else {
                    _drawer.graphics.lineTo(x,y);
                }
            }
        }
    }
}

この分岐処理はGraphics.moveTo()とGraphics.lineTo()という全く別のメソッドを「直接」使うから生じている。つまり、これらメソッドをコントロールする管理クラスを新たに作って、管理クラス経由で「間接」的に扱えば(delegateする、と言う)回避することが可能になるわけだ。具体的にはforループ内ではその管理クラスの単一メソッドのみを呼び出すようにする。

b. if文が不要な場合

以下の例では管理クラスLineDrawer.asを導入して描画させている。線の始点であろうがなかろうが常にLineDrawer.draw()というメソッドを使うので結果if文がなくなっている。

package {
    import flash.display.Sprite;
    import com.borealkiss.controls.LineDrawer;

    public class Main extends Sprite{
        private var _drawer:LineDrawer;
        private var _data:Array = [0,10,0,10,0,10,0,10,0,10];//Test data
        
        public function Main(){
            _drawer = new LineDrawer();
            addChild(_drawer);
            draw();
        }
        
        private function draw():void{
            _drawer.graphics.lineStyle(1,0x0);
            for (var i:int=0; i<_data.length; i++){
                var x:Number = 20*i;
                var y:Number = 20*_data[i];
                _drawer.draw(x,y);
            }
        }
    }
}

以下if文がなくなったカラクリ部分について。

b-1. LineDrawer.as (管理クラス)

Main.asから呼ばれるのはLineDrawer.draw()メソッドのみ。ポイントはLineDrawer.draw()内で実際に働く中身の入れ替えを行っているところ。最初にLineDrawer.draw()が呼ばれたときに実際に働くのがプロパティ”_moveToState”にセットされたクラス(MoveToState.as)、それ以降はプロパティ”_lineToState”にセットされた「別」クラス(LineToState.as)で、それぞれdraw()というメソッドを持っているがメソッドの内容は全く別物になっている(b-3, b-4参照)。そしてこの二つのクラスの受け皿に担当するプロパティ”_currentState”はこの両クラスを参照できるようにそれぞれの同一継承元であるクラス型(IState.as)で定義されている。これがポリモーフィズム(クラス型の多様性)。イメージとしてはクラス型を日本人型に設定しておき、東京人型クラスも大阪人型クラスも参照できるような状態。

package com.borealkiss.controls{
    import flash.display.Shape;
    
    public class LineDrawer extends Shape{
        private var _moveToState:IState;
        private var _lineToState:IState;
        private var _currentState:IState;
        
        public function LineDrawer(){
            _moveToState = new MoveToState(this);
            _lineToState = new LineToState(this);
            init();
        }

        public function draw(x:Number,y:Number):void{
            _currentState.draw(x,y);
            _currentState = _lineToState;
        }
        
        public function init():void{
            _currentState = _moveToState;
        }

    }
}

b-2. IState.as (インターフェイス)

LineDrawer.as(管理クラス)内で呼び出されて働かされるクラスMoveToState.asとLineToState.asが継承している親玉クラス。これがあるからポリモーフィズムが可能になる。重要なのはMoveToStateクラスとLineToStateクラスが共通して継承しているクラスが存在していることなので、インターフェイスを使わなくてもアブストラクト的なクラスを作成して代用してもよい(MoveToState extends XXX, LineToState extends XXX, のXXXに相当するクラス)。

package com.borealkiss.controls{
    public interface IState{
        function draw(x:Number,y:Number):void;
    }
}

b-3. MoveToState.as

折れ線グラフの始点描画時点でのみ呼ばれるクラス。MoveToState.draw()はGraphics.moveTo()を実際に行う。

package com.borealkiss.controls{
    public class MoveToState implements IState{
        protected var _drawer:LineDrawer;
        
        public function MoveToState(drawer:LineDrawer){
            _drawer = drawer;
        }

        public function draw(x:Number,y:Number):void{
            _drawer.graphics.moveTo(x,y);
        }
    }
}

b-4. LineToState.as

折れ線グラフの始点以降で呼ばれるクラス。LineToState.draw()はGraphics.lineTo()を実際に行う。MoveToStateクラスを継承しているので必然的にIStateインターフェースも継承している。

package com.borealkiss.controls{
    public class LineToState extends MoveToState{

        public function LineToState(drawer:LineDrawer){
            super(drawer);
        }
        
        override public function draw(x:Number,y:Number):void{
            _drawer.graphics.lineTo(x,y); 
        }
    }
}

追記 2008/12/17

以下の記事で引用いただいたのですが、プログラミングの思想という点で大変参考になります。extendsとかimplementsが好きな方にオススメ。

あるStateパターンの実装について

Read More

ActionScript 3.0 Design Patterns (Adobe Developer Library)

プログラムの設計思想と具体的な王道設計パターンについて豊富に例を挙げつつ説明してる本。内容自体はどのプログラミング言語(OOP)にも共通なのだけど、わざわざActionScriptを題材にしている点が貴重。ActionScriptに馴染みのある人は親近感を持ちやすいのではないかと思う。章ごとに各デザインパターンの性質、最低限の骨組み、簡単な例、少し複雑な例と書かれていて段階的に読めるからわかりやすかった。さらに章内容が独立しているので、例えばStrategy > Composite > Observer > Model-View-Controllerパターンと必要な部分だけを読むことで短期間でwebアプリケーション制作に役立つ設計技術が習得できそうな感じがする。個人的にはデザインパターン云々よりもそれ以前のOOPに関する知識・思想の理解が深まったのが収穫。タイトルのActionScriptとDesign Patternはおまけかな。

良書だと思うけど、難点も結構有り。デザインパターンの説明をする際に、日常生活の例を挙げて説明することが多いのだが、これが非常にわかりにくい(日本人的でないというのもあるかも)。まずは必要最低限のプログラムの骨格を提示するところから始まる方がよかったのではないかと思う。さらにプログラム全体のフローチャート図がたくさん挙げられるが、図の説明が全くないので理解不能。図は非常にわかりやすそうな絵柄をしているだけに残念で仕方ない。最後に、ソースの誤字脱字が半端なく多い(10-20どころではない)。typoというレベルではなく、引数が書いてなかったりさらには一行きれいさっぱり書かれていなかったりするものもありちょっとびびる。

こういう弱点もふまえてオススメの読み方としては、

  1. 第一章のOOP基本概念を読む
  2. 各デザインパターン章の「Minimalist example」以降を読む(それ以前は前置きが長い)
  3. 「Minimalist Example」で理解できなければそれ以前の概念部分を読む
  4. 各章最後部はプログラムが複雑になっていくだけなので、自分が必要だと思ったところまでで適当に切り上げる(逆に言うと説明不足ということはないほど例題が豊富)

追記 2008/07/19

最終章「13. Symmetric Proxy Pattern」は未読。

追記 2008/07/20

完読。

追記 2008/08/08

レーダーチャートによる評価レビューはこちら

Read More

Design PatternsのSingletonパターンついてのメモ。例えば以下のような勇者クラス(Yusha.as)を作成したとする。

package singleton{
    public class Yusha{
        public function sayHello():void{
            trace("僕が勇者だ");
        }
    }
}

RPGにおいて勇者は一般に物語中に一人しか存在しない。従って以下のように複数勇者が作成できるアプリケーション(Main.as)はRPG的に問題がある。

package {
    import flash.display.Sprite;
    import singleton.Yusha;

    public class Main extends Sprite{
        public function Main(){
            var y1:Yusha = new Yusha();
            var y2:Yusha = new Yusha();
            var y3:Yusha = new Yusha();

            y1.sayHello();//僕が勇者だ
            y2.sayHello();//僕が勇者だ
            y3.sayHello();//僕が勇者だ
        }
    }
}

そこでSingletonパターンを利用する。例えばCreating Mashups with Adobe Flex and AIRでは以下のような方法でインスタンスを唯一作成する方法が紹介されている。

package singleton{
    public class Yusha{
        
        private static var _instance:Yusha;
        
        public static function getInstance():Yusha{
            if (Yusha._instance == null){
                Yusha._instance = new Yusha();
            }
            return Yusha._instance;
        }
    }
}

この方法だとgetInstance()メソッドを使う限り勇者は複数作成されないが、以下のように直接Yushaクラスを作成された時に対処できなくなる。

//In Main.as
var y:Yusha = new Yusha();

Yushaクラスのコンストラクタをprivateにできれば一件落着だけど、ActionScript 3.0ではコンストラクタをprivateにできない。そこで同パッケージ内にDummyクラスを用意、それをYushaクラスの引数としてYushaクラスのコンストラクタにパッケージ外部からアクセスできないようにする(疑似private化)。

//Yusha.as
package singleton{
    public class Yusha{
        
        private static var _instance:Yusha;
        
        public function Yusha(d:Dummy){   
        }
        
        public static function getInstance():Yusha{
            if (Yusha._instance == null){
                Yusha._instance = new Yusha(new Dummy());
            }
            return Yusha._instance;
        }
        
        public function sayHello():void{
            trace("僕が勇者だ");
        }
    }
}
//Dummy.as
package singleton{
    internal class Dummy{
    }
}

これによりYushaクラスは作成時にDummyクラスを引数とし、さらにDummyクラスはsingletonパッケージ外部からアクセス不能なので、MainクラスからのYushaクラスの呼び出しはgetInstance()メソッドからのみ可能となる。

//In Main.as
var y1:Yusha = new Yusha();//Error
var y2:Yusha = new Yusha(new Dummy());//Error
var y3:Yusha = Yusha.getInstance();//OK

これにより勇者は唯一無二作成されることになりました。めでたしめでたし。

[関連記事]
Tanablog: 複数版 Singleton パターン

ActionScript 3.0 Design Patterns (Adobe Developer Library)

追記:2008/07/11

Dummyクラスはsingletonパッケージ内であればアクセス可能なので、記事タイトルの「疑似private」は「疑似internal」とした方が良いかもしれない。AS3で1ファイルに複数のクラスを定義する方法 – てっく煮ブログのように、DummyクラスをYushaクラスファイル内部に定義することで、Dummyクラスへのアクセスをprivate化することもできる。

Read More