はじめに
Python
を用いて解説しますが、どの言語にも通底するお話を書きます
for
ネスト、無くしたいですよね。
コードレビューやCodeClimateでよく「可読性が低い」として改善を求められますね。
しかし、直せと言われたって、直すアイデアはすぐには出てこない。
そこで、汎用性のあるリファクタリングアイデアを2つ持ってきましたのでご覧ください
そもそもなんでfor
ネストは読みにくいの?
持論ですが、恐らくfor
のネスト自体は悪くないんですよ。
例えば$1000 \times 1000 \times 1000$の立方体空間内のRGB値の合計を求めるスクリプトを、あえてネストして書くと
for x in range(1001):
for y in range(1001):
for z in range(1001):
r, g, b += get_rgb(x, y, z)
反発が出そうなのであえて「読みやすい」とは言いませんが、しかし言うほど読みにくくないですよね?
for
がネストされているものの、実質的な処理は3つ目のforにしかありませんから、「ああ、考えられうるxyz
の組み合わせでイテレーションしてるんやね」とただちに分かるのです。
ではこれは?
設定はなんでもいいですが、例えば個別指導の教師を割り当てるプログラムとしましょう
for student for students:
if not student.is_coming_today():
continue
all_allocated = True
for subject in student.select_subjects_today():
allocated = False
for teacher in teachers:
if not teacher.is_coming_today():
continue
if teacher.is_available(subject):
student.allocate(subject, teacher)
teacher.allocate(subject, student)
allocated = True
break
if not allocated:
all_allocated = False
if not all_allocated:
student.tell_not_all_allocated()
これは別に解読不可能だからはないものの、どこか心理的に「読みたくないな」と思いますよね。
前者と後者の違いは:
- 最後だけでなく途中の
for
文にも処理がある - フラグ処理や
break
/continue
によるループ制御処理が話をややこしくしている - 複数の責務が混在している
ことではないでしょうか。
人間がfor
文を読むとき、「どのfor
が重なっているのか」「今はどこのfor
の話か」を念頭に置きながら読んでいると思います。
- 1について、読む場所によって「どの
for
が重なっているのか」がチラチラ変わったら、脳が疲れ、「読みたくない」という心理を生む。 - 2について、
break
/continue
に遭遇する度に「今はどこのfor
の話か」が切り替わるので、脳が疲れ、「読みたくない」という心理を生む。 - 3について、ただでさえ面倒な構文をもっと肥大化(fat)にされたら、脳が疲れ、「読みたくない」という心理を生む。
ことが言えるかと思います。
案
これを解消するべく、リファクタリング案を2つもってきました。
forの中身を関数化する
常套手段ですね。すぐ思いつくんじゃないですか。
前述の問題点1, 2を読みやすくする手段ですね。
しかし不慣れだと、「このfor
の中にあるbreak
/ continue
/ フラグどうすればいいんだろ…」と迷うかもしれません。
break
、continue
はそのまま保持し、フラグ変数は返り値として返すことで解決します。
こちらが冒頭の例です。
即興につき関数名はあまりよろしくないかもしれませんのでご了承を。
def allocate_students_teachers():
for student in students:
if not student.is_coming_today():
continue
all_allocated = _allocate_teachers_for_student(student)
if not all_allocated:
student.tell_not_all_allocated()
def _allocate_teachers_for_student(student):
all_allocated = True
for subject in student.select_subjects_today():
allocated = _allocate_teacher_for_student_subject(student, subject)
if not allocated:
all_allocated = False
return all_allocated
def _allocate_teacher_for_student_subject(student, subject):
for teacher in teachers:
if teacher.is_available(subject):
student.allocate(subject, teacher)
teacher.allocate(subject, student)
return True
return False
ちゃんとコメントをつけてあげれば、可読性がけっこうよくなったと思えそうじゃないですかね。
しかし、デメリットとして、得たいデータによっては、引数・返り値が多くなり、単純な記述量が多くなることが指摘されます。
先に候補者リストを作って一重for
にする
自分で思いついた手法です。根本的解決にはなりませんが、読みやすくはなります。
全探索をしないといけないときに有効です。
例えば立方体空間内で、何らかの条件に合致した座標について、なんかするとき(設定は適当です)
for x in range(1001):
if not nanka_joken_x(x):
continue
for y in range(1001):
if not nanka_joken_y(y):
continue
for z in range(1001):
if not nanka_joken(z):
continue
nanka_shori0(x, y, z)
nanka_shori1(x, y, z)
if nanka_joken0(x, y, z):
nanka_shori2(x, y, z)
このように
- 探索する点についてのリスト(イテレーター)を作り、
- そのリストについて一重
for
で処理部分を記述
をしてみることができます。
#処理部分
for x, y, z in compute_candidates():
nanka_shori0(x, y, z)
nanka_shori1(x, y, z)
if nanka_joken0(x, y, z):
nanka_shori2(x, y, z)
#候補者リスト
def compute_candidates():
return ((x, y, z)
for x in range(1001)
if nanka_joken_x(x)
for y in range(1001)
if nanka_joken_y(y)
for z in range(1001)
if nanka_joken_z(z))
このように、for
ネストの元凶である探索部分を先に済ませ、候補者リストcompute_candidates()
とし、処理部分は候補者リストについてforeach
すると。
見ての通り、根本的な解決にはなっていません。compute_candidates()
内は3重ループのままです。
しかし、compute_candidates()
内は「候補を探索しているんやね」と念頭に置けるので意識的には読みやすく(候補者リストを作るだけであればfor
内は複雑になるとは考え難い)、本質部分ともいえる処理部分は各段に読みやすくなりました。
処理部分を狙った場所(関数化しない場合もする場合も)に配置したいときに一考の価値があるのではないでしょうか。
最後に
みなさんにもリファクタリング案があれば、ぜひお教えください。
いいね頂けると泣きながら喜びます><