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」でぐぐったら、これもやっぱり、同じような事既にやられてた。そりゃそうか。