インスタンスを抽象的に扱う

まず「抽象的」という言葉が難しいのかな。俺も最初の頃、一体何なのかわからなかった。プログラムに全く縁もゆかりも無い相方に、オブジェクト指向の話をすこしだけ聞かせたことがあって、「抽象的って、要は大ざっぱってこと?」と問われた。なるほど、良い表現だ。

Android携帯HT-03A」というオブジェクトがあったとする。それを大ざっぱに扱う(いや、別に物理的に雑に扱う訳ではなく)とはどういうことか。結論を言えば、「文脈上、細かい事を気にしなくて良い場合は、細かい事を記述しないこと」だ。

  • Android携帯として扱う → HT-03Aではなく、DesireやNexusOneで良い場合は、HT-03Aである、という限定をしない
  • 携帯電話として扱う → Android携帯ではなく、ガラケーでも良い場合は、そういう限定をしない
  • 電子機器として扱う → そもそも携帯の話じゃなくて、電子機器の話という文脈であれば、これでよい
  • ある物体として扱う → さらに携帯ではなくて、手に触れられる物体であれば用が足りる場合
  • "何か"として扱う → もう「概念」だとか「行動」だとかでもいい、何かしらであれば良い場合

下に行けば行くほど、大ざっぱだ。つまり抽象的だ。

話は少し変わるが、コードを書くというのは文章を書くことによく似ている。ある事柄を表現したい場合、同じ事柄を表現するにあたっても、人によって書くコードは微妙に異なる。同じ人が同じ事柄を表現する場合であっても、気分次第で僅かな違いはあるだろうから「一字一句全く同じである」ことは珍しい。いろんな書き方があって、正解はない。要は伝わりやすければいいんだ。

文脈上、必要最低限の具象的(抽象的の反対語)な単語を選んで文章(コード)を書く、とはどういうことか。見ていこう。

日本語での文章の場合

例えば「今時の人はほぼ全員が携帯を持っている、私もHT-03Aを持っているしね」という趣旨の内容を文章で表現する場合(もう表現済みだけどw)、この *HT-03A* をどんどん抽象化していくとこうなる。

  1. 「今時の人はほぼ全員が携帯を持っている、私もHT-03Aを持っているしね」
  2. 「今時の人はほぼ全員が携帯を持っている、私も携帯電話を持っているしね」
  3. 「今時の人はほぼ全員が携帯を持っている、私も電子機器を持っているしね」
  4. 「今時の人はほぼ全員が携帯を持っている、私もある物体を持っているしね」
  5. 「今時の人はほぼ全員が携帯を持っている、私も何かを持っているしね」

HT-03Aを抽象化していくのだ。しかし、抽象化は、ある一線を越えると意味不明になる。この例では、3番以降が意味不明だ。

では、抽象化の反対、具象化をしていくとどうなるのか。

  • 「今時の人はほぼ全員が携帯を持っている、私も黒いHT-03Aを持っているしね」
  • 「今時の人はほぼ全員が携帯を持っている、私もAndroid1.6にアップデートしてある黒いHT-03Aを持っているしね」
  • 「今時の人はほぼ全員が携帯を持っている、私もAndroid1.6にアップデートしてあってそろそろ電池がヘタってきている黒いHT-03Aを持っているしね」
  • 「今時の人はほぼ全員が携帯を持っている、私もAndroid1.6にアップデートしてあってそろそろ電池がヘタってきていて"戻る"ボタンの塗装が剥げてきている黒いHT-03Aを持っているしね」
  • …(もういいですか?w)

無意味な具象化は、意味不明にならないにしても無駄と混乱を引き起こす。

文章もコードも、不要な事柄を記述せずシンプルな状態を保つことで、十分に要件を満たし、かつ理解が簡単な表現ができる。上記例文の中で、要件を満たしていて(つまり意味不明でなく)理解が簡単なのは2番。つまりこれが最適な文章だ。言い換えると、文脈の中で意味が壊れない範囲で一番抽象的なものが最適解なのだ。

Javaでのコードの場合

/**
 * このプログラムを実行すると、標準出力に "foo" と "bar" を表示する。出力の順番は保証しない。
 */
public class AbstractionSample {
  public static void main(String[] args) {
    List<String> strings = new ArrayList<String>();
    strings.add("foo");
    strings.add("bar");
    
    for (String string : strings) {
      System.out.pringln(string);
    }
  }
}

ここで *HT-03A* にあたるのが List だ。上記のコードのmainの1行目を考える。上に書いたコード3番として、これを抽象化すると4番以降、具象化すると1〜2番になる。番号が小さいほど具象、大きいほど抽象だ。

  1. ArrayList strings = new ArrayList();
  2. AbstractList strings = new ArrayList();
  3. List strings = new ArrayList();
  4. AbstractCollection strings = new ArrayList();
  5. Collection strings = new ArrayList();
  6. Iterable strings = new ArrayList();
  7. Object strings = new ArrayList();

