💂

DRY原則=コードを重複させるな、ではない!

2024/06/10に公開

先日、某SNSでこんな記事を見かけました。

https://gigazine.net/news/20240605-google-not-dry-prematurely/#:~:text=DRYは「Don't Repeat,危険性が高まります。

一見DRYを厳しく適用するとコードの保守性が向上して良さそうに見えますが、GoogleのCode Healthグループはブログに「DRY原則の適用を厳格にしすぎると抽象化が早まってしまい、将来の変更が必要以上に複雑になる」と時期尚早なDRYの適用を避けるべきと述べました。

DRY原則を早期に適用させると、今後拡張する際に手間がかかるケースがあるから要注意だよ〜、的な内容です。
"はじめはコードの重複を多少許容しておき、時期が経って共通ケースが増えてからDRY原則を適用させるでも遅くは無い" とのこと。

そもそも、「DRY原則=コードを重複させるな!」ではない

上記の通り、コードが重複していれば必ず共通化すべきだ、とは私自身も感じません。
場合によってはコードの重複が拡張を容易にするケースだってあります。
これらの違いは何か?私も同様の経験があったのですが、言語化しきれるには至れず。
もう少し自分の中で掘り下げたいので、私は以下の本[1]を購入し理解を深めてみました。
https://www.ohmsha.co.jp/book/9784274226298/

1. コードが重複しているけど、「知識」が異なるケース

具体例

例えばお酒を扱う店で、顧客が注文した商品を登録するPOSTのAPIがあるとします。
未成年にお酒を販売するわけにはいきませんので、まずは顧客の年齢をチェックします。
そして注文したお酒の本数をチェックするために、以下のようなバリデーションを実装するでしょう。

def validate_age(value):
  validate_type(value, :integer)
  validate_min_integer(value, 0)

def validate_quantity(value):
  validate_type(value, :integer)
  validate_min_integer(value, 0)

このコードは一見重複しており、DRY原則に違反するように見えますが、実際はそうとは限りません。
これらの関数は、「異なる二つの物事が同じ規則を有している」に過ぎないのです。
それは偶然であり、二重化ではありません。

「知識」が異なるなら、コードの重複を許容しても良い

書籍で紹介されていた中に コードが重複していても、表現する知識が異なればそれは二重化ではない とありました。
ここで言う知識とは何でしょうか?機能?領域?ドメイン?外部スキーマ?
色々な解釈が出来るかと思いますが、まずは、コードの重複が悪ではなく、拡張性を高めるケースがある事が伝われば幸いです。

2. コードのロジック以外にも当てはめて考えられる事はたくさんある

a. ドキュメントの二重化

コメントとコードの内容が重複しているケースです。
以下のように生年月日をチェックする関数があるとしたら...

def validate_birthday(value):
   """
   現在の日付を取得し、valueとの年数の差分を計算する関数です。
   年齢が20歳未満または70歳より上の場合はエラーを発生させます。
   それ以外の場合はvalueをそのまま返します。
   """
   today = datetime.date.today()
   age = relativedelta(today, value).years

   if (age < 20 or age > 70):
       raise ValueError(
           "年齢は20歳以上70歳以下である必要があります",
       )

   return value

もし今後、要件変更により20-70という範囲が変わったら。
コードだけでなくコメントも修正する必要があります。
極力、変数名や関数名から意図が推測できる様にしましょう。
それでも補いきれない場合にコメントを使用します。

b. データにおけるDRY原則の違反

簡単な例ですが、以下のclassがあったとしましょう。

class Line {
  Point start;
  Point end;
  double length;
}

ご覧の通り、直線をあらわすクラスで、始点・終点・直線の長さを持っています。
このコードには二重化が潜んでいます。
始点または終点が変わると、lengthも変更しなければなりません。
そのため、演算メソッドに変えるという方法もあります。

class Line {
  Point start;
  Point end;
  double length() { return start.distanceTo(end); }
}

可能な限りアクセサ関数を定義し使用するのがオススメ。

c. 開発者間の二重化

開発者それぞれで、同じ機能を別々に実装してしまっていたケース。
チームの風通しを改善し、互いのタスクを理解する必要がありそうです。

まとめ

DRY原則すなわち「コードを重複させるな」とは限りません。
重複するコードが存在しても、あらわす”知識”が異なるのであれば、
共通化は時期尚早である可能性が高いです。

余談

以前、フロントエンドのコードレビュー時に「このコンポーネントは重複して保持させても良いかもしれません」と提案した事を思い出しました。
そこでは顧客データの入力フォームを異なる2画面に実装していたのですが、1つのコンポーネントの中で、差分のある項目だけをif文で分岐して表示させ、残りは共通で表示するように修正していました。
しかし、以下の理由でコードの重複を提案しました。

  • そもそも2画面で異なる機能を提供している事
    つまり、表現する知識が異なる
  • 片方の入力フォームだけを対象とした要件変更が入る可能性がある事
    例えば、レイアウトを変える。バリデーションを変えるなど。

共通の項目にバグが発生した場合、2箇所に修正が必要となりますが
それ以上に拡張性のメリットが上回ると判断しました。
フロントエンドにおいても非常に応用が効く考え方なのかもしれません。

脚注
  1. 達人プログラマー
    ある程度開発経験のあるプログラマーが対象ですが非常に良い本だと感じました。オライリーのEffective C++やC++ Coding Standardsなどを思い出しました。
    全53項目ある中で、気になる部分だけを抜粋し、ふと読み返すのがオススメだと感じます。 ↩︎

Discussion