この記事は?
ことみん Advent Calendar 2022 5日目の記事です!
この記事は Object-Oriented Conference での『オブジェクト指向のその前に - 凝集度と結合度』という発表のスライドを読み、自分でまとめながら勉強したときのメモです。(今年の2月ぐらい)
オススメされて読んだスライドですが、とても分かりやすかったです。
この記事を読み始めた皆さん、ここまでで大丈夫なので実際のスライドを見てみてください。
勉強メモ
凝集度
- 凝集度は高いか低いかで表現する
- 関連性が無い処理が一つの関数内に実装されている→最悪、凝集度が低い(偶発的凝集)
時間的凝集
- 状況に応じて実施しなければいけない
- 時間的凝集の関数内では、機能的凝集の関数を実行するだけにする
func 時間的凝集な関数() (*Config, *DB, *Logger) {
// 初期化処理など、特定の時間に実行される複数の処理がまとまっている
// 以下の処理は機能やデータにおいて関係がない
// また、実行順序に意味はない
config := loadConfig()
db := connectDB()
logger := getLogger()
return config, db, logger
}
// 時間的凝集の関数は、このように具体的な処理を書くのではない
// 極力機能的凝集の関数を実行することに徹するべきである
func ダメな時間的凝集の関数() (loggFileName string, dbSource string) (*Config, *DB, *Logger) {
// Configの読み込み(省略)
// DBへの接続(省略)
// ログの初期化(省略)
return config, db, logger
}
- 凝集度を理解していると、新しいアプローチを適切に理解出来る
- React HooksのuseEffectの話
論理的凝集
ユースケースが異なるが、処理が似ている実装をするときに気をつけなければいけない
最初の想定
func usecase1() {
a()
b()
c()
d()
}
↓「usecase2が必要になりました。 c()はusecase1のときだけ実行するようにします。」
func usecase1and2(isUsecase1 bool) {
a()
b()
// usecase1のときだけ実行
if isUsecase1 {
c()
}
d()
}
- これはやりがちでよくないことと自覚できてないことが多い
- 私もパッと見は良さそうって思った
なぜよくないか
- 2つのユースケースだけじゃなくて、これが3つ4つと増えるとそのモジュールはフラグだらけになる
- 複数のユースケースを表現する関数は、他のユースケースへの影響がある関数になってしまう
- 例えば、Usecase1を変更した結果、Usecase2,3,4にバグが発生する可能性があるとか
他のユースケースへの影響が大きくなってくると、凝集度が低くなってよくない
→影響範囲の理解が難しくなる
論理的凝集を回避する方法
- ユースケースごとに関数を定義する
- usecaseを表現する関数が、論理的凝集から時間的凝集になる
usecase1and2
を2つに分ける
func usecase1() {
a()
b()
c()
d()
}
func usecase2() {
a()
b()
d()
}
ユースケース関数を呼び出すmain関数
func main(){
// 1を使う時
usecase1and2(isUseacse1: true)
// 2を使う時
usecase1and2(isUseacse1: false)
}
上記は呼び出し側がusecase1か2かを実行時点で理解していることを意味する
それなら以下で良い
func main(){
//1を使う時
usecase1()
//2を使う時
usecase2()
}
Usecaseを再利用したくなる場合
- 再利用したくなる理由
- 時間的凝集の関数に詳細な実装を書きすぎているから
- 例) a〜dに関連する処理が20行ぐらいずつあるとか
- 実行順序を表現することをDRYにしたいから
- 例)
a()
の前にb()
を呼び出すように変更すると、usecase1と2の両方を書き直さなければいけないとか
- 例)
- 時間的凝集の関数に詳細な実装を書きすぎているから
多くの場合は論理的凝集を回避した方が正解
- 順序に意味がある場合は
- 順序が変わる場合はほとんどおこらない
- 順序が変わっても、その場合は機能しなくなるので最低限の動作確認で気がつく
論理的凝集を回避するメリット
- 論理的凝集を回避しつつ、実装詳細を機能的凝集として分離すると、ユースケースを表現するレイヤーから条件分岐が消える
-
usecase1and2
からusecase1
とusecase2
にしつつ、a()
やb()
をメソッドで分離すると、usecase1and2
からif isUsecase1 {}
の条件分岐が消える
-
- ↑の状態になると、ユースケースで必要な処理だけが書かれるため、可読性が大幅に向上する
- こういう実装みたことあるかも
- また別の類似したユースケースが生まれた場合に、他のユースケースからコピペで作れるようになる
- 単一責任になる
- 単一のユースケースだけを責務に持つ
-
usecase1
だけの責務を持つ、usecase2
のことは考えなくて良い
-
- 機能を時間軸のどこに配置するかだけを責務として持つ
-
a()
やb()
のどちらを先に実行かを考えるだけ
-
- つまり、対象の実行順序が変わった時だけユースケースの関数を変更する
-
usecase2
のときだけa()
の前にb()
を追加する変更をするだけで良い状態
-
- 単一のユースケースだけを責務に持つ
論理的凝集のまとめ
- ユースケースが異なるが、処理が似ている実装をするときに気をつけなければいけない
- 本当にそのユースケースを再利用する必要があるのかを考えなくてはいけない
- 論理的凝集を回避するとユースケースの凝集度が上がる
- 他のユースケースに影響を与えなくなる
- ユースケースから条件分岐がなくなる
- 言語やプラットフォームに限らず発生する
凝集度まとめ
- すべて機能的凝集にすればいいという単純な話ではない
- 機能的凝集を目指す方がいいですが、現実的にはそうはならないことがある
- レビューの際にはなぜ現在は機能的凝集ではなく、時間的凝集や論理的凝集を選んだかを説明できることが大切
→なぜこの実装をこう書いたのかを自分で説明できると良い。プルリクに書いてもいいかも
結合度まとめ
結合度が低いと良い関数であり、高いと悪い関数であり、結合度にはランクが存在している
この記事が例も乗っていて分かりやすそう
感想
- 時間的凝集や論理的凝集の箇所に出てきた例をみて「この辺で使われていそう」だなと考えつつ読めた
- 関数の分割の仕方でこの関数は何をする関数なのか、をちゃんと考えて説明出来るようにして実装したいなと思った
- 設計に時間を使うって何を考えたらいいのかあまりイメージがついてなかったけど、こういうことなんだなとちょっと分かったような気がしてる
- 「いいぞ」と言われるスライドを自分でまとめてみる良い機会になったと思う、やってよかった
- 週1,2回、2,30分ずつに分けて読むことで何度も読み返すから理解がしやすかったし、まとめるのもそんなに大変じゃなかった(と思う)
おわりに
この記事を読み始めた皆さんはここまでで大丈夫なので実際のスライドを見てみてください。
ここまで読んだ人はいないはず。