search
LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

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

本記事は、 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など)バリエーションがあり、それを求めるアクターが違うので、「計算とその結果を表示する責務は別」とも考えられそうです。 

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
What you can do with signing up
0