Python | テスト・設計・品質:責務定義

Python Python
スポンサーリンク

「責務定義」って何?一言でいうと「この子は何を担当するのかを決めること」

責務定義は、クラス・関数・モジュールなどに対して「あなたは何を担当するのか?」をはっきり決めることです。
もっと砕くと、「このコードは何をする“係”なのか」を決める作業です。

ここが曖昧だと、何でもかんでも一つの関数やクラスに押し込んでしまい、結果として「何をしているのか分からない」「ちょっと直すと別のところが壊れる」という状態になります。
逆に、責務がはっきりしていると、「どこを直せばいいか」「どこをテストすればいいか」が一瞬で分かるようになります。

責務定義は、設計・テスト・品質の土台です。
ここがふわっとしていると、どれだけテストを書いても、どれだけツールを入れても、根本的な苦しさは消えません。


まずは「責務がごちゃ混ぜ」の悪い例を見てみる

なんでもやる「神関数」の例

次のようなコードを想像してください。

import json
from pathlib import Path
from dataclasses import dataclass

DATA_FILE = Path("users.json")

@dataclass
class User:
    id: int
    name: str
    email: str

def handle_user():
    # ファイルから読み込み
    if DATA_FILE.exists():
        data = json.loads(DATA_FILE.read_text(encoding="utf-8"))
        users = [User(**item) for item in data]
    else:
        users = []

    # ユーザー追加(ここでは適当に)
    name = input("name: ")
    email = input("email: ")
    new_id = (max((u.id for u in users), default=0) + 1)
    users.append(User(id=new_id, name=name, email=email))

    # バリデーション
    if "@" not in email:
        print("invalid email")
        return

    # 保存
    data = [u.__dict__ for u in users]
    DATA_FILE.write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

    # 表示
    print("registered:")
    for u in users:
        print(f"{u.id}: {u.name} <{u.email}>")
Python

この handle_user 関数は、何をしているでしょうか。
ファイルから読み込む、ユーザー入力を受け取る、IDを採番する、バリデーションする、保存する、一覧表示する。
つまり、「全部やっている」状態です。

これが「責務が定義されていない」典型例です。
この関数をテストしようとすると、ファイルI/Oも、入力も、表示も全部巻き込むことになり、途端にしんどくなります。


責務を分けて定義するとどう変わるか

役割ごとに「誰が何を担当するか」を決める

さっきのコードを、「誰が何を担当するか」を意識して分けてみます。

ここでは、ざっくり次の責務に分けてみます。

ユーザーというデータとルールを表す
ユーザーの保存・読み込みを担当する
ユーザー登録というユースケースを担当する
入出力(CLI)を担当する

これをコードに落とすと、こうなります。

# domain.py
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str

    def validate(self) -> None:
        if "@" not in self.email:
            raise ValueError("invalid email")
Python
# repository.py
import json
from pathlib import Path
from typing import Protocol
from domain import User

class UserRepository(Protocol):
    def load(self) -> list[User]:
        ...
    def save(self, users: list[User]) -> None:
        ...

