Edited at

PythonでボトムアップDDD 【値オブジェクト】

最近、話題のDDD(ドメイン駆動設計)の導入部として最適な値オブジェクトについて書こうと思います。

筆者がDDDの勉強をするときに、参考にさせてもらっている記事があります。

この記事が「C#」で書かれています。そこで、今回は、解説部分はそこまで深入りせずに、コード部分をPythonに翻訳します。

用語の説明やコードの詳細説明に関して、下記の参考文献を参照ください。

参考文献: ボトムアップドメイン駆動設計

筆者が訳した「pythonでボトムアップDDD」

PythonでボトムアップDDD 【値オブジェクト】

PythonでボトムアップDDD 【エンティティ】

PythonでボトムアップDDD 【ドメインサービス】

PythonでボトムアップDDD【値オブジェクト・エンティティ・ドメインサービスを利用する】

PythonでボトムアップDDD 【リポジトリ】

PythonでボトムアップDDD 【テスト用のリポジトリ】

PythonでボトムアップDDD 【ユーザの登録・変更・削除・取得など】


バージョン


  • Python 3.7.0


目次


  • 値オブジェクトの説明

  • 値オブジェクトのルール

  • 値オブジェクトを作る理由

  • 参考文献

  • おまけ


値オブジェクトの説明

値オブジェクトとは、「一意に識別して変更を管理する必要がないモノ」。

適切に設計していれば、値を想定外に書き換えられてしまうリスクがなくなり、安心して開発ができます。


値オブジェクトのルール

値オブジェクトを作るときのルールが3つ存在します。


  • 状態を変更不可能にする

  • 同一の値オブジェクト同士を同じオブジェクトと判断できる

  • 交換可能である


状態を変更不可にする

pythonでイミュータブル(変更不可)なクラスを作るには、dataclassを使うと良いでしょう。

ただ、dataclassはpython3.7から使用可能です。

(dataclassに関して参考になる記事)

import dataclasses

@dataclasses.dataclass(frozen=True)
class FullName:
family_name: str
first_name: str

full_name = FullName("matsuoka", "kota")
print(full_name.family_name)

上で生成したインスタンスのfamily_nameを書き換えようとすると、

full_name.family_name = "tanaka" # family_nameを書き換える

怒られます。

Traceback (most recent call last):

File "/note_1.py", line 12, in <module>
full_name.family_name = "tanaka"
File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'family_name'

これで、値を変更されることがなくなりました。


同一の値オブジェクト同士を同じオブジェクトと判断できる

値は、その値同士で比較することができます。

例えば、数字だと「0」と「1」を比較したり、文字列だと「Thank you」と「ありがとう」などです。

これ同様に、値オブジェクトもそのオブジェクト同士を比較できる必要があります。

import dataclasses

@dataclasses.dataclass(frozen=True)
class FullName:
family_name: str
first_name: str

def __eq__(self, other):
return (self.first_name == other.first_name) and \
(self.family_name == other.family_name) and \
isinstance(other, FullName)

full_name1 = FullName("matsuoka", "kota")
full_name2 = FullName("tanaka", "hiroshi")
print(full_name1 == full_name1)
print(full_name1 == full_name2)

__eq__()を使って、値オブジェクト同士の比較を実現しました。


交換可能である

値は、交換可能です。

ここの説明は、参考文献の「完全に交換可能である」の説明が非常にわかりやすいので、参考にしてみてください。「変更」と「交換」の違いをはっきりさせてくれるでしょう。

上で実装したFullNameクラスを使って表すと、full_nameは、「matsuoka kota」から「tanaka hiroshi」へ交換されています。

full_name = FullName("matsuoka", "kota")

print(full_name.family_name, full_name.first_name)
# -> matsuoka kota

full_name = FullName("tanaka", "hiroshi")
print(full_name.family_name, full_name.first_name)
# -> tanaka hiroshi


値オブジェクトを作る理由

では、なぜ、値オブジェクトをわざわざ作るのかを考えてみましょう。

その理由は、値自身に自分のことを管理させたいと筆者は解釈しています。

値が「自分を管理するため」にやることが2つ。


  • 存在してはいけない値は存在させない

  • 間違った代入はさせない


存在してはいけない値は存在させない

例えば、上のFullNameクラスのfamily_nameを10文字以下の値しか持たないという定義を値オブジェクト自身に持たせます。

これにより、FullNameクラスのfamily_nameには存在してはいけない「11文字以上の文字列」が、存在しなくなりました

import dataclasses

@dataclasses.dataclass(frozen=True)
class FullName:
family_name: str
first_name: str

def __post_init__(self):
if len(self.family_name) > 10:
raise Exception("10文字以下にしてください。")

full_name = FullName("matsuoka", "kota")
print(full_name.family_name, full_name.first_name)

full_name = FullName("daijoujidani", "hiroshi")
print(full_name.family_name, full_name.first_name)


間違った代入はさせない

これは、先程の「存在してはいけない値は存在させない」と似ています。

import dataclasses

@dataclasses.dataclass(frozen=True)
class UserId:
id: int

@dataclasses.dataclass(frozen=True)
class UserName:
name: str

class User:
def __init__(self, user_id: UserId, user_name: UserName):
self.user_id = user_id
self.user_name = user_name

user_id = UserId(1)
user_name = UserName("kota")

user_valid = User(user_id, user_name) # 正しい代入
user_invalid = User(user_name, user_id) # 間違った代入

pythonは動的型付け言語なので、上記の最終行のように型が間違っていても動いてしまいます。

Pycharmを使用していると、警告を出してくれるので、型を間違えていることに気づくことができるでしょう。


参考文献


おまけ

参考文献に載せたブログ記事にはいつも助けられています。ぜひ、みなさんも読みながら、ご自身が使い慣れている言語で写経してみてください。