先に最適解を言ってしまおう。上記のサンプルコードにおいて最適な1行目は、5番である。文脈の中で意味が壊れない範囲で一番抽象的な物だからだ。意味が壊れるとはどういうことかというと、簡単に言ってしまえばコンパイルエラー。Iterable型だとaddできないし、Object型だとadd出来ない上に拡張for文で回すこともできない。上記のコードが動作する中で一番抽象的なのが5番なのだ。

(ちなみに、javadocに「fooとbarの順番を保証する」仕様を明示した場合は、3番が最適。この辺りが難しい。)

未だに、どこかのコードを見ると、1番のコードを見ることがある。newした実装型でそのまま受け取る。一番頭を使わなくて良いので楽なのだろう。もしくは「抽象的な型で受け取る」という発想がそもそもないかもしれない。まぁしかし、ここを敢えて1番で定義する、という「信念を持った選択」というのもあり得る(後述)。

3番だった場合、「ArrayListやLinkedListをnewした時はListで受ける、HashMapをnewした時はMapで受ける、HashSetの時はSet…。」という思考停止ルールによって書かれている可能性がある。しかし、状況に応じて、しっかり考えた上で、やはり信念を持って型を選択したいところだ。

信念を持った型の選択

「ここで、このインスタンスに必要とされる機能は、どのレベルなのか」、それを考えて型を選択すること。これが大事。

  • ただ何かのJavaオブジェクトであればいいのであればObject型
  • 拡張for文で(順序を保証せず)ただ全要素を回せれば用が足りるのであればIterable
  • それに加えて、add/remove/size等の基本的な集合操作が求められるのならばCollection
  • それに加えて、要素の順序を保証し、特定の要素のインデックス等を提供する必要があるならばList
  • ensureCapacity(下記)等の必要性があるのならば、ArrayList

マイナーだが ArrayList#ensureCapacity(int minCapacity) というメソッドをご存知だろうか*1

ArrayListには(sizeではなく)capacityという考え方がある。このクラスは、順序付けられた集合(集合=コレクション)を、内部的には配列として管理している。配列には必ず固定されたサイズがあるのはご存知の通り。ではArrayList中に隠し持っている配列のサイズが10であり、既にListの要素数は10、という状況で新しい要素をaddしたらどうなるか?

新しい、もっと大きい要素数の配列をnewして、古い配列から要素をコピー、その上で新しい要素を加える、ということをする。配列の要素数に余裕があれば、この配列の更新は行われない。つまり、ArrayListへaddする場合、この「配列の更新を伴うadd」または「伴わないadd」が起こり、それぞれの実行時パフォーマンスに(僅かだと思うが)差がある。僅かとは言え、塵ツモだ。

あるアプリケーションで、「事前準備に時間が掛かってもいいから、このaddをする瞬間には最大限のパフォーマンスを発揮して欲しい」という要件があったとする。

ArrayList<String> strings = new ArrayList<String>();
// ... 色々省略

// 事前準備:この後、stringsに要素が3つaddされる。その時、最大限のパフォーマンスを発揮したい
strings.ensureCapacity(strings.size() + 3);

// パフォーマンスが要求される処理開始
strings.add("foo");
strings.add("bar");
strings.add("baz");
// パフォーマンスが要求される処理終了


このような要件を満たすためにensureCapacityが使われる(のだと思うw)。で、このメソッドはListインターフェイスには無く、ArrayListにしか無い。という状況では、信念を持ってArrayListを選択する必要があるわけだ。

型の選択まとめ

その文脈で必要とされる、最低限のインターフェイスを持った型を選択せよ。ってことですね。

で、これはローカル変数の型だけでなく、メソッドの引数型と戻り値型についても同じ。あるメソッドの仕様を考えた時、「Listを返す」という仕様が浮かんだとする。しかしそれを何も考えずに実装してはいけない。

public List<String> getSomethings() { /* ... */ }

この戻り値は要素の順序を保証する必要はあるだろうか? 順序に意味はあるだろうか? 無いとなったらこうだ。(あるのなら、getSomethingsのjavadocで、どんな順序なのか明示すべし。)

public Collection<String> getSomethings() { /* ... */ }

返した後にsizeを取ったりできる必要はあるだろうか? 単に拡張for文で回せるインスタンスであれば用が足りないだろうか? そうならば、こうだ。

public Iterable<String> getSomethings() { /* ... */ }

このような選択をするのも「API設計」という奴である。楽しいですね。

宣伝

抽象化すると「出来る事が減る」。必要最低限のことしかできないので、将来の拡張の可能性を潰してしまう「好ましくない状態」なのではないか?

その答えについては、日経ソフトウエア2010年9月号「Javaで始めるプログラミング 第5回」でお答えしています。買ってネ。

日経ソフトウエア 2010年 09月号 [雑誌]

日経ソフトウエア 2010年 09月号 [雑誌]

*1:私も今調べて知ったw