Java8でmixinをがんばってみる
Java8からinterfaeのデフォルト実装が使えるようになります。インタフェースは複数implemntsできます。したがって複数のdefault実装付インタフェースを組み合わせてクラスを構成することができそうです。mixiinによる実装の再利用です。(この場合implementと呼んでよいのか疑問ですが)。うまく使えばScalaのトレイトみたいなことが出来るはず。
それでは実例を。Userオブジェクトを検索して、その人にメールを送るという架空のアプリを考えます。
まずはUserオブジェクト。
いちいち複数ファイルを書くのは面倒なので、今回はすべてstaticなネストクラス(とネストインタフェース)として定義します。
public class MyTest { public static class User { String id; User(String id) {this.id = id;} @Override public String toString() {return id;} }
はい適当ですね。
そしてUserを検索してくるUserRepositoryのインタフェースと、文字列をメール送信するMailGatewayを定義します。こちらには実装をつけません。(今回のポイント)
//ステップ1 public interface UserRepository { User findUser(String id); } public interface MailGateway { void send(String str); }
こちらも適当です。
そしてUserRepositoryやMailGatewayに依存し、これらのサービスを組み合わせて必要な処理をおこなうMailSenderクラスを定義します。
//ステップ2 /** UserRepositoryとMailGatewayに依存するサービス */ public static abstract class MailSender implements UserRepository ,MailGateway { User u ; public void sendMail(String id) { u = findUser(id); //UserRepositoryのメソッド send(u.id); //MailGatewayのメソッド } }
こちらは抽象クラスとして定義します。必要なインタフェースを定義していないので当然ですね。(抽象クラスなのでなんとなくインスタンス変数も持たせました)
ここからが重要なところです。肝心の各種インタフェースを実装したインタフェースを定義します(上手い呼び名なし)
//ステップ3 /**実装*/ public interface UserRepositoryDefault extends UserRepository { public default User findUser(String id) { return new User(id); } } /**実装*/ public interface MailGatewayDefault extends MailGateway { public default void send(String str) { System.out.printf("%sにメールを送ったよ\n",str); } }
まあ適当です。ここまでで準備完了。
そしてここからが美味しいところ、それぞれの実装を組み合わせたクラスを定義します。
//ステップ4 //以下ですべてを繋ぐ /** * defaultメソッドで実装されたインタフェース宣言を組み合わせたもの * 基本実装は必要無いが、mixinされたもの同士実装がぶつかるときは、ここでケアする * */ public static class MailSenderImpl extends MailSender implements UserRepositoryDefault, MailGatewayDefault {}
ちょっとScalaっぽい見た目です。
ここではMailSenderを継承し、さらに実装が書かれたインタフェースをimplentsしています。中身は必要ないので一行でかけます。
もしデフォルト実装インタフェース同士のメソッドシグネチャがかぶってしまった場合、コンパイルエラーが出るので、このクラスでオーバーライドして調整する必要があります。(きちんとしたトレイトがある言語ならば、ここら辺を上手くケアしてくれるのです)
そして、このクラスの利用はこんな感じです。(本来ならばMailSenderImplの生成はクラスの利用者から見ないところで行われるでしょう)
/** 実行*/ public static void main(String[] args) { MailSender ms = new MailSenderImpl(); ms.sendMail("id"); }
この仕組みを利用することによってたとえばテスト時にはMailGatewayだけをMockにするといったことが簡単にできます。
また、簡易AOP的なこともできます。たとえばMail送信時にログをとりたいなら、、、、
/**実装*/ public interface LogMailGateway extends MailGatewayDefault { public default void send(String str) { System.out.println("メールを送信しそう"); MailGatewayDefault.super.send(str); System.out.println("メールを送信したみたい"); } }
上記のようなデコレータをつくっておいて
public static class MailSenderImpl extends MailSender implements UserRepositoryDefault, LogMailGateway {}
このように組み合わせることが可能です。簡易DIとかにも応用できそうですね。
Scalaのトレイトのようにインスタンス変数をもったり、線形化によって仮想の継承関係をつくって、メソッド衝突を防いだりすることはできませんが、その分シンプルで理解しやすいでしょう。局面を限れば非常に有効なパターンだと思います。