Objective-CのプロトコルとカテゴリとJavaのリフレクション 〜特別付録: Javaのprivateメソッドを外から実行する禁断の裏技〜

最近、Objective-Cと触れ合う機会がなくて色々忘れていたので、復習がてらプロトコルとカテゴリの復習をしてみた。題材は、僕の中でバイエルのようになっているStateパターン

でもそれだけだとつまらないので、最後にちょいワルなサンプルも載せるよ。

プロトコルを使ってStateパターン

更新するたびに、状態1と状態2を切り替えるスイッチのようなものを、Stateパターンで作ってみる。

まずプロトコルから。

State.h

#import <Foundation/Foundation.h>

// Stateプロトコル
@protocol State<NSObject>
-(id<State>) getNextState; // 更新処理を行い、次状態を返す
@end

次に、プロトコルを採用したコンクリートな状態クラス。

State1.h

#import <Foundation/Foundation.h>
#import "State.h"

// 状態1クラス
@interface State1 : NSObject<State>
@end

State1.m

#import "State1.h"
#import "State2.h"

// 状態1の実装
@implementation State1
-(id<State>) getNextState {
    NSLog(@"This is State1");
    return [[State2 alloc] init];
}
@end

State2.h

#import <Foundation/Foundation.h>
#import "State.h"

@interface State2 : NSObject<State>
@end

State2.m

#import "State1.h"
#import "State2.h"

// 状態2の実装
@implementation State2
-(id<State>) getNextState {
    NSLog(@"This is State2");
    return [[State1 alloc] init];
}
@end

最後に、mainメソッド。

main.m

#import <Foundation/Foundation.h>
#import "State1.h"

// mainメソッド
int main(int argc, const char * argv[])
{
    @autoreleasepool {
        id<State> state = [[State1 alloc] init];    // 状態1で初期化
        state = [state getNextState];               // 次状態に遷移
        state = [state getNextState];               // 次状態に遷移
        state = [state getNextState];               // 次状態に遷移
    }
    return 0;
}


実行すると、以下のように状態が切り替わっていることが分かる。

実行結果

2013-05-14 20:25:08.625 StatePatternSample[55932:303] This is State1
2013-05-14 20:25:08.627 StatePatternSample[55932:303] This is State2
2013-05-14 20:25:08.627 StatePatternSample[55932:303] This is State1

で、ここからが本番。

カテゴリを使ってStateパターン

先程のソースは、実はカテゴリを使って書き換える事が可能だ。大まかな方針としては、

  1. すべての基底クラスであるNSObjectにgetNextStateメソッドを追加
  2. NSObjectを継承したState1, State2が、各々getNextStateメソッドをオーバーライド

という感じ。いわゆる非形式プロトコルというやつだ。

State.h

#import <Foundation/Foundation.h>

// NSObjectを勝手に拡張
@interface NSObject (State) // ←この括弧の中身がチャームポイント
- (id)getNextState;
@end

State1.h

#import "State.h"

// 状態1クラス
@interface State1 : NSObject
@end

State1.m

#import "State1.h"
#import "State2.h"

// 状態1の実装
@implementation State1
-(id) getNextState {
    NSLog(@"This is State1");
    return [[State2 alloc] init];
}
@end

State2.h

#import <Foundation/Foundation.h>
#import "State.h"

@interface State2 : NSObject
@end

State2.m

#import "State1.h"
#import "State2.h"

// 状態2の実装
@implementation State2
-(id) getNextState {
    NSLog(@"This is State2");
    return [[State1 alloc] init];
}
@end

main.m

#import <Foundation/Foundation.h>
#import "State1.h"

// mainメソッド
int main(int argc, const char * argv[])
{
    @autoreleasepool {
        id state = [[State1 alloc] init];    // 状態1で初期化
        state = [state getNextState];               // 次状態に遷移
        state = [state getNextState];               // 次状態に遷移
        state = [state getNextState];               // 次状態に遷移
    }
    return 0;
}

Stateプロトコルを噛ませず、各状態クラスはNSObjectを直に継承する形となっているため、気持ちちょっとスッキリしている。

Objective-Cのこうした柔軟な仕様に触発されたのか、最近妙にC#動的言語へとパラダイムシフトしてきている。

そんなわけで、今日は本当はObjective-CからC#ネタへと繋げたかったのだが、なんか僕のMonoDevelopの機嫌が悪いので大人の事情により急遽Javaネタに方針転換する事にした。

JavaのリフレクションでStateパターン

Javaで素直にStateパターンを書こうとする場合、通常はインタフェース(Objective-Cでいうところのプロトコル)を使う。

たとえば、個々の具体的な状態を表すState1クラスとState2クラスがあるとしよう。2つのクラスが同じインタフェースを介して論理的に紐づいていないと、両者を統一的に扱う事はできない。


…というのは大ウソで、リフレクションと呼ばれる反則技を使うことにより全く関係のないクラス同士を一緒くたにしてしまうことができる。以下に実例。

State1.java

package com.tercel_tech.StatePatternSample;

public class State1 {
    public Object getNextState() {
        System.out.println("This is State1");
        return new State2();
    }
}

State2.java

package com.tercel_tech.StatePatternSample;

// このクラスは、State1とは全く無関係
public class State2 {
    
    // たまたま同じ名前のメソッドを持っているけど
    public Object getNextState() {
        System.out.println("This is State2");
        return new State1();
    }
}

Main.java

package com.tercel_tech.StatePatternSample;

import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) {
        // ここまではわりとふつう。
        Object state = new State1();
        
        // ↓こう書くことはできない。
        // state = state.getNextState();
        
        // ↓代わりに、こう書くことならできる。
        try {
            state = state.getClass().getDeclaredMethod("getNextState").invoke(state);
            state = state.getClass().getDeclaredMethod("getNextState").invoke(state);
            state = state.getClass().getDeclaredMethod("getNextState").invoke(state);
            
        } catch (IllegalAccessException | IllegalArgumentException
                | InvocationTargetException | NoSuchMethodException
                | SecurityException e) {
            e.printStackTrace();
        }   
    }
}

ご覧のとおり、State1とState2には特に結びつきがない。にもかかわらず、stateオブジェクトの実体は統一的なコードで切り替わっている。

これがリフレクションというやつで、とにかく指定した名前のメソッドを見つけたら問答無用で実行しようとする恐ろしいヤツなのである。

リフレクションでprivateを暴く

さらに恐ろしいことに、このリフレクションを使うとprivateメソッドを外から実行できてしまう。

ある日、プログラマのたーせるさんが上司に頼まれてこういうクラスを作ったとしよう。

package com.tercel_tech.StatePatternSample;

public class Hoge {
    
    // つまらない普通のメソッド
    public void work() {
        System.out.println("私はすばらしいプログラマです");
    }
    
    private void secret() {
        // 仕事のストレスでつい書いてしまったお見せできないメソッド。
        // privateなので絶対にバレない。はず。
        System.out.println("ばーかばーかお前のかーちゃんでべそ!");
    }
}

言うに事欠いてprivateメソッドにとんでもない悪口が。

しかしprivateはクラスの外から絶対に実行される事はないって研修で習ったので、たーせるさんは安心しきっていた。

ちなみにIT企業の新人向け技能研修の講師は割と平気でウソをつく*1ので注意が必要である。

呼び出し側でこう書かれたら終わりだ。

Main.java

package com.tercel_tech.StatePatternSample;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) {
        Hoge hoge = new Hoge();
        
        try {
            Method method = hoge.getClass().getDeclaredMethod("secret");
            method.setAccessible(true);  // ←privateメソッドを破る禁断の一行 
            method.invoke(hoge);
            
        } catch (Exception e) {
            e.printStackTrace();
        }   
    }
}

javaコマンドを打つと、まさかの実行結果が。

実行結果

ばーかばーかお前のかーちゃんでべそ!

見事にprivateメソッドが破られている。悪用しちゃダメだよ。

最後に

まぁそんな感じで、脱線に脱線を重ねてそろそろ着地点が見えなくなってきたので強引にまとめるー。

Javaのリフレクションに関してはまず入門書に載っていないと思うので、よい子のみんなは学校の友達に「オレってハッカー!」と自慢してみよう∩( ・ω・)∩


…でも本当はC#の話がしたかった。本当はC#(ry

*1:受講生の混乱を避けるための教育的配慮である。

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