BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース DRY原則の利用: コードの重複と密結合の間

DRY原則の利用: コードの重複と密結合の間

原文(投稿日:2012/05/25)へのリンク

DRYは重複とそれに伴うメンテナンスの問題を軽減するものだが、誤用すると密結合を生み、可読性を損うおそれがある。教訓:ソフトウェア開発原則は、ほかの原則やパターン、プラクティスを考慮して適用しなくてはならない。

DRYは Don’t Repeat Yourself の略語であり、Andy Hunt氏とDave Thomas氏が書籍「The Pragmatic Programmer: From Journeyman to Master」(邦訳:「達人プログラマー―システム開発の職人から名匠への道」)で最初に言及したソフトウェア開発原則だ。その原則はこう述べている。

知識のあらゆる部分はそのシステムにおいて単一で、曖昧さのない、信頼できる表現でなくてはならない。

ここでHunt氏は重複による負の影響と、それゆえにDRYを利用することの重要性を強調した。ポートランド・パターン・リポジトリのWikiには次のように書かれている。

重複(故意あるいは目的のある重複)は悪夢のようなメンテナンス、ひどい分解、論理的矛盾をもたらすおそれがあります。

重複とその結果として起こりうる矛盾は、アーキテクチャ、要件、コード、ドキュメントなど、至るところで生じるおそれがあります。その影響は、コード上の実装ミスや開発者の混乱から完全なシステムエラーまで、多岐にわたります。

2000年問題の修正で最も困難だったのは、いかなるシステムにおいても単一の日付の抽象化がなかったことだと言う人もいます。日付と日付処理の知識は広範囲にちらばっているのです。

DRYはソフトウェア工学における必須原則のように見えるが、例外もあるとAnders Munch氏は述べている

この原則には例外パターンがあります。一貫性を保証するための効率よい仕組みが備わっていれば知識に複数の表現があっても構いません。

  • C関数の定義と宣言: コンパイラは矛盾があると警告し、プログラマはそれに対処しなくてはならないため、通常は同期します。
  • ユニットテスト: 矛盾があるとテストが失敗するため、だれかがそれに対処しなくてはなりません。
  • 自動生成されるもの: 定期的に再生成することで一貫性が保たれます。

こうした例外は実のところ、DRYの背景にある原理を強化するものだ。しかし次のような疑問が浮かんでくる。プログラマはDRYを極端にとりすぎていないだろうか?誤解したり、誤用しているケースはないだろうか?

Dave Thomas氏は早いうちからこう注意を促していた。「DRYのことを、コードを重複させてはいけない、という意味にとっている人が多いようですが、そうではありません。DRYの背景にある考えは、それよりもっと大きなものなのです」。それはDRY原則をソフトウェアシステム全体に広げることだ。

DRYは、あらゆるシステム知識は1つの信頼できる、曖昧さのない表現を持つべきであると言っています。何かを開発する上で、あらゆる知識が単一の表現を持つべきです。システムの知識は単なるコードよりずっと大きなものです。データベーススキーマ、テスト計画、ビルドシステム、さらにはドキュメントまでも指しています。

こうした知識があるとして、どうしてあなたは個々の機能を表現する1つの方法を見つけなくてはならないのでしょう? 明らかに言えるのは、同じものを表現するのに複数の方法があると、いつか2、3の異なる表現がお互いそぐわなくなるのです。そうでなくとも、どこかに変更があると、それらを並行してメンテナンスしなくてはならないという頭の痛い問題を抱えることになります。そして変更は起こるものなのです。もしあなたがフレキシブルでメンテナンス可能なソフトウェアを望んでいるのなら、DRYはとても重要です。

問題は「どうやってこれら異なる知識を一度にすべて表現するか?」です。もしコードだけであれば、コードを一箇所にまとめてメソッドやサブルーチンを使うことで、繰り返しを避けることができるでしょう。しかしデータベーススキーマのようなものは、どうすればよいのでしょうか? こういったところには、コード生成ツールや自動ビルドシステム、スクリプト言語の利用など、本にある別のテクニックが使えます。これらを利用することで、あなたは単一の信頼できる表現を手に入れて、コードやDDL(データ記述言語)といった信頼できない(二次的な)作業成果物を生成できるようになります。

DRYの利用にまつわる問題は随分昔に解決したように見えたのだが、QCon London 2012でもGreg Young氏やDan North氏など数名がその誤用のおそれについて注意喚起している。そこでInfoQはさらに詳しく追求した。DRYに関して、どんな問題に遭遇したか尋ねたところ、Young氏は次のように答えた。

DRYにしたがうことに対する基本的な議論は、ものごとには別の側面があるということです。"DRY"にしたがうことで、ソフトウェアに結合や複雑さをもたらすのは珍しいことではありません。トレードオフの一方は非常に測定しやすいですが(複数の場所にあるバグを修正しなくてはならないときの時間当たりの面々の数)、もう一方はかなり困難です(DRYの名のもとにソフトウエアにもたらされた結合や複雑さ)。

