読者です 読者をやめる 読者になる 読者になる

キー値監視を使ってみよう

こんにちは。

本日の題材は、『増補改訂版Java言語で学ぶデザインパターン入門』のObserverパターンをObjective-Cで実装してみる! ……です。

Observerパターンとは、たとえば「GUIのボタンが押された!」とか「パラメータが閾値を突破した!」とか、とにかく状態が変わったタイミングですぐ次のアクションを起こせるように、対象を観察 (Observe) するための方法です(厳密な定義ではないですが……)。

増補改訂版Java言語で学ぶデザインパターン入門』p.256より引用。

Observerパターンを使ったサンプルプログラムを読んでみましょう。ここで作るサンプルプログラムは、数をたくさん生成するオブジェクトを観察者が観察して、その値を表示するという単純なものです。ただし、表示の方法は観察者によって異なります。DigitObserverは値を数字で表示しますが、GraphObserverは値を簡易グラフで表示します。

本日登場するクラス一覧

名前 解説
RandomNumberGenerator ランダムに数を生成するクラス
DigitObserver 数字で数を表示するクラス
GraphObserver 簡易グラフで数を表示するクラス

このほか、動作確認用の main 関数もあります。

Objective-CJavaほどガチガチにクラス設計をしなくてもよい分、元ネタと比較すると継承の階層が浅くなっています。

たとえば、原著には記載のあった抽象クラス(NumberGenerator)やインタフェース(Observer)はことごとく削除しました。Objective-Cにはそもそも抽象クラスがない・Objective-CではObserverのための基底クラスやインタフェースをわざわざ作る必要がないなど、Javaとはだいぶ事情が異なります。

RandomNumberGeneratorクラス

RandomNumberGeneratorクラスは、乱数を生成するものです。numberプロパティには、現在の乱数値が保持されます。
executeメソッドは乱数(0〜49の範囲)を20個生成し、そのつど観察者に通知します。

ちなみに、Observerパターンのために必要なメソッドは NSObject で用意され、ヘッダファイル Fondation/NSKeyValueObserving.h にいくつかの非形式プロトコルとして定義されています。

RandomNumberGenerator.h
#import <Foundation/Foundation.h>

@interface RandomNumberGenerator : NSObject
- (void)addObserver:(NSObject *)observer;
- (void)removeObserver:(NSObject *)observer;
- (void)execute;
@property NSNumber *number;
@end
RandomNumberGenerator.m
#import "RandomNumberGenerator.h"
static NSString * const kKeyPath = @"number";

@implementation RandomNumberGenerator

- (void)addObserver:(NSObject *)observer
{
    [super addObserver:observer     // 追加する観察者オブジェクト
            forKeyPath:kKeyPath     // 観察対象のプロパティ(ここではnumber)
               options:NSKeyValueObservingOptionNew // 変更後の値を監視
               context:NULL];       // 通知を受け取るメソッド(ここではNULL)
}

- (void)removeObserver:(NSObject *)observer
{
    [super removeObserver:observer      // 削除する観察者オブジェクト
               forKeyPath:kKeyPath];    // 観察対象だったプロパティ
}

- (void)execute
{
    for(int i = 0; i < 20; ++i) {
        self.number = @(arc4random() % 50);
    }
}

@end

DigitObserverクラス

DigitObserverクラスは、観察した数を「数字」で表示するためのものです。observeValueForKeyPath: ofObject: change: context: メソッドの引数として与えられた object からキー値コーディングを使って数を取得し、それを表示します。ここでは、表示の様子がよくわかるようにsleepForTimeInterval: メソッドを使ってスピードを遅くしています。

DigitObserver.h
#import <Foundation/Foundation.h>

@interface DigitObserver : NSObject
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context;
@end
DigitObserver.m
#import "DigitObserver.h"

@implementation DigitObserver
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context {
    
    // 変更後の値を取得
    // objectはRandomNumberGeneratorオブジェクト
    // keyPathはnumberプロパティにアクセスするためのキーパス文字列
    NSNumber *number = [object valueForKey:keyPath];
    
    // numberを画面に表示
    printf("%s", [[NSString stringWithFormat:
                   @"DigitObserver: %@\n", number] UTF8String]);
    
    // おまけ: 100ミリ秒待つ
    [NSThread sleepForTimeInterval:0.1f];
}
@end

GraphObserverクラス

GraphObserverクラスは、観察した数を ***** のような「簡易グラフ」で表示します。

