Objective-CとCocoaの立場から、デザインパターンを眺めてみるこの取り組みも、いよいよ佳境に入ってきた。残すところ数パターンとなってきた。だが、まだまだクセの強いパターンが残っている。
今回からは、Interpreterパターンを取り上げよう。文法や言語を読み込むために使われるパターンだ。
Interpreterパターンとは
Interpreterパターンは、その名の通り、インタプリタと考えてしまえばいいだろう。インタプリタは、計算機の処理系の一種で、プログラミング言語などを読み込みながら処理するものだ。たとえば、BASICインタプリタや、Perlインタプリタなどがある。
このインタプリタのためのパターンが、Interpreterパターンだ。インタプリタそのものをデザインパターンにしてしまうとは、なかなか大胆な発想だ。
Interpreterパターンでは、文法規則の1つ1つをクラス化してしまうという、これまた大胆な方法を使う。GoF本では例として、Bool表現の文法を取り上げている。これをこの記事でも使おう。
次のような、Bool表現のための文法規則があるとする。
BooleanExp ::= VariableExp | Constant | OrExp | AndExp | NotExp | '('BooleanExp')'
AndExp ::= BooleanExp 'and' BooleanExp
OrExp ::= BooleanExp 'or' BooleanExp
NotExp ::= 'not' BooleanExp
Constant ::= 'YES' | 'NO'
VariableExp ::= 'A' | 'B' | ... | 'X' | 'Y' | 'Z'
これが、論理値のための文法規則となる。この文法を使えば、次のような表現を評価出来る訳だ。
(YES and x) or (y and (not x))
さて、Interpreterパターンでこの文法をどう取り扱うかというと、それぞれの文法をクラス化する。つまり、BooleanExpクラス、AndExpクラス、OrExpクラスなどを、順次作っていく訳だ。
これは確かに、文法のオブジェクト指向化という観点で見れば、非常に直接的なアプローチだ。だが、計算機科学の世界では、インタプリタやコンパイラを作るときは、文字列を直接パースして解析木を作り、変換していくアプローチの方が一般的であるし、パーサの作成も汎用化されている。
GoF本でも、Interpreterパターンを適用する際には、「文法が単純」であり、「効率が重要な関心事ではない」場合が適切であろう、と書かれている。つまり、速くて軽いインタプリタを作るのであれば、従来の手法の方がよい。それに対して、単純な文法で、変更や拡張が頻繁に行われるような場合に、Interpreterパターンが登場する余地がある、ということだろう。
Bool表現のためのインタプリタ実装
では、InterpreterパターンのObjective-Cでの実装例を紹介しよう。
取り上げるのは、先ほど紹介したBool表現だ。GoF本では、実装の一部のみが紹介されている。ここでは、これを実現する完全な実装をやってみよう。ソースコードが長くなるが、ご容赦いただきたい。
作成するクラスは、文法のためのルートクラスになるBooleanExp。そのサブクラスであるAndExp、OrExp、NotExp、Constant、VariableExp。そして、変数に代入された値を管理するContextだ。
クラス図は、次のようになる。
では、作っていこう。まず、BooleanExpクラスだ。これは、文法のルートとなるクラスである。文法の主目的は、与えられた文を評価することだ。そこで、evaluate:というメソッドを1つ用意しよう。引数は、Contextクラスになる。返り値は、Bool値の文の評価はYESかNOかになるので、Bool型を返す。
List 1.
@interface BooleanExp : NSObject
{
}
- (BOOL)evaluate:(Context*)context;
@end
このクラスは、ルートクラスなので、実際には直接使われることはない。そこで、evaluateメソッドの実装では、NOを返すだけにしておく。
次は、AndExpクラスだ。これは、論理積の文法を表すクラスだ。論理積を計算するには、2つの論理値が必要になる。そこで、初期化メソッドで、この値を与えることにしよう。
List 2.
@interface AndExp : BooleanExp
{
BooleanExp* _operand1;
BooleanExp* _operand2;
}
- (id)initWithOperand1:(BooleanExp*)operand1 operand2:(BooleanExp*)operand2;
@end
AndExpメソッドのevaluate:メソッドでは、これらを評価して論理積を計算することになる。つまり、AndExpの実装は次のようになるだろう。
List 3.
@implementation AndExp
- (id)initWithOperand1:(BooleanExp*)operand1 operand2:(BooleanExp*)operand2
{
self = [super init];
if (!self) {
return nil;
}
// Operand1とOperand2を設定する。
_operand1 = [operand1 retain];
_operand2 = [operand2 retain];
return self;
}
- (BOOL)evaluate:(Context*)context
{
// Operand1とOperand2のANDの値を返す。
return [_operand1 evaluate:context] && [_operand2 evaluate:context];
}
@end
同様に、論理和のためのOrExpと、否定のためのNotExpの定義、実装は、次のようになる。
List 4.
@interface OrExp : BooleanExp
{
BooleanExp* _operand1;
BooleanExp* _operand2;
}
- (id)initWithOperand1:(BooleanExp*)operand1 operand2:(BooleanExp*)operand2;
@end
@interface NotExp : BooleanExp
{
BooleanExp* _operand;
}
- (id)initWithOperand:(BooleanExp*)operand;
@end
List 5.
@implementation OrExp
- (id)initWithOperand1:(BooleanExp*)operand1 operand2:(BooleanExp*)operand2
{
self = [super init];
if (!self) {
return nil;
}
// Operand1とOperand2を設定する。
_operand1 = [operand1 retain];
_operand2 = [operand2 retain];
return self;
}
- (BOOL)evaluate:(Context*)context
{
// Operand1とOperand2のORの値を返す。
return [_operand1 evaluate:context] || [_operand2 evaluate:context];
}
@end
@implementation NotExp
- (id)initWithOperand:(BooleanExp*)operand
{
self = [super init];
if (!self) {
return nil;
}
// Operandを設定する。
_operand = [operand retain];
return self;
}
- (BOOL)evaluate:(Context*)context
{
// OperandのNOTの値を返す。
return ![_operand evaluate:context];
}
@end
次は、定数を表すConstantだ。このクラスは、論理値を引数として取る。
List 6.
@interface Constant : BooleanExp
{
BOOL _value;
}
- (id)initWithValue:(BOOL)value;
@end
ということは、評価は簡単だ。この保持している論理値の値を返すだけである。
List 7.
@implementation Constant
- (id)initWithValue:(BOOL)value
{
self = [super init];
if (!self) {
return self;
}
// valueを設定する。
_value = value;
return self;
}
- (BOOL)evaluate:(Context*)context
{
// valueの値をそのまま返す。
return _value;
}
@end
次は、変数を表すVariableExpだ。これは、変数名を指定して初期化することにする。
List 8.
@interface VariableExp : BooleanExp
{
NSString* _name;
}
- (id)initWithName:(NSString*)name;
@end
変数の評価をどうするかだが、ここでContextを使う。Contextには、現在の変数のそれぞれの値が設定されているのだ。変数の名前を渡して、現在の値を取得することが出来る。
List 9.
@implementation VariableExp
- (id)initWithName:(NSString*)name
{
self = [super init];
if (!self) {
return nil;
}
// 変数名を設定する。
_name = [name retain];
return self;
}
- (BOOL)evaluate:(Context*)context
{
// コンテキストから、変数名を指定して値を取得する。
return [context lookupWithName:_name];
}
@end
最後は、Contextだ。これは、変数名とその値を管理しておくためのものだ。そのために、NSMutableDictionary型の辞書を1つ用意しておこう。変数の値の取得はlookupWithName:で、設定はassignValue:toName:で行う。
List 10.
@interface Context : NSObject
{
NSMutableDictionary* _nameDict;
}
- (BOOL)lookupWithName:(NSString*)name;
- (void)assignValue:(BOOL)value toName:(NSString*)name;
@end
実装は、次のようになるだろう。それぞれのメソッドで、変数名をキーとして辞書にアクセスすることになる。
List 11.
@implementation Context
- (id)init
{
self = [super init];
if (!self) {
return self;
}
// インスタンス変数を初期化する。
_nameDict = [[NSMutableDictionary dictionary] retain];
return self;
}
- (BOOL)lookupWithName:(NSString*)name
{
// 変数名の辞書から値を取得する。
return [[_nameDict objectForKey:name] boolValue];
}
- (void)assignValue:(BOOL)value toName:(NSString*)name
{
// 変数名の辞書に値を設定する。
[_nameDict setObject:[NSNumber numberWithBool:value] forKey:name];
}
@end
これで、実装は完了だ。実際に使ってみよう。次の文を評価するものとする。
(YES and x) or (y and (not x))
評価の手順は次のようになる。まず、変数xとyを作成しよう。これは、VariableExpクラスを作ることになる。さらに、定数'YES'を作る。これはConstantクラスだ。
これらに論理演算を適用していこう。まず、'not x'だ。これはNotExpクラスを作り、オペランドとして、xを渡してやる。同様に、'YES and x'、'y and (not x)'と作っていこう。最後に、論理和を作れば式の出来上がりだ。
次に、変数に値を設定してやる必要がある。このためにContextクラスを作ろう。xとyに、それぞれ論理値を設定してやる。
これで準備が完了だ。最後に、評価を行う。評価は、Contextを引数として、evaluate:メソッドを呼べばいい。これで、式の評価を、YESまたはNOで得ることが出来る。
List 12.
// 変数'x'と'y'を作成する。
VariableExp* x;
VariableExp* y;
x = [[VariableExp alloc] initWithName:@"X"];
y = [[VariableExp alloc] initWithName:@"Y"];
// 定数'YES'を作成する。
Constant* yes;
yes = [[Constant alloc] initWithValue:YES];
// 'not x'を表す式を作成する。
NotExp* notExp;
notExp = [[NotExp alloc] initWithOperand:x];
// 'YES and x'を表す式を作成する。
AndExp* andExp1;
andExp1 = [[AndExp alloc] initWithOperand1:yes operand2:x];
// 'y and not(x)'を表す式を作成する。
AndExp* andExp2;
andExp2 = [[AndExp alloc] initWithOperand1:y operand2:notExp];
// '(YES and x) or (y and not(x))'を表す式を作成する。
BooleanExp* exp;
exp = [[OrExp alloc] initWithOperand1:andExp1 operand2:andExp2];
// 変数'x'にYESを、変数'y'にNOを設定する。
Context* context;
context = [[Context alloc] init];
[context assignValue:YES toName:@"X"];
[context assignValue:NO toName:@"Y"];
// 式を評価する。
BOOL result;
result = [exp evaluate:context];
ずいぶん長くなってしまったが、これがInterpreterパターンのObjective-Cでの実装例だ。文法がそのままクラスに転換されている様子が分かったと思う。