入出力と業務ロジックを分けることによる嬉しさ

本記事は、 [DDD\-Community\-Jp Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dddcj)の4日目です。

# はじめに

自分が入出力とロジックは分けたいなと思っておりますが、
なんで分けたほうがいいのか？　というのがあまり言語化ができてなかったので、書いてみたいなと思いました。

# 入出力とロジックを分けることによるメリット

- 入出力とロジックを分けて、業務ロジックだけに集中することができる
    - そのロジックが表すことに名前付けたり
- そもそも入出力と業務ロジックが一緒だと、変更するところが2箇所になってしまう
    - たぶん、単一責任の原則[^1]

[^1]: [クリーンアーキテクチャ](https://www.amazon.co.jp/Clean-Architecture-%E9%81%94%E4%BA%BA%E3%81%AB%E5%AD%A6%E3%81%B6%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%81%AE%E6%A7%8B%E9%80%A0%E3%81%A8%E8%A8%AD%E8%A8%88-%E3%82%A2%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%89%E3%83%AF%E3%83%B3%E3%82%B4-%EF%BC%B2%EF%BD%8F%EF%BD%82%EF%BD%85%EF%BD%92%EF%BD%94-%EF%BC%A3%EF%BC%8E%EF%BC%AD%EF%BD%81%EF%BD%92%EF%BD%94%EF%BD%89%EF%BD%8E-ebook/dp/B07FSBHS2V/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3&qid=1607098140&s=digital-text&sr=1-1)にある単一責任原則の章では、「モジュールは、たった1つのユーザやアクターに対して責務を負うべき」と書いてあります。なので、「BMI計算とその結果を出力する」というのは、1つのアクターだから良いのではないか？　という意見もありそうです。でも、出力は表現方法に応じて（CSVやJSONなど）バリエーションがあり、それを求めるアクターが違うので、「計算とその結果を表示する責務は別」とも考えられそうです。


# 入出力と業務ロジックの関心事は別

身長と体重を入力したら、BMIを出力するプログラムを考えてみます。

これの結果を出力するとしたら、こんな感じになりそうです。


```python
def main(lines):
    height, weight = map(int, lines.split())
    # 体重 [kg] / 身長 [m] / 身長 [m]
    bmi = int(weight / (height / 100) / (height / 100))

    print(bmi)

if __name__ == '__main__':
    lines = '150 60'
    main(lines)

```

このmain関数の中には、2つの責務があります。

- BMIの計算
- 計算結果をターミナルに出力する

2つ責務があることによって、どういうことが起こりうるかと言いますと、

- 業務ロジックの関心事から出てくる変更要因
    - BMIの計算式が間違っていたから変更する
- 入出力の関心事から出てくる変更要因
    - 計算結果をデータベースに保存したい
    - 計算結果をCSVにしたい
    - 計算結果をJSON形式にしたい

といったように、2つ責務があることによって、様々な理由から、このmain関数に対して変更が求められてきます。
そして、大体は入出力にまつわる変更（出力先を変えたい、表現方法を変えたい）の方が、個人的な感覚では多いように思っています。

# 入出力と業務ロジックを分ける

入出力と業務ロジックを分けてみます。

```python
class Body_Calculator:
    def calc_bmi(self, height: int, weight: int) -> int:
        # 体重 [kg] / 身長 [m] / 身長 [m]
        return int(weight / (height / 100) / (height / 100))

def main(lines):
    height, weight = map(int, lines.split())

    bmi = Body_Calculator().calc_bmi(height, weight)

    print(bmi)

if __name__ == '__main__':
    lines = '150 60'
    main(lines)

```

BMI計算をするのは、`Body_Calculator().calc_bmi()` のメソッドの中で行われることになりました。
これによって、先程の変更理由を例に上げると

- 業務ロジックの関心事から出てくる変更要因
    - BMIの計算式が間違っていたから変更する　-> `calc_bmi()` を変更する
- 入出力の関心事から出てくる変更要因     
    - 計算結果をデータベースに保存したい　　　-> `main()` を変更する 
    - 計算結果をCSVにしたい　　　　　　　　　-> `main()` を変更する 
    - 計算結果をJSON形式にしたい　　　　　　-> `main()` を変更する 


変更する箇所が限定的になってきました。

# 計算ロジックのインターフェースを見直す

BMI計算のロジックを見てみます。

```python
    def calc_bmi(self, height: int, weight: int) -> int:
        # 体重 [kg] / 身長 [m] / 身長 [m]
        return int(weight / (height / 100) / (height / 100))
```

これは、一見良さそうに見えます。
ですが、中身の計算を見ていると、暗黙的に身長はcmであることを要求しています。

型ヒントでintを要求しているので、1.6[m]などを渡さないと思いますが（いまのコードだと、map(int...)のところでエラーは出ます）引数名を変えてみます。

```python
    def calc_bmi(self, height_cm: int, weight_kg: int) -> int:
        # 体重 [kg] / 身長 [m] / 身長 [m]
        return int(weight_kg / (height_cm / 100) / (height_cm / 100))
```

## 値オブジェクトを導入する

引数名を変更するだけでは心許ないので、値オブジェクトを導入してみます。
入力は文字列で来るので、文字列→intに変換するメソッドを用意します。

```python
@dataclass
class Height:
    value: int

    @staticmethod
    def of(data: str) -> Height:
        try:
            return Height(int(data))
        except ValueError as e:
            raise ValueError('身長はcmで入力してください')

@dataclass
class Weight:
    value: int

    @staticmethod
    def of(data: str) -> Weight:
        try:
            return Weight(int(data))
        except ValueError as e:
            raise e

```

`calc_bmi()` の引数も値オブジェクトの型にします。

```python
class Body_Calculator:
    def calc_bmi(self, height_cm: Height, weight_kg: Weight) -> int:
        # 体重 [kg] / 身長 [m] / 身長 [m]
        return int(weight_kg.value / (height_cm.value / 100) / (height_cm.value / 100))

```

呼び出し側も変えていきます。

```python
def main(lines):
    h, w = lines.split()

    bmi = Body_Calculator().calc_bmi(Height.of(h), Weight.of(w))

    print(bmi)

```

これによって、 `calc_bmi` は HeightとWeightを要求しているのを明示しています。


# 業務ロジックを入出力から引き剥がしておくことで、その業務ロジックの表現に集中ができる

[現場で役立つシステム設計の原則](https://www.amazon.co.jp/dp/B073GSDBGT/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)の第5章に「ドメインモデルを育てる」という節があります。
その中で、

> サービスクラスに安易に業務ロジックを書くより、ぎこちない名前でもドメインモデルにクラスを追加するほうが、業務ロジックの整理として優れています -- P.156

と書かれています。

これに近い感覚を持っていて、
たとえ、print文とかでも、まずは入出力と業務ロジックという2つの関心事を分離し、
そのロジックに対して何かぎこちなくても良いので名前とインターフェースを与えておくと、そこから設計をカイゼンしていくにはどうしたら良いのか、の考える取っ掛かりになるのではないかと思っております。

### ちょっとしたポイント

何かしら、いい名前が思いつかなかった場合は、すっごくあり得ない名前をつけて、違和感を際立てる作戦をたまにやります。

- 普通に英語でコードを書いているなら、日本語でクラス名、メソッド名をつけてしまう
- ほげほげほげ、みたいな名前にしてしまう
- 絶対、目につく、ヤバい名前にしておくと、そこの名前を考えなきゃっていう意識が向きやすい
- あえて、ぎこちなさを極端に演出する方法

# まとめ

入出力と業務ロジックの関心は別

- アプリケーションのサービス連携対応や、◯◯形式に出力できるようになりましたなどは、アプリケーションの機能ではあるが、業務ロジックの関心事ではない。
- 入出力のロジックは、煩雑になりやすいので、それが業務ロジックと混在していると、「どれが業務ロジックだったっけ…？」と混乱してしまう
    - データベース操作
    - JSON形式の変換など
- 入出力にまつわる変更は、あくまで自分の実感値だけど、業務ロジックの変更より多い
    - だから、入出力は入出力で一つの関心事としてまとめておき、すぐ追加したり差し替えられたりするようにしておくと、メンテナンス性があがると思っている