5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DDD-Community-JpAdvent Calendar 2020

Day 4

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

Last updated at Posted at 2020-12-04

本記事は、 DDD-Community-Jp Advent Calendar 2020の4日目です。

はじめに

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

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

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

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

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

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

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関数に対して変更が求められてきます。
そして、大体は入出力にまつわる変更(出力先を変えたい、表現方法を変えたい)の方が、個人的な感覚では多いように思っています。

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

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

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計算のロジックを見てみます。

    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...)のところでエラーは出ます)引数名を変えてみます。

    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に変換するメソッドを用意します。

@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() の引数も値オブジェクトの型にします。

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))

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

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

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

    print(bmi)

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

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

現場で役立つシステム設計の原則の第5章に「ドメインモデルを育てる」という節があります。
その中で、

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

と書かれています。

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

ちょっとしたポイント

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

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

まとめ

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

  • アプリケーションのサービス連携対応や、◯◯形式に出力できるようになりましたなどは、アプリケーションの機能ではあるが、業務ロジックの関心事ではない。
  • 入出力のロジックは、煩雑になりやすいので、それが業務ロジックと混在していると、「どれが業務ロジックだったっけ…?」と混乱してしまう
    • データベース操作
    • JSON形式の変換など
  • 入出力にまつわる変更は、あくまで自分の実感値だけど、業務ロジックの変更より多い
    • だから、入出力は入出力で一つの関心事としてまとめておき、すぐ追加したり差し替えられたりするようにしておくと、メンテナンス性があがると思っている
  1. クリーンアーキテクチャにある単一責任原則の章では、「モジュールは、たった1つのユーザやアクターに対して責務を負うべき」と書いてあります。なので、「BMI計算とその結果を出力する」というのは、1つのアクターだから良いのではないか? という意見もありそうです。でも、出力は表現方法に応じて(CSVやJSONなど)バリエーションがあり、それを求めるアクターが違うので、「計算とその結果を表示する責務は別」とも考えられそうです。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?