ソフトウェアも密を避けるべき
この記事は夏のリレー連載2023第8日目の記事です。
新型コロナウイルスの感染拡大を防ぐために今もなお「3つの密を避けましょう!」と言われておりますが、これは感染症対策だけでなくソフトウェア開発においても「密」を避けることが重要と筆者は考えます。
そこで今回は「ソフトウェアも密を避けるべき」というテーマのもと、ソフトウェアの結合度についてお話していきたいと思います。
モジュール結合度
#今更感満載ではありますが「モジュール結合度」のお話から始めていきます。
情報処理技術者試験にも出題されるくらいなので、ソフトウェア開発に携わっている方なら一度は耳にしたことはありますよね。
モジュール結合度とは、モジュール同士の結びつきや関係性の強さを表す指標で、この結合度合いが強いほど「密結合」となり、関連するモジュールに変更が生じた場合の影響範囲は大きくなります。
反対に、この結合度合いが弱いほど「疎結合」となり、モジュールの独立性も高まるので変更による影響を受けにくくなります。
モジュール結合度は、データの受け渡し方法や参照などにより、6段階のレベルに分類されます。
モジュール結合度が弱い順に並べると下表のとおりです。
レベル | 種類 | 概略 |
---|---|---|
1 | データ結合 | 引数により、単純なデータのみの受け渡しを行う。 |
2 | スタンプ結合 | 引数により、構造化されたデータのまとまりで受け渡しを行う。 |
3 | 制御結合 | 与えられた引数の内容によって、関数やメソッドの戻り値が変わる。 |
4 | 外部結合 | 外部から供給されたデータを複数のモジュールで参照している。 |
5 | 共通結合 | 共通領域で定義されたデータ(グローバル変数など)を複数のモジュールで参照している。 |
6 | 内容結合 | モジュールの内部に依存しており、直接的に参照/利用している。 |
保守のしやすさの観点では、モジュール間の結びつきを可能な限り弱い状態に保てるのが望ましいです。
このようなことからも、モジュール間の結合度をいかに弱められるかが、ソフトウェアを設計するうえで重要と言えるでしょう。
なお、「モジュール」と言っても人それぞれ、その粒度や捉え方が異なることと思われます。
このため、「モジュール = クラス」という前提を置き、以降ではクラス間の結合度についてお話させていただきます。
個々のモジュール結合度に関して、上表の概略だけですと具内的なイメージがわきにくいと思います。
それらについては改めて別の機会に、実装例を加えながら説明させていただければと考えています。
ご理解いただきたく存じます。
継承による密結合
#クラス間の密結合は、さまざまな要因で引き起こります。
今回は密結合の一要因として考えられる、クラス間の「継承」に着目してお話したいと思います。
継承は、多くのオブジェクト指向プログラミングの入門書でも紹介されているように、とても魅力的な仕組みですよね。
だって、継承を使えば、スーパークラス(=親クラス)の特性を引き継げるから、サブクラス(=子クラス)はそれぞれで異なる実装(差分)だけですみますものね。
クラスを跨いで共通的な要素をスーパークラスにまとめることで、開発生産性や保守性の向上にもつながるし、「なんてステキな仕組みなんでしょう!」と捉えてしまっても仕方ありませんよね。
たしかに、そういう見方もありますけど、便利がゆえに安易に継承してしまうのは危険が伴います。
実装段階におけるクラス間の関係性には、大きく分けて「継承」「関連」「依存」の3つがあります。
この関係性の中で一番強い結びつきは「継承」になります。
結合度合いが強いほど変更が生じた場合の影響範囲は大きくなるというお話は、前述の「モジュール結合度」で触れましたよね。
これはクラス間の関係性にも同じことが言えて、一番結びつきの強い「継承」も同様の事態を招く恐れがあります。
では、具体例を交えながら、どのような弊害があるのかを見ていきたいと思います。
上ではクラス間の関係性を簡単に説明させていただきましたが、UMLのクラス図を用いることで詳細かつ厳密に定義、表現することも可能です。
実装上の仕組みとクラス図での表現において一部、用語の違いから、そのあたりの説明は省略させていただきました。
クラス間の関係性についてさらに詳しく知りたいという方は、弊社「人材育成サービス」の教育コースをお勧めいたします。
継承の弊害
#QRコード決済における付与ポイントの算出処理を例に、継承による問題点を見ていきましょう。
たとえば、継承を使って次のようなクラス構成になっていたとします。
スーパークラスの「ポイント算出クラス」で共通的な算出処理を実装しています。
サブクラスの「PoyPoyポイント算出クラス」は固有の実装がないため、「ポイント算出クラス」の継承のみを行っています。
一方、サブクラスの「楽楽Payポイント算出クラス」では、「ポイント算出クラス」の処理を呼び出しつつ、楽楽Pay固有の実装を加えています。
コード例で示すと、次のようになります。
public class ポイント算出クラス {
public int 通常時のポイントを算出する(BigDecimal 購入金額) {
// 購入金額の1%をポイントとして付与
// e.g. 10,000円のものを購入したら、100ポイント付与される
var ポイント還元率 = BigDecimal.valueOf(0.01);
return 購入金額.multiply(ポイント還元率).intValue();
}
public int キャンペーン時のポイントを算出する(BigDecimal 購入金額) {
// 購入金額の2%をポイントとして付与(通常時の2倍のポイント付与)
// e.g. 10,000円のものを購入したら、200ポイント付与される
var ポイント還元率 = BigDecimal.valueOf(0.02);
return 購入金額.multiply(ポイント還元率).intValue();
}
}
public class 楽楽Payポイント算出クラス extends ポイント算出クラス {
@Override
public int 通常時のポイントを算出する(BigDecimal 購入金額) {
// 固定で10ポイントをプラス
return super.通常時のポイントを算出する(購入金額) + 10;
}
@Override
public int キャンペーン時のポイントを算出する(BigDecimal 購入金額) {
// 同様に固定で10ポイントをプラス
return super.キャンペーン時のポイントを算出する(購入金額) + 10;
}
}
少し無理矢理な感じもありますが、楽楽Payで決済した際には、通常時とキャンペーン時に固定で10ポイントが加算される仕様だったとします。
たとえば、10,000円の商品を購入したとき、通常時は110ポイント、キャンペーン時は210ポイントが付与されます。
しばらくの間はこれが問題になることはありませんでした。
しかし、ある契機でスーパークラスの「ポイント算出クラス」のリファクタリングが実施されました。
なんと、キャンペーン時のポイント算出は通常時のポイントの2倍だから、通常時の算出結果を2倍するように変更されたのです。
良いリファクタリングとは言えませんので、あくまでも例としてお読みください。
public class ポイント算出クラス {
/* ----- <中略> ----- */
public int キャンペーン時のポイントを算出する(BigDecimal 購入金額) {
// 購入金額の2%をポイントとして付与(通常時の2倍のポイント付与)
// e.g. 10,000円のものを購入したら、200ポイント付与される
return 通常時のポイントを算出する(購入金額) * 2;
}
}
この変更によってどんな不具合が生じたかと言いますと、キャンペーン時に楽楽Payで決済した際、これまでよりも多くポイントが付与されることになります。
たとえば、キャンペーン時に10,000円の商品を楽楽Payで決済すると、通常時の2倍のポイントなので220ポイントとなり、これに楽楽Pay特典の10ポイントが加算され、付与されるポイントの合計が230ポイントとなりました。
あれ?何も仕様を変更していないのに、スーパークラスのリファクタリングの前後で、20ポイントの誤差が生じてしまいましたね。
まさに、継承による密結合が原因となって引き起こした事象と言えるでしょう。
また、この原因を特定するのにも、スーパークラスとサブクラスの双方の処理を交互に追いかける必要があるため、良い設計でないことが見て取れるでしょう。
じゃあどうする?
#じゃあ、どうするかと言いますと、クラス間の結合度を弱くすれば良いですよね。
「継承」は一番結びつきが強い関係になるので、このようなケースではクラス間を「関連」で結んであげるのが推奨されています。
これを「委譲」と呼んでいます。
それでは、先ほど例示したクラス構成を「継承」から「委譲」に変えてみようと思います。
「楽楽Payポイント算出クラス」では、利用する「ポイント算出クラス」をインスタンス変数として宣言し、それぞれのメソッドから呼び出すだけです。
「委譲」を選択しても、次のように楽楽Pay固有のポイント算出処理を加えることができます。
public class 楽楽Payポイント算出クラス {
// ポイント算出クラスとの関連(委譲)
private final ポイント算出クラス ポイント算出 = new ポイント算出クラス();
public int 通常時のポイントを算出する(BigDecimal 購入金額) {
// 固定で10ポイントをプラス
return ポイント算出.通常時のポイントを算出する(購入金額) + 10;
}
public int キャンペーン時のポイントを算出する(BigDecimal 購入金額) {
// 同様に固定で10ポイントをプラス
return ポイント算出.キャンペーン時のポイントを算出する(購入金額) + 10;
}
}
このようにすることで、継承のときに起きた問題は解消されました。
また、今後の変更に対する影響も受けにくくなることと思います。
熟練エンジニアの間では、継承に対して疑問視や危険視する見方もあります。
筆者も継承による密結合を避けるために、継承よりも委譲を使用するように心がけています。
ここぞというときにだけ、継承を使用するようにしています。
「継承は悪だ!」という意見をよく耳にしますが、そこまで否定するつもりはありません。
アプリケーションアーキテクチャによる基本方針や利用するフレームワークのお作法に沿って、継承をどうしても使用する場面はあるかと思います。
ただ、継承のメリットやデメリットを理解したうえで、注意して扱わないと危険が伴うということだけは忘れてはいけませんね。
最後に
#近年、モノリシックなシステムからマイクロサービス化の動きがよく散見されます。
大きなひとつのシステムをマイクロサービスに分離したからと言って、本当にそれで幸せになれるのでしょうか。
逆に、マイクロサービス間のつながりが「密」に行われてしまうことで、個々のマイクロサービスの独立性も損なわれ、結果としてモノリシックなシステムよりも複雑怪奇なシステムが誕生してしまうのではないかと危惧しております。
今回はソフトウェアの結合度について、クラス間の関係性を中心にお話させていただきましたが、このようなマイクロサービスアーキテクチャを策定するうえでも同じことが言えると思います。
「結合度」だけで解決できない問題や課題も多くありますけど、いつの時代になってもシステムやソフトウェアの関係性は「疎」であるのが一番ですね。
うまくまとめられませんでしたが、最後までご覧いただき、ありがとうございました。