合成関数を用いたSLA原則の実現
1. 目的
SLA原則に準拠したコードを書きたい
- SLA原則に準拠した保守性の高いコードを書きたい
- エントリーポイント/パブリック関数にあたるロジックは抽象度の高い関数/メソッドの羅列だけで処理の内容を表現したい
2. 課題
3. 解決策
処理のステップを関数化し、関数の羅列で処理のステップを表現する
- 処理のステップを関数化し、ステップごとの処理を独立させる
def step1(dto):
dto.value = dto.value + 10
return dto
def step2(dto):
dto.value = dto.value + 40
return dto
def step3(dto):
dto.value = dto.value * 2
return dto
関数間で共有されるデータはDTOを引き回す
- 処理のステップをコマンド化した関数はそれぞれ独立しており、状態を共有できない
- 多くの処理は前処理の結果に依存することになる為、データを共有する仕組みが必要になる
- データ共有の仕組みの為、DTO(Data Transfer Object)を導入する
class Dto:
def __init__(value, is_stop = False):
self.value = value
self.is_stop = is_stop
- DTOは保持したいデータのプロパティ(上記のサンプルコードでは
value
)と処理中断フラグ(is_stop
)を持つ
-
value
にあたるプロパティは共有されるデータの数だけいくつあっても構わない(ここでは簡単のため一個にしている)
関数合成でパイプラインを構築する
- DTOと関数のリストを受け取る関数を定義する
- 関数リストを順番に実行する
- 引数として受け取ったDTOに処理結果を格納し、次の関数の入力にしている
- for文ではなくreduce関数でも同じことはできそうだが、reduce関数では
is_stop
に対応できない気がしたのと、コードの可読性はfor文の方が高いのでfor文を採用
def exec(dto, *fns):
for f in fns:
dto = f(dto)
if dto.is_stop: break
return dto
- 使用イメージは↓の通り
- 処理のステップが関数のリストとして表現されており、具体的なロジックが漏れ出る事もなくSLA原則を守れている
if __name__=="__main__":
dto = dto(0, False)
result = exec(
dto,
step1,
step2,
step3
)
print(result.value)
4. メリット
制御構造とビジネスロジックを分解できる
- 関数呼び出しや中断といったコードの制御構造とビジネスロジックを完全に分離することができる
-
exec
関数は関数の実行と中断のみ行い、ビジネスロジックの要素は存在しない
def exec(dto, *fns):
for f in fns:
dto = f(dto)
if dto.is_stop:
break
return dto
- 一方、ビジネスロジックを担当する関数群はビジネスロジックの実装のみを責務とし、制御構造を意識する必要はない
def step1(dto):
dto.value = dto.value + 10
return dto
def step2(dto):
dto.value = dto.value + 40
return dto
def step3(dto):
dto.value = dto.value * 2
return dto
処理の追加/削除が容易になる
- 処理のステップが関数として独立している為、処理の追加や削除は該当ステップの関数に対して行うことが自明になる
ステップ毎のテストが容易になる
- ステップ毎の関数はDTOの状態にのみ依存する為、適切なDTOを組み立てることで参照透過性のある挙動が得られる(ただし、処理内容による)
- 単一の関数に全ての処理が書き下されている場合に比べて、ユニットテストの事前準備コードを楽に書くことができる
- 単一の関数に全ての処理が書き下されている場合、前段階の処理を確実に通過するテストデータの作成が必要となるが、ステップ毎に処理が分割されている場合、ステップ毎に必要なデータの準備だけで済む
5. デメリット
ロジックが分散するのでプログラムの理解が困難になる
- 処理全体のステップが関数に分散されるので、ソースコードを読む際の認知負荷は多少高くなる
プログラマに一定の抽象化能力が要求される
- プログラムを愚直に書き下していくスタイルに比べ、処理を抽象化し処理を命名する必要がある分、それなりの抽象化能力が要求される
- 抽象化能力が不足している場合、設計やコーディングが苦痛になり、最終的には拒絶反応を起こす恐れがある
DTOの状態を意識する必要がある
- ステップが進行するごとにDTOの内容は変化していくので、デバッグ時にDTOの状態がどうなっているか把握する能力が必要になる
- 実行時のDTOの内容はコードからは読み取れないので、デバッグ実行やログ出力で内容を確認する必要が生じる
- ユニットテストで入出力のあるべき姿を可能な限りコード化しておく事が望ましい
- 契約による設計を取り入れて、事前条件/事後条件のアサーションを入れるのも方法のひとつである
6. 注意事項
参考資料
Appendix-1 ソースコード全体
class Dto:
def __init__(value, is_stop=False):
self.value = value
self.is_stop = is_stop
def exec(dto, *fns):
for f in fns:
dto = f(dto)
if dto.is_stop: break
return dto
def step1(dto):
dto.value = dto.value + 10
return dto
def step2(dto):
dto.value = dto.value + 40
return dto
def step3(dto):
dto.value = dto.value * 2
return dto
if __name__=="__main__":
dto = dto(0, False)
result = exec(
dto,
step1,
step2,
step3
)
print(result.value) # => 100