原則や思想#
思想・心がけ#
「自分はバカで、とんでもないミスをするものだ」と考える#
ミスをする前提だからこそ、大きな問題が起きる前に早く気づくためにテストを書く
半年後の自分は他人#
自分が読むコードでも他人が読めるように書く
原則#
KISS: Keep It Simple, Stupid#
「設計や実装は、できる限り単純で分かりやすく保て」 という原則。
複雑な実装よりも、単純で直感的なもののほうが理解しやすく、ミスも少ない。
YAGNI:You Aren’t Gonna Need It#
「それはきっと必要にならない」。必要になるまで機能を実装するな、という原則。
将来使うかもしれない機能を先回りして作るのは避けるべき。
実際に必要になるまでは、それを実装するコスト・複雑さ・バグのリスクを回避する。
DRY: Don’t Repeat Yourself#
「同じコードを繰り返して書くな」という原則。
定数リテラルが直接埋め込まれていることも含む。定数は共通化するべき。
同じ処理は適切にループ処理を書いたり、抽象化して共通の関数に入れたりするべき。
(処理の内容を日本語に置き換えただけのようなコメントもやめるべき)
PIE: Program Intently and Expressively#
「意図を表現してプログラミングせよ」という意味。
最も詳細なソフトウェアの設計書はコードになるので、読み手に意図が伝わるように書く。
また、コメントには「なぜそうしたのか」を書く。(Howはコード、Whyはコメント)
SLAP: Single Level of Abstraction Principle#
「抽象化レベルの統一」。コードを書くとき、機能の複雑さや抽象化のレベルに応じて多層に分離し、それぞれの層における抽象化のレベルは揃えるべき、というもの。
レベルが揃った関数へとコードが分割されていると、
要約性をもつ:関数の一覧が目次のようになる
閲覧性が良くなる:分割された関数は小さなコードの塊になり、読みやすくなる
といったメリットがあるため。
関数を構造化すると、各関数は自身より1段階低いレベルの関数を呼び出す処理が中心となる。このような他の関数を呼び出すコードで構成された関数を 複合関数 (composed method) という。 複合関数は極力小さくするのがよく、1つの複合関数で呼び出す関数たちの抽象レベルも揃えるのがよい。
モジュールも同様に同様の抽象レベルのものを同階層に揃えるようにすると要約性と閲覧性が改善されて読みやすくなる。
例
# 悪い例(複雑な1つのメソッド)
class Calculator:
def calculate_circle_area(self, radius):
# 全て1つのメソッドに詰め込んだ例
if radius <= 0:
raise ValueError("半径は正の数である必要があります")
pi = 3.14159
area = pi * radius * radius
return round(area, 2)
簡単のため行数が短い最低限のコードにしているが、これが仮に50行や100行となっていたらかなり複雑。
# 良い例(Composed Method)
class Calculator:
def calculate_circle_area(self, radius):
"""円の面積を計算(メインの処理フロー)"""
self._validate_radius(radius)
return self._compute_area(radius)
def _validate_radius(self, radius):
"""半径の検証"""
if radius <= 0:
raise ValueError("半径は正の数である必要があります")
def _compute_area(self, radius):
"""面積の計算"""
pi = 3.14159
area = pi * radius * radius
return round(area, 2)
OCP: Open-Closed Principle#
オープン・クローズドの原則。
コードは次の2つの性質を満たすように設計すべきというもの
拡張に対して開いている(Open for extension)→ 新しい振る舞いを追加できるようにしておく。
修正に対して閉じている(Closed for modification)→ 既存のコードを変更せずに、新しい機能を実現できるようにする。
※どのコードでもOCPを適用すると、変更可能性は高まるが複雑で冗長になる。変更されなかったら無駄に冗長なだけになってしまう。変更を予測しすぎず、変更が多いことがわかったら対応するくらいでいい
例
ログの出力方式を指定してログを出せるようにしたいとする。
同じ関数に入れて条件分岐する場合、新しい出力方法を追加するたびに関数を修正する必要があり、閉じていない
# OCPに従わない実装例
def log(message, log_type="console"):
if log_type == "console":
print(message)
elif log_type == "file":
with open("log.txt", "a") as f:
f.write(message + "\n")
elif log_type == "slack":
send_to_slack(message)
# 新しい出力方法を追加するたびに、この関数を修正する必要あり
↓はStrategyパターン(同じインターフェースをもつ関数を作って使用時に選択するパターン)を使ってOCPに従うよう実装した例
# OCP準拠例
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message): pass
class ConsoleLogger(Logger):
def log(self, message):
print(message)
class FileLogger(Logger):
def log(self, message):
with open("log.txt", "a") as f:
f.write(message + "\n")
class SlackLogger(Logger):
def log(self, message):
send_to_slack(message)
# 使用例
def run_process(logger: Logger):
logger.log("処理を開始します")
関心の分離#
関心(ソフトウェアの機能や目的)を独立したモジュールとして、他のコードから分離すること。
例えばMVCはModel, View, Controllerに若手いる
アスペクト指向プログラミング(AOP: Aspect-Oriented Programming) は、 横断的関心事(cross-cutting concerns) を分離するためのプログラミングパラダイム
横断的関心事は複数のモジュールに横串で関係するもので、例えば
ログ記録
認証・認可
例外処理
トランザクション管理
など。
Pythonだとデコレーターを使って実装できる
def logging_decorator(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} 開始")
result = func(*args, **kwargs)
print(f"{func.__name__} 終了")
return result
return wrapper
@logging_decorator
def process_order():
print("注文処理中...")
DRY, WET, AHA#
「コードの重複を避けよ」という原則(DRY: Don’t Repeat Yourself)は有名だが、字義通りにDRYにすることのデメリットも指摘されている。
多少のコードの重複を受け入れたほうが結果として可読性・保守性が上がることもある。
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system
(あらゆる知識は、システム内で単一の、明確で、権威ある表現を持たなければならない。)
You can ask yourself “Haven’t I written this before?” two times, but never three.
(「これ、前にも書いたっけ?」と自分自身に尋ねることは 2 回まで、3 回はないようにする)
Stop trying to be so DRY, instead Write Everything Twice (WET) - DEV Community
prefer duplication over the wrong abstraction