著作一覧 |
ここ数日、死ぬほど後悔しまくっているので、あらためて(というのは、数年前にも一度後悔しまくって、そのときの知見はあらかた処方箋とかコーディングの掟に書いているからだが)後悔しないための書き方をいくつか紹介する。
これは本当に重要。しかも簡単でありながら効果は絶大。
だめな例。
public class FooBar { private Connection conn; ... protected void setup() { ... conn = DriverManager.getConnection(url); ... }
urlを指定することや、DriverManagerの実装を交換すれば良いだろうと想定していても(というか、Connectionならそういう方法もあり得るが、そうはいかないものも多い)、そのための手間が大き過ぎれば意味がない。
良い例。
public class FooBar { private Connection conn; ... protected void setup() { ... conn = newConnection(url); ... } protected Connection newConnection(String url) { ... return DriverManager.getConnection(url); }
利用方法
public class FooBarTest extends TestCase { FooBar target; protected void setUp() throws Exception { target = new FooBar() { protected Connection newConnection(String url) { return new MockConnection(); } } }
ファクトリーメソッドパターンを適用しておけば、オブジェクトの生成時に割りこめる。当然だが、効果は絶大。
この問題は比較的すぐに気づいたのだが、いかんせん、本当にインフラ中のインフラになってしまったコンポーネントはごく初期に開発したためにprivateを使い過ぎていて、厄介なことこのうえない。
だめな例。
public class FooBar { private Connection conn; ... protected void setup() { ... conn = DriverManager.getConnection(url); ... }
良い例。
package com.example.foo; public class FooBar { Connection conn; ... protected void setup() { ... conn = newConnection(url); ... }
ファクトリメソッドパターンを適用すれば万事解決だと思うとそうはいかないことが稀にある。稀にしかないからこそ、ちゃんと用意をしておいたほうが良い。
利用方法
package com.example.foo; public class FooBarTest extends TestCase { ... public void testSpecialCase() throws Exception { target.doSomething(); MockConnection stat1 = (MockConnection)target.getConnection(); // #getConnectionはprotected target.conn = new MockConnection(); target.doSomething(); assertEquals(stat1.getCondition(), ((MockConnection)target.getConnection()).getCondition()); ...
ここで、フィールドをprivateにしたままでパッケージプライベートなsetConnectionメソッドを導入するという代替案を考え付くかも知れないが、メソッド呼び出しとフィールドの上書きはユースケースが異なるので、上記のような処理の役には立たない。仮に立つのであればその程度の利用方法なので、あまり考えてもしょうがないとも言える。
セッタメソッドの公開とフィールドの公開の両立が必要な例
package com.example.foo; public class MoreFooBar extends FooBar { void setConnection(Connection c) { ... ← この部分の迂回が必要な状況(開発後1年で必要になるとしたら、それは設計ミスだとは思うが) super.setConnection(c); }
追記:上に関連してメソッドを(構造化の要領で)機能単位に分割する(サブルーチンのインスタンス化)というのがここに入る。
これは今となっては当然のことだが。
悪い例
protected void foobar(Baz baz) { ... AR ar = newAR(); ... ar.foo(baz.getBzz()); ar.bar(baz.getBzz()); ... }
良い例
protected void foobar(Baz baz) { ... AR ar = newAR(); ... ar.foo(baz); ar.bar(baz); ... }
arをファクトリメソッドを利用して交換可能にしたにも関わらず、限定的な引数を与えているため、Bazオブジェクトが持つ情報をarは利用できない。その時点ではAPIを狭くした良いインターフェイスと考えていたとしても、処理は精緻化複雑化するものだ。与える情報を絞るのではなく、与えた情報を絞らせるように設計すべき。(で、上の例であればファクトリメソッドをオーバーライドしてARを入れ替えるだけでは済まないので、結局foobarメソッドをすべてコピー&ペーストして2行だけ変更するということになってしまう。これではファクトリメソッドを導入してもそれほど役に立たない)
追記:これについては補足した。
3年程度の短期寿命と考えていても、リリースしてしまえば中期、長期にわたってソフトウェアは使われることがあるという2000年問題の教訓は今でも有効だ。
つまり刹那的インターフェイスを避ける。
刹那的インターフェイスというのは、強い制約をもたらすものを指す。上に挙げた例では、new、private、オブジェクトの部分的な引き渡しがそれにあたる。他にもたった一つのfinalメソッドのために、クラスをまるまるコピー&ペーストして別クラスを作らざるを得ないというような例もある。というわけでfinalも極めて刹那的なインターフェイスだ。
ここで挙げた内容は、ミドルウェアやフレームワークの内部のオブジェクトにしか当てはまらないと考えることもできるが、必ずしもそうではない。これらは、プログラムの寿命が長ければ長いほど重要になる。なぜならば、寿命が長いということは、そのプログラムのコードの信頼性が高いということであり、それはつまり元にしやすいということに通じるからだ。
コピー&ペーストと、継承と、コンポジションの3種類の再利用方法があり、いずれも一長一短がある。コピー&ペーストはたまたま発覚しなかったバグが見つかった場合に大きなダメージとなる。コンポジションの場合は狭過ぎるインターフェイスによって拡張に耐えられなくなる。継承は細切れな派生クラスの深いツリーによって手が負えない塊になる危険を持つ半面、長期的に成長するソフトウェアに大しては安定した基盤を提供できる(コンポジションと異なり狭いインターフェイスの問題は、内部に食い込めるだけに抑制できる。結局、コンポジションと継承はどちらも重要なのである)。
Javaプログラミングの処方箋 (Programmer’s foundations)(宇野 るいも)大体、ここに書いたことの通りだ。
ジェズイットを見習え |
#1・フィードバック法定式に応用するのには、自分的に扱い易い。リーダータイプナのかも知れない・・此処一年以内に、活用分野は現れてくるように、先を見うる・エレクトロニクスのテクノロジー系統専門大を、観るに当ってそれを工学の範ちゅう擱いて置くべきであろうと考える次第。