はじめに
ソフトウェア設計におけるモジュール結合について学んでおり、
その中で表題のアンチパターンについて知ったので、
今回はこれを記事にしてみたいと思います。
外部結合とは
外部結合とはグローバル変数(static変数も含めて)などを参照する場合に発生するモジュール結合のアンチパターンのことです。
また、前回の共通結合とは異なり、データ構造全てではなくその一部を共有する程度に留まっているものを指し、
その点においては共通結合よりはモジュール結合における憂慮性は低いかとも思われますが、
依然として、看過すべきではない改善の余地のある結合度合いを示すものでもあります。
共通結合については、僭越ながら、以下の記事をご参照ください。
具体例:グローバル変数を使った値の受け渡し
今回も実際のコードを例に見てみましょう。
/// 外部結合に設計上のアンチパターン
int? parameter = null;
int? result = null;
class Calculator {
int calculate() {
if (parameter == null) {
throw Exception('parameter is null');
}
result = parameter! * 2;
return result!;
}
}
class Caller {
final Calculator calculator;
Caller(this.calculator);
void callCalculator() {
parameter = 5;
print(calculator.calculate());
}
}
上記のコードはグローバル変数であるparameterとresultに対する参照を行なっており、
中でもCalculatorクラスのcalculateメソッドや、CallerクラスのcallCalculatorメソッドは、
何ら引数を定義せず、これらのグローバル変数を参照・更新するように設計されています。
これらグローバル変数が定数でないことも影響していますが、
こうなると、これらの変数の現在の状態がどうなっているのかを追跡するのは大変難しくなり、メンテナンス性と頑健性が下がってしまいます。
こういったコードは以下のようにリファクタリングをした方がいいでしょう。
/// 外部結合を引数と戻り値を用いて解消した例
class Calculator {
int calculate(int parameter) {
return parameter * 2;
}
}
class Caller {
final Calculator calculator;
Caller(this.calculator);
void callCalculator() {
final result = calculator.calculate(5);
print(result);
}
}
上記のコードはCalculatorクラスのcalculateメソッドに、これまでグローバル変数で定義していたparameterを引数として昇格させつつ、
CallerクラスのcallCalculatorメソッドでは同様にresultを同メソッドのfinal定数として昇格させることで、
不用意な状態遷移が起こらないようにしています。
こうすることでコードの認知上の負荷は軽減され、将来のメンテナンス性や頑健性が担保されています。
具体例:『デメテルの法則』に反している設計パターン
次は以下のコードを見てみましょう。
void equipArmor(int menberId, Equipment newArmor) {
if (party.members.get(menberId).equipments.canChange) {
party.members.get(menberId).equipments.armor = newArmor;
}
}
このコードは下記のデメテルの法則に反したコードをクラス設計により解消する記事内で紹介したアンチパターン例です。
解消方法については、僭越ながら、上記記事に譲りますが、このコードも見方を変えれば外部結合をしていると見なせるかと思います。
と言うのも、本来equipArmor関数がやりたいことは「装備を変更できるなら変更する」ことであり、
その意味でもequipmentsのcanChangeやarmorだけが知りたい情報のはずです。
ところが、上記コードはこれらの値を知るために不必要なチェインを挟んでしまっており、
それゆえに状態(情報)が漏れ出てしまっています。
これもまた、データ構造そのものとは言はずとも、ひとつひとつのチェインがグローバル変数のように機能してしまっていると捉えられ、
それゆえに外部結合をしていると言えるかと思われます。
具体例:static変数の参照
これまでは外部結合によるアンチパターン例を見てきましたが、
ここで必ずしもアンチパターンとは言えない以下のコードを見てみましょう。
/// static変数による外部結合の例
class ClassA {
static const name = 'ClassA';
String getName() {
return name;
}
}
class ClassB {
String getClassAName() {
return ClassA.name;
}
}
class ClassC {
void Greet() {
print('Hello, ${ClassA.name}!');
}
}
上記コードはClassA内のstaticフィールドをClassBとClassCという別クラスで参照するコードです。
これもグローバル変数の利用という点では外部結合のコードになりますが、
必ずしもアンチパターンに陥っているとは言えません。
不変なクラスフィールドとして提供されていることにより、
これを利用するモジュールには共通の状態を提供することができ、
影響範囲とその状態も把握がしやすくなっているからです。
眼前のフィールドをstaticな定数として定義するべきか否か、その将来的な影響力を考えるという設計上の観点は問うことは出来るかとも思われます。
このように、一方が変わればもう一方の依存先もまた変わることを示すモジュール設計上の概念のことを『コナーセンス』と言います。
モジュール結合がアンチパターンに陥っているかは、その結合がどのような特性を持っているかを考え、
- モジュールの結合強度
- モジュール同士の距離感
- モジュールの変更頻度
と言う観点にたって複眼的に分析することが必要です。
参考