追えない?...追えない!
id:nowokay:20080613#1213327527 をみて、ついったーで色々かきだしたらgdgdになっちゃったんで整理することにしました。
...けど長過ぎ...ちょっとずつ推敲して整理しなきゃ...^^ゞ
gdgdになった原因は単純明快で、
たちが悪いのは、オブジェクト指向だったり30行メソッドとかメンテナンスしやすさテストしやすさの名の下に、無意味に細切れにされて散らばったコード。
...オブジェクト指向とか、短いメソッドとか、大好きです。
メンテナンスやテストしやすさのためにメソッドを細切れにするの、よくやります。
継承と、あとは型をスーパークラスで持つという、型ロンダリングとでもいえるテクニックを併用すると、なかなか追いにくいコードを書くことができます。
...継承するばあい、ほぼ確実にスーパークラスの型で持つのとセットです...
Compositeなんかも大好きで、Componentを省略して[Leaf]<|---[Composite]てしてLeafに書いたメソッドを集約メソッドとしてオーバライドしたりとか、よくします...
親クラスに実装を置くのがあんまりよくないのはわかってるし、使い捨てじゃなかったらちゃんと抽象Component作ったりしますけど、手抜きはよくします。これはもうごめんなさいとしか言いようが無いけですど。
てな具合で、字面を追ってるといちいち自分に当てはまってくるんです。しかも、基本的にはむしろ「そうあるべき」と思ってる部分に当てはまるんですね。
すごくない人が言ってるんだったら、
JUnitもこんな感じやしメソッド・クラスの粒度に慣れてて自分では追いやすいし、
もうコードの整理整頓も出来ないのか、何百何千行もあるメソッドの方がわからんやろ、とか、
log4jのlog.debug()の中でtoString()呼んでたらあんたObject.toString()の中身を読むんか、とか、
まぁオブジェクト指向わからんかわいそうな人やねんな、勉強しいや、くらいでスルーしてたんです。
「追いにくい」て言う人で割と多いのが、理解するために追おうとして、しかもたいてい深さ優先で追おうとするんですよね。
本当に実行パスの通りにコードを追いかけようとする。
でも他のクラスのメソッド呼んでる場合、「メッセージ」であっても「抽象データ型」の操作であっても
オブジェクト指向である限りその中身は意識しちゃいけない。
まあ基本的に型とメソッド名、引数から自明な、何をするためのメソッドなのか、だけを意識してそのメソッドを理解しないといけない。
それこそクラスブラウザで1メソッドを切り取って理解するように理解して行く。
そうすれば数行から数十行で書かれた、よく定義されたメソッドと言うのは読みやすいと思うんです。
だから、「追いにくい」と言う言葉を聞くと、スレッドのつもりになって追おうとするのがいけない、定義を理解しろ、と言うのが最初に思う事です。
相手がすごくない人だったら、の話ですけど。
でも今回は...
粒子法で、ざっぱ〜ん!みたいなのを見てて、すごい人だと認識してたんですね。読んだ時点ですでに。
で、そんな風にスルーするわけにも行かなくなって、ちゃんとした人の「追いにくい」に対する思考と、どうしようも無い人の「追いにくい」に対する文句が頭の中でぐっちゃになっちゃったんです。
ビックリするようなテクニックを使って追いにくいコードを書く人がいます。
すごいな〜とは思うけど、できればやめて欲しいですね。
とか
クラス作ればいいものを、配列で値を束ねたり、そこでなぜかListを使ったり、しかもそのListの中にListやMapが入ってたり、もちろんGenericsなんかは使ってなかったり。
こういう、データ構造の不適切というのは、読みにくさをコード全体に散りばめることができるので、かなり効果的です。
なんかは、そんなややこしい事にはならないんですけどね...
実際、私がプログラムを書くと
30行すらでか過ぎ!て感じで、単に勤務時間計算のスクリプトを書いても
import java.text.* /**時間を表すクラス。1日の中の特定の時点を表すのか、量としての時間を表すのか、中途半端。 * まあ、1日の中の時点を表す時間は1日の始まりからの時間量で表す、と思えば... * ValueObjectにしたいけど簡単にnew Time(hour:9, minute:30)と書きたいのであきらめた */ class Time { private def format = new MessageFormat("{0,number}:{1,number,00}") BigInteger hour = 0 BigInteger minute = 0 /** 0:00から何分後かでこの時間を表す算出プロパティ */ public BigInteger getByMinute(){ hour * 60 + minute } /** new Time(hour:9, minute:30)て書くためのデフォルトコンストラクタ */ public Time() {} /** new Time("9:30")て書くためのコンストラクタ */ public Time(String time) { def parsed = format.parse(time) hour = parsed[0] minute = parsed[1] } /** 8:90みたいなへんな表現を9:30みたいにする。 */ def normalize() { hour = byMinute / 60 minute = byMinute % 60 return this } /** 足し算。 8:30 + 1:45 = 10:15。 */ def plus(Time rth) { return new Time(minute:byMinute + rth.byMinute).normalize() } /** 引き算。 8:30 - 1:45 = 6:45。 */ def minus(Time rth) { return new Time(minute:byMinute - rth.byMinute).normalize() } /** このオブジェクトの文字列表現。 new Time(hour:9, minute:15).toString() == "9:15" */ public String toString() {return format.format([hour, minute].toArray())} /** Time同士の比較。 new Time("9:15") == new Time("8:75") */ public boolean equals(Object obj) { if (super.equals(obj)) { return true } else if (obj.getClass() != this.getClass()) { return false } else { return byMinute == ((Time)obj).byMinute } } /** equalsを書いたのでhashCodeも。 */ public int hashCode() {(int)byMinute} } /** 1日分の作業を表すクラス */ class Work { Time start Time end /** 休憩時間 */ Time rest /** work.start = "9:30"みたいに書くためのセッタ。 */ public void setStart(String start) {this.start = new Time(start)} /** work.end = "19:30"みたいに書くためのセッタ。 */ public void setEnd(String end) {this.end = new Time(end)} /** work.rest = "1:30"みたいに書くためのセッタ。 */ public void setRest(String rest) {this.rest = new Time(rest)} /** 拘束時間。 */ def getSpentHour() {end - start} /** 勤務時間の取得。new Work(Start:"9:30", end:"19:10", rest:"1:15").workingHour == new Time("8:25") */ def getWorkingHour() {spentHour - rest} } println ([ new Work(start:"9:30", end:"19:00", rest:"1:00"), new Work(start:"9:30", end:"19:00", rest:"1:00"), new Work(start:"9:30", end:"18:20", rest:"1:00"), new Work(start:"9:30", end:"18:30", rest:"1:00"), new Work(start:"9:30", end:"18:30", rest:"1:00"), new Work(start:"9:30", end:"18:10", rest:"1:00"), ]*.workingHour.sum() )
て感じになっちゃいます。(テスト付きの元コードはたんぶらにあります。元々はGroovyのスクリプト中のテスト方法がメインのメモなもんで中途半端な上にコメントは書いてないですが...)
...たちが悪いって言われてる細かいメソッドの嵐です。細かいクラスの嵐です。実はTDDで作ったテストしやすい事が最優先の設計です...
でも多分そういう話では無くて
「数行から数十行で書かれた、よく定義されたメソッド」の「よく定義されたメソッド」とか、「親クラスに実装を置くのがあんまりよくない」とか、
その辺が多分ポイントなんでしょうね。
「無意味に細切れにされて散らばった」なんて書いてはるんで、変な所でぶちぶち切られた凝集性が低くて他のメソッドと密に結合したようなものを指してるんでしょうね。
もちろん密に結合していれば追わざるを得ないし、凝集性が低ければ追いにくい。それはOOがどうこうじゃないけど、オブジェクト指向やらTDDやらでメソッドの適正サイズはCの関数より短くなったのもたしか。そういう意味では無実でもない。
そんなメソッドがさらにオーバーライドされたりして、追うに追えなくなってしまったら、もう目の当てようも無いのも確か。へたに「オブジェクト指向」とか言ってやってるとエラい事になる、というのも無いとは言い切れません...
継承を駆使すると、迷宮のようなコードを作りあげることができ
るのも否定しようのない事実ですし...
以前私にとってのモデリングの勉強会で大御所な方から「構造化の思想でオブジェクト指向の仕組みを利用するとぐちゃぐちゃになる。CやCOBOL風に使うならstaticメソッドonlyにした方が良い」という様な事を聞いた事が有るんですが、それを思い出しました。
オブジェクト指向的に作ってて、データがおかしくなったときにどう追うんだろうって思い起こしてみると、起きてる現象からおかしくなってるデータを特定したら、もうバグが有るのはほぼ確実にそのデータを持ってるオブジェクトだろう、て見方をする気がする。
まぁ正当に「追う」てことをするとすればほぼ確実に障害調査の際なんで、ちゃんと「処理とデータをわけない」指向になってれば処理のつながりが動的な分は処理とデータがより強く結びついてる事で結構相殺出来るんじゃないかとおもうんですよね。
で、処理とデータをわける指向なのに処理が動的に切り替わると、もうそういう追いつめようも無いのでお手上げになるのかな、とそんな事を思ったのです。
蛇足ですが、
すごいコードとすごくないコードで、同じような行数で同じことができるならすごくないコードの方がえらいですね。
おんなじコード量で同じ事しててわかりやすいコードとわかりにくいコードがあるなら、わかりやすいコードの方がすごいコードだと思う...
小難しい事をしても出来る事が変わらなくて、コード行数も減らせず、特にメリットもないならそのコードはすごくないです。
まぁ、「すごいコード」という言葉で言わんとする事が違うだけですけどね^^;