1.5 コードを関数・クラス・モジュールに整理する利点
コードの整理とは、ひとつながりの長いコードを、意味のあるまとまり——関数・クラス・モジュール——に分けることである。本ページでは、なぜわざわざ分けるのか、3つのまとまりがそれぞれ何を担うのか、そして「分けすぎ」の落とし穴までを見ていく。
なぜコードを分割するのか
長い1本のコードを分割する理由は、再利用・可読性・テスト・変更の局所化という4つの利点がまとめて得られるからである。逆に言えば、分けない長大なコードはこの4つすべてで不利になる。
数百行の処理が1か所にべったり書かれている状態を想像してほしい。次のような困りごとが起きる。
- 再利用できない。 同じ処理を別の場所でも使いたくても、塊から切り出せないので、コピーして貼り付けるしかない。コピーが増えると、後で直すときに全部を直して回るはめになる。
- 読めない。 どこからどこまでが1つの仕事なのか、境界が見えない。全体を頭に入れないと一部すら理解できない。
- テストできない。 1.3 で見たように、テストは「ある入力にこの結果」を確かめる。だが処理が分かれていないと、途中の一部分だけを取り出して確かめられない。
- 変更が怖い。 ある箇所を直すと、無関係に見える別の箇所が一緒に壊れる。影響範囲が読めない。
分割すると、これらが裏返る。意味のあるまとまりに名前をつけて切り出せば、その名前で再利用でき、名前が読み手の道しるべになり、まとまり単位でテストでき、変更の影響もそのまとまりの中に閉じ込められる。この「変更の影響を狭い範囲に閉じ込める」ことを 変更の局所化 と呼び、大きなコードを安全に育てるうえで特に重要だ。
関数・クラス・モジュールはどの粒度を担うか
3つは「まとまりの大きさ」が違う。関数がいちばん小さく「ひとつの処理」を、クラスが「データとそれを扱う処理のセット」を、モジュールが「関連する関数やクラスを束ねたファイル」を担う。小さいまとまりを大きいまとまりが包む、入れ子の階層になっている。
flowchart TD
M["モジュール (ファイル)
関連する道具を束ねる"] --> C["クラス
データと処理をひとまとめに"]
M --> F1["関数
ひとつの処理に名前をつける"]
C --> F2["メソッド
そのクラスに属する関数"]- 関数(メソッド) — いちばん小さい単位。「入力を受け取り、ひとつの仕事をして、結果を返す」ひとまとまりに名前をつけたものだ。クラスに属する関数を特にメソッドと呼ぶ。「何をするか」を1つに絞るのが基本である。
- クラス — 関連するデータと、それを扱う関数(メソッド)を1つに束ねたもの。たとえば「機器」を表すクラスなら、機器名や IP といったデータと、「設定を生成する」「状態を確認する」といった処理を一緒に持つ。バラバラの変数と関数が散らばるのを防ぎ、「このデータはこの処理で扱う」という関係を1か所にまとめる。
- モジュール — 関連する関数やクラスを束ねた、1つのファイル(または複数ファイルのまとまり)。Python では
importで他のファイルから呼び出せる。「ネットワーク機器を扱う道具一式」のように、テーマごとにファイルを分ける単位だ。
階層が分かれているのは、まとまりにも「ちょうどよい大きさ」があるからだ。小さな処理は関数に、関係するデータと処理はクラスに、テーマで関係するもの同士はモジュールに——と粒度ごとに器を分けることで、どこに何があるかが探しやすくなる。
整理前と整理後の最小例
「機器名のリストを受け取り、有効な接頭辞のものだけ大文字にして表示する」処理を考える。まず、すべてが1か所にべったり書かれた整理前。
devices = ["router1", "switch1", "router2"]
result = []
for d in devices:
if d.startswith("router"):
result.append(d.upper())
for r in result:
print(r)短いうちは読めるが、「絞り込み」「大文字化」「表示」という3つの別々の仕事が混ざっている。絞り込み条件だけ別の場所でも使いたい、となると切り出せない。仕事ごとに関数へ分けると、次のようになる。
def filter_by_prefix(devices, prefix):
return [d for d in devices if d.startswith(prefix)]
def to_upper(devices):
return [d.upper() for d in devices]
def show(devices):
for d in devices:
print(d)
# 組み立てて使う
routers = filter_by_prefix(["router1", "switch1", "router2"], "router")
show(to_upper(routers))行数はむしろ増えた。しかし filter_by_prefix は他の場所でも再利用でき、それぞれを単体でテストでき、「絞り込みの条件を変えたい」ときは filter_by_prefix の中だけ直せばよい。さらに関数名が「ここで何をしているか」をそのまま説明してくれる。これが整理の見返りである。
整理しすぎる弊害
整理は多ければよいわけではない。分割しすぎると、かえって読みにくく変更しにくくなる。「短いから」と1〜2行の処理まで何でも関数に切り出すと、ひとつの動きを追うのにいくつものまとまりを行き来することになり、全体像がつかめなくなる。これを過度な細分化という。
判断の目安は「そのまとまりに、意味の通る名前をつけられるか」「2回以上使うか、あるいは独立してテストしたいか」だ。名前がうまくつけられない切り出しは、たいてい切る場所を間違えている。1回しか使わずテストの必要もない数行を、無理に関数へ追い出す必要はない。整理の目的は分けること自体ではなく、再利用・可読性・テスト・変更の局所化を得ることである。目的に照らして、ちょうどよい粒度を選ぶ。
Note
「ファイル(モジュール)はとにかく細かく分けたほうがきれい」という思い込みも、過度な分割の一種である。関係の薄い機能を1ファイルに詰め込むのは良くないが、いつも一緒に使う密接な処理を別々のファイルにばらまくと、追いかけるのが大変になる。基準は行数ではなく「関係の強さ」だ。強く関係するものは近くに、関係しないものは別の器に置く。
本ページはCisco DevNet Associate(200-901) Exam Topics v1.1を学習範囲の根拠として参照。文章・図表はすべて新規作成。