はじめに
DRYの原則と言えば普段の開発の至る所で流用される設計原則かと思われます。
『同じコードはあちこちに繰り返さず一纏めにして抽出する』
そのわかりやすさ、使いやすさゆえに本来とは異なるニュアンスで使われることがあるため、
今回はこのことについて記事にしてみたいと思います。
DRYの原則の誤用
例えば以下のような定価クラスがあったとします。
/// 定価クラス
class RegularPrice {
int amount;
static const int _MIN_AMOUNT = 100;
RegularPrice._internal(this.amount);
factory RegularPrice.create(int amount) {
if (amount < _MIN_AMOUNT) {
throw ArgumentError('Amount must be at least $_MIN_AMOUNT');
}
return RegularPrice._internal(amount);
}
}
オブジェクト生成はfactoryコンストラクタが窓口になるように設計しており、内部でバリデーションなどのロジックを介入させるようにしています。
通常のコンストラクタはprivateにしており、ルール外のオブジェクト生成を許さないよう、オブジェクト生成の方法が限定化されている訳です。
生成するオブジェクトの質の違いを反映すべくfactoryコンストラクタをそれぞれ別々に実装する、といったことも出来ます。
次に、このクラスを使ってオブジェクトを生成する通常割引クラス、夏季割引クラスを実装してみます。
/// 通常割引価格クラス
class RgularDiscountedPrice {
int amount;
static const int _MIN_AMOUNT = 0;
static const int _DISCOUNT_AMOUNT = 400;
RgularDiscountedPrice._internal(this.amount);
factory RgularDiscountedPrice.create(RegularPrice regularPrice) {
int discountedAmount = regularPrice.amount - _DISCOUNT_AMOUNT;
if (discountedAmount < _MIN_AMOUNT) {
discountedAmount = _MIN_AMOUNT;
}
return RgularDiscountedPrice._internal(discountedAmount);
}
}
/// 夏季割引価格クラス
class SummerDiscountedPrice {
int amount;
static const int _MIN_AMOUNT = 0;
static const int _DISCOUNT_AMOUNT = 300;
SummerDiscountedPrice._internal(this.amount);
factory SummerDiscountedPrice.create(RegularPrice regularPrice) {
int discountedAmount = regularPrice.amount - _DISCOUNT_AMOUNT;
if (discountedAmount < _MIN_AMOUNT) {
discountedAmount = _MIN_AMOUNT;
}
return SummerDiscountedPrice._internal(discountedAmount);
}
}
それぞれのクラスのfactoryコンストラクタの引数に定価クラスRegularPriceをとっていることで、
その効能を享受できるだけでなくモック差し込みによるテスタビリティも上げています。
「定価というモデルとその振る舞いを一本にまとめて共通の効能を提供する」という点で言えば、これもDRY原則の一つかと思われます。
しかしここで、以下のコードが気になった方もいらっしゃるのではないかと思われます。
----
/// 通常割引factoryryのロジックの一部
int discountedAmount = regularPrice.amount - _DISCOUNT_AMOUNT;
if (discountedAmount < _MIN_AMOUNT) {
discountedAmount = _MIN_AMOUNT;
}
return RgularDiscountedPrice._internal(discountedAmount);
---
/// 夏季割引factoryのロジックの一部
int discountedAmount = regularPrice.amount - _DISCOUNT_AMOUNT;
if (discountedAmount < _MIN_AMOUNT) {
discountedAmount = _MIN_AMOUNT;
}
return SummerDiscountedPrice._internal(discountedAmount);
---
通常割引と夏季割引のオブジェクト生成のロジックが全く同じです。
すると「これも一纏めにすればいいのでは」と思うかもしれませんが、この場合は抽出は避けた方がいいです。
それは、
通常割引と夏季割引の割引方法が異なるようになったら
こういったケースによる弊害があり得るからです。
例えば、
「通常割引の金額ベースの割引方法と異なり、夏季割引が5%OFFといった倍率ベースの割引手段になった」
こうなってしまうと内部の計算ロジックも無論変わってしまいます。
もし、この仕様の違いを知らずにオブジェクトを生成しようとすれば思わぬ余波が及ぶことで想定外の振る舞いを引き起こしてしまいかねません。。。
このRgularDiscountedPriceクラスとSummerDiscountedPriceクラスは元々異なる関心事、役割、責務、目的のもとにモデリングされているが故に、別々に設計・実装されているはずです。
そう考えると、一見すると同じロジックに見えたとしても安易に抽出するのは避けたほうが無難でしょう。
DRYの原則の使い過ぎに気を付ける、のではなく、それが共通の関心事に基づくものなのか否か、
それ次第で設計原則の適用を考えた方がいいでしょう。
参考