「仕様」と「実装の詳細」(2)

繰り返す。Fooクラスを利用するMainクラスを書く時、XはFooの「仕様」に依存すべきであり、Fooの「実装の詳細」に依存すべきではない。

public class Foo {
  public String bar;

  public Foo(String bar) {
    this.bar = bar;
  }

  public String getBar() {
    return bar;
  }
}

public class Main {
  public static void main(String[] args) {
    Foo foo = new Foo("hoge");
    System.out.println(foo.bar); // (1)
    System.out.println(foo.getBar()); // (2)
  }
}

上記のクラスはbarフィールドがpublicである。しかしFooのインターフェイスを考えた時、そこにbarは残らないので、これは「実装の詳細」である。Mainから foo.barを参照する(1)は「実装の詳細」に依存する例である。foo.getHoge() でhogeの値を取得する(2)は「仕様」に依存する例である。後者の方が望ましいとされる。

実装の詳細ではなく、仕様に依存すべきであるのは、柔軟性(つまり、何かを変更する際に影響を受ける範囲を小さく限定できるような性質)を得られるからである。この事はオブジェクト指向を説明する各所で言われていることだ。ただ、次の点に言及していることは意外に少ないと感じている。

柔軟性を得られる代わりに、直接性(ぱっと見の分かりやすさ)を犠牲にしている

このように柔軟性と直接性はトレードオフ*1だ。

例えば上記の例では、(1)であれば「Fooのフィールドbarを読み出している」事が明確であり、さらに「読み出し以外に余計なことも絶対にしない」ことも明確だ。(2)の場合だと、そもそも「フィールドを返しているのか?」さえも怪しい。もしかしたらメソッド内部で何かしらの計算をしてその場で値を作って返しているかもしれない。フィールドを返していたとしても、その前後でイベントの発火処理や、barフィールド以外を「変更」しているかもしれない*2

他に、「実装クラスではなくインターフェイスに依存する」という指針で柔軟性を得ると、やはり以下のように直接性が失われる。実装クラスに直接アクセスしていれば、「このメソッドってどんな処理をしているんだろう?」と思ってソースを参照した時に、すぐ中身を見ることができる。しかしインターフェイスであった場合にはその「処理内容」は書かれていない、というか書けない。中でどんな処理をやるのかぱっと見分からない。どのクラスの処理に飛ぶのか、ランタイムにしか分からない*3

柔軟性を得ていないのに、直接性を犠牲にしないこと

public class Foo {
  private String bar;

  public Foo(String bar) {
    setBar(bar); // (a)
  }

  private void setBar(String bar) { // (b)
    this.bar = bar;
  }

  public String getBar() {
    return bar;
  }

  public void baz(String bar) {
    // 何かの処理
    setBar(bar); // (c)
    // 何かの処理
  }
}

上記のコードは、個人的にあまりよくないと思っている。価値観の問題なのでいろいろだとは思うけど。

まず(b)を見ていただきたい。このメソッドはprivateなのでシグネチャもろとも「実装の詳細」である。barフィールドも「実装の詳細」だ。つまりどちらも実装の詳細。barの変更に関して、フィールド直接アクセスでも、setBarを介したアクセスでも、柔軟性が得られていない*4

という前提で(a)と(c)を見ると。直接性が失われたコードがある。柔軟性がロクに得られないのに、直接性だけを無駄に失っていると感じる。何のためにメソッド経由でアクセスしているのが、イマイチよく分からない例である。

まとめ

  • 「実装の詳細」に依存せず、「仕様」に依存することで柔軟性を得ることができる場合がある
  • 「柔軟性」を得る時は、大抵「直接性」が失われる(トレードオフ)。
  • 柔軟性をロクに得られないのに、無駄に直接性を失うこともある

意味もわからずルール*5を適用することだけに固執して思考停止すると損をするよ、というお話でした。

*1:オブジェクト指向は直接的ではないのでコードを書くときに頭を使う(コストが高い)が、柔軟なので変更のコストは低い。対する手続き型は直接的なのでコードを書くときに頭を使わないが、柔軟性に欠けるので変更のコストが高い。

*2:getterで何かを変更するのはあまりよろしくないが、やっていない保証はない。

*3:緻密に追って行けば分かる事多いけど。手間がかかる。

*4:というのは言い過ぎなのは分かっているがw 通常よりもメリットが少ない。

*5:「フィールドは必ずprivateにし、そのフィールドにはsetter/getterを介してアクセスする」というルール。