DRY原則とは?重複してよいコードの見分け方と誤った共通化の避け方
DRY原則を「コードをコピペしない決まり」として覚えていると、まとめてはいけない箇所まで1本の関数に押し込んでしまいます。原典である『達人プログラマー』のTip 15が禁じているのはコードの重複ではなく、知識の重複です。この違いを外すとどうなるか。見た目がそっくりなだけの2つの処理を共通化し、片方の仕様変更でもう片方を壊す事故につながります。本記事では原文の定義を起点に、重複を消してよい条件、誤った共通化が壊れていく過程、そして適用を待つための基準を、Ruby 4.0.6で実行したコードとあわせて整理しました。Ruby on Railsの公式ガイドがDRYを2大原則の1つに挙げる一方、公式ドクトリンの9本の柱にはDRYが無いという食い違いも、一次資料で確認します。
まとめ
- 原典の定義は「すべての知識はシステム内において、単一かつ明確な、そして信頼できる表現を持たなければならない」。対象はソースコードに限らず、データベーススキーマ・テスト計画・ビルドシステム・ドキュメントを含みます。
- DRY違反かどうかは、見た目が一致しているかではなく、変更理由が同じかで決まります。同じ7桁チェックでも郵便番号と会員番号は別の知識です。
- 誤った共通化は、フラグとパラメータが増え続ける形で悪化します。Sandi Metz氏は「重複は、誤った抽象よりもはるかに安い」と述べ、抽象を元に戻す手順を示しました。
- 判断がつかないうちは重複させたまま待ちます。3回目に現れた時点で抽出するのがRule of Three、急いで抽象化しない態度を名付けたのがAHAです。
- Railsの公式ガイドはDRYを2大原則の1つに挙げますが、公式ドクトリン「The Rails Doctrine」の9本の柱にDRYはありません。両者に共通して置かれているのはConvention over Configurationのほうです。
DRY原則の定義:原典が禁じたのは知識の重複
DRYはDon’t Repeat Yourselfの略で、Andy Hunt氏とDave Thomas氏が1999年の著書『The Pragmatic Programmer』(邦題『達人プログラマー』)で中心的な原則として提示しました。日本語では「DRY原則」「DRYの原則」「DRYの法則」「ドライ原則」と表記が揺れますが、指しているものは同じです。
達人プログラマー Tip 15 の原文と適用範囲
原典の定義は、著者らが公開しているTip一覧のTip 15(31ページ)に置かれています。
「Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.(すべての知識はシステム内において、単一かつ明確な、そして信頼できる表現を持たなければならない)」
ここで番号について1点補足します。このTip一覧は2019年の20周年記念版(第2版)から抜粋された全100項目で、Tip 15という番号も31ページというページ数も第2版のものです。1999年の初版を手元に持っている場合、同じ番号を引いても別のTipに当たります。以下、本記事のTip番号はすべて第2版に従います。
定義文の主語はknowledge、つまり知識です。codeでもfunctionでもありません。この一文はソースコードの見た目に触れていないため、同じ文字列が2か所にあること自体はDRY違反の証拠になりません。逆に、まったく違う書き方をした2つの実装が同じ業務ルールを表しているなら、それはDRY違反です。
適用範囲がコードの外まで及ぶことは、著者らの扱い方からも読み取れます。同じ一覧のTip 11「English is Just Another Programming Language」は、ドキュメントに対してもDRY原則を守るよう求めています。データベーススキーマ、テスト計画、ビルドシステム、ドキュメントといった成果物すべてが対象で、仕様書とコードに同じルールを二重に書くこともDRY違反にあたります。
OAOO原則との対比
DRYとよく混同されるのが、エクストリームプログラミング由来のOnce and Only Once(OAOO)原則です。日本語版Wikipediaの「Don’t repeat yourself」は両者を対比し、DRYがアプリケーションに必要な情報(通例は設定情報)を扱うのに対し、OAOOはコードの機能的な振る舞いを扱うと整理しています。DRYは、単一の情報源に対して複数の実装がアクセスすることを許容します。一方のOAOOは、設定値がどこに保存されていようと、コードの実装は1つであることを求めます。
この差は実務では次のように現れます。税率という知識を1か所に置いたうえで、計算処理が請求書側と表示側の2か所に存在する状態は、DRYには反しませんがOAOOには反します。優先順位の付け方はこう考えてください。情報源そのものの変更頻度が高いならDRYを優先して定義を1か所に寄せ、実装が枝分かれして読みにくくなっているならOAOOを優先して処理を1本にします。両者を同じものとして扱うと、どちらを直すべきかの議論が噛み合いません。
DRY違反の判定:見た目の一致ではなく変更理由
実務で迷うのは、2か所のコードが本当に同じ知識なのかという一点です。判定の軸は片方の仕様が変わったとき、もう片方も必ず同時に変わるかにあります。同時に変わるなら同じ知識で、集約してかまいません。片方だけが変わりうるなら別の知識で、たまたま今は同じ形をしているだけです。
偶然の重複を共通化したときの巻き添え
郵便番号と会員番号がどちらも7桁の数字だとします。バリデーションのコードは完全に一致します。
def valid_postal_code?(s) = s.match?(/\A\d{7}\z/) # 郵便番号は7桁
def valid_member_no?(s) = s.match?(/\A\d{7}\z/) # 会員番号も7桁
ここで「同じ正規表現なのだからDRYに反する」と考え、1本のメソッドにまとめたとします。その後、会員番号だけが8桁に変わる要求が来ます。共通メソッドを8桁に直すと、郵便番号のバリデーションが巻き添えで壊れます。
def valid_8digits?(s) = s.match?(/\A\d{8}\z/)
valid_8digits?("12345678") # 会員番号 : true
valid_8digits?("1000001") # 郵便番号 : false(巻き添えで不合格)
Ruby 4.0.6では、郵便番号の「1000001」はfalseを返します。桁数が一致していたのは偶然です。郵便番号の桁数を決めているのは日本郵便の仕様、会員番号の桁数を決めているのは自社の採番規則で、決定者が違います。決定者が違うものは、別の知識として別々に置いてください。
本質的な重複を1か所へ集約する例
逆に、集約すべき重複は次の形をしています。消費税率という1つの知識が、3つのメソッドに数値として散っている状態です。
def price_with_tax(price) = price * 110 / 100
def invoice_total(items) = items.sum * 110 / 100
def tax_amount(price) = price * 10 / 100
税率が変わったとき、3か所すべてを直す必要があり、1か所でも漏れれば請求額と表示額がずれます。しかも漏れは、テストが通ってしまう種類の不整合として残ります。税率を決めているのは税制という単一の権威なので、表現も1つにします。
TAX_RATE_PERCENT = 10
def price_with_tax(price) = price * (100 + TAX_RATE_PERCENT) / 100
def invoice_total(items) = items.sum * (100 + TAX_RATE_PERCENT) / 100
def tax_amount(price) = price * TAX_RATE_PERCENT / 100
price_with_tax(1980) # 2178
tax_amount(1980) # 198
整数演算にしているのは、金額を浮動小数点で扱うと丸め方によって請求額がずれるためです。Ruby 4.0.6では、価格1,980円に対して2178と198が返ります。
重複を消す前の3つの問い
コードを1本にまとめる前に、次の3点を確認してください。
- 2か所は同じ業務ルールを表しているか。仕様変更の依頼が来たとき、必ず両方が同時に直るか。
- 片方だけが変わる要求が来たとき、共通部分にフラグを追加せずに対応できるか。
- 重複を残した場合の修正漏れリスクと、後から抽象を分解し直すコストでは、どちらが大きいか。
3つ目が判断の分かれ目になります。修正漏れは発見が早く、影響範囲も局所的です。誤った抽象を剥がす作業は、次章で扱うSandi Metz氏の復旧手順、すなわち呼び出し側へのインライン展開から不要な条件分岐の削除までを、依存が増えたあとの状態でやり直すことを意味します。着手時点でどちらのコストが大きいかを見積もってください。
そもそもDRYが有用でない場合
DRYを適用しないほうがよい状況も、日本語版Wikipediaが列挙しています。小規模なコンテキストでは、DRYに基づいて設計する労力のほうが、2つの別々のデータのコピーを維持管理する労力よりはるかに大きくなります。設定管理、人間が読むためのドキュメント、ソースコード生成、単体テストも、あえて重複を残す判断が働く領域として挙げられています。さらに、DRYを厳格に強制することが疎結合の原則を破る場合があり、他の設計目標との釣り合いを取る必要があるとされています。
DRYはあくまで原則であって、すべての場面に無条件で適用する規則ではありません。
誤った共通化のコスト:抽象が壊れていく8段階
誤った共通化がどう悪化するかを、Sandi Metz氏が2016年1月20日の記事「The Wrong Abstraction」で8段階に分解しています。要約すると、開発者Aが重複を見つけて抽象として抽出し、時間が経ち、現在の抽象とほぼ合致するが完全には合致しない要求が現れます。開発者Bはパラメータを受け取れるように改造し、その値による条件分岐を足します。さらに要求が届くたびにパラメータと条件分岐の追加が繰り返され、コードは理解不能になります。最後に新しい開発者が、絡まったコードを前に立ち往生する、という筋書きです。
前節の郵便番号と会員番号も、まさにこの経路をたどります。8桁化のときに巻き添えを避けようとすると、たいてい次の形になります。
def valid_number?(s, member: false)
member ? s.match?(/\A\d{8}\z/) : s.match?(/\A\d{7}\z/)
end
このmember引数が、抽象が壊れた瞬間の目印です。呼び出し側は自分が何者かを共通メソッドに教えなければならず、共通化によって減ったはずの知識が、引数という形で呼び出し側に漏れ出しています。この先、法人会員だけ英字を許す、旧番号は6桁も通す、といった要求が来るたびに引数と分岐が増えていきます。
同記事の主張は「duplication is far cheaper than the wrong abstraction」(重複は誤った抽象よりもはるかに安い)という一文に集約されます。抽象化するかどうか迷った時点では、抽象化しないほうが期待コストは低くなります。抽象が正しかった場合に得られるのは可読性の改善ですが、間違っていた場合に払うのは、条件分岐が増殖したあとの分解作業だからです。判断に必要な情報がまだ揃っていないうちは、重複を残して先送りしてください。
すでに壊れた抽象を抱えている場合の手順も同記事が示しています。抽象を呼び出している側に処理をインライン展開して戻し、各呼び出し側で不要な条件分岐を削り、そのうえで改めて共通部分を見直す、という順序です。詳しい進め方はリファクタリングとは?24の観点とタイミング・進め方で整理しています。
適用を待つ基準:Rule of ThreeとAHA
では、いつ抽象化に踏み切るのか。実務で使われている目安は2つあります。
3回目まで待つRule of Three
Rule of Threeは、似たコードが2つある段階ではリファクタリングを行わず、3回目に現れた時点で新しい手続きとして抽出する、という経験則です。Don Roberts氏が提唱し、Martin Fowler氏が1999年の『リファクタリング』で紹介したことで広まりました。「Three strikes and you refactor(3回目でリファクタリング)」という言い回しで知られています。
2回では判断材料が足りないから待つ。それがこの規則の要点です。2つの実例だけでは、共通部分がどこまで共通なのかを見誤ります。3例目が出てくると、3つに共通して現れる部分と、実例ごとに違う部分の輪郭がはっきりします。
WETとAHA:重複を許容する側の語彙
DRYの対語としてWETが使われます。Write Everything Twice(すべてを2回書く)やWe Enjoy Typing(タイプするのが楽しい)の略で、もとは重複だらけのコードを揶揄する言葉でした。2019年前後からは、DRYのやり過ぎへの反動として、意図的に重複を残す方針を指して使われる場面も増えています。
この態度に名前を付けたのがAHAです。Avoid Hasty Abstractions(急いだ抽象化を避ける)の略で、考案したのはCher Scarlett氏。Kent C. Dodds氏が2020年6月22日の記事で「an acronym I got from Cher Scarlett」と出所を明記したうえで取り上げ、広く知られるようになりました。同記事はSandi Metz氏の「prefer duplication over the wrong abstraction」を引いています。Rule of Threeが回数で線を引くのに対し、AHAは「確信が持てるまで抽象化しない」という判断基準を置いている点が違います。回数を満たしていても知識が同じだと確信できないなら、抽象化を見送ってかまいません。
Ruby on RailsとDRY:公式ガイドと公式ドクトリンの食い違い
RailsとDRYの関係は、公式ドキュメントの中でも一枚岩ではありません。入門ガイド「Getting Started with Rails」は、Railsの哲学が2つの主要な指針から成るとしたうえで、その1つにDon’t Repeat Yourselfを名指しし、Tip 15とまったく同じ定義文を引用しています。もう1つがConvention Over Configurationです。
ところが、DHHが書いた「The Rails Doctrine」を確認すると、DRY、Don’t Repeat Yourself、repeat yourselfのいずれの語も本文に現れません。duplicationという語が「Value integrated systems」の節に1回出るだけです。同じ公式でありながら、入門ガイドは2大原則の一角に据え、ドクトリンは柱に数えていません。
9本の柱とConvention over Configuration
ドクトリンが挙げる柱は次の9つです。Optimize for programmer happiness、Convention over Configuration、The menu is omakase、No one paradigm、Exalt beautiful code、Provide sharp knives、Value integrated systems、Progress over stability、Push up a big tent。両文書に共通して置かれているのは、DRYではなくConvention over Configurationのほうです。ドクトリンはこう説明しています。
「If we can depend on a Person class mapping to people table, we can use that same inflection to map an association declared as has_many :people to look for a Person class.(Personクラスがpeopleテーブルに対応すると当てにできるなら、同じ活用規則を使って、has_many :people と宣言された関連からPersonクラスを探し当てられる)」
この順序は実装方針にも効きます。Rails上で重複を消したいとき、最初に検討すべきは共通メソッドの抽出ではなく、規約から外れている命名やディレクトリ構成を規約側に寄せることです。
命名規約が重複を消す仕組み
ドクトリンが例示するPersonとpeopleの対応は、ActiveSupportのInflectorが担っています。Rails 8.1.3.1(2026年7月29日リリース)で実際に確認できます。
require "active_support/all"
ActiveSupport::VERSION::STRING # "8.1.3.1"
"Person".tableize # "people"
"people".classify # "Person"
クラス名とテーブル名の対応を各所に書かずに済むのは、この変換規則が単一の権威になっているからです。逆に規約を外れたテーブル名を使うと、モデルごとにself.table_nameを書くことになり、命名という知識が各モデルに散ります。Railsで重複が増えているときは、共通化の不足ではなく規約からの逸脱を先に疑ってください。そのほうが早く直せます。
KISS・YAGNI・SOLIDとの棲み分け
設計原則は似た場面で引き合いに出されるため、何を判断する道具なのかで区別すると混乱しません。
| 原則 | 判断する対象 | 典型的な問い | 出典 |
|---|---|---|---|
| DRY | 知識の表現箇所 | この知識は何か所にあるか | Hunt / Thomas 達人プログラマー(1999) |
| KISS | 解法の複雑さ | もっと単純な方法はないか | Kelly Johnson / Lockheed Skunk Works |
| YAGNI | 実装する時期 | 今それが必要か | Ron Jeffries / エクストリームプログラミング |
| SOLID | クラスの責務と依存 | 変更理由は1つか | Robert C. Martin |
| Rule of Three | 抽出のタイミング | 3回現れたか | Don Roberts / Fowler リファクタリング(1999) |
DRYとSOLIDは「変更理由」という共通の語彙でつながります。SOLIDの単一責任原則はクラスが変更される理由を1つに絞るよう求めますが、DRY違反かどうかの判定でも変更理由が同じかを問いました。同じ変更理由を持つコードを1か所に集めるという意味で、両者は同じ方向を向いています。
一方でKISSは単純さを見る原則なので、DRYと衝突することがあります。共通化した結果コードが読みにくくなったなら、DRYよりKISSを優先する判断がありえます。判断軸の詳細はKISS原則(KISSの法則)とは?意味・由来とシンプルな設計の実践で扱いました。
YAGNIが見るのは着手時期です。「まだ必要ないものを作らない」という点で、AHAの「確信が持てるまで抽象化しない」と発想が近く、共通化を先送りする判断を後押しします。こちらはYAGNI(ヤグニ)原則とは?意味・読み方とメリット・デメリットを解説にまとめています。
よくある質問
DRY原則とは何ですか?
『達人プログラマー』のTip 15(第2版)で示された「すべての知識はシステム内において、単一かつ明確な、そして信頼できる表現を持たなければならない」という原則です。対象は知識であり、ソースコードの文字列が一致しているかどうかではありません。データベーススキーマ、テスト計画、ビルドシステム、ドキュメントも対象に含まれます。
DRY原則違反とは何を指しますか?
1つの知識が複数の場所に別々の表現として置かれている状態です。税率が3つのメソッドに数値で散っている状態が典型例で、片方を直してもう片方を忘れると不整合が起きます。逆に、7桁チェックが郵便番号と会員番号の両方にある状態は、変更理由が別なので違反ではありません。片方だけが変わる可能性が業務上ないと確認できているなら、集約してかまいません。
DRY原則とSOLID原則の違いは何ですか?
DRYは知識が何か所に表現されているかを見る原則で、SOLIDはクラスの責務と依存関係の持ち方を定めた5つの原則の総称です。適用の粒度が違い、DRYはコードの外(スキーマやドキュメント)にも及びますが、SOLIDはオブジェクト指向設計の内側を対象とします。ただしSOLIDの単一責任原則とDRYは「変更理由が同じかを問う」という点で重なります。
WETコードとは何ですか?
DRYの対語で、Write Everything TwiceまたはWe Enjoy Typingの略です。もとは重複だらけのコードを皮肉る表現でしたが、現在はDRYのやり過ぎに対する反動として、判断がつくまで意図的に重複を残す方針を指して使われることもあります。後者の意味で使う場合は、AHA(Avoid Hasty Abstractions)のほうが誤解が少ない語です。
Rubyのdry-rb(dry-schemaやdry-cli)はDRY原則のことですか?
別物です。dry-rbはRuby向けのライブラリ群の名前で、dry-schema 1.16.0はキーとルールでスキーマを定義するDSL、dry-cli 1.4.1はコマンドラインインターフェースを作るフレームワーク、dry-validation 1.11.1はバリデーションライブラリです。名前にdryが付いているだけで、設計原則としてのDRYを実装したものではありません。「ruby dry」で検索した場合、設計原則の解説を探している人とgemを探している人が混在するため、混同しないよう注意してください。