DRY原則の適用範囲について
こんにちは。
株式会社CHILLNNという京都のスタートアップにてCTOを担っております永田と申します。
多くのソフトウェアエンジニアは、習熟の過程で、DRY(Don't Repeat Yourself)原則の過剰な適用がむしろコードを難読化させる経験をします。
自分自身も例に漏れず、後から考えれば不必要な関数の抽象化により、コードが複雑化していくということを痛みを伴って体験してきました。
自分はよくコードレビューの中で、不必要な抽象化を行なっているコードに対して「過剰なDRYになってしまっているよ!」と表現をしてきました。(スーパードライですね。)
本記事では、うまく扱わないとむしろコードを難読化させてしまうDRY原則について、自分の経験を踏まえ、この原則の適用範囲を説明することを目指します。
はじめに
DRYとは、Andy HuntとDave Thomasが書籍「達人プログラマー―システム開発の職人から名匠への道」で提唱したソフトウェア開発原則です。
書籍の中で、DRY原則は以下のように定義されています。
すべての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない。
この定義の中で重要なのは「知識」という表現です。
DRYとは、「知識の重複を許さない」という原則であり、
それは「コードの重複を許さない」ことを意味しません。
では、ここでいう「知識」とは一体どのようなものなのでしょうか?
アプリケーションの知識とは
コードを書いている中で現れる共通部分は以下の二つに分けることができます。
- 再利用可能な関数として抽象化されるべきもの(= DRY原則に従うべき重複コード)
- 現時点でたまたま共通になっているだけのもの(= DRY原則に従うべきではない重複コード)
後者のコードを誤って再利用可能な関数として切り出してしまうと、修正のたびに参照元のすべての仕様を都度考慮しなくてはならなくなります。
このような関数は、アプリケーションの修正を行うたびに条件分岐が増え、関数の内部で呼び出し元を特定するために利用する引数が増加していきます。
つまり、DRY原則を適用させるべきアプリケーションの知識とは、
「呼び出しのコンテキストによらず、将来にわたって同じ動作が期待されるもの」であると考えられます。これは「共通の知識とは、むしろ定義であり、処理のプロセスではない」と言い換えることもできるでしょう。
共通の知識とそうでないものの見分け方
上記の考察で「将来にわたって同じ動作が期待される」という表現をしました。
これはとても曖昧な表現です。
アプリケーションの将来の仕様が予測不可能である以上、アプリケーションごとに期待が異なることはもちろん、開発者によっても期待は異なります。
そのため、一般的な正解はありません。
極端な例を挙げると、将来的に追加開発を一切行うことがないのであれば、すべての共通部分はDRY原則に従って共通化されていたとしても問題はないでしょう。
(それは果たして"ソフト"ウェアなのかという疑問は生じますが)
正解はない、で終わりにしてしまっては記事にする意味がないので、
ここではチームでの議論の出発点になるようなきっかけを提案します。
理想的なDRYで書かれている関数は、引数と返り値がチーム内の誰にとっても自明です。
「関数にコンテキスト上の制約がないこと」が共通の知識の前提になります。
関数を見たとき、
- この引数は一体何なのか?
- この関数の実行結果はどんなコンテキストで有効なのか?
という疑問が生じ、その答えを得るために参照元を辿る必要があるならば、それは過剰なDRYになってしまっている可能性が高いでしょう。
つまり、関数の説明をする際に、その関数を呼び出すコンテキストを伴って説明をする必要があるのであれば、関数として共通化すべき処理ではなく、個別実装すべき処理だということができます。
共通化された関数をレビューする際は、以下の二つの問いを立てれば良いでしょう。
- 引数として、コンテキスト情報を与えていないか?
- 関数だけを抜き出して眺めたとき、返り値の説明が不足しないか?
DRY原則に従わない処理は、関数として切り出すべきではないのか?
上記の考察では「共通化すべき」という前提を置いていました。
処理の一部を関数化することは、処理を抽象化し、再利用可能にするということを意味します。
つまり、関数化には、再利用可能にするだけではなく、抽象化するというメリットも存在しています。
DRY原則とは異なる原則として「単一責任の原則」という原則が存在します。
関数として処理を書き下す際、抽象度の異なる処理を分離する手段として関数化が有効です。
関数が特定のコンテキストに依存していることを明示的に示すことで、可読性を向上させることができます。
弊社では、ディレクトリ構造によるコロケーションの表現によって関数の適用範囲を明示的に示しています。
一箇所でしか使われないからといって、関数化をすべきではないということではありません。
まとめ
関数による分割には、以下の二つの目的があります。
- 知識の共通化
- 処理の抽象化
前者は、DRY原則に従ったものであり、
後者は、単一責任の原則に従ったものだと考えることができます。
目的の異なる二つの手法を分けて理解し、異なった表現にすることで、保守性の高いコードを実現することができます。参考になれば幸いです。
Discussion