こんにちは、神は死んだ。
今回もなんとなく作っている個人プロジェクトから得た指標について書いていこうと思います。
今回はinterfaceについてです。
めっさ長いので、読むのが面倒な時は一番下のまとめっぽいものを読むといいかもしれません。まとめだけじゃ意味が分からないかもしれませんが。
interface
今までぼくはinterfaceで二つの使い方をしていました。
そして今回の個人プロジェクトでインターフェイスの使い勝手を良くする方法をゴーストに囁かれました。
イミュータブル化
class Data implements Immutable {
private int i;public void set(int i) {
this.i = i;
}@Override
public int get() {
return i;
}
}
interface Immutable {
public int get();
}
Data d = new Data();
d.set(10);Immutable im = d;
一つはイミュータブルを簡単に実装する事です。
上記のように、Dataクラスの変数をImmutableインターフェイスの変数に代入する事により、簡単にイミュータブルを実装する事が出来ます。
もちろん、これはget()を実行する毎に値が変わるような、副作用のないメソッドである事が大前提になります。
処理のデータ化
二つ目は処理をデータとして扱いたい時に使用する、というものです。
でもはっきりいってこれはinterfaceはあんまり関係ない……かも。
この使用方法はJavaのライブラリにあるインターフェイスを参考にしました。
java.util.Observerを見てみましょう。
public interface Observer {
public void update(Observable o, Object arg);
}
ObserverインターフェイスはObservableクラスに格納される為に存在するインターフェイスです。名前の通り、Observerパターンに関するものです。
Observableクラス(正確にはObservableクラスを継承したクラス)にObserverインターフェイスを実装したクラスを格納します。ObservableクラスはsetChanged()、notifyObservers()の順にメソッドを呼び出す事でObserverを実装したクラスのupdate(Observable, Object)を一つずつ実行していきます。
このように、Observableクラスから見ると、Observerインターフェイスを実装したクラスというのは配列に格納したデータのように見えます。
ぼくはObserverインターフェイスの扱われ方を元に、処理をデータとして扱いたい時はそれ専用のインターフェイスを宣言*1し、インターフェイスを実装したクラスに短い処理を書く、という方法をとっています。
例えば配列に処理のデータを格納し、配列の最初から最後まで順番に実行していく機構を作れば、あとは配列のデータを並べ変えるだけで動的に処理を変更する事が可能です。
単純に処理をデータにした場合、基本的な性質はC++の関数オブジェクトと同じです。ただ、Javaでは()をオーバーライドできないので、()の代用となるメソッドを使う点が違います。
処理がデータになるという点が異様に聞こえるかもしれません。が、例えばJavaScriptのfunctionはデータ型の扱いなりますので、決しておかしな概念ではありません。
インターフェイスの使い勝手の向上
今まで継承はほとんど使用してなかったのですが、良い使用法を見つけました。
インターフェイスの思想をより詳細にする
interface List {
public List add(int i);
public int get(int index);
public List remove(int index);
public int size();
}
abstract class AbstractList implements List {
private int size;
@Override
public final List add(int i) {
_add(i);
size++;
return this;
}
@Override
public final int get(int index) {
return _get(index);
}
@Override
public final List remove(int index) {
_remove(index);
size--;
return this;
}
@Override
public final int size() {
return size;
}
protected abstract void _add(int i);
protected abstract int _get(int index);
protected abstract void _remove(int index);
}
class Array extends AbstractList {
private ArrayListarray = new ArrayList ();
@Override
protected void _add(int i) {
array.add(i);
}@Override
protected int _get(int index) {
return array.get(index);
}@Override
protected void _remove(int index) {
array.remove(index);
}
}
例えば、貴方がListという動的な配列に関するインターフェイスを作ったとしましょう。
Listには
-
- 値を追加するadd
- 追加された値を参照するget
- 追加された値を削除するremove
- 追加された値の数を参照するsize
というメソッドがあります。
このままListインターフェイスを実装したクラスを作ってもいいのですが、それではListインターフェイスで想定した意味を逸脱する可能性があります。もしかするとsize()で返される値が追加された値の数とは全く関係ないものかもしれません。何故かsize()にListインターフェイスに規定されていない動作を書いているかもしれません。それはListインターフェイスを使う時には不適切なものです。
そのような問題を解決する為に、AbstractListを宣言しListインターフェイスで規定された動作をより忠実に再現できるようにします。
AbstractListはListインターフェイスをメソッドを実装しています。
size()以外のメソッドはほぼprotected abstractなメソッドに処理をお任せしています。また、size()はadd(int)とremove(int)が実行されるたびに値の変更が発生するのが決まっているので、AbstractList上でsize()が適切な挙動を行えるようにしています。size()はこれ以上オーバーライドされる必要はないので、finalなメソッドにするとさらに扱いやすくなるでしょう。
このようにListインターフェイスの規定を満たすような実装を再現しやすいようにAbstractListを実装していきます。
ArrayクラスはAbstractListを継承したクラスです。
AbstractListで宣言されたprotected abstractなメソッドをここで実装します。単純にListインターフェイスを実装するよりもsize()を気にせず実装できるようになっています。size()はListインターフェイスの規定を再現しやすくするAbstractListによって実装されているので、何も考えなくてもArrayクラスはListインターフェイスの規定をクリアしやすくなります。
初めからAbstractなクラスを作れば良いのではないのか?
インターフェイスの規定をより再現しやすくする為のAbstractなクラスを書くぐらいなら、初めからAbstractなクラスを書けば良いのでは? と思うかもしれません。
しかしAbstractなクラスがスーパークラスの一番底になるのはよくないと思います。
class Linked extends AbstractList {
private LinkedListlinked = new LinkedList ();
public void addAll(java.util.Listlist) {
linked.addAll(list);
}
@Override
protected void _add(int i) {
linked.add(i);
}@Override
protected int _get(int index) {
return linked.get(index);
}@Override
protected void _remove(int index) {
linked.remove(index);
}
}
例として上記にAbstractListを継承したLinkedクラスを作りました。
LinkedクラスはaddAll()というメソッドを持っています。addAll(java.util.List)は値の追加を行いっているにもかかわらず、size()の値は追加前と変わらないという状態になります。値が追加されたのなら、size()の戻り値は増えるべきです。
もし、AbstractList内でsize()の整合性を維持しようとしてaddAll(java.util.List)とprotected abstractなメソッドを作った場合、今まで作ったAbstractListのサブクラスは全てaddAllのprotected abstractなメソッドを実装しなければなりません。AbstractListが広範囲で使われていた場合、面倒な修正を迫られる事になるでしょう。
Abstractなメソッドをインターフェイスの代わりとして使うと、変更に対して異様に弱くなります。
まとめっぽいもの
- インターフェイスはイミュータブル化、関数オブジェクトの実装に使える。その他の使い方は今のところ思いつかない。
- インターフェイスは「可能な限り抽象的なメソッド」、「インターフェイスを実装するクラスの大まかな設計思想」を持つべき。
- インターフェイスに大まかな設計思想があるなら、インターフェイスを実装したAbsractなクラスで設計思想を明確にしてあげる。
- インターフェイスの実装方法は複数存在するので、インターフェイスを実装したAbsractなクラスは実装方法によっては二つ以上存在しうる。*2
- ここに書かれている事が全てではない。
書くの疲れた。
多分どっか矛盾があるかも。あと例となるソースコードはもうちょっと例に足るものを使えばよかったわね。神は死んだ。