GraphObserver.h
#import <Foundation/Foundation.h>

@interface GraphObserver : NSObject
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context;
@end
GraphObserver.m
#import "GraphObserver.h"

@implementation GraphObserver
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context {
    
    // 変更後の値を取得
    // objectはRandomNumberGeneratorオブジェクト
    // keyPathはnumberプロパティにアクセスするためのキーパス文字列
    NSNumber *number = [object valueForKey:keyPath];
    
    // numberを画面に表示
    printf("GraphObserver: ");
    for (int i = 0; i < [number intValue]; ++i) {
        putchar('*');
    }
    putchar('\n');
    
    // おまけ: 100ミリ秒待つ
    [NSThread sleepForTimeInterval:0.1f];
};
@end

main関数

main関数では、RandomNumberGeneratorのインスタンスを1個作り、その観察者を2個作ります。observer1はDigitObserverの、observer2はGraphObserverのインスタンスです。

addObserverメソッドを使って観察者を登録した後、[generator execute] を使って数を生成します。

main.m
#import <Foundation/Foundation.h>

#import "RandomNumberGenerator.h"
#import "DigitObserver.h"
#import "GraphObserver.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        RandomNumberGenerator *generator = [[RandomNumberGenerator alloc] init];
        DigitObserver *observer1 = [[DigitObserver alloc] init];
        GraphObserver *observer2 = [[GraphObserver alloc] init];
        
        [generator addObserver:observer1];
        [generator addObserver:observer2];
        
        [generator execute];
        
        [generator removeObserver:observer1];
        [generator removeObserver:observer2];
    }
    return 0;
}

実行結果

GraphObserver: *******************
DigitObserver: 19
GraphObserver: ***************************************
DigitObserver: 39
GraphObserver: *****
DigitObserver: 5
GraphObserver: *************
DigitObserver: 13
GraphObserver: *********
DigitObserver: 9
GraphObserver: **************
DigitObserver: 14
GraphObserver: ********************
DigitObserver: 20
GraphObserver: *********************************
DigitObserver: 33
GraphObserver: **********************************
DigitObserver: 34
GraphObserver: *********
DigitObserver: 9
GraphObserver: **
DigitObserver: 2
GraphObserver: ********
DigitObserver: 8
GraphObserver: ***********************************************
DigitObserver: 47
GraphObserver: **********************
DigitObserver: 22
GraphObserver: *************
DigitObserver: 13
GraphObserver: *****************************************
DigitObserver: 41
GraphObserver: ***************************
DigitObserver: 27
GraphObserver: **********************************
DigitObserver: 34
GraphObserver: *************************
DigitObserver: 25
GraphObserver: ************************************
DigitObserver: 36
Program ended with exit code: 0

はいっ。

こんな感じで、2個の観察者は、値が変わったタイミングを見事に捕捉しています。

面白いことに、各 Observerクラスは、積極的にRandomNumberGeneratorの値を取りに行くような動きをしません。

値が変わったタイミングで、必要な処理がフックされます。

……おっと、Observerパターンの説明が長くなってしまった(・ω・ lll)


本日ご紹介したObserverパターンのコードには、キー値監視というObjective-C特有の機能が使われています。

ものすごいざっくり言うと、オブジェクトのプロパティに対して“動的に”アクセスする仕組みが使われています(監視対象をコンパイル時ではなく実行時に決めることもできる)。

そういった意味で、JavaのObserverパターンよりかなり柔軟な動きを作れるのですが、いろいろ制約もあります。

その一部を『詳解 Objective-C 2.0 第3版』のpp.547-548あたりから引用します。

あるプロパティに対してキー値コーディングを使ったアクセスができることを、そのプロパティがキー値コーディングに準拠している、あるいはKVC準拠 (compliance) である、といいます。

(中略)

プロパティが属性(スカラ値や単純型のオブジェクト)、または対一リレーションシップの場合、KVC準拠であるための条件は次の通りです。プロパティの名前が "name" であるとして説明します。

1. (a) name または isName というアクセサメソッドを実装している。あるいは、
(b) name (または _name)というインスタンス変数を持っている。
2. 可変なプロパティの場合、さらに setName: も実装する。そのキーに対して検証を行うのが適切である場合には、検証メソッド (validateName: error: )を実装する。ただし、setName: メソッドから、検証メソッドを呼び出してはいけない。

今回は RandomNumberGenerator の number プロパティが、クラスの外からは参照専用として機能しているため、検証メソッドの実装は端折りました。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.