😆

モジュール結合度を理解する

2020/11/27に公開

みなさんこんばんわ。ソードアート・オンラインが放映されてからもう8年ぐらい経ってると気づいて絶望してます。

さておき情報処理国家試験やプログラミングの概念的なものを学んでいると、モジュール結合度という概念が出てくることがあります。
これはモジュール間の結合度は疎結合であるべきで~、みたいな話です。

画像

うんうんそれな~。という感想を持ちますがいくつかに分類されている結合度を具体例を持って説明できるかと言われると、多くの人はできないのではないかと思います。

そこで今回は
・それぞれの結合度についての具体例(結合度:高->低)
・結局結合度とどう付き合うべきか
というお話をしようかと思います。
**ちなみにモジュール結合度における具体例は様々存在し、絶対的な正解が存在しないことはご了承ください。**自分も改めて調べてマジで混乱しました。

内容結合(結合度:高)

あるモジュールが別モジュールの内部実装に依存している状態を指します。モジュール同士が一番密に結合した状態です。ソーシャルディスタンスを保ちましょう。

public String getEmployeeFullNameById(int employeeId) {
   Employee emp = employeeRepository.findById(employeeId);
   return emp.getFirstName() + emp.getLastName();
}

上記のソースコードは一見getterを通してプロパティにアクセスしているので、実装に依存していないように思えますが違います。例えば仕様変更でEmployeeクラスにmiddleNameというプロパティが追加された場合、getEmplopeeFullNameByIdというフルネームを取得するこのメソッドは名前どおりの役目を果たせません。

public String getEmployeeFullNameById(int employeeId) {
   Employee emp = employeeRepository.findById(employeeId);
   return emp.getFullName();
}

ではどうするかというと、Employeeクラス側にフルネームを取得するためのメソッドを設けてあげるのが正解かなと思います。こうすることでemployeeにおけるフルネームというのが何を指すのかというのを、呼び出す側は気にしなくて良くなります。

共通結合

ある上書き可能なグローバル変数を複数のモジュールで共有している状態を指します。

public static RunningMode ServerRunnigMode = RunningMode.DEVELOPMENT;

public void executeExternalApi() {
 String url = SampleClass.ServerRunnigMode == RunningMode.DEVELOPMENT
   ? "https://development.local/execute" : "https://production.local/execute";

 new RestTemplate().postForObject(url, null, null);
}

これはわかりやすいですね。可変なグローバル変数はいつどこで変更されるか分からないのが問題なのです。

public void executeExternalApi() {
 String url = SampleClass.serverRunningMode == RunningMode.DEVELOPMENT
   ? "https://development.local/execute" : "https://production.local/execute";

 new RestTemplate().postForObject(url, null, null);
}

public void runInDevelopMode() {
 // privateな変数
 SampleClass.serverRunningMode = RunningMode.DEVELOPMENT;
}

このように
・そもそもグローバル変数を参照しない
・変数をグローバルに公開せず、外部から変更を受け付ける際はメソッド経由とする
といった対応方法があると思います。

外部結合

外部ライブラリやIOデバイスへの接続手段を複数のモジュールで共有している状態を指します。

public int getServerStatus() {
 // HttpClinetは外部ライブラリのクラス
 HttpClient client = HttpClient.newHttpClient();
 HttpResponse<String> response = client.send(
     HttpRequest
         .newBuilder(new URI("http://development.local/"))
         .GET()
         .build(),
     BodyHandler.asString()
 );
 return response.statusCode();
}

例えば上記のコードですが、サーバーの状態を取得するためにHTTPリクエストを飛ばしています。このgetServerStatus()を各所から参照するだけなら全然OKです。しかし、HttpClientという外部ライブラリに含まれるクラスを様々な場所で参照している場合は、外部ライブラリに依存してしまっていると言えるでしょう。

対策として外部ライブラリやIOデバイスへの依存を局所化するのが好ましいです。正直アプリを開発している以上それらへの依存をゼロにすることはできませんから。

制御結合

モジュールが実行する命令を外部のデータによって制御している状態を指します。

public void registerUser(User user) {
 if(user.getCountry() == Country.JP) {
   // 国内ユーザーの登録処理
   return;
 }
 // 海外ユーザーの登録処理
}

registerUser()メソッドから実際の登録処理を呼び出す想定ですが、国ごとに違うであろう登録処理をregisterUser()メソッドが知らなければいけないことが問題となります。

public void registerUser(User user) {
 UserRegisterService userRegisterService = user.getCountry() == Country.JP
     ? this.japaneseUserRegisterService() : this.foreignUserRegisterService();

 userRegisterService.register(user);
}

このように登録処理の詳細は別クラスに隠蔽し、registerUserクラスはどの国のユーザー登録処理においても同じ処理を実行するだけにするのが対策となります。

スタンプ結合

モジュールに引数として渡されたオブジェクトの一部しか使わない状態を指します。

public String findUserEmailAddress(User user) {
 return UserEmailManageService.findByUserId(user.getId());
}

userIdだけあれば事足りそうですが、Userインスタンスを渡してしまっています。別に問題ないように見えますがUserインスタンスのデータ構造に依存してしまっているのが問題です。Userクラスからidプロパティが消えた時点で、上記のコードはfindByUserId()の行でエラーが発生します。

対策としては
・必要な値を全て個々の引数として渡す
・引数に必要な機能を定義したinterfaceだけ渡す
というリファクタリングを施せば良いです。

public String findUserEmailAddress(int userId) {
 return UserEmailManageService.findByUserId(userId);
}

とはいえ引数で渡したインスタンスの一部プロパティしか使わないというケースはよく起こりえます。それを無理にキレイにしようとすると逆にコードが複雑になる恐れがあるので、ある程度の妥協が必要だと思います。

具体的には関数に渡す引数が4個以上になった場合は、引数を構造体にするのが良さそうです。

データ結合(結合度:低)

参照するモジュールで使うデータだけを、個々の引数で受け渡ししている状態を指します。引数が多くなるとモジュールを参照するために、自分が必要じゃないデータを渡す必要があったりします。

public findBuyableProduct(int userId, CountryType countryType) {
 ResourceQuery query = new ResourceQuery();
 query.addIntFilter(userId);
 query.addStringFilter(countryType.value());
 return productRepository.findByQuery(query);
}

スタンプ結合の対極ともいえるデータ結合ですが、先述したとおり引数が4個以上になった場合は、引数を構造体にするのが良さそうです。

結局結合度とどう付き合うべきか

これまで色々説明してきましたが、**こんな複雑な指標と全力で向き合う必要は無いと思います。**無視していいわけではないですが。

理由としては全ての処理をデータ結合にすることはできないと思うからです。また、外部結合の項で説明したとおりアプリケーションはどうあがいてもIOデバイスや、外部ライブラリに依存する箇所が存在するためです。

ただ、知っていて高い結合度を許容するのと知らずに高い結合度のモジュールを産み出すのでは、後処理やメンテナスをする人へのフォローの仕方が変わります。そういう意味ではこういった概念を知っておくのもいいのかなと。

ときどき「引数なんか多いなぁ」とか「ifの条件分岐による処理内容の変化が辛いなぁ」とか思ったときに結合度の話を思い出して軽く悩むくらいがちょうどいいんですよ。きっと。

雑記

最近ちょくちょくアウトプットしてますが、承認欲求を10%ぐらいで書くとページビューが少なくても気にならないことに気づきました。

Discussion