はじめに
2020/02/16(日)に「Object-Oriented Conference」というカンファレンスが行われました。
そこで聴講した「オブジェクト指向のその前に — 凝集度と結合度」というセッションに感銘を受け、自分でも凝集度についてまとめたいと思い、本記事を書きました。
私はiOSアプリエンジニアなのでiOS + Swiftの目線となっていますが、「凝集度」という概念はプログラミング言語に関係なく適用できます。
「凝集度」とは?
英語では「Cohesion」といいます。
日本語でも複数の呼び方があり、IPAでは「強度(Strength)」と呼んでいます。
あるコードがどれだけそのクラス(関数)の責任分担に集中しているかを示す度合いです。
凝集度が低いほど関連のない処理が混ざっていて、高いほど機能が絞られていて望ましいです。
凝集度が高いと何がいいか?
可読性が上がる
1つのことしか行っていない関数は、処理の目的が明確なので可読性が高いです。
関数は「一連の処理に名前を付けること」とも考えられるので、関数名が適切だとさらに読みやすいです。
再利用性が上がる
機能ごとに処理が分割されているので、再利用性が高いです。
凝集度が低い関数は様々な処理が混ざっているので、他で使い回しづらいです。
主な凝集の種類
主な凝集の種類を、凝集度の低順に紹介します。
偶発的凝集(最悪、凝集度が低い)
英語では「Coincidental Cohesion」といいます。
最も凝集度が低い凝集であり、関連のない複数の処理が1つの関数内に実装されている状態です。
func doCoincidentalCohesion() {
// インジケータのアニメーションとラベルの生成処理は関連がない
activityIndicatorView.startAnimating()
nameLabel = UILabel()
nameLabel.backgroundColor = .blue
view.addSubView(nameLabel)
}
論理的凝集
英語では「Logical Cohesion」といいます。
論理的に似ている処理が1つの関数内に実装されている状態であり、引数などをフラグにして処理を分岐させているのが特徴です。
func createNameLabel(isBlue: Bool) {
// ラベルの色を青にするかどうかを引数のフラグで判断している
nameLabel = UILabel()
if isBlue {
nameLabel.backgroundColor = .blue
} else {
nameLabel.backgroundColor = .white
}
view.addSubView(nameLabel)
}
時間的凝集
英語では「Temporal Cohesion」といいます。
特定の時間に実行される処理が1つの関数内に実装されている状態です。
基本的には避けるべきですが、ビューのライフサイクルやメイン関数では避けられません。
iOSアプリ開発の場合、 viewDidLoad()
などが当てはまります。
override func viewDidLoad() {
super.viewDidLoad()
// ラベルの生成中にインジケータを表示している
activityIndicatorView.startAnimating()
nameLabel = UILabel()
nameLabel.backgroundColor = .blue
view.addSubView(nameLabel)
activityIndicatorView.stopAnimating()
}
機能的凝集(最良、凝集度が高い)
英語では「Functional Cohesion」といいます。
単一の処理が1つの関数内に実装されている状態です。
理想的であり、どの関数も基本的には機能的凝集を目指すべきです。
func createNameLabel() {
// 「名前ラベルの生成」という機能のみ持っている
nameLabel = UILabel()
nameLabel.backgroundColor = .blue
view.addSubView(nameLabel)
}
凝集度を高くするには
基本的には機能的凝集を目指すことで凝集度が高くなります。
偶発的凝集
それぞれが機能的凝集となるように関数を分割します。
ここでは「インジケータのアニメーション開始」と「名前ラベルの生成」という単一の機能を持った関数に分割しました。
// 機能的凝集の関数
func startIndicator() {
activityIndicatorView.startAnimating()
}
// 機能的凝集の関数
func createNameLabel() {
nameLabel = UILabel()
nameLabel.backgroundColor = .blue
view.addSubView(nameLabel)
}
論理的凝集
論理的凝集された関数の共通部分を関数に抜き出し、それを呼び出す複数の関数を実装します。
ここでは「名前ラベルの生成」を共通部分の関数として抜き出し、「青い名前ラベルの生成」と「白い名前ラベルの生成」の関数から呼び出しています。
分岐を減らせるので、テストコードが書きやすくなります。
func createBlueNameLabel() {
createNameLabel()
nameLabel.backgroundColor = .blue
}
func createWhiteNameLabel() {
createNameLabel()
nameLabel.backgroundColor = .white
}
private func createNameLabel() {
nameLabel = UILabel()
view.addSubView(nameLabel)
}
時間的凝集
先述した通り、ビューのライフサイクルなど時間的凝集を避けられないことがあります。
その場合、処理をベタ書きするのでなく、機能的凝集された関数を呼び出すのみにするのが望ましいです。
override func viewDidLoad() {
super.viewDidLoad()
startIndicator()
createNameLabel()
stopIndicator()
}
// 機能的凝集の関数
private func startIndicator() {
activityIndicatorView.startAnimating()
}
// 機能的凝集の関数
private func stopIndicator() {
activityIndicatorView.stopAnimating()
}
// 機能的凝集の関数
private func createNameLabel() {
nameLabel = UILabel()
nameLabel.backgroundColor = .blue
view.addSubView(nameLabel)
}
おまけ:参考文献
冒頭で紹介したセッションのスピーカーさんに参考文献を教えていただきました。
発表聞いていただきありがとうございますー!
— そな太@GraphQLはいいぞ (@sonatard) February 16, 2020
あまり近年まとまってる資料はないのですが新しい書籍だとこちらに数ページ記載されていますhttps://t.co/ClFdg1EYtu
古典だとこちらがソースに近い書籍のようですhttps://t.co/u3OMN41MtE
各書籍のリンクを載せます。
おわりに
関数の切り出し方に悩む人は多いと思いますが、この記事が少しでも指標になると嬉しいです。