DRY原則とは1つの知識を1箇所で明確に表現すること

DRY原則とは?

『The Pragmatic Programmer: From Journeyman to Master』(1999年)(翻訳『達人プログラマー』2000年)で最初に言及された原則です。

定義としては以下です。

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

知識のあらゆる部分はそのシステムにおいて単一で、曖昧さのない、信頼できる表現でなくてはならない。 https://www.infoq.com/jp/news/2012/05/DRY-code-duplication-coupling/

要するに、「1つの知識は、システム内で1箇所で明確に表現されないといけない」という原則です。

知識とは何でしょうか?コンセプト、概念と説明されることもあります。

DRY原則のよくある誤解

コードの重複ではない

DRY原則は、コードの重複とは説明されていません。コードの重複はDRY原則に違反している可能性が高いですが、違反していないかも知れません。

コードだけに限らない

コードに限られてたものではありません。

『達人プログラマー』の著者の Dave Thomas も以下のように説明しています。

A system's knowledge is far broader than just its code. It refers to database schemas, test plans, the build system, even documentation. https://www.artima.com/intv/dry.html

システムの知識は単なるコードよりずっと大きなものです。データベーススキーマ、テスト計画、ビルドシステム、さらにはドキュメントまでも指しています。 https://www.infoq.com/jp/news/2012/05/DRY-code-duplication-coupling/

コードの重複を取り除く

商品クラス

例えば、商品があるとします。

class 商品
{
    private string $商品名;
    private int $価格;

    public function __construct(string $商品名, int $価格)
    {
        $this->商品名 = $商品名;
        $this->価格 = $価格;
    }

    public function 価格(): int
    {
        return $this->価格;
    }
}

通常値引計算クラス

10%を割り引く通常値引というものがあるとすると、以下のようにクラス化できます。

class 通常値引計算
{
    public function 値引額(商品 $商品): int
    {
        $discount = $商品->価格() * 10 / 100;

        return (int) $discount;
    }
}

消費税計算クラス

また、10%の消費税を課税しないといけないということで、以下のようなクラスも出てきます。 本物の消費税は複数税率とか計算方法もややこしいですが、ここでは単純に全て10%とします。

class 消費税計算
{
    public function 税額(商品 $商品): float
    {
        $tax = $商品->価格() * 10 / 100;

        return $tax;
    }
}

そうすると、以下のコードは重複しています。DRY原則違反でしょうか?

$商品->価格() * 10 / 100;

共通化する

とりあえず、同じ処理なので共通化してみましょう。

class ???
{
    public static function 計算する(商品 $商品): float
    {
        return $商品->価格() * 10 / 100;
    }
}

さて、このクラスは何を表現しているのでしょうか? やっていることは商品価格の10%を計算することです。よって、少しぎこちないですが、 「商品価格の10パーセント計算」とクラス名を付けましょう。

class 商品価格の10パーセント計算
{
    public static function 計算する(商品 $商品): float
    {
        return $商品->価格() * 10 / 100;
    }
}

すると、「通常値引計算」と「消費税計算」は以下のようにリファクタリングされます。

class 通常値引計算
{
    public function 値引額(商品 $商品): int
    {
        $discount = 商品価格の10パーセント計算::計算する($商品);

        return (int) $discount;
    }
}
class 消費税計算
{
    public function 税額(商品 $商品): float
    {
        $tax = 商品価格の10パーセント計算::計算する($商品);

        return $tax;
    }
}

値引率の変更

ここで、通常値引が10%ではなく、20%に変わるとします。 「商品価格の10パーセント計算」クラスを変更しようとする人はいるでしょうか? もし、変更すると以下のようになります。

class 商品価格の10パーセント計算
{
    public static function 計算する(商品 $商品): float
    {
        return $商品->価格() * 20 / 100;
    }
}

「商品価格の10パーセント計算」クラスが20%を返すようになります。 流石にこれをする人はいないでしょう。

通常値引の変更なので「通常値引計算」クラスを以下のように変更します。

class 通常値引計算
{
    public function 値引額(商品 $商品): int
    {
        $discount = $商品->価格() * 20 / 100;

        return $discount;
    }
}

「商品価格の10パーセント計算」クラスが必要だったかどうかは疑問が残りますが、作ったからといって問題は起こりませんでした。

過度な共通化をするには

たまたま同じ10%なので共通化してしまい、その後、片方の率が変更になり、他方にも影響を与えてバグってしまうようにするにはどうすればいいでしょう?

10%という知識を通常値引計算クラス、消費税計算クラスから完全に取り除く必要があります。

「商品価格の10パーセント計算」をもっと抽象的な名前に変更しましょう。 これくらいでどうでしょうか?

class 商品関連計算
{
    public static function 計算する(商品 $商品): float
    {
        return $商品->価格() * 10 / 100;
    }
}

何だかよくわかりませんが、商品に関連した計算をしているクラスができました。

すると、「通常値引計算」と「消費税計算」は以下のようになります。

class 通常値引計算
{
    public function 値引額(商品 $商品): int
    {
        $discount = 商品関連計算::計算する($商品);

        return (int) $discount;
    }
}
class 消費税計算
{
    public function 税額(商品 $商品): float
    {
        $tax = 商品関連計算::計算する($商品);

        return $tax;
    }
}

これで、10%という通常値引計算および消費税計算の知識の一部が両クラスから完全に消えました。

ここで、消費税率が15%に変更になるとすると、何だかよくわからない「商品関連計算」クラスの計算ロジックを15%に変更すると、通常値引も15%になってしまうという状況をやっと作り出せます。

これはDRY原則に従っているか?

さて、「商品関連計算」クラスは 1つの知識を明確に表現しているでしょうか?していませんね。何を表現しているのかよくわからないクラスですから。これはDRY原則違反でしょう。

過度な共通化の要件

「通常値引計算」「消費税計算」という概念(知識)をクラスとして分けている時点で、両者を混同するのはかなり難しいことがわかります。

現実にこれらを混同するためには、違う概念であるということが理解できていない、あるいは、そのようなことを全く考えていない必要があります。

つまり、異なる概念であることを理解できていない場合は、このような過度な共通化をしてしまう可能性があるということです。

1つの知識を1箇所で表現するには

結局、1つの知識を1箇所で表現するには、知識・概念を正しく把握することが必要です。

例えば、色々な種類の値引計算があるとして、それらの関係はどうなのか?ある値引は別の値引に関連・依存するのか?しないのか?それらはビジネスに関するルールであり知識です。

値引Aが値引Bに依存すると思っていれば、値引Aが値引Bに依存するコードを書いてしまいます。本当は両者は完全に独立したものであったとしても。

ビジネスに関する知識が不確かならば、誤って違う概念を混同したり、同じ概念を2箇所以上に書いてしまう可能性が高くなります。

まとめ

  • DRY原則とは「1つの知識は、システム内で1箇所に明確に表現されないといけない」という原則です。
  • コードの重複だけがDRY原則の対象ではありません。
  • 知識が不確かなら、DRY原則を守ることは難しくなるでしょう。

参考

Date: 2021/01/13

Tags: programming, php