概要
「とにかく動く」コードを書こうとすると、特にメインの処理が煩雑になりがちです。
ということで、メインの処理をきれいにするための、簡単なリファクタリング例を考えてみたいと思います。
弊社のインターン生に読んでもらいたくて書いてますが、せっかくなのでQiitaで公開します。
リファクタリング例
「自動改札を通る」シミュレーションプログラム
条件
- 「乗客(
Passenger
)」は「きっぷ(Ticket
)」を持ちます。 - 「きっぷ(
Ticket
)」には「運賃(fare
)」と「有効期限日(expiry_date
)」があります。 - 「乗客」が改札を通るには、以下の条件を満たしていることが必要です。
- 「きっぷ」の「運賃」が、「最低乗車運賃(
min_fare
)」以上である。 - 「きっぷ」の「有効期限日」が、「今日
today
」以降である。
- 「きっぷ」の「運賃」が、「最低乗車運賃(
最初の実装
Passenger
クラスとTicket
クラスをそれぞれ実装します。
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
として公開するようにしています。
メインの処理は以下のように書きました。
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
の中でわざわざチェックするのではなく、きっぷから直接教えてもらえば良いでしょう。
class Ticket:
@property
def is_expired(self) -> bool:
return date.today() > self.__expiry_date
「自動改札機(TicketGate
)」クラスの導入
改札を通過できる条件というのはすなわち、「自動改札機」が開く条件です。
なので、各条件とその判定は、「自動改札機」にやってもらいましょう。
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
の中の条件の書き方は色々あると思います。
今回はなんとなく早期リターンを使って書きました。
改善後のコード
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
の中がスッキリしましたし、各クラスにロジックを寄せたことで、テストも書きやすくなりました。
このコードをより良くする方法は他にも色々あると思うので、コメント欄等に書いていただけると嬉しいです。
まとめ
このように、
- 必要なクラスがあればそれは新たに切り分ける。
- クラスでできることは、そのクラスにやってもらう。
などなど繰り返すことで、見通しよく、テストしやすいコードを書くことができます。
もっと学んでみたい方は、「オブジェクト指向」「デザインパターン」などについて、ぜひ勉強してみてください。
おわりに
弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。
興味のある方はこちらをご覧ください。