対象
今回の記事の対象は初学者~中級者、一人でクラスを書くことを始めてきた人に意識してほしいことを綴ってみます。
なので、関数とはなにか、クラスとはなにか、と言うのがわかってる人が対象です。
なお、私が書籍を読んで、経験をもとにしたまとめなので、間違いやご意見あればいただけると助かります。
クラスの結合度
書籍や資格によって厳密な言い方は異なりますが、結合度とは以下の通りになります。
上から下にいくにつれて「結合度が弱く」なっていきます。一番上の結合が結合度(=依存度)が高いです。
- 内容結合
- 共通結合
- 外部結合
- 制御結合
- スタンプ結合
- データ結合
- メッセージ結合
前述の通り結合の名前は違えど言っていることは変わりないので読んでみてください。
何でもかんでも結合度が弱ければ良いというわけでは無いですが、一般的には結合度が低いほうが可読性は高まります。
本編
内容結合
隠すべきプロパティがパブリックで宣言されているクラスを指します。
これは、外部から隠蔽すべきプロパティが触れてしまうので余計な不具合を生みます。
また、メソッドを呼び出す順序が限定されることもこれにあたります。
計算をしたい、という理由で
Caluculator.calc();
とすれば良いだけかもしれませんし、本来これがベストですよね。
でもこの計算処理に「準備」、「後処理」が必要だったらどうなるでしょうか。
Caluculator.parameter = 42;
Caluculator.prepare();
Calucurator.calc();
これが結合度が高すぎる状態です。これは、
Caluculator.parameter = 42;
Calucurator.calc();
Caluculator.prepare();
これでは「計算」のあとに「準備」なので用途としては不正です。
本来関数にこのような「順序」があっては結合度が高すぎるのでいけませんよ、ということですね。
この場合何がベストか、というのはこのcalc()以外の関数及びパラメータをプライベートにして外部からのアクセスを不可にすることで「順序を意識しない」ということが可能になりスッキリします。
順序に依存しているパターンですね。
共有結合・外部結合
この結合は・グローバル変数の多様・外部デバイス、共有メモリなど、コードから誰でも見えるリソースの利用がされている状態を指します。
ただ、これは「健全な共有・外部結合」があり、それはDBなどデータの永続化が目的の場合が該当します。
オブジェクト間の値渡しを目的としての利用がNGなのです。
外部結合と共有結合の違いとしては何を渡すかによります。
データ構造を持つ場合、つまりグローバル変数がなにかのオブジェクトの場合は「共有結合」、単一のデータの場合を「外部結合」と呼びます。
この違いは「スタンプ結合」「データ結合」も同じです。
関数の呼び出しは引数、返り値を使うということが基本です。これを行わずに引数の代わりにできるのがグローバル変数ですよね。それは、この基本から外れるアンチパターンなのでNGです。可読性、頑健性が損なわれます。
制御結合
制御結合は、「何かを決定するフラグを渡して、呼び出し先の挙動」を変える、つまり、前述の基本に準じて引数が真偽値型だった場合、それを利用して条件分岐をする際に発生します。
この条件分岐の仕方がキーポイントです。
不必要に分岐の粒度が大きい状態または条件分岐間で動作の関連性が薄い場合を指します。
if (boolFlag: Boolean) {
a = true
b = false
c = "AAA"
} else {
a = false
b = true
c = "AAA"
}
この処理が制御結合状態の一つで、無駄なところが多いですよね。
a = boolFlag
b = !boolFlag
c = "AAA"
のみで済む話なのです。またこれには「動作を理解するために関係ない条件分岐の内容も読まないといけないリスク」も存在しているためNGです。
また、
if (boolFlag:Boolean) {
// DB更新に関する処理
} else {
// 画面描画に関する処理
}
これも制御結合がされている状態で「条件分岐間で関連性が薄い場合」にあたります。関連性がない分岐はやめましょう。
これらから言えるのは「条件分岐はなるべくしないようにしましょうね」という結論です。
スタンプ結合・データ結合
引数や返り値を用いた結合の中で一番結合度が弱い状態です。具体的には、値の受け渡しに引数や返り値を用い、制御結合でない(不要な条件分岐がない)状態を指します。
なお、スタンプ結合は共有結合のようにオブジェクトの状態、データ結合は単一な変数の場合の結合を指します。
この「値の受け渡しに引数や返り値を用いている」状態がベストというわけでもなく、そこに適切かどうかがキーポイント。
まずはNG例を紹介します。
fun showUserProfile(userName: String, profileImageUrl: String) {
// なんか処理
}
ちゃんと引数が使えていますし、「見たところ」、同じユーザのプロフィールを表示するように思われます。
しかし、以下のデメリットがあります。
showUserProfile("hogehoge.com", "kona")
showUserProfile(user1.userName, user2.profileImageUrl)
前者は第一引数と第二引数が入れ替わってしまうパターン。実引数としては正しいですね、両方とも文字列です。しかし、kona
がURLとして認識されてしまうため仮引数としては不具合を生んでしまいます。
また、後者は同じユーザの表示でなくなってしまっています。これも不具合です。
これはデータ結合の弱点ですね。これの回避策として、スタンプ結合が挙げられます。
fun showUserProfile(userModel: UserModel) {
// なんか処理
}
これがスタンプ結合。先程の引数の入れ替わりも、違うユーザが渡ってくることもなくなります。
結合度が低いデータ結合だから良い、というわけではないということがわかりましたね。場合によっては結合度の高いスタンプ結合のほうが良いコーディングになりえます。
メッセージ結合
引数や、返り値がない、かつ、共有結合や外部結合のようにグローバル変数の利用もしていない状態です。
値の受け渡しがないので、値の受け渡しによる不具合がない、ということになります。
果たしてこれが正解なのか、最善なのかというとこれまた違います。
fun updateUser(){
// 更新処理
}
このように引数がないということは、関数に対して何も情報がなく、上記の場合だと「誰の情報を更新すれば良いんだ?」となってしまうんですよね。かといって、
fun updateUser(){
val user = getUser()
UserRepository.user = user
}
とすると、これまた「誰のデータやねん」ってなりますし、関数の中のボリュームが多くなり、知らぬ間に内容結合相当の結合度が発生してしまいます。(処理の順番に依存したり。)
むやみに引数も返り値も削除してしまうとその値を渡すために、内容結合、共通結合、外部結合が発生する危険性もあります。
最後に
今回は関数、クラスの「結合度」についてお話しました。
結合度が高いと、処理の順序に依存したり、グローバルな変数に依存したりとデメリットが大きいです。引数や返り値を使いましょう。
かといって、引数や返り値を使ったからと言って、データ結合のように単一の変数が引数の場合は引数の順序などのせいで不具合が生じることもあります。
じゃあ、引数や返り値をなくせば結合度はないじゃん!と極端な手段に走ってしまうとそれも違ってきますよね。
結合度が高い(=依存性が高い)ことが正しいわけでは無いのはもちろんですが、結合度が低い(=全く依存しない)のも正しいわけではありません。
適切にコーディングしていきましょう。
個人的にはスタンプ結合あたりが良いかな、と落ち着きました。
以上です。読んでくださってありがとうございました。