ループの分離
しばしばループ処理の中で複数の処理をしたくなりますが、果たして本当に必要なのでしょうか。
いくつかのサンプルコードを見ながらメリット・デメリットについて考えてみましょう。
まずは 1回のループ処理の中で複数の処理 を実行する場合です。
コード自体はとてもシンプルである slice を for 文でイテレートさせるときに年齢の合計と給料の合計を求めるコードです。
type human struct {
Age int
Salary int
}
func main() {
people := []human{
{Age: 22, Salary: 150000},
{Age: 28, Salary: 250000},
{Age: 30, Salary: 500000},
}
averageAge := 0
totalSalary := 0
for _, p := range people { // このループでは年齢の合計と給料の合計の2つの異なる処理をしている
averageAge += p.Age
totalSalary += p.Salary
}
averageAge = averageAge / len(people)
fmt.Println(averageAge)
fmt.Println(totalSalary)
}
パッと見違和感を感じない時もあるかと思います。
ただ、1つのループの中で関係のない計算が2つ実行されています。そのため給料の合計を算出することと関係のない年齢の合計の処理まで理解する必要があります。まぁ、これぐらいシンプルなコードであれば問題ないとは思います。問題になるのはさらに複数の処理を実行する時、コードが複雑になった時などでしょう。
【メリット】
- 1つのループ処理で済む
【デメリット】
- 1つのループ処理の中で実行したい処理が複雑になる
- 大規模なコードになると可読性が著しく低下する
次に、ループ処理を分離
してみましょう!
func main() {
people := []human{
{Age: 22, Salary: 150000},
{Age: 28, Salary: 250000},
{Age: 30, Salary: 500000},
}
averageAge := 0
for _, p := range people { // 年齢の合計のみ
averageAge += p.Age
}
averageAge = averageAge / len(people)
totalSalary := 0
for _, p := range people { // 給料の合計のみ
totalSalary += p.Salary
}
fmt.Println(averageAge) // 26
fmt.Println(totalSalary) // 900000
}
エンジニアによっては、同じようなループ処理が2回実行されるので気持ち悪いと感じる人もいると思います。(シンプルコードであるが故に、なぜ分けるのか?と思う人もきっといるはず。)
また、ループ処理を複数回実行することにより速度を気にする人もいると思います。
【メリット】
- 1つのループで1つの責務を担うことができる
- 他のループ処理の実行内容を知る必要がない
【デメリット】
- 同じループ処理を複数回実行する必要がある
- 場合によっては処理速度に影響がでる?
処理速度に関して懸念される人もいるかもですが、実際、数千、数万ぐらいであればほとんど処理速度に違いは出ないと思います。実行する処理の複雑度によりますが、まずは責務をしっかり分けた上で、ループ処理がボトルネックになるのであればその段階でループを減らすことを考えてみれば良いのではないでしょうか?
そう考えると、デメリットはほぼ無くなります。
さらに 関数の分離
まで行うとさらに見通しが良くなります。
func main() {
people := []human{
{Age: 22, Salary: 150000},
{Age: 28, Salary: 250000},
{Age: 30, Salary: 500000},
}
fmt.Println(averageAge(people)) // 26
fmt.Println(totalSalary(people)) // 900000
}
func averageAge(people []human) int {
averageAge := 0
for _, p := range people {
averageAge += p.Age
}
return averageAge / len(people)
}
func totalSalary(people []human) int {
totalSalary := 0
for _, p := range people {
totalSalary += p.Salary
}
return totalSalary
}
関数の分離
をすることで、今後同じような処理の場合は使い回すことができるようになりました。
golang の場合はさらにレシーバーを使うことができます。(golangではクラスという概念がないため、オブジェクト指向言語で例えると 関数群をクラスへ集約
していることになります。)
type people []human
func main() {
p := people{
{Age: 22, Salary: 150000},
{Age: 28, Salary: 250000},
{Age: 30, Salary: 500000},
}
fmt.Println(p.averageAge()) // 26
fmt.Println(p.totalSalary()) // 900000
}
func (p people) averageAge() int {
averageAge := 0
for _, human := range p {
averageAge += human.Age
}
return averageAge / len(p)
}
func (p people) totalSalary() int {
totalSalary := 0
for _, human := range p {
totalSalary += human.Salary
}
return totalSalary
}
これによって、 averageAge()
メソッドと totalSalary()
メソッドは people型をレシーバーとする関数として使うことができるようになります。より関数の責務が明確になりコードの可読性が高まりましたね。averageAge()
とtotalSalary()
の内部的な実装を読まずとも main() 内部を読むだけで理解できるコードになっていますね。
func main() {
p := people{
{Age: 22, Salary: 150000},
{Age: 28, Salary: 250000},
{Age: 30, Salary: 500000},
}
fmt.Println(p.averageAge()) // 26
fmt.Println(p.totalSalary()) // 900000
}