class JsonUserRepository:
    def __init__(self, path: Path) -> None:
        self._path = path

    def load(self) -> list[User]:
        if not self._path.exists():
            return []
        data = json.loads(self._path.read_text(encoding="utf-8"))
        return [User(**item) for item in data]

    def save(self, users: list[User]) -> None:
        data = [u.__dict__ for u in users]
        self._path.write_text(
            json.dumps(data, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
Python
# usecase.py
from dataclasses import dataclass
from domain import User
from repository import UserRepository

@dataclass
class RegisterUserInput:
    name: str
    email: str

def register_user(repo: UserRepository, data: RegisterUserInput) -> list[User]:
    users = repo.load()
    new_id = (max((u.id for u in users), default=0) + 1)
    user = User(id=new_id, name=data.name, email=data.email)
    user.validate()
    users.append(user)
    repo.save(users)
    return users
Python
# cli.py
from pathlib import Path
from repository import JsonUserRepository
from usecase import register_user, RegisterUserInput

def main() -> None:
    repo = JsonUserRepository(Path("users.json"))
    name = input("name: ")
    email = input("email: ")
    try:
        users = register_user(repo, RegisterUserInput(name=name, email=email))
    except ValueError as e:
        print(e)
        return

    print("registered:")
    for u in users:
        print(f"{u.id}: {u.name} <{u.email}>")

if __name__ == "__main__":
    main()
Python

ここで、それぞれの責務を言葉にすると、こうなります。

User は「ユーザーとは何か」と「ユーザーのルール(バリデーション)」を担当する。
JsonUserRepository は「ユーザーをJSONファイルに保存・読み込みすること」を担当する。
register_user は「ユーザーを登録するユースケース(流れ)」を担当する。
main は「CLIとしての入出力」を担当する。

これが「責務定義」です。
「このクラス(関数・モジュール)は何の係か?」を、はっきり言葉にできる状態です。


責務定義がテストと品質にどう効いてくるか

責務が小さくはっきりしていると、テスト対象も小さくはっきりする

さっきの分割後のコードをテストすることを考えてみます。

例えば、「ユーザー登録のユースケースだけ」をテストしたいとき、CLIもファイルI/Oも巻き込む必要はありません。
テスト用のリポジトリを用意して、register_user だけをテストできます。

from usecase import register_user, RegisterUserInput
from repository import UserRepository
from domain import User

class InMemoryUserRepository(UserRepository):
    def __init__(self) -> None:
        self.users: list[User] = []

    def load(self) -> list[User]:
        return list(self.users)

    def save(self, users: list[User]) -> None:
        self.users = list(users)

def test_register_user_success():
    repo = InMemoryUserRepository()
    input_data = RegisterUserInput(name="Taro", email="taro@example.com")

    users = register_user(repo, input_data)

    assert len(users) == 1
    assert users[0].email == "taro@example.com"
Python

これは、「責務が分かれているからこそ」できるテストです。
register_user の責務が「ユーザー登録の流れ」だけに絞られているので、そこだけを切り出してテストできます。

もし責務がごちゃ混ぜのままだと、
ファイルI/Oや入力まで含めた巨大なテストを書くか、
そもそもテストを書くのを諦めるか、どちらかになりがちです。

責務が明確だと、バグの「居場所」が狭くなる

例えば、「メールアドレスのバリデーションがおかしい」というバグが見つかったとします。

責務が曖昧なコードだと、「どこにバリデーションが書いてあるのか」を探すところから始まります。
あちこちに if "@" not in email: が散らばっているかもしれません。

責務が定義されているコードなら、「ユーザーのルールは User にある」と分かっています。
つまり、User.validate を見ればいい、とすぐに分かります。

これは、デバッグの速さ=品質の維持コストに直結します。
責務定義は、「バグの居場所を狭くする」ための武器でもあります。


責務定義の核心:「一つのものに“理由の違う変更”を詰め込まない」

責務定義の考え方の中で、特に重要なのが「単一責務の原則(SRP)」です。
これは、「一つのクラス(モジュール・関数)は、一つの理由でしか変更されないべき」という考え方です。

もう少し噛み砕くと、「違う理由の変更を同じ場所に詰め込むな」ということです。

例えば、さっきの handle_user は、次のような理由で変更される可能性があります。

ファイル形式を変えたい(JSON → CSV)
入力方法を変えたい(CLI → Web)
バリデーションルールを変えたい(メールのチェックを厳しくする)
表示形式を変えたい

これらが全部一つの関数に詰まっていると、
どの変更をしても handle_user を触ることになります。
そのたびに、他の部分を壊すリスクが生まれます。

責務定義をして分割すると、こうなります。

ファイル形式を変えたい → JsonUserRepository を触る
入力方法を変えたい → main(CLI)を触る
バリデーションルールを変えたい → User.validate を触る
表示形式を変えたい → main の表示部分を触る

「変更の理由」が違うものは、違う場所に分かれている。
これが単一責務の原則であり、責務定義の核心です。


初心者が責務定義を練習するときの具体的な視点

「この関数(クラス)は何担当?」と日本語で言えるか試す

コードを書いたあとに、自分にこう問いかけてみてください。

「この関数は、何の係?」
「このクラスは、何を担当している?」

もし一言で言えないなら、そのコードは責務が混ざっている可能性が高いです。

例えば、

「ユーザー登録の流れを担当している」
「ユーザーの保存・読み込みを担当している」
「CLIの入出力を担当している」

のように言えれば、かなり良い状態です。

逆に、

「まあ、いろいろやってる」

となるなら、そこは分割候補です。

「テストを書くとしたら、何をモックにしたいか」を考える

テストのことを考えると、責務の境界が見えやすくなります。

例えば、「ユーザー登録のロジックだけをテストしたい」と思ったとき、
ファイルI/Oや入力はモック(差し替え)したくなります。

ということは、

ユーザー登録のロジック
ファイルI/O
入力

は、それぞれ別の責務として分けるべきだ、というヒントになります。

「ここはモックにしたい」と感じるところは、
だいたい「外部との境界」であり、責務の分かれ目でもあります。


まとめ(責務定義は「誰が何を担当するかを決めて、変更とテストを楽にする技術」)

責務定義を初心者目線でまとめると、こうなります。

責務定義は、クラス・関数・モジュールに対して「あなたは何を担当するのか?」をはっきり決めることであり、「一つのものに違う理由の変更を詰め込まない」ことが核心にある。
責務が明確だと、「どこを直せばいいか」「どこをテストすればいいか」が一瞬で分かるようになり、バグの居場所が狭くなり、テストもしやすくなる。
練習としては、「このコードは何の係かを日本語で一言で言えるか」「この部分だけテストしたいとき、何をモックにしたくなるか」を意識しながら、少しずつ責務を分けていくのが現実的で、設計と品質がじわじわ良くなっていく。

タイトルとURLをコピーしました