著作一覧 |
昨日書いた後悔先に立たずの公開だが、ぶくまこめで「。与える情報を絞るのではなく、与えた情報を絞らせるように設計すべき。」に対する反応があって、それも正しい反応だと思った。もっとも2行あるのは例示のあやなので、ちょっとそこに突っ込まれてもとは思うが。
たとえば、次に示すBase64EncoderクラスのAPIは何がなんでもおかしい。
public class ComponentInfo { public byte[] getGuid() { ... } public byte[] getCpuInfo() { ... } } ... public class Base64Encoder { public String encodeGuid(ComponentInfo info) { ... } public String encodeCpuInfo(ComponentInfo info) { ... } }
これはどう考えてもあり得ないでしょう。こんなメソッドの決め方をしていたら、一体、どれだけメソッドが必要となるか。正しくは、
public class Base64Encoder { public String encode(byte[] data, int offset, int length) { ... } public String encode(byte[] data) { ... } ... public Reader encode(InputStream stm) throws IOException { ... } }
とすべきだ。クラス名もBASE64エンコーダだし。
この例のBase64Encoderクラスのようなオブジェクトは、呼び出しの末端に来て、この中で別のオブジェクトをコンポジションしていることもなければ、他のオブジェクトをラップすることもない。このタイプ(つまりはユーティリティだ)のオブジェクトが実装すべきAPIは、絞りに絞ったものとすべきだ。具体的には、プリミティブとせいぜいjava.langパッケージのオブジェクトとその配列。もっとも、上の例ではjava.ioのオブジェクトを含めているが、ケースバイケースではそれもありだろう。
しかし、それは逆に言えばファサードのような他のオブジェクトのコンポジションクラスであれば、絞れば良いということではない、ということだ。
次のクラスは、情報を収集して、最後にレポートを出す。
public class CompoenentInfoSet { /** * {@link com.example.foo.ComponentInfo1}オブジェクトのGuidをレポート用に設定する。 * @param guid 対象クラスのCompoenetInfoが返すGuidの値。 */ public void setRawId(byte[] guid) { ... } /** .... public void setHash(int hash) { ... } ... public String report() { ... } }
利用するコードは以下となる。
.... ComponentInfo ci = new ComponentInfo(this.getClass()); // と何気なくnewを使って自滅するのであった ... CompoenentInfoSet set = newCompoenentInfoSet(); set.setRawId(ci.getGuid()); set.setHash(this.getClass().hashCode()); ...
ここで、CompoenentInfoSetクラスのAPIは一見すると妥当なように見えるかも知れない。が、それは違う。
この時点では、RawIdとして与えるのはCompoenetInfo#getGuidで、Hashとして与えるのは、selfが属するクラスのhashCodeかも知れない。
しかし、5年後の夏には、GuidだけでCompoentInfoを示すのでは困るかも知れない。そのために、拡張が必要となったとしよう。
public class ExtendedCompoenentInfoSet extends ComponentInfoSet { public void setCpuInfo(byte[] cpuinfo) { ... } // 追加
もちろん、5年前には想像もしていなかったことだ。
で、それはそれとして、元のプログラムも現役で稼働しているのだから、同じく継承クラスを作って、でも、基本的な処理は元のクラスと同じだから、と見てみると
.... ComponentInfo ci = new ComponentInfo(this.getClass()); // と何気なくnewを使って自滅するのであった ...(10行程度のフラットな処理) CompoenentInfoSet set = getCompoenentInfoSet(); set.setRawId(ci.getGuid()); set.setHash(this.getClass().hashCode()); ...(その他20行のフラットな処理。しかし最後にはデータベースへレポートを保存するような二度と書きたくはないようなコードもある)
幸いにも、ComponentInfoSetは、ファクトリメソッドパターンのインスタンスで生成するようになっている。が、残念、使えない。どうやって、新しく追加したsetCpuInfoメソッドを呼べば良い? ファクトリメソッドで割りこめても、cpuInfoの元ネタのci変数にファクトリメソッドからアクセスすることは不可能だ。
かくして、コピー&ペースト再利用となり、メンテすることになるコード資産が倍増した。
もし、以下のようであったらどうだろうか?
public class ComponentInfoSet { /** * {@link com.example.foo.ComponentInfo1}オブジェクトが持つGuidをレポート用に設定する。 * @param ci 対象クラスのCompoenetInfoのインスタンス。 */ public void setComponentInfo(ComponentInfo ci) { ... } /** ... public void setClassObject(Class c) { ... } ... public String report() { ... } }
つまり、ComponentInfoSetクラスは、別に呼び出し側がわざわざオブジェクトからバイト列を抜き出してやらなくても、自分がどの情報を利用して何をするかは知っている。だからこそ、元々のメソッド名も、setRawIdだったり、setHashCodeだったりしていたわけだ。
これはDRY原則に通じる。すべてのオブジェクトが何をするかの詳細を知っている必要はない。詳細はシステムの末端(ただし、冒頭で示したようにユーティリティなどの「機能」特化オブジェクトは除く)のオブジェクトが知っていれば良い。知識をあまねく分散させたり中央集権でやるのではなく、知識(というよりもそれを利用して果たすべき責務)を適切に分配することが重要だ。
このクラスを利用するコードは元のコードから次のように変わる。
.... ComponentInfo ci = new ComponentInfo(this.getClass()); // と何気なくnewを使って自滅するのであった ... CompoenentInfoSet set = newCompoenentInfoSet(); set.setComponentInfo(ci); set.setClassObject(this.getClass()); ...
一方、ExtendedComponentInfoクラスは次となる。
public class ExtendedComponentInfoSet extends ComponentInfoSet { /** * {@link com.example.foo.ComponentInfo1}オブジェクトが持つGuidとCpuInfoをレポート用に設定する。 * @param ci 対象クラスのCompoenetInfoのインスタンス。 */ public void setComponentInfo(ComponentInfo ci) { ... } ... // もちろん、reportメソッドの実装なども変わる。 }
もし、こうであれば、5年後には、単にnewComponentInfoSetファクトリメソッドをオーバーライドして、ExtendedComponentInfoSetクラスのインスタンスを生成して返すように変えたクラスを作れば、すべての作業が完了する。コードの重複も生まれない。既存の処理に対する影響はゼロ。もし、データベースをいじくるところで問題が出たりしたら、その時にはじめて、本格的に元のクラスのコードのコピー&ペーストなりを行えばよい。
たったひとつの冴えたやり方は、ケースバイケースにいくつものやり方から適切な方法を選択して設計することだ。
たったひとつの冴えたやりかた (ハヤカワ文庫SF)(ジェイムズ・ティプトリー・ジュニア)ジェズイットを見習え |