この記事はNuco Advent Calendar 2022の15日目の記事です。
なぜ「読めるコード」を書くのか
「読めるコード」を書くことで、コードを読んだだけで何がしたいのかが明確にわかるため、修正や機能追加の際にもコードの解読に時間と労力を割かずにすみます。
また、理解を誤ったまま目的からそれた実装をすることを避けられます。
以降紹介するチェックポイントと、ご自分のコードと照らし合わせてクリアできているか確認をしてみてください。
7つのチェックポイント
「読めるコード」を書くためには以下の7つのチェックポイントを意識することが重要です。
これらのチェックポイントは、障害を起こさないために意識すべき観点で、コードレビューの際にはこれらのチェックポイントを守れているかどうかを基準に、そのコードが良いコード(読めるコード)かどうかを確認します。
そのため、コードを書く際にも意識すべき観点として使用します。
①コードはシンプルに
当然のことですが「複雑」なものよりも「単純」なもののほうが理解しやすいです。
例えば
「あのカップル、比翼連理だね」
よりは
「あのカップル、とても仲が良いね」
のほうがわかりやすいはずです。 ※比翼連理(ひよくれんり)=男女仲がとても良いこと
コードを書く際にも同じことが言えます。
ソフトウェアの障害は決まって「複雑な箇所」で発生します。シンプルで見通しの良いコードを書くことを常に心がけましょう。高等なテクニックを使わず、単純なやり方を選択します。そうすることでコードが複雑にならず、障害が起こりにくいコードを書くことができます。
悪い例①:
Pythonのabc
ライブラリを用いることで、抽象基底クラスを表現することができる。
以下の例は、乗り物が出す警告音と、乗り物が走る様子をprint
するためのコード。
Bicycle
Car
Train
クラスがVehicle
という抽象基底クラスを継承している。
こうすることでVehicle
クラスを継承するクラスは、Vehicle
クラスの持つalarm
progression
という名前のメソッドを持つことが保証されるが、初心者には理解が難しいコードになっている。
特別必要がない限り使わないようにするのが良い(Pythonで抽象基底クラスが必要になる場面はほとんどない)。
import abc
def vehicle_ability(vehicle):
vehicle.alarm()
vehicle.progression()
class Vehicle(metaclass=abc.ABCMeta):
def __init__(self):
pass
@abc.abstractmethod
def alarm(self):
pass
@abc.abstractmethod
def progression(self):
pass
class Bicycle(Vehicle):
def alarm(self):
print("リンリン")
def progression(self):
print("自転車がシャカシャカ進む。")
class Car(Vehicle):
def alarm(self):
print("プップー")
def progression(self):
print("自動車がブンブン進む。")
class Train(Vehicle):
def alarm(self):
print("ファーン")
def progression(self):
print("列車がガタゴト進む。")
bicycle = Bicycle()
vehicle_ability(bicycle)
car = Car()
vehicle_ability(car)
train = Train()
vehicle_ability(train)
良い例①:
Pythonでは、別々のクラスで同じ名前のメソッドを実装することができるため、わざわざ抽象基底クラスを用いなくてもよい。
無駄に高度なテクニックを使わないようにすることで、見通しがよく誰もが理解できるコードになっている。
def vehicle_ability(vehicle):
vehicle.alarm()
vehicle.progression()
class Bicycle:
def alarm(self):
print("リンリン")
def progression(self):
print("自転車がシャカシャカ進む。")
class Car:
def alarm(self):
print("プップー")
def progression(self):
print("自動車がブンブン進む。")
class Train:
def alarm(self):
print("ファーン")
def progression(self):
print("列車がガタゴト進む。")
bicycle = Bicycle()
vehicle_ability(bicycle)
car = Car()
vehicle_ability(car)
train = Train()
vehicle_ability(train)
悪い例②:
Mixinという技法がある。
機能だけを持つSingerMixin
WriterMixin
クラスを作成し、SingerSongWriter
クラスで継承することでSingerSongWriter
クラスに機能を追加している。初心者にはわかりにくいコードだろう。
class ArtistBase:
def __init__(self, name):
self.name = name
class SingerMixin:
def sing(self):
print('la la la...')
class WriterMixin:
def write(self):
print('Do Le Mi...')
class SingerSongWriter(ArtistBase, SingerMixin, WriterMixin):
pass
singer_song_writer = SingerSongWriter('Narita')
singer_song_writer.sing()
singer_song_writer.write()
Naritacan sing songs.
Naritacan write lyrics.
良い例②:
わざわざMixinを用いず、SingerSongWriter
クラスでSinger
Writer
クラスのインスタンスを引数として受け取ることで、それぞれのクラスが持つsing
write
メソッドを呼び出せるようにしている。
class ArtistBase:
def __init__(self, name) -> None:
self.name = name
class Singer:
def sing(self, name):
print('la la la...')
class Writer:
def write(self, name):
print('Do Le Mi...')
class SingerSongWriter:
def __init__(self, name, singer, writer) -> None:
self.artist_base = ArtistBase(name)
self.singer = singer
self.writer = writer
def sing(self):
self.singer.sing(self.artist_base.name)
def write(self):
self.writer.write(self.artist_base.name)
②同じようなものは同じような形に
同じような働きのものは、同じような使い方をしたいと思いませんか?
もし、鉛筆とシャーペンは同じ持ち方で使うことができるのに、ボールペンにだけグリップが付属しており、そこを握って使用するようになっていたら、とても使いにくいと思います。
鉛筆もシャーペンもボールペンも、文字や図を書く道具です。そのため、同じような持ち方で利用できるほうが使いやすいはずです。
同じことを同じ形で扱うことにこだわりましょう。
統一感のあるコードは読みやすく、理解しやすいものになります。
また「異質なもの」が目立つようになり、バグの原因を発見しやすくなります。
悪い例:
先程の『①コードはシンプルに』の例と同じく、乗り物が出す警告音と、乗り物が走る様子をprint
するためのコード。
Bicycle
、Car
、Train
はいずれも”警告音をprint
するメソッド”と”走る様子をprintするメソッド”を持っている。
これらは同じ働きをするメソッドだが、名前がクラスによってバラバラなため、呼び出す側(今回はvehicle_ability
関数)で呼び出す際に「なんの乗り物なのか」を意識する必要がある。
def vehicle_ability(vehicle):
if type(vehicle) == Bicycle:
vehicle.bell()
vehicle.moving()
elif type(vehicle) == Car:
vehicle.horn()
vehicle.driving()
elif type(vehicle) == Train:
vehicle.whistle()
vehicle.going()
else:
print("other vehicles")
class Bicycle:
def bell(self):
print("リンリン")
def moving(self):
print("自転車がシャカシャカ進む。")
class Car:
def horn(self):
print("プップー")
def driving(self):
print("自動車がブンブン進む。")
class Train:
def whistle(self):
print("ファーン")
def going(self):
print("列車がガタゴト進む。")
bicycle = Bicycle()
vehicle_ability(bicycle)
car = Car()
vehicle_ability(car)
train = Train()
vehicle_ability(train)
良い例:
同じような働きをするメソッドの名前が統一され、呼び出す際に「なんの乗り物なのか」を意識しなくて良くなっている。
def vehicle_ability(vehicle):
vehicle.alarm()
vehicle.progression()
class Bicycle:
def alarm(self):
print("リンリン")
def progression(self):
print("自転車がシャカシャカ進む。")
class Car:
def alarm(self):
print("プップー")
def progression(self):
print("自動車がブンブン進む。")
class Train:
def alarm(self):
print("ファーン")
def progression(self):
print("列車がガタゴト進む。")
bicycle = Bicycle()
vehicle_ability(bicycle)
car = Car()
vehicle_ability(car)
train = Train()
vehicle_ability(train)
③反対の条件についても考える
「対称であること」は、物事の性質や使い方を理解する手助けとなります。
例えば、ブラウザの左上には「戻る」と「進む」がセットで配置されています。
矢印アイコンのわかりやすさに加え、これらが隣り合わせで配置されていることで、ユーザーは「左が戻る、右が進む」と直感的に理解できます。
コードを書く際にも「対称であること」にこだわることで、読む際に使い方の予測がつきやすくなるため、コード理解のスピードを早めてくれます。
さらに、「戻るがあったら、進むもあるよな」のように、パターンの考慮漏れを防いでくれ、障害を減らすことができます。
悪い例:
座標がどの象限にあるかを求めるquadrant
メソッドの中で、「対称であること」を意識せずに条件分岐を行ったために、条件漏れが起きてしまっている(self.x < 0
の場合が考慮されていない)。その結果、p3 = Point(-1, -1)
のパターンでNone
が返ってきている。
「対称であること」を意識していればself.x == 0
やself.y == 0
の条件も漏れていることに気がつけたかもしれない。
class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
@property
def quadrant(self) -> int:
if self.x > 0:
if self.y > 0:
return 1
if self.x > 0 and self.y < 0:
return 4
if self.y > 0:
return 2
p1 = Point(1, 1)
p2 = Point(-1, 1)
p3 = Point(-1, -1)
p4 = Point(1, -1)
p5 = Point(1, 0)
print(p1.quadrant)
print(p2.quadrant)
print(p3.quadrant)
print(p4.quadrant)
print(p5.quadrant)
1
2
None
4
None
良い例:
self.x
が正のときのself.y
の正・負を考慮し、次にself.x
が負のときのself.y
の正・負を考慮するといった具合に、「対称であること」に気をつけて条件分岐を行うことができているため、条件漏れを防ぐことができている。
class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
@property
def quadrant(self) -> int:
if self.x > 0:
if self.y > 0:
return 1
elif self.y < 0:
return 4
else:
# 座標軸上にある場合、-1を返す
return -1
elif self.x < 0:
if self.y > 0:
return 2
elif self.y < 0:
return 3
else:
# 座標軸上にある場合、-1を返す
return -1
else:
# 座標軸上にある場合、-1を返す
return -1
p1 = Point(1, 1)
p2 = Point(-1, 1)
p3 = Point(-1, -1)
p4 = Point(1, -1)
p5 = Point(1, 0)
p6 = Point(0, 0)
print(p1.quadrant)
print(p2.quadrant)
print(p3.quadrant)
print(p4.quadrant)
print(p5.quadrant)
print(p6.quadrant)
1
2
3
4
-1
-1
④役割分担を適切に
例えば、設立したての会社をイメージしてみましょう。
まだ小さなその会社では、すべてのメンバーが、営業や経理、採用などの業務を担っています。
規模が小さいうちは良いですが、採用も営業もうまくいき、どんどん大きくなってきたらどうでしょうか。
一つの部署で、すべてのメンバーが、商品の売り込みや採用などを行っていると、一人ひとりがやるべき仕事の種類が増えて煩雑化し、大きなミスのもとになりえます。
なので多くの企業では、事業規模が拡大すると、下のように役割ごとに部署を作ってメンバーを割り振ることになります。
営業部:商品の売り込み
経理部:お金の管理
人事部:採用や研修
ソフトウェア開発の際にもディレクトリやクラス、メソッドの役割を明確にします。
プロジェクトのディレクトリを役割ごとに分け、同じ種類の処理はディレクトリやクラスをまたいで定義しないようにします。
そうすることで、コードを読む際に全体の構成を把握しやすくなり、必要に応じて階層を下ることで、より詳細なレベルを把握できるため、コードが読みやすくプログラマの意図が伝わりやすくなります。結果、障害の発生を防ぐことができます。
悪い例:
UserRepository
クラスが、データベースと直接やり取りするget
メソッドを持っている。
このUserRepository
は、
- データベースから直接データを取得する。
-
User
オブジェクトを作る。
の2つの役割を担っており、処理がごちゃつきやすく、エラーの温床になる可能性が高い。
import pymysql
class UserRepository:
def __init__(self):
pass
def get(self, user_id):
# データベースからユーザーを取得する処理
con = pymysql.connect(
...
)
...
return User(id=result.id, name=result.name)
良い例:
データベースとやり取りするためのMysqlClient
クラスを定義。ユーザー情報を取得するにはget
メソッドをUserRepository
クラスから呼び出すという流れになっている。
そうすることで、UserRepository
→ MysqlClient
→ データベース
の階層構造が生まれる。
UserRepository
クラスはデータベースとのやり取りについて気にかける必要がなくなり、User
オブジェクトの生成にのみ責任を持つ形になる。責任を持つ範囲を細かく分けることで、コードが複雑化・肥大化することを防ぐことができる。
import pymysql
class UserRepository:
def __init__(self, db_client):
self.db = db_client
def get(id):
query = f"SELECT id, name FROM user WHERE id = {id};"
result = self.db.get(query)
return User(id=result.id, name=result.name)
class MysqlClient:
def __init__(self):
self.connection = pymysql.connect(...)
def get(self, query):
with con.cursor() as cur:
....
return result
⑤処理の流れは直線に
食堂で食事を受け取る流れを想像してみてください。
通常は
入り口 で 食券を買う
カウンター手前 で お盆と箸を取る
カウンター中央 で 食券を渡す
カウンター奥 で 食事を受け取る
のような流れだと思います。
ところがこれが
入り口 で 食券を買う
カウンター奥 で お盆と箸を取る
カウンター手前 で 食券を渡す
カウンター中央 で 食事を受け取る
のように、行ったり来たりさせられるとどうでしょう。
「次はどこに行けばいいのか」がわかりにくく、右往左往してしまうかもしれません。
コードを書く際にも、複雑な条件分岐で制御したり、状態を多く持たせたりすると、わかりにくいコードになります。
階層の上位から下位に流れていく処理の流れを意識し、余計な繰り返しを避ける。
その結果コードが読みやすくなり、修正や機能追加などが容易になります。
以下のように、グループとそのメンバーを表すクラスがあるとします。
class Member:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
class Group:
def __init__(self, members: list):
self.members = members
悪い例:
複数のグループのメンバーから未成年を探すコード。
groups
の中からfor
文で1つずつgroup
を選び、さらにそのgroup
のmembers
からmember
を1人ずつ選び、if
文で未成年かどうかを判定…というように、for
文やif
文を重ねることで処理の流れが複雑になり、わかりにくいコードになっている。
groups = [
Group(
members=[
Member(name='John', age=18),
Member(name='Ben', age=16)
]
),
Group(
members=[
Member(name='Mike', age=15),
Member(name='Beth', age=20)
]
)
]
for group in groups:
members = group.members
for member in members:
member_name = member.name
if member.age >= 18:
print(f"{member_name} is adult." )
else:
print(f"{member_name} is underage.")
良い例:
for
文を重ねる(ネストさせる)のではなく、各group
からmember
を選ぶ処理と、member
が未成年かどうかを確認する処理に分ける。
そうすることでfor
文やif
文の繰り返しを避けられ、読みやすいコードになる。
all_members = []
for group in groups:
all_members += group.members
for member in all_members:
if member.age >= 18:
print(f"{member.name} is adult.")
else:
print(f"{member.name} is underage.")
⑥曖昧さをなくす
5人の友だちに「今夜7時に、渋谷駅 に集合ね!」と伝えた場合、5人が同じ場所に集まることはできるでしょうか。
JRユーザーのAさんはJR渋谷駅の新南口改札
田園都市線ユーザーのBさんは東急渋谷駅の渋谷ヒカリエ1改札
...
のように、バラバラの線のバラバラの改札に集合してしまうかもしれません(普通は確認しますが)。
全員が同じ場所に集合するには、「今夜7時に、JR渋谷駅の新南口改札前 に集合ね!」というように、明確に集合場所を伝えなければいけません。
コードを書く際にも、曖昧な変数名を用いたりコードを複雑にしたりすることで、理解が難しくなります。
だれもが一目見て、明らかに正しいと言えるコードを書くようにしましょう。コードだけで明証できない場合、明証の手段は選びません。コメントを残したりドキュメントを作成したりなどして、コードの正しさを明らかにします。
悪い例:
税込価格を計算する関数。
関数名や変数名が曖昧で、「何のための関数なのか」「何を表す変数なのか」がわからない。
# calculateは計算という意味だが、「何を」計算する関数なのか不明瞭
# 引数のa,bが何を表すのかがわからないので、処理の意味が理解できない
def calculate(a, b):
return a + a * b
良い例:
関数名や変数名が、それらの働きや性質を説明するものとなっているため、コードを読んだだけで関数の使い方や中で行われている計算の意味を理解できる。
# include_taxという関数名から「税込み価格」を計算する関数だとわかる
# price_without_taxが「税抜価格」、tax_rateが「税率」だとすぐにわかる
def include_tax(price_without_tax, tax_rate):
tax_included_price = price_without_tax + price_without_tax * tax_rate
return tax_included_price
⑦様々な場合を想定する
「昨日買ったロールケーキが冷蔵庫にあるはずだから、帰ったら食べよう」
楽しみにしながら自宅に帰り着き、冷蔵庫を開けるとそこにはロールケーキはなく、母親に尋ねてみると「急にお客さんが来たから、冷蔵庫にちょうどあったロールケーキ、お出ししたのよ」とのこと。
「確実に食べられる」と思っていたロールケーキにありつけず、代わりのおやつは何もなし…。
よくあるシチュエーションですよね。
ロールケーキが 「あるに決まってる」 と考え、なかったときのことを想定せずにいたことで、思いがけず悲しい思いをすることになってしまいました。
ロールケーキを買った時点で、家族に「私のだから食べないでね!」などと伝えておけばよかったかもしれません。
さらに、万が一なくなっていたときのことを考えて、家にチョコレートでも常備しておけば、それを食べてしのぐことができたかもしれませんね。
ありえないと思える条件をあえて考慮し、もしもの場合に備えることは、コードを書く際にも大切です。
if
文に対してelse
文を考慮したり、ある変数に対してNULL
チェックを行ったりすることで、思いがけない障害を防ぐことができます。すべての動作を想定し、曖昧な部分をなくすよう心がけましょう。
悪い例:
現在の点数に獲得点数を加算した結果を得るためのコード。
calculate_score_earned
関数がNone
を返す可能性を考慮できていない。
current_score = 40
score_earned = calculate_score_earned()
new_score = 0
new_score = current_score + score_earned
print(new_score)
良い例:
calculate_score_earned
関数がNone
を返した場合を考慮できている。
current_score = 40
score_earned = calculate_score_earned()
new_score = 0
if score_earned is not None:
new_score = current_score + score_earned
else:
new_score = current_score
print(new_score)
※上記のコードはcalculate_score_earned
関数がNone
を返すという予期しない事態になったとき、エラーを握りつぶしてしまう可能性があります。calculate_score_earned
関数がNone
を返した場合、本当にnew_score = current_score
として良いかどうかは、よく考えるべきです。
まとめ
コードはソフトウェアの「設計書」です。設計書を読めば、誰もがそのソフトウェアの機能や動作、意図を把握できなければなりません。難しい処理をせず、シンプルで明確なコードを書くことを心がけましょう。
おわりに
弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。
興味のある方はこちらをご覧ください。