はじめに
ソフトウェア設計におけるモジュール結合について学んでおり、
その中で表題のパターンについて知ったので、今回はこれを記事にしてみたいと思います。
制御結合とは
モジュールBの振る舞い、動作、処理をモジュールAから渡される情報(例:フラグ、タイプ)に左右されている状態、
すなわち内部実行フローを制御されている状態にある時、
このモジュール間には制御結合というモジュール結合が生まれています。
ifやswitchなどがそれに該当しますが、こう言った条件分岐を使うシーンはよくあります。
時にはその分岐が連続することもあるかと思います。
冗長なif文の解消については、僭越ながら、以下の記事をご参照ください。
プログラミングにおいては条件分岐はどこかで発生しえるものでもあり、それ故に制御結合そのものが悪いものとは言えないかとも思われます。
ただ、今回はその中でも以下のような条件分岐における問題点と改善点について取り上げてみたいと思います。
具体例:条件分岐内のメンテナンス性が低い
以下のようなコードがあったとします。
void updateView(bool isError) {
if (isError) {
resultView.isVisible = true;
errorView.isVisible = false;
iconView.image = CROSS_ICON;
} else {
resultView.isVisible = false;
errorView.isVisible = true;
iconView.image = CHECK_ICON;
}
}
よくあるシンプルなコードですが、分岐内の処理が「何を更新しているのか」を把握するには、
それら全ての分岐内の詳細を調査する必要があります。
あまり斜め読みに適した構造にはなっていないです。
加えて、新たな分岐が増えた際はresultView、errorView、iconViewの値を全て忘れずにあてがってあげねばならず、そうせずともコンパイルエラーにはなりません。
ビューが増えた場合も同様です。
仕様変更時やバグの調査の際は少し厄介になり、メンテナンス性が低そうです。
こういったコードは以下のように「操作対象による分割」でリファクタリングをした方がいいでしょう。
void updateView(bool isError) {
resultView.isVisible = !isError;
errorView.isVisible = isError;
iconView.image = getIconImage(isError);
}
Image getIconImage(bool isError) {
return isError ? CROSS_ICON : CHECK_ICON;
}
ここでいう「操作対象による分割」とは、「エラーがあるか否か」といった「条件への着目(条件による分割)」ではなく、
「どんなViewが更新されるのか」といった「参照・更新の対象への着目」を行うことを指し、
これにより斜め読みがしやすくなり、ビューの追加時も対応しやすくなりました。
また今回は副次的に条件分岐自体をなくすこともでき、認知上の負荷の軽減にも役立っています。
具体例:条件分岐内の処理同士の関連性が低い
では次はこんなパターンを見てみましょう。
class ProfileViewPresenter {
void updateUserView(DateType dateType) {
switch (dateType) {
case DateType.userName:
var userName = getUserName(dateType.userId);
userNameView.text = userName;
break;
case DateType.birthDate:
var birthDate = formatBirthDate(dateType.birthDate);
birthDateView.text = birthDate;
break;
case DateType.ProfileImage:
var profileImageBitmap = getProfileImageBitmap(dateType.userId);
profileImageView.image = profileImageBitmap;
break;
}
}
}
dateTypeという引数を用いて条件分岐をしていますが、それぞれの分岐先の処理の関心事、目的、責務、役割はどうも異なっています。
それぞれの「何をしたいか、何をしているか」が異なっているのです。
こうなっていると斜め読みがしにくいのもありますが、
class Caller {
void callUpateUserView() {
switch (dateType) {
case satisfiesCondition:
var dateType = DateType.userName;
profileViewPresenter.updateUserView(dateType);
break;
case satisfiesAnotherCondition:
var dateType = DateType.birthDate;
profileViewPresenter.updateUserView(dateType);
break;
case satisfiesYetAnotherCondition:
var dateType = DateType.ProfileImage;
profileViewPresenter.updateUserView(dateType);
break;
}
}
}
このような呼び出し元の実装があった場合、その双方の条件分岐の行き来をしながら振る舞いを理解しなければならず、
バグが生まれる原因の元になったりします。
そこで上記のようなコードは以下のようにリファクタリングをしてみましょう。
class ProfileViewPresenter {
void updateUserView() {
var userName = getUserName();
userNameView.text = userName;
}
void updateBirthDateView() {
var birthDate = formatBirthDate();
birthDateView.text = birthDate;
}
void updateProfileImageView() {
var profileImageBitmap = getProfileImageBitmap();
profileImageView.image = profileImageBitmap;
}
}
class Caller1 {
void someMethod() {
profileViewPresenter.updateUserView();
}
}
class Caller2 {
void someMethod() {
profileViewPresenter.updateBirthDateView();
}
}
class Caller3 {
void someMethod() {
profileViewPresenter.updateProfileImageView();
}
}
どこかストラテジーパターンに似ているところがありますが、このリファクタリングにおいても「単一責任の原則」に根ざして、メソッドそのものを分けています。
こうすることで呼び出し元は関心事に応じてメソッドを呼ぶだけで済み、条件分岐自体が不要になりました。
このようなリファクタリングは「呼び出し時に引数が静的に決まる」、つまり「どこがメソッドを呼び出すかにより引数が決まっている」場合に有効な方法です。
そうではない際は、先述の拙文によるリファクタリング手法か、または処理内容自体に関連性が見られるならば「操作対象による分割」を用いたリファクタリングが有効かと思います。
参考