169
206

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

株式会社NucoAdvent Calendar 2022

Day 15

「読めるコード」を書くための7つのチェックポイント

Last updated at Posted at 2022-12-14

この記事はNuco Advent Calendar 2022の15日目の記事です。

なぜ「読めるコード」を書くのか

「読めるコード」を書くことで、コードを読んだだけで何がしたいのかが明確にわかるため、修正や機能追加の際にもコードの解読に時間と労力を割かずにすみます。
また、理解を誤ったまま目的からそれた実装をすることを避けられます。

以降紹介するチェックポイントと、ご自分のコードと照らし合わせてクリアできているか確認をしてみてください。

7つのチェックポイント

「読めるコード」を書くためには以下の7つのチェックポイントを意識することが重要です。
これらのチェックポイントは、障害を起こさないために意識すべき観点で、コードレビューの際にはこれらのチェックポイントを守れているかどうかを基準に、そのコードが良いコード(読めるコード)かどうかを確認します。
そのため、コードを書く際にも意識すべき観点として使用します。

①コードはシンプルに

当然のことですが「複雑」なものよりも「単純」なもののほうが理解しやすいです。
例えば

「あのカップル、比翼連理だね」

よりは

「あのカップル、とても仲が良いね」

のほうがわかりやすいはずです。 ※比翼連理(ひよくれんり)=男女仲がとても良いこと

コードを書く際にも同じことが言えます。
ソフトウェアの障害は決まって「複雑な箇所」で発生します。シンプルで見通しの良いコードを書くことを常に心がけましょう。高等なテクニックを使わず、単純なやり方を選択します。そうすることでコードが複雑にならず、障害が起こりにくいコードを書くことができます。

悪い例①:
Pythonのabcライブラリを用いることで、抽象基底クラスを表現することができる。
以下の例は、乗り物が出す警告音と、乗り物が走る様子をprintするためのコード。
Bicycle Car TrainクラスがVehicleという抽象基底クラスを継承している。
こうすることでVehicleクラスを継承するクラスは、Vehicleクラスの持つalarm progressionという名前のメソッドを持つことが保証されるが、初心者には理解が難しいコードになっている。
特別必要がない限り使わないようにするのが良い(Pythonで抽象基底クラスが必要になる場面はほとんどない)。

bad.py
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では、別々のクラスで同じ名前のメソッドを実装することができるため、わざわざ抽象基底クラスを用いなくてもよい。
無駄に高度なテクニックを使わないようにすることで、見通しがよく誰もが理解できるコードになっている。

good.py
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クラスに機能を追加している。初心者にはわかりにくいコードだろう。

bad_2.py
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メソッドを呼び出せるようにしている。

good_2.py
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するためのコード。
BicycleCarTrainはいずれも”警告音をprintするメソッド”と”走る様子をprintするメソッド”を持っている。
これらは同じ働きをするメソッドだが、名前がクラスによってバラバラなため、呼び出す側(今回はvehicle_ability関数)で呼び出す際に「なんの乗り物なのか」を意識する必要がある。

bad.py
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)

良い例:
同じような働きをするメソッドの名前が統一され、呼び出す際に「なんの乗り物なのか」を意識しなくて良くなっている。

good.py
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 == 0self.y == 0の条件も漏れていることに気がつけたかもしれない。

bad.py
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の正・負を考慮するといった具合に、「対称であること」に気をつけて条件分岐を行うことができているため、条件漏れを防ぐことができている。

good.py
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つの役割を担っており、処理がごちゃつきやすく、エラーの温床になる可能性が高い。

bad.py
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クラスから呼び出すという流れになっている。
そうすることで、UserRepositoryMysqlClientデータベースの階層構造が生まれる。
UserRepositoryクラスはデータベースとのやり取りについて気にかける必要がなくなり、Userオブジェクトの生成にのみ責任を持つ形になる。責任を持つ範囲を細かく分けることで、コードが複雑化・肥大化することを防ぐことができる。

good.py
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

⑤処理の流れは直線に

食堂で食事を受け取る流れを想像してみてください。
通常は

入り口 で 食券を買う
カウンター手前 で お盆と箸を取る
カウンター中央 で 食券を渡す
カウンター奥 で 食事を受け取る

のような流れだと思います。
ところがこれが

入り口 で 食券を買う
カウンター奥 で お盆と箸を取る
カウンター手前 で 食券を渡す
カウンター中央 で 食事を受け取る

のように、行ったり来たりさせられるとどうでしょう。
「次はどこに行けばいいのか」がわかりにくく、右往左往してしまうかもしれません。

コードを書く際にも、複雑な条件分岐で制御したり、状態を多く持たせたりすると、わかりにくいコードになります。
階層の上位から下位に流れていく処理の流れを意識し、余計な繰り返しを避ける。
その結果コードが読みやすくなり、修正や機能追加などが容易になります。

以下のように、グループとそのメンバーを表すクラスがあるとします。

classes.py
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を選び、さらにそのgroupmembersからmemberを1人ずつ選び、if文で未成年かどうかを判定…というように、for文やif文を重ねることで処理の流れが複雑になり、わかりにくいコードになっている。

bad.py
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文の繰り返しを避けられ、読みやすいコードになる。

good.py
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渋谷駅の新南口改札前 に集合ね!」というように、明確に集合場所を伝えなければいけません。

コードを書く際にも、曖昧な変数名を用いたりコードを複雑にしたりすることで、理解が難しくなります。
だれもが一目見て、明らかに正しいと言えるコードを書くようにしましょう。コードだけで明証できない場合、明証の手段は選びません。コメントを残したりドキュメントを作成したりなどして、コードの正しさを明らかにします。

悪い例:
税込価格を計算する関数。
関数名や変数名が曖昧で、「何のための関数なのか」「何を表す変数なのか」がわからない。

bad.py
# calculateは計算という意味だが、「何を」計算する関数なのか不明瞭
# 引数のa,bが何を表すのかがわからないので、処理の意味が理解できない
def calculate(a, b):
    return a + a * b

良い例:
関数名や変数名が、それらの働きや性質を説明するものとなっているため、コードを読んだだけで関数の使い方や中で行われている計算の意味を理解できる。

good.py
# 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を返す可能性を考慮できていない。

bad.py
current_score = 40
score_earned = calculate_score_earned()
new_score = 0

new_score = current_score + score_earned

print(new_score)

良い例:
calculate_score_earned関数がNoneを返した場合を考慮できている。

good.py
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として良いかどうかは、よく考えるべきです。

まとめ

コードはソフトウェアの「設計書」です。設計書を読めば、誰もがそのソフトウェアの機能や動作、意図を把握できなければなりません。難しい処理をせず、シンプルで明確なコードを書くことを心がけましょう。

おわりに

弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。

興味のある方はこちらをご覧ください。

169
206
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
169
206

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?