【Python】テストしやすいコードに近づけるための初歩

## 概要

「とにかく動く」コードを書こうとすると、特にメインの処理が煩雑になりがちです。

ということで、メインの処理をきれいにするための、簡単なリファクタリング例を考えてみたいと思います。

弊社のインターン生に読んでもらいたくて書いてますが、せっかくなのでQiitaで公開します。

## リファクタリング例

### 「自動改札を通る」シミュレーションプログラム

#### 条件

- 「乗客(`Passenger`)」は「きっぷ(`Ticket`)」を持ちます。
- 「きっぷ(`Ticket`)」には「運賃(`fare`)」と「有効期限日(`expiry_date`)」があります。
- 「乗客」が改札を通るには、以下の条件を満たしていることが必要です。
    - 「きっぷ」の「運賃」が、「最低乗車運賃(`min_fare`)」以上である。
    - 「きっぷ」の「有効期限日」が、「今日`today`」以降である。

#### 最初の実装

`Passenger`クラスと`Ticket`クラスをそれぞれ実装します。

```python
from datetime import date

class Ticket:
    def __init__(self, fare: int, expiry_date: date):
        self.__fare = fare
        self.__expiry_date = expiry_date

    @property
    def fare(self) -> int:
        return self.__fare

    @property
    def expiry_date(self) -> date:
        return self.__expiry_date


class Passenger:
    def __init__(self, ticket: Ticket):
        self.__ticket = ticket
    
    @property
    def ticket(self) -> Ticket:
        return self.__ticket

```

条件の上2つについて、そのままクラスにしてみた感じですね。

外部からの予期せぬ書き換えを防ぐため、各インスタンス変数は`property`として公開するようにしています。

メインの処理は以下のように書きました。

```python
def main():
    # 「きっぷ」の発行
    ticket = Ticket(fare=250, expiry_date=date(2022, 9, 18))
    # 「乗客」の設定
    passenger = Passenger(ticket=ticket)

    # 「最低乗車運賃」
    min_fare = 200
    # 「今日」の日付
    today = date.today()
        
    # 自動改札を通過できるかチェック
    if passenger.ticket.fare >= min_fare and passenger.ticket.expiry_date >= today:
        print("改札を通ったよ！")
    else:
        print("改札を通れないよ...")
```

これでも十分動くコードにはなっています。

汎用的に使う場合は、`Ticket`のパラメータを自由に変えられるようにする必要がありますが、今回は便宜上このパラメータで固定して考えます。

#### 何が問題か？

これだけだと本当に大したコードじゃないのでリファクタリングの必要性が薄いんですが笑、以下のような問題が挙げられます。

##### ①改札の通過条件の変更に弱い

例えば「きっぷに利用開始日を追加する」なんてことになったら、`if`文の条件が更に複雑になります。

この条件を使っているのがこの`main`だけなら良いのですが、何人も通過させようとしたり、別の箇所でも同じことをしたりしている場合は、それら全てを変更しなければいけません。

通過条件は、別途切り出して任意の場所から参照できるようにしておいた方が良いでしょう。

##### ②ロジックのテストができない

基本的に、メインの処理内にロジックを書かない方が良いです。

いざ単体テストを書こうとして、「テストできる関数がない...」と途方にくれた経験がある人もいるでしょう。

少なくともこの判定部分は関数なり、別途`main`から切り出したほうが良さそうです。

#### 改善アイデア

##### それぞれのクラスでできることは、そのクラスにやってもらう

例えば、「きっぷが有効期限切れかどうか」なんて、そのきっぷを見れば一目瞭然です。

なので、`main`の中でわざわざチェックするのではなく、きっぷから直接教えてもらえば良いでしょう。

```python
class Ticket:

    @property
    def is_expired(self) -> bool:
        return date.today() > self.__expiry_date
```

##### 「自動改札機(`TicketGate`)」クラスの導入

改札を通過できる条件というのはすなわち、「自動改札機」が開く条件です。

なので、各条件とその判定は、「自動改札機」にやってもらいましょう。

```python
class TicketGate:
    def __init__(self, min_fare: int):
        self.__min_fare = min_fare

    # チケットを見て通過可能か判定する。
    def is_passable(self, ticket: Ticket) -> bool:
        if ticket.is_expired:
            return False
        if ticket.fare < self.__min_fare:
            return False
        return True
```

`is_passable`の中の条件の書き方は色々あると思います。

今回はなんとなく早期リターンを使って書きました。

#### 改善後のコード

```python
from datetime import date

class Ticket:
    def __init__(self, fare: int, expiry_date: date):
        self.__fare = fare
        self.__expiry_date = expiry_date

    @property
    def fare(self) -> int:
        return self.__fare

    @property
    def is_expired(self) -> bool:
        return date.today() > self.__expiry_date


class TicketGate:
    def __init__(self, min_fare: int):
        self.__min_fare = min_fare

    def is_passable(self, ticket: Ticket) -> bool:
        if ticket.is_expired:
            return False
        if ticket.fare < self.__min_fare:
            return False
        return True


class Passenger:
    def __init__(self, ticket: Ticket):
        self.__ticket = ticket
    
    # 改札の通過
    def pass_gate(self, gate: TicketGate):
        if gate.is_passable(self.__ticket):
            print("改札を通ったよ！")
            return
        print("改札を通れないよ...")


def main():
    ticket = Ticket(fare=250, expiry_date=date(2022, 9, 18))
    passenger = Passenger(ticket=ticket)
    gate = TicketGate(min_fare=180)

    # 自動改札を通過
    passenger.pass_gate(gate)


if __name__ == '__main__':
    main()
```

`main`の中では、乗客がきっぷを持って改札を通るだけの実装になりました。

`main`の中がスッキリしましたし、各クラスにロジックを寄せたことで、テストも書きやすくなりました。

このコードをより良くする方法は他にも色々あると思うので、コメント欄等に書いていただけると嬉しいです。


## まとめ

このように、

- 必要なクラスがあればそれは新たに切り分ける。
- クラスでできることは、そのクラスにやってもらう。

などなど繰り返すことで、見通しよく、テストしやすいコードを書くことができます。

もっと学んでみたい方は、「オブジェクト指向」「デザインパターン」などについて、ぜひ勉強してみてください。


## おわりに

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

興味のある方は[こちら](https://www.recruit.nuco.co.jp/)をご覧ください。