ボク、if文。わるいモンスターじゃないよ!

id:aroundthedistance に召還されたぜ。

http://d.hatena.ne.jp/aroundthedistance/20100727/1280227851

…その昔なー。Seasar Conferenceで「あなたのコードからnewとifが消えます、魔法のDI」みたいなセッションをした。今思い出して「釣りすぎたぜサーセン」という気分になったことをまず懺悔しておく。

この doBusinessん中のif〜else ifをなんとかしたい。
…(中略)…
ちょっとすっきりした。けどまだifが残ってるよね。

ポリモーフィズムの例をもうちっと実用的に書いてみた。 - 都元ダイスケ IT-PRESS

どんだけif文悪者なんだ。そこまで嫌ならば、一度もif文を書かずにコードを書けばいい。無理だがなw と自嘲。

if文に限らず、問題になるのは濫用なのだ。"ある知識"がトッ散らかってると、色々な不具合が出て来る。

いくらデザインパターンを適用しても公開したインターフェイスを叩く場合は、どのインスタンスなのかを明確にする必要がある気がして、そこなんとかなんねーのって思ってた。

http://d.hatena.ne.jp/aroundthedistance/20100727/1280227851

結論から言えば、なんとかなんねーし、なんとかする必要が実は無い、というのが解だと思う。
先日 ArrayList list = new ArrayList(); じゃなくて List型で受けると良い(場合があるよ)って話をしたけども、いくら抽象的なList型で受け取ったとしても、List型自体はnewできない訳で、どっかでArrayListという実装型が出て来ざるを得ない。ここでの議論の対象は「ArrayListが出て来ること」ではなく「どこにArrayListが出て来るか」なのだ。

俺もなかなか上手い説明が出来ないところなのだが、オブジェクト指向ではしばしば「○○が××を知っている」という表現をする。この表現を使うと、「誰かがListの実装がArrayListであることを知っている」のが問題なのではなく、「誰がListの実装を知っているべきか」を考えるのだ。そうすると、自ずとnewすべき場所が決まるのではないか。

さて。

if (formatId == CSV) {
  // ... (A)
} else if (formatId == XML) {
  // ... (B)
}

この様な分岐も「知識」の一種である。これについても、アプリケーションの各所に散らばってしまうことが問題なのであって、if文自体が悪者な訳ではない。このコードを書いた時点では、データのフォーマットはCSVXMLの二択だったのだろう。しかし、仕様変更により「YAMLも扱えるようにしなきゃいけない」となった時に困難が始まる。分岐の知識が各所に散らばっている場合、全てのif文を漏れなく修正するのは大変だからだ。

知識がトッ散らかっているがゆえに、修正漏れが起こる可能性がある。そうすると、「AかBのどちらかの処理が絶対に実行される」という前提で後続のコードが書かれていた場合、バグとなる*1わけだ。

同じロジック(理屈)で分岐する場所を一カ所に集中させておく、つまり「知識の共通化」をしておけば、その一カ所を変更するだけで、他の全ての部分が自動的に追従する。これが「堅牢なコード」って奴なのだろう。

一番基礎的な共通化は以下のような感じ。これはbeforeはSystem.out.println が悪い訳でも、 "(" + ... + ")" が悪いわけでもない。同じ処理なのに共通化していないのが問題なのだ。だから、afterでも「System.out.println と "(" + ... + ")" は消えない」のだ。共通化された一カ所だけが「残る」。誰かが「出力先は標準出力であること」と「括弧でくくること」という知識を知らなきゃいけないのだから。

// before
System.out.println("(" + foo + ")");
System.out.println("(" + bar + ")");
System.out.println("(" + baz + ")");

// after
printWithBrackets(foo);
printWithBrackets(bar);
printWithBrackets(baz);

void printWithBrackets(String str) {
  System.out.println("(" + str + ")");
}

「処理をメソッドとして切り出す」というリファクタリングは、共通化テクニックの一種である。他にも共通化のテクニックはいつくかあって、その一つが継承だったりポリモーフィズムだったりする。

// before
public class American {
  private int name;
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public String greeting() {
    return "Hello, my name is " + getName() + ".";
  }
}

public class Japanese {
  private int name;
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public String greeting() {
    return "こんにちは、私の名前は" + getName() + "です。";
  }
}

// after
public abstract class Person {
  private int name;
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public abstract String greeting();
}

public class American extends Person {
  public String greeting() {
    return "Hello, my name is " + getName() + ".";
  }
}

public class Japanese extends Person {
  public String greeting() {
    return "こんにちは、私の名前は" + getName() + "です。";
  }
}

これは、継承を使って、nameフィールドの管理をPersonという基底クラスに移動して共通化した例。やはり、フィールドやアクセサが悪いわけではなく、共通化されていないのが問題だ。というわけで、フィールドもアクセサも消えはしない。1つだけ残るのだ。名前は誰が知っているべきなのか、に基づいてる。

ここまでは「共通化」と言ってきたが、処理が共通化できた後は、その継承ツリーを上方にたどって行く事で「ロジックを書く場所を移動できる」ようになる。Personの上に、さらに抽象的なクラスがあった場合、そのオブジェクトが「名前」を持っている場合は、nameフィールドとそのアクセサをその抽象クラスに移動させることもできる。ただ、移動させることが適切なのかどうかは文脈に依存するわけで、一概に判断できない。色々考えた末、「どこに書くのが適切なのか」を判断する必要がある。コードを書く場所を適切に選ぶことによって、さらに堅牢なコードという奴に近づいていく。


さて、先日、良エントリからトラバを頂いたのでご紹介したい。

クラスのインターフェース(使い方)は「バランス」だとか「使い勝手」だとかいう怪しげなものを基準にして決めるべきではなくて、クラス単体で見たときにどうしてそうなっているかを論理的に

「説明」できるかどうかが重要。

クラス設計の話 - 虚飾の王

その通りですね。上記の例で言えば「なぜnameをPersonに書いたのか」が説明できなきゃいけない。

DIのお話を書いてみる。 - 都元ダイスケ IT-PRESS で書いたDIの例は、場合分けロジックを上へ上へと押し上げまくった結果、最終的にJavaの世界を飛び出して、XMLの世界に行ってしまった。このエントリでは、DIを説明しようとするが為に「なぜ」を置き去りにして、とにかく「XMLの世界にぶっとばせる」ということだけを説明しただけなのだ。この場合分けロジックをJavaではなくXMLで表現することがはたして適切なのか?ということを、実はしっかり考えなければならない。

まとめ

メソッド切り出しや継承などのテクニックを使うことによって、「ある知識」について記述する場所をコントロールできるようになる。「知識を持たないこと」が大事なのではなく「知らなくて良い知識を無駄に持たないこと。その知識を記述する最適な場所を適切に選ぶ。」のが大事なんだ。

というわけだが。なんか文章まとまってねーなー、と思いつつも投稿。

*1:こんなバグにいち早く気づくため、自分はこのif文の最後にelse句を付けて、例外をぶっとばすのが好きだ。余談だが。