Composed Method
1. 背景
長いメソッドの可読性(保守性)を向上させたい
- メソッドの行数が30行を超えてくると徐々に理解するのに努力が必要になり、100行を超えた辺りから素早く理解することは困難になる
- メソッドの行数が多いことはコードの理解容易性や保守性の観点からメリットになることはひとつもない
- したがって、長いメソッドは分割することで可読性や保守性を向上させたい
2. 課題
メソッドで行われる処理が具体的過ぎる
- メソッドの行数が多くなり過ぎるのは、ひとつのメソッドに記述される処理が具体的過ぎることが原因である
- ↓のサンプルでは
user_id
を検索条件にユーザー情報、ユーザーの購入履歴、購入履歴に含まれる商品の詳細情報を返却するという仕様のプログラムについて考える
class Program:
def __init__(self, user_db, item_db):
self.user_db = user_db
self.item_db = item_db
def run(self, user_id):
# データのチェック
if user_id is None:
raise ValueError()
# データの取得1
user_data = self.user_db.find_by_id(user_id)
user_history_data = self.user_db.fetch_history(user_id)
# データの加工
item_id_list = []
for i in user_history_data:
item_id_list.append(i['item_id'])
# データの取得2
item_data = self.item_db.find_item_id(item_id_list)
# データの加工2
history = []
for h in user_history_data:
for item in item_data:
if h['item_id'] == item['id']:
tmp = {}
tmp['購入日'] = h['購入日']
tmp['item'] = item
history.append(tmp)
# データの合成
result = {}
result['user'] = user_data
result['history'] = tmp
return result
-
run
メソッドは30行程度かつ特段複雑な処理をしているわけではないので、理解が難しいわけではない
- とはいえ、ひとつのメソッドで行われる処理が複数あり、今後修正が入る度にコードが複雑化していくことが予想される
3. 解決策
処理のブロック単位でメソッドを分割する
class Program:
def __init__(self, user_db, item_db):
self.user_db = user_db
self.item_db = item_db
def run(self, user_id):
# データのチェック
self._validation(user_id)
# データの取得1
user_data = self._fetch_user_data(user_id)
user_history_data = self._fetch_history_data(user_id)
# データの加工
item_id_list = self._extract_item_id(user_history_data)
# データの取得2
item_data = self._fetch_item_data(item_id_list)
# データの加工2
history_data = self._create_history_data(user_history_data, item_data)
# データの合成
result = self._create_result_data(user_data, history_data)
# 処理結果を返却
return result
def _validation(user_id):
if user_id is None:
raise ValueError()
def _fetch_user_data(user_id):
return self.user_db.find_by_id(user_id)
def _fetch_history_data(user_id):
return self.user_db.fetch_history(user_id)
def _fetch_item_data(item_id_list):
return self.item_db.find_item_id(item_id_list)
def _extract_item_id(user_history_data)
result = []
for i in user_history_data:
result.append(i['item_id'])
return result
def _create_history_data(user_history_data, item_data)
history = []
for h in user_history_data:
for item in item_data:
if h['item_id'] == item['id']:
tmp = {}
tmp['購入日'] = h['購入日']
tmp['item'] = item
history.append(tmp)
return history
def _create_result_data(user_data, history_data)
result = {}
result['user'] = user_data
result['history'] = history_data
return result
- メソッドを分解することでクラス全体の行数は増加したが、
run
メソッドの行数は減少した
-
run
メソッドの処理内容も具体的な処理は別メソッドにカプセル化しつつ、メソッド名で何をしているかがより明確になった
-
run
メソッドに記述されるメソッド呼び出しの粒度が揃っているのでコードの可読性が向上した
- 分解された処理はそれぞれのメソッドにカプセル化され、スコープが独立することで修正に強い構造になった
4. メリット
コードが分かりやすくなる
- それぞれのメソッドの行数が短くなるので、メソッドの責務や処理内容が分かりやすくなる
- メソッド群を呼び出すメソッドは抽象化された名前が付けられたメソッドのリストになるので、処理内容がより明示的になる
5. デメリット
忌避感を抱く人もいる
- プログラミングスキルが低いエンジニアはひとつの関数に全てのロジックが記述されているコードを好む者が多い
- ユニットテストを上手く作れず、printfデバッグを駆使して実装を進めるタイプのエンジニアはひとつのメソッドに全てのロジックが記述されている方がよいと考える[1]
- プロジェクトを主導する立場のエンジニアがそのようなタイプである場合、
Composed Methodパターン
の導入に否定的/消極的となることが予想される
6. 注意事項
小さなメソッドが増えることに対する懸念
- 小さなメソッドが増えることでコード量が多くなり、処理の内容を理解することが難しくなるのではという懸念がある
- しかし、以下の理由からむしろ処理内容の理解は容易になる
- (1) 小さなメソッドのお蔭で責務が明確になる
- (2) メソッドが小さい分、変数のスコープが限定されるので、メソッド単体で把握すべき変数の状態が少なくなる
- (3) メソッド名が「処理内容」を直接表すので、必要になるまで具体的な処理内容を知る必要がなくなる
- (4) メソッド名が「処理内容」を直接表すので、それらのメソッドを呼び出すメソッドを見れば大体の処理内容の見当がつく
脚注
- [1] printfデバッグをするなら別関数のロジックはデバッグが面倒になるだけというのは実際ある
参考資料