A Value object is a basic building block from tactical DDD (Domain Driven Design).
In today’s blog post, I will show you how to build value objects with Python π
Definition
As wikipedia says:
In computer science, a value object is a small object that represents a simple entity whose equality is not based on identity: i.e. two value objects are equal when they have the same value, not necessarily being the same object.
More detailed explanation can be found in those two books:
I canβt recommend them enough βοΈ Β , especially the second one.
Characteristics of a value object
- Describes the features of a specific domain concept
- Binds many attributes into a logical business concept
- Has no identity; it’s identified by the values of the attributes
- Comparison is done through comparing values of the attributes
- It’s immutable
- Has no methods that can change the object’s state (if such a method is needed a new object should be returned instead)
- Side effect free behavior
Benefits
- Built-in validation
- Internal implementation is easy to change because our object has a stable interface
- Encapsulates the logic, for example: validation
- Improves code readability
- Helps you fight with Primitive Obsession
- Easy to unit test
Examples
I decided to use dataclasses in my examples due to the following reasons:
frozen=True guarantees the object’s immutability
- by default dataclasses are shipped with
__eq__() method (eq=True) which enables objects comparison
Using pure python classes is possible but inefficient as you have to take care of the above by yourself.
Some examples in this blog post may be perceived as half-baked. The reason for that is the fact that
some of them are taken from my projects, and others written explicitly for the purpose of this blog post π.
All of them, together with unit tests are available on my
GitHub π - https://github.com/szymon6927/szymonmiks.pl/tree/master/blog/examples/src/value_object_example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
from dataclasses import dataclass
from decimal import Decimal
from typing import Union
@dataclass(frozen=True)
class Price:
value: Decimal
def __post_init__(self) -> None:
if self.value < 0:
raise ValueError("Price can not be smaller than 0")
@classmethod
def zero(cls) -> "Price":
return cls(Decimal(0))
@classmethod
def of(cls, value: Union[int, str, float]) -> "Price":
return Price(Decimal(value))
def discount(self, percentage: int) -> "Price":
return Price(Decimal(percentage * self.value / 100))
def __add__(self, other: "Price") -> "Price":
return Price(self.value + other.value)
def __sub__(self, other: "Price") -> "Price":
return Price(self.value - other.value)
def __mul__(self, other: "Price") -> "Price":
return Price(self.value * other.value)
def __str__(self) -> str:
return f"{self.value:.2f}"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import re
from dataclasses import dataclass
from email.utils import parseaddr
from typing import ClassVar
@dataclass(frozen=True)
class EmailAddress:
value: str
_trusted_domains: ClassVar[list[str]] = [
"zut.edu.pl",
"pg.edu.pl",
"amsterdamumc.nl",
] # can be much more here
def __post_init__(self) -> None:
real_name, email_address = parseaddr(self.value)
if not real_name and not email_address:
raise ValueError("Incorrect email address!")
regex_result = re.search(
r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+",
email_address,
)
if not regex_result:
raise ValueError("Incorrect email address!")
@classmethod
def academical(cls, email_address: str) -> "EmailAddress":
email = cls(email_address)
domain = email.value.split("@")[1]
if domain not in cls._trusted_domains:
raise ValueError("Non-academical email address!")
return email
def change(self, new_email_address: str, is_academical: bool) -> "EmailAddress":
if is_academical:
return self.academical(new_email_address)
return EmailAddress(new_email_address)
def __str__(self) -> str:
return self.value
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from dataclasses import dataclass
@dataclass(frozen=True)
class OpenSSHPublicKey:
key: str
def __post_init__(self):
if "ssh-rsa" not in self.key:
raise ValueError("Provided OpenSSH key has incorrect format!")
def __str__(self) -> str:
return self.key
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
from dataclasses import dataclass
@dataclass(frozen=True)
class SSH2PublicKey:
key: str
def __post_init__(self) -> None:
if not self.key.startswith(
"---- BEGIN SSH2 PUBLIC KEY ----"
) and not self.key.endswith("---- END SSH2 PUBLIC KEY ----"):
raise ValueError("Provided SSH2 public key has incorrect format!")
def __str__(self) -> str:
return self.key
|
1
2
3
4
5
6
7
|
from dataclasses import dataclass
@dataclass(frozen=True)
class AnalysisParameters:
has_cf_correction: bool
has_batch_correction: bool
|
Summary
I hope you enjoyed it π
A value object is a great starting point to improve readability of your code and make it cleaner.
It’s also an excellent technique if you struggle with refactorization, as it may fix the following issues:
- duplicated validation
- multiplied representation of the same concept
- a large
utils file π
I hope you see the benefits of using value objects after reading this blog post.
Please let me know, I would love to hear your opinion!