「適切に」したがえば、DRYが結合や複雑さをソフトウェアにもたらすことはない、と主張する人もいるでしょう。これは幻想のようにすら見えます。私は結合や複雑さをもたらすことなく、DRYに完全にしたがったコードが書けます。しかし、これは私が完全な知識を持っているとすればです。

私たちはRSpecの作者でありリードデベロッパであるDavid Chelimsky氏にも尋ねた。彼はDRYが「 必ずしも適切であるわけでない(適切であるときもあるが)」という場面を目にしてきたと言う。彼は次のような例をあげた。

describe "Person#full_name" do
it "concats the first and last names" do
   first_name = "John"
   last_name = "Doe"
   person = Person.new(:first_name => first_name, :last_name => last_name)
   person.full_name.should eq "#{first_name} #{last_name}"
end
end

上のコードは重複を避けており、DRYのすぐれた実践だと見えるかもしれないが、Chelimsky氏は次のような読みやすいコードの方が好みだと言った。

describe "Person#full_name" do
it "concats the first and last names" do
   person = Person.new(:first_name => "John", :last_name => "Doe")
   person.full_name.should eq "John Doe"
end
end

そして、こう付け加えた。

DRYを本当には理解していないのに素晴しいものだと思っている人にとって、この例で"John"と"Doe"が2度出てくるのは黒板を爪で引っ掛くようなものです。でも私は正反対です。この方が名字と名前にある関係、そしてfull_nameの結果がわかりやすいと思います。

Chelimsky氏は最近見かけたObjectifyというフレームワークのコードについても指摘した。以下のイタリックになっている部分は、

def request_resolver

  klass = Objectify::NamedValueResolverLocator

  @request_resolver ||= klass.new.tap do |resolver|

    resolver.add(:controller, self)

    resolver.add(:params, params)

    resolver.add(:session, session)

    resolver.add(:cookies, cookies)

    resolver.add(:request, request)

    resolver.add(:response, response)

    resolver.add(:flash, flash)

    resolver.add(:renderer, Renderer.new(self))

  end

end

次のように書き換えられた。

{:controller => self, :params => params, :session => session, :cookies => cookies, :request => request,
:response => response, :flash => flash, :renderer => Renderer.new(self) }.each do |key, value| resolver.add(key, value)
end

Chelimsky氏はこの変更に対してコメントした。「これはDRYのやりすぎではないですか。変更前の方がメンテナンスしやすい(読みやすく、修正しやすい)と思います。」

Chelimsky氏が遭遇した大きな問題のひとつは「"Don't Repeat Yourself"という言葉がメモリデバイスになることを目的としているのに、"DRY"が"Don't Repeat Yourself"のためのメモリデバイスになっており、結局はこれが多くの人の頭に「原則」として植え付けられている」ことだ。彼はこのアプローチは「重複をなくすときに結合を増やす」という別の問題を引き起こすおそれがあると注意した。

同じオブジェクトにある2つのメソッドがある同じ仕事をしているとき、私たちはそれら2つのメソッドが委譲する第三のメソッドを抽出します。もとの2つのメソッドはいずれも抽出されたメソッドに結合しており、間接的ですがお互い結合しています。これは単一オブジェクトのコンテキストにおいては、まったく論理的であって危険はないように見えます。しかし、2つのオブジェクトを横断した同様の振舞いを考えるとどうでしょう?重複をなくすため、私たちはそれらが依存する新しいオブジェクトを導入することになるか、よくあることですが、さらにひどくて悲惨なことに、あるオブジェクトをほかのオブジェクトに依存させます。後者のアプローチでは、関係のないオブジェクト間に依存関係を作ってしまうことが多く、やがて進化の妨げになります。新しいオブジェクトを導入すると、システム全体の界面が増えてしまい、導入時やリファクタリング時に余計な検討や配慮を必要とします。

DRYを極端にとりすぎるのを避けるため、Chelimsky氏はほかの開発原則とバランスをとるよう提案する。

DRYは重要ですが、もちろん、BobおじさんのSOLID原則や、たとえば結合を疎にしたり凝集を高めるといった概念も重要です。常に1つの原則を適用するだけでは十分ではありません。あなたはこれらすべてを考慮して、状況に応じて相対的な価値を検討する必要があります。どの調味料が魚に合うのか、どの調味料がステーキに合うのかを知るようなものです。実際のところ、どちらもうまく合うこともあれば、そうでないときもあります。

DRYは重要な原則だが、それを誤用すると、密結合を増やし、読みやすさを損うといった問題を引き起こすおそれがある。ここでの教訓は、いくら原則がすばらしいからといって、ほかのすぐれたプログラミングプラクティスを軽視してはいけないということだ。

この記事に星をつける

おすすめ度
スタイル

BT