- グラフィックエディタのリサイズコードを、重複排除を中心に書き直したものの、結局元に戻すことになった経験を通じて、クリーンコードへの執着が実際の変更容易性を損なうことがあると示している
- 既存実装では
Rectangle、Oval、Header、TextBlock のハンドルごとの数式が繰り返されており、新しい実装では方向と図形を分離して組み合わせる方式で 重複を除去しようとした
- リファクタリング直後はコードサイズが半分に減り、変更箇所も一か所に集約されたが、その後、図形ごと・ハンドルごとの 特殊な挙動 が必要になると、抽象化がかえって足かせになり得た
- 技術的判断よりも大きな問題は、元のコードを書いた同僚と相談せずに master に直接チェックインした点であり、チームの 信頼構築 を損なう可能性がある
- 「きれいさ」は目標ではなく、複雑なシステムを扱うための道具にすぎず、コードの見た目よりもチームが一緒に 変化に対応 できるかどうかのほうが重要である
グラフィックエディタのリサイズコードにおける重複
- グラフィックエディタのキャンバスには、四角形や楕円のような図形を縁の小さなハンドルでリサイズするコードがあった
- 実装は動作していたが、図形やハンドルの方向ごとに位置とサイズを計算する数式が繰り返されていた
Rectangle は resizeTopLeft、resizeTopRight、resizeBottomLeft、resizeBottomRight のようなメソッドを持っていた
Oval は resizeLeft、resizeRight、resizeTop、resizeBottom のようなメソッドを持っていた
Header と TextBlock もそれぞれリサイズ用メソッドを持っていた
- ユーザーが Shift を押すと、リサイズ中の アスペクト比維持 も処理する必要があり、そのぶん数式はさらに増えていた
重複排除のための抽象化
- 重複は二つの軸で見えていた
- 同じ方向のハンドル同士で繰り返されていた:
Oval.resizeLeft() と Header.resizeLeft() はどちらも左ハンドルをドラッグする動作だった
- 同じ図形内のメソッド同士でも繰り返されていた:
Oval の複数メソッドはいずれも楕円を扱っていた
- 新しい構造では
Directions と Shapes にコードを分け、方向ごとの動作 と 図形ごとの動作 を組み合わせようとした
Directions には top、left、bottom、right を置いた
Shapes には Oval、Rectangle を置いた
createHandle、createBox でハンドルと図形を組み合わせた
- この方式では
fourCorners、fourSides、twoSides のようなハンドル構成を作り、それを使って Rectangle、Oval、Header、TextBlock を生成した
リファクタリング直後の錯覚
- リファクタリング後、コード全体のサイズは半分に減り、重複は消えたように 見えた
- 特定の方向や図形の動作を変えるとき、複数メソッドではなく一か所だけ修正すればよいと考えた
- 夜遅くに変更を master にチェックインし、同僚の雑然としたコードをきれいに解きほぐしたつもりでいた
翌日に明らかになった問題
- 翌日、上司は変更を元に戻すよう求め、当時は既存コードより新しいコードのほうがきれいだと信じていたため、納得しがたかった
- 時間がたって振り返ると、その判断は二つの理由で正しかった
- 元のコードを書いた人と話さないままコードを書き直してチェックインしていた
- 重複削減を得る代わりに、要求変更への対応しやすさ を犠牲にしていた
- 健全なエンジニアリングチームは継続的に信頼を築く必要があり、同僚のコードを大規模に書き換えることは協業に大きな打撃を与え得る
誤った抽象化のコスト
- その後、複数の図形の複数のハンドルについて、多くの 特殊ケースや挙動 が必要になった
- 新しい抽象化構造でこうした要求を受け入れようとすると、コードは何倍にも複雑になり得た
- 逆に、元の「雑然とした」構造は各メソッドが分離されていたため、このような変更を適用しやすい側面があった
- コードの重複排除は無料ではなく、ある能力を得る代わりに別の能力を失うことがある
「きれいさ」を考え直す
- 「クリーンコード」と重複排除に執着する時期は、多くの開発者が経験し得る
- 自分が書いたコードに自信がないとき、厳格な lint ルール、命名規則、ファイル構造、重複なしといった測定可能なものに誇りを結びつけやすい
- 抽象化を作れるようになると、繰り返しコードを見るたびに 抽象化こそ美徳 だと信じて適用したくなることがある
- 「きれいさ」や「汚さ」という感覚が、実際にどのようなエンジニアリング上の結果と結びつくのかを深く考える必要がある
- コードがどう見えるかだけでなく、チームの人たちと一緒にコードがどう 変化し修正されていくか が重要である
クリーンコードを手放すべきとき
- クリーンコードは目標ではなく、複雑なシステムを理解するための試みである
- コードベースで変更がどのような影響を及ぼすか確信が持てないとき、方向性を与えてくれる 防御機構 になり得る
- 最初のうちは、関数抽出やクラスのリファクタリングで複雑なコードを単純化する経験が楽しいこともある
- ただし、そこで立ち止まって「クリーンコード狂信者」になってはならない
- クリーンコードを道しるべにしつつも、ある時点ではそれを手放さなければならない
1件のコメント
Hacker News の意見
コードの重複は時には問題ないが、だからといって Clean Code が悪いという証拠にはならない
リファクタリングに少し踏み込みすぎたように見えるし、「繰り返される数学処理の10行」を関数に切り出す程度なら、もっとすっきりしていただろう
その10行を関数にしなかったチームメンバーも良かったわけではないので、自分なら PR を却下したと思うが、直接書き直しはしなかっただろう
他人の PR を書き直してしまうのは、議論の余地を閉ざす手早い侮辱であり、そのコードがそう書かれた文脈を知らないことも多い
コーディング標準がないなら PR はレビューして却下すべきで、「Clean Code は重要ではない」という形でコミットさせていると、数か月もしないうちに誰も働きたがらないコードベースになる
ここで学ぶべきなのは Clean Code ではなく、コミュニケーションと配慮である
元の作者が、低品質なコードが実際にどのように時間を浪費するのかを学ぶ機会も失われる
Clean Code 的なアプローチの方が優れていたとしても、ここでの核心はコードではなく、有用なフィードバックなしに自分の時間を浪費したことにある
最善だったのは、元の作者が基準を引き上げ、本人は実際に手を入れるべき別の仕事をすることだった。つまり正解はコードレビューだった
こうした人たちは実際の問題を解くより、問題ではないものを解こうとして、将来の問題まで作ってしまうことが多い
最初の実装では、その程度の10行ほどのごく小さなコードは抽象化しない方が正しい。まさにその作業だけをするコードは後で直しやすく、検証されていない利益のために10行を抽象化しても意味は小さい
Clean Code はまったく重要ではなく、それに似たルールや「原則」の大半も重要ではない。むしろ、時間とともにコードベースにハチの巣のような構造を積み上げ、それを正当化しようとする人たちがしがみつくものだ
結果として、過度に抽象化され、変更しにくく、遅く、理解しづらいコードになりやすい
原則が必要なら、プログラムが実際に行うことであるデータ変換を、明確で変更しやすい形で表現できているかだけを見ればよい
SOLID 原則の大半は、データ変換が何で、どのように起こるのかを見る助けにならないか、むしろ妨げになるため、実際のプログラムを理解するうえでは概してあまり役に立たない
提案されたリファクタリングを見ると、その10行は実は同一ではなく、似ていただけだった
長方形の左上の角をリサイズする数式と、楕円の右下を変える数式は互いに異なる。表面的には似ているが、ある箇所では
+の代わりに-が入るようなものだこの場合、複雑さは幾何そのものにあり、それを「抽象化」しても汚さを別の場所に移すだけで、根本的には消えない
特殊な数体系のような巧妙な数学的抽象化はあり得るが、言語に組み込まれていないなら、基本整数で特殊な演算を実装するコストが、得られる利益を上回る可能性が高い
「Clean Code」にはリブランディングが必要だ
25年以上のキャリアの中で、デザインパターン、抽象化、重複排除などに対する終わりのない反発をずっと見てきた
いつも出てくる理由は、抽象化はコードをより複雑にする、Clean Code を書く時間がない、すべて好みと意見にすぎない、というものだ
しかし Clean Code の目的は、要件が変わったときにコードをより単純で保守しやすくすることにある
ソフトウェアの価値は、時間とともに変えられることにある。そうでないなら、実装も保守もはるかに簡単で安価な固定回路を使えばよい
リファクタリングがこの目標を達成できておらず、上司が将来の要件変更に対して保守しにくくなったと説得力をもって言えるなら、そのコードは「クリーン」ではなかったのだ
元の作者と議論しなかったことは、Clean Code の範囲外にある手順と礼儀の問題である。無礼に独断で動いたからといって、Clean Code の価値がなくなるわけではない
有名な本 Clean Code は「重複がないこと」をよく設計されたシステムの条件として挙げ、重複を追加作業・追加リスク・不要な複雑さの主敵だとしている
この定義では、重複排除が単純化と同義になるが、それは間違っている
重複排除は依存関係の導入である。その依存関係が問題をうまくモデル化しているなら良い抽象化であり単純化になり得るが、そうでなければ抽象化の仮面をかぶった圧縮にすぎない
ちなみにこの引用は、Clean Code に登場する Kent Beck の Simple Design への言及である
ひとつは、あるコードが良いという主観的な感覚であり、もうひとつは本 Clean Code と関連する指針に従うことだ
この2つはかなり違うものだと感じる人が、ますます増えている。本の指針の一部に従うと、むしろまったくクリーンではないコードになることもある
理想的な世界では誰もがクリーンなコードを書きたいはずだが、Clean Code がその目標を達成する良い方法だと感じる人は減りつつある
特に過度な抽象化と重複排除は、多くの人にとって汚いコードの源泉と見なされるようになった
時には技術的負債がプロダクト要件そのものにエンコードされていて、リファクタリングやベストプラクティスでは直せず、痛みを減らす程度しかできない
Clean Code の目的と実際の実践結果は大きく異なる。人々は要件を目の前にしていても、コードの将来の状態をおおむね予測できないからだ
Clean Code は「もしも?」という恐ろしい問いを呼び込み、これはリリースを際限なく先延ばしにするパンドラの箱になりやすい
ほとんどの場合、分かっていることに合わせてコーディングし、ドメイン経験のある人がアーキテクチャを主導する方がよい
良い抽象化の多くはすでにライブラリに入っている。もちろん残念ながら、常にそうとは限らない
悪い抽象化は時間を食いつぶすし、コードはチームの他の人とコミュニケーションする行為でもあるため、自分が考える方式よりも、チームメンバーがそのコミュニケーションをどう受け取るかの方がはるかに重要だ
同僚がコピー&ペーストで大量のコードを書き、あなたはコミット後にリファクタリングした
同僚は上司に不満を言い、上司はあなたを叱り、次からはコードベースに汚れを残しておくと結論づけたようなもの
ここから学ぶ教訓はあるが、それがコピー&ペーストのほうが関数を書くより優れているという意味ではない
リファクタリングは、元の作者をタグ付けした別の変更として出してレビューを受けるべきだった
ただし、同僚が直接話さず上司に持ち上げ、上司が戻せと言ったのはかなり悪い兆候
あり得る返答は「いいえ、理由は……」か、まれに「そうですね、明日やります」くらい
騒ぎにならず全員が問題を避けられ、誰かが学ぶこともある
しかし筆者は重要な文脈を付け加えている。要件変更の可能性と重複削減を引き換えにし、後になって図形ごとのハンドルに多くの特殊ケースや振る舞いが必要になったということ
これは偶発的なDRYと本質的なDRYの違いを示している
ライブラリAとBが互いに一貫して見えるためコードが同じになることは多いが、実際には偶然かもしれない
lib Aを包むラッパーとlib Bを包むラッパーが同じコードを含んでいても、互いに完全に別のものを包んでいるなら、それを抽象化すると結果は悪くなる
「15個」も同じ。「すべてのリストは常に15個を表示すべき」なら15を抽象化するのが正しいが、「ホームページの上位15曲」と「曲の下のコメントのデフォルト15件」がそれぞれ別なら、その15をまとめるのはより悪い選択
2つ目の危険信号は、リファクタリング自体も良いリファクタリングではない点。モジュール性が不足していて依然として結合しており、重複を少し減らしただけ
「君が書いたコードはいまいちだから僕が書き直した。不満ある?」のように聞こえる可能性がある
最善の解決策は「遅れたレビュー」の形で説明を求め、解決策を提案し、元の作者が単に無視しないことを願う程度だろう
要点は、コードは将来変わるということを認識すること。要件も変わり、関連するコードも変わる
その変更能力を制限する抽象化を作ると、将来の自分がコードベースを適応させる能力を損なう
特に多くの時間と労力を費やした抽象化には感情的に執着してしまい、変化を優雅な解法に押し込めようとしてさらに時間を無駄にすることもある
KISSが正しい
この事例は「誰も偉そうな人は好きではない」問題のように見える
特にジュニア開発者が、仕事は自分とコンピュータの間だけにあるのではなく、自分、コンピュータ、そして他人の間にあるのだとまだ学んでいないときによく起きる
よりクリーンなバージョンは、元の作者と実際に会話し、より良い方法に合意していれば受け入れられた可能性が高い
記事タイトルは「さよなら、クリーンコード」よりもチームで働く方法を学んだのほうが合っているように見える
より複雑なコードは脅威として受け取られ、管理ラインを通じて押さえ込むべき競争相手と見なされた可能性がある
ジュニアなら、この言葉が文脈依存だと知るべき。多くの職場では単純さが柔軟性より重要で、混乱を招く言語機能を避け、キーボードの前で長時間過ごすやり方やそうした言語が好まれる
一方で、生涯学習、ソフトウェアクラフトマンシップ、プロセスの省察を重視する哲学は、このような環境と完全に衝突する
より良くなろうとする試みは、成長しない人たちには人気がない
どちらも有効な人生の選択。ソフトウェアは7時間働いて家に帰るのに都合のよい場所でもあり、想像力と知性だけが限界の探検の世界でもある
新しいものを発見したいなら、未知のものを恐れる人たちと何十年も過ごすことには注意すべき
もっと短く言えば、同僚たちとインセンティブがずれているなら去ることを検討すべき
この記事には長く愛されるだけの理由があり、同じ落とし穴にはまる多くの開発者に目を開かせてきた
一人で作業していても、きれいな抽象化を過剰に設計して、将来その抽象化に足をすくわれることがある
問題は、変更がレビューなしで直接mainブランチにコミットされていることにある
すべてのコード変更には、ペアプログラミングであれ従来のPRであれ、こうした拡大を防ぐために同僚レビューがあるべき
上司は悪い上司だった。叱るのではなく、良いプロセスが欠けていることに気づくべきだった
コードレビュー文化のあるチームなら、意欲が先走った新人がこうした試みをしても、同僚が「ありがとう、でもこれは違う」または「良い考えだね、こうしてみよう」と言ったはず
それとは別に、十分な理由なしにコードをリファクタリングすべきではないと思う。悪いコードでも、ほとんど修正する必要のないコードかもしれない
リファクタリングに最も適したタイミングは、振る舞いを変える必要があるのに既存の構造がそれを非常に難しくしていると気づいたとき
残念ながら、ここでは間違った教訓を学んでしまったように思う
なぜみんな、彼が代わりに何をすべきだったかだけを話すのか分からない。これは十分に正当な教訓だと思う。
金融では、ある程度似ているが完全には同じではない商品を常に扱う。株式オプションと外国為替オプションはどちらもオプションで、行使価格があり、Black-Scholes の何らかのバージョンで価格付けされるが、実際にはものすごく違う。
新しく加わった人が多くの動作を共通化したくなる誘惑は大きく、私たちはしばしば元記事の上司の立場で、若い開発者に落ち着いて分けておくよう言うことになる。
クリーンコードに別れを告げようということではなく、過度な抽象化を避けてこそ、実際にクリーンコードを維持できるという意味だ。
一般的なハンドルが行うことはアフィン変換、つまり移動・回転・スケーリング・反射であり、均一スケーリングなら相似変換だ。これはどんな図形にも動作することが保証されている。
本当の問題は、十分に理解していないものを抽象化してはいけないという点にある。将来どんな可能性が出てくるか分からないなら、なおさらだ。
たとえば楕円が円と楕円だけだと思っていると、最初の「本物の」oval が登場したときにかなり驚くことになる。
筆者が汚いコードと「非クリーン」なコードを積極的に区別していないので、他の人たちが区別しようとしているのだ。
DRY には価値があるが、抽象化にはコストが伴う。常にトレードオフだ。
Haskell のような言語が好きな理由の一つは、抽象化の最大主義がコーディングのレベルではなく言語レベルに組み込まれているからだ。
Applicative が何をすべきかについては、みんな同意している。課題は、アイデアからコードへ向かうパイプラインのより右側、つまり問題に合った Applicative を見つける側へ移る。
ほとんどのオブジェクト指向デザインパターンは、この環境では自然で明確に定義された対応物を持ち、プロジェクトごとの抽象化を減らす助けになる。
初心者にとっては言語を学ぶ初期コストが大きく、他の Haskell コードを開いたら初めて見るコンパイラ拡張が12個ほどあって、それを理解しなければならないことがよくある。
それでもそのコンパイラ拡張は以後すべてのプロジェクトに持っていけるので、純利益だと思う。
専門家にとっては、cats が RAM をどれくらい食うか見当をつけるには博士号が必要だという欠点がある。
あるコードが Applicative や Monad を使っていれば、その法則のおかげで動作が分かる。
オブジェクト指向デザインパターンにはそのような法則はない。ゆるく定義された概念やパターンなので、個人の解釈やカスタマイズには開かれているが、それが正確に何で、どんな動作や出力を期待すべきかを示す具体的な法則はない。
ただしブログ記事の例は、クリーンコードの道を進んだ結果、将来の自分の足を撃つことが本当に多かったと言わざるを得ない。
後で再び理解するために抽象化を勉強しなければならないなら、その抽象化は有益ではない。
オブジェクト指向が最も自然に合うのは GUI ウィジェットとエディタの実装だ。
オブジェクト指向の他の規則を統合する規則は、尋ねるな、命じろだ。
この例なら、ハンドルは既知の方法で共有され、図形ごとの制御点があるはずだ。ボックスのハンドルが変わったら
updateControlPoints(changedHandle)を呼び、制御点が変わったらupdateBoxHandles(changedControlPoint)を呼べる。他にもきれいな分離は可能だろうが、私ならそうする。
しかし、どのパーシングライブラリを使うか、
Data.Text.StrictとData.Text.Lazyのどちらを使うか、ポイントフリースタイルをどれだけ使うか、.と>>>や他のコンビネータのうち何を使うか、どの言語拡張を使うかについては、なかなか合意できない。Haskell は好きだが、他の言語と似た問題を抱えている。人々は、仕事をする最善の方法について本当に合意できない。
Haskell の型システムと予測可能な性能を得られる。
繰り返される数学部分だけを別関数に移し、サイズ変更関数から呼べばよかった。
サイズ変更関数そのものが重複して見えたのは、それがインターフェースを成していたからであり、だからそれをなくすリファクタリングは誤りだった。
しかし、それらの関数が行っていた数学は、コードを整理する対象として完全に妥当だ。
結果として小さく純粋な関数が大量にできるが、テストとリファクタリングが楽なので問題にはならない。
同じ問題を解く方法やパターンはたくさんあるが、他の人が使っている既存のインターフェースを取り払うのは良い出発点ではない。
抽象化の利点は、他を傷つけずに内部をより柔軟に変えられるところにある。
良い最初の一歩は、言われているように下層により柔軟な数学関数を作り、残りはそのままにしておくことだ。
その次に、インターフェースが明確なファサードを提供しつつ実装は抽象化されるよう、チームとどうコミュニケーションするかをもっと深く話し合えばよい。
ここで描かれているのは悪い抽象化のように感じる。
本当の問題は、人々が2、3個の例だけを見て設計し、将来の利用を受け入れられるほど十分に汎用的かどうかを確認しないことにある。
下から上へ抽象化すべきで、上から下へ抽象化してはいけない。
すべての処理が1か所で起こり、
2^n通りの組み合わせを扱う単一の地点を作る必要はない。それでも重複を減らし、1か所で変更でき、必要なときにはカスタム変更の能力も維持できる。
だから「悪い抽象化を避けよ」や「将来の利用を受け入れられるようにせよ」と言うだけでは不十分。
一般に、誰も未来を予測することはできない。
抽象化は、正しいと感じられるときでさえ極めて慎重に使うべきだ。極端な懐疑主義者になるくらいの方がよい。
数学コードで1つをいじると、すべてに影響する。
どこかの時点では、重複コードを一部残し、コード構造上の手がかりやコメントで類似性を示し、時間が経つにつれて本質的な違いをコメントで強調する方が楽になる。
この種のコードに対するクリーンコードのアプローチは、変化する要求をうまく扱えない。
結局、呼び出し元に応じて複数のコードパスが入ったヘルパー関数の塊になりがちだ。
ときにはこれを抽象化できることもあるが、すでに気づいた人もいるだろうように、前の文はオブジェクト指向のポリモーフィズムを説明している。
しかし、正確な要求を事前にどうやって知れるというのか。このような早い段階で「クリーンコード」を試みても、悪い抽象化か過剰設計に終わるだけだ。
正しいアプローチは何もせず、起こり得る問題をメモしておき、問題領域についてもっと分かるまで待つことだ。
元の例のように少し重複していても、理解しやすいコードならあまり心配しない。
しかし、同じ問題を3か所で直したり、新機能のコードを3か所に追加しなければならないなら心配する。
そのとき、変更または追加すべきコードをどうにか共通の場所へ押し込めないか考え始める。
人々はデザインパターンやDRYを読み、学習中なのでたいてい誤って適用し、その責任を原典のせいにしがちだ。
Rob Pike は、Go があのような形になった理由の1つとして「小さな依存より小さなコピーの方がよい」と言ったことがある。
プログラミングをすればするほど、その意味が分かるようになる。
繰り返しそれ自体は問題ではない。重要なのは、いま読んでいて、場合によっては修正する単位を把握することだ。
DRY はしばしば、さらに別の抽象化を追加するという意味になり、その抽象化が十分に直交していなければ、今度は1つではなく2つの単位を考慮しなければならないため、状況を悪化させたことになる。