『Four Better Rules for Software Design』が面白かった
最近、『Sustainable Web Development with Ruby on Rails』という Ruby on Rails 設計のベストプラクティスについての書籍を読みました。
作者の David Bryant Copeland のブログを流し読みしていたところ、面白い記事を書いていたので紹介します 👇️。
Four Better Rules for Software Design
著者について
David は書籍の中でも「クリーン」「再利用」といった理想主義的・抽象的な言説に対する敵意を隠さず、「わかりやすい」「誰でも変更できる」といった性質を重視すべきであることを繰り返し強調し、徹底的なリアリストの立場を取っています。
また、アンチ Uncle Bob であることも明確に述べています。(SOLID is not solidという SOLID 原則に対する逆張り本(?)を出版してるほど)
今回紹介する記事もそんな感じで、キレイゴトを言うな!みたいなノリのことが書かれています。
以下、記事の要約です。
記事の要約
この記事は、Kent Beck が提唱する、Beck Design Rulesというソフトウェアデザインの 4 原則を紹介する記事への反論として書かれています。 まず、Kent Beck が エクストリームプログラミングで述べた 4 原則は以下のようなものです。
- Runs all the tests
- すべてのテストを実行する
- Has no duplicated logic
- 重複したロジックを持たない
- States every intention important to the programmer
- プログラマーにとって重要な意図をすべて伝える
- Has the fewest possible classes and methods
- 必要最小限のクラスとメソッド
David はこの 4 原則には改善の余地があるとして、以下の 4 原則を提唱します。
- is well-covered by passing tests.
- テストによって十分カバーされている
- has no abstractions not directly needed by the program.
- 必要のない抽象化は行わない
- has unambiguous behavior.
- 明確な振る舞いをもつ
- requires the fewest number of concepts.
- 必要最小限の概念
以下、記事における David の主張を要約します。
Removing Duplication Requires Abstractions and Abstractions Breed Complexity (重複の排除には抽象化が必要だが、抽象化は複雑さを生む)
重複したコードを排除するためには抽象化が必要ですが、多くの場合、誤った方法で抽象化が行われており、かえって複雑さを生み出していることがほとんどです(DRY 原則は疑わしい設計の自己正当化のために悪用されているとまで言っています!)。
以下のような、引数にフラグやブール値などを含むメソッドや関数は、コードを DRY 化したものの、まったく同じ振る舞いを持っているわけではなかった場合に生じます。
public void call(Map payload, boolean async, int errorStrategy) {
// ...
}
このようなコードは元の重複したコードよりも多くのユースケースを処理する必要があるため、テストや理解が難しくなります。
多くの場合、コードが似ているのは単なる偶然で、重複を削除するよい方法が見つかるまでは重複を残しておくほうが賢明です。
Programmer Intent is Meaningless—Behavior is Everything (プログラマーの意図は無意味、振る舞いがすべて)
プログラム言語や設計、コードに対して「プログラマーの意図が明らかになる!」という褒め方をすることがあります。しかし、いくら意図が伝わっても、動作がわからなければなんの意味もありません。
例えば、以下の React コンポーネントからは、「最終更新日」というメッセージとともに日付をレンダリングすることを意図しています。しかし、this.props.date
が設定されていない場合は正常に動作しません。プログラマーがこれを意図しているのか、単に忘れているのかはわかりませんが、結局ここで重要なのはどのように振る舞うかです。
function LastModified(props) {
return (
<div>
Last modified on
{props.date.toLocaleDateString()}
</div>
);
}
以下のようにすれば、コードの振る舞いはより明確で、プログラマーの意図は無意味です。
function LastModified(props) {
if (props.date) {
return (
<div>
Last modified on
{props.date.toLocaleDateString()}
</div>
);
} else {
return <div>Never modified</div>;
}
}
(プログラマーの意図ではなく)コードの「振る舞い」が明確であればあるほど、コードの変更は容易になります。「振る舞い」を明確にするためには、より多くのコードを書いたり、より明示的にしたり、あちこちで重複させたりするとこを意味する場合があります。
Conceptual Overhead Creates Confusion and Complexity (概念上のオーバーヘッドが混乱と複雑さを生み出す)
設計やコードに存在する概念 (concept) が多ければ多いほど、理解は難しくなります。 概念上のオーバーヘッドを削減すると、抽象化の数が自然に減り、動作を理解しやすくなります。
システムの概念の数を減らせば、それだけ多くの人が理解することができます。つまり、システムを変更できる人の数も増えます。
多くの人が安全に変更できるソフトウェア設計は、限られた人しか変更できないソフトウェア設計よりも明らかに優れています(これが、非常に抽象的な多くの概念を深く理解する必要がある関数型プログラミングが主流になることは決してないと考える理由です)。
「新しい概念を導入してはいけない」のではなく、新しい概念の導入にはコストがかかるということであり、コストに見合うメリットが得られるかどうかを慎重に検討する必要があります。設計の際には、エレガントさや美しさについて考えるのはやめ、そのソフトウェアで何をしようとしているのか?を絶えず思い出す必要があります。
You Don’t Hang Code on a Wall—You Change It (コードは壁に飾るものではなく、変更されるもの)
コードは美術館に飾る芸術作品ではありません。頻繁に変更されるものです。これを難しくする設計、限られた人しか変更できなくする設計は見直されるべきです。
コードを書いたり、システムを設計したりするとき、果たしてシステムの振る舞いが理解しやすくなっているか、自問してみてください。目の前の問題を解決することではなく、もっと抽象的な問題を追いかけてしまっていないですか?
常に、振る舞いを示しやすく、予測しやすく、理解しやすくする方向を選びましょう。
そして、概念の数は可能な限り最小限の抑えましょう。
感想
多くの人が、過度な一般化や OOP のコンセプトの過剰適用によって理解や変更が著しく困難になっている(そして、さらに誤った理解で変更が加えられ電子九龍城と化している)コードに出会ったことがあるのではないでしょうか。多くの場合で、早すぎる抽象化や込み入った設計コンセプトの導入は単に複雑さを増すだけ、というのは非常に重要な指摘だと感じています。
ただ、正直この記事は逆張りしすぎというか、イデア的な設計論に対する反発心が先立ちすぎている気がします。特に意図と振る舞いに関するあたりはよくわかりませんでした。
適切な抽象化とはなんなんだろう
適切な抽象化とやりすぎな抽象化の境目はどこにあるんでしょうね。
何をどのように抽象化すべきかと同様に、何を抽象化すべきでないかについても考えていく必要がありそうです。
個人的には、抽象化を行うべきかどうかの方針としては、現状以下のような感じで考えています。
- 明らかにドメイン・コード共に重複が認められる場合を除いて、すぐには抽象化しない
- 3, 4 箇所で重複が認められてはじめて、抽象化を検討する
- 抽象化後のクラスやメソッドのインターフェースが複雑になりそうな場合は、筋が悪い可能性が高いのでやめておく
- A Philosophy of Software Designでいう深いモジュールにならなそうな場合