Proxyを使って強引にキャストする実験

というのが出来そうなので書いてみた

ソースコード

package sample.proxycast;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@SuppressWarnings("unchecked")
public final class ProxyCastUtils {
    private ProxyCastUtils() {
    }
    /**
     * インタフェースに定義されたメソッドをtargetに委譲するProxyを生成する
     * @param <T> インタフェースの型
     * @param target 委譲先のオブジェクト
     * @param interfaze インタフェース
     * @return 指定したインタフェースを持つproxy
     */
    public static <T> T asInterface(Object target, Class<T> interfaze) {
        if (target == null) {
            throw new IllegalArgumentException("target is null.");
        }
        if (! confirmsTo(target, interfaze)) {
            throw new IllegalArgumentException("" + target.getClass() + " does not confirm to " +  interfaze);
        }
        Object proxy = Proxy.newProxyInstance(interfaze.getClassLoader(),
                new Class[] { interfaze },
                new CastInvocationHandler(target));
        return interfaze.cast(proxy);
    }
    
    /**
     * 渡されたobjectがinterfazeに定義されたメソッドを全て実装しているかをチェックする
     * @param object Object
     * @param interfaze Interface
     * @return 実装していればtrueを戻す
     */
    public static boolean confirmsTo(Object object, Class interfaze) {
        if (object == null) {
            throw new IllegalArgumentException("object is null.");
        }
        if (! interfaze.isInterface()) {
            throw new IllegalArgumentException("" + interfaze + " is not interface.");
        }
        Class aClass = object.getClass();
        Method[] methods = interfaze.getMethods();
        for (Method method : methods) {
            Method compatibleMethod = getCompatibleMethod(aClass, method);
            if (compatibleMethod == null) {
                return false;
            }
        }
        return true;
    }

    private static Method getCompatibleMethod(Class aClass, Method method)
            throws SecurityException {
        String methodName = method.getName();
        Class[] parameterTypes = method.getParameterTypes();
        try {
            return aClass.getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }
    
    /**
     * メソッドの呼び出しを単純にtargetに委譲するInvocationHandler
     */
    public static class CastInvocationHandler implements InvocationHandler {
        /** 委譲先のオブジェクト */
        private Object target;

        public CastInvocationHandler(Object target) {
            super();
            if (target == null) {
                throw new IllegalArgumentException("target is null.");
            }
            this.target = target;
        }

        /**
         * メソッドの呼び出しを単純にtargetに委譲する
         */
        public Object invoke(Object obj, Method method, Object[] arguments) throws Throwable {
            Class targetClass = target.getClass();
            Method targetMethod = getCompatibleMethod(targetClass, method);
            return targetMethod.invoke(target, arguments);
         }

    }

}

サンプル

同じようにgetDataName()とgetId()というメソッドを持つ二つのクラスEmployeeとDepartmentというクラスのインスタンスを、直接継承関係のないLoggableインタフェースにキャスト(?)してログ出力

public interface Loggable {
    Integer getId();
    String getDataName();
}

public class Logger {
    public void logEdited(Loggable loggable) {
        System.out.println("Edited: " + loggable.getDataName() + "(" + loggable.getId() + ")");
    }
}

呼び出し部分

        Employee employee;	// Employeeのインスタンスが入っている
        Department department;	// Departmentのインスタンスが入っている
        Logger logger;		// Loggerのインスタンスが入っている
        
        // EmployeeをLoggableに強引にキャスト
        Loggable employeeLoggable = ProxyCastUtils.asInterface(employee, Loggable.class);
        logger.logEdited(employeeLoggable);
        
        // DepartmentをLoggableに強引にキャスト
        Loggable departmentLoggable = ProxyCastUtils.asInterface(department, Loggable.class);
        logger.logEdited(departmentLoggable);

備考

Objective-Cなんかだと、呼び出したのと同一シグネチャのメソッドがレシーバに実装されていれば、とりあえず実行してくれる。


Javaの場合、そういうときは共通のメソッドをインタフェースにくくり出すのが基本なんだろうけど、あまり本質的でない処理(例えば「データの編集履歴デバッグ用のログに出力する」など)の時は、各クラスにインプリするのは(パッケージ間の依存関係が複雑になるなどして)あまり嬉しくない。のでそういう時にはこの方法も使えない事は無いかも。


というのは言い訳で、ぶっちゃけObjective-C(PDO)の-confirmsToProtocol:と-setProtocolForProxy:のイメージですわ。リモート呼び出しとかORBに使えないかという魂胆。RemoteObjectInvocationHandlerってそういう感じなのかな。


まあしかし邪道だとは思う。

2008/3/31追記

そういえばDuck typingって言葉あったな、と思って「java duck typing proxy」でぐぐったら、これもやっぱり、同じような事既にやられてた。そりゃそうか。