40
loading...
This website collects cookies to deliver better user experience
Email
(consisting of a single email attribute), Money
(consisting of amount and currency), DateRange
(consisting of a start date, and an end date), GPSCoordinates
(made of latitude and longitude), or Address
(consisting of a street, zip code, city, state, etc.). Apart from the attributes, all of the above can (and should) include some kind of validation logic too.DateRange
value object using Python dataclasses
module:from dataclasses import dataclass
from datetime import date
class BusinessRuleValidationException(Exception):
"""A base class for all business rule validation exceptions"""
class ValueObject:
"""A base class for all value objects"""
@dataclass(frozen=True)
class DateRange(ValueObject):
"""Our first value object"""
start_date: date
end_date: date
def __post_init__(self):
"""Here we check if a value object has a valid state."""
if not self.start_date < self.end_date
raise BusinessRuleValidationException("end date date should be greater than start date")
def days(self):
"""Returns the number of days between the start date and the end date"""
delta = self.end_date - self.start_date + timedelta(days=1)
return delta.days
def extend(self, days):
"""Extend the end date by a specified number of days"""
new_end_date = self.end_date + timedelta(days=days)
return DateRange(self.start_date, new_end_date)
DateRange
, we are using @dataclass(frozen=True)
decorator.dataclass
itself, which compares the class instance as if it were a tuple of its fields.__post_init__
method using simple logic to check invariants. It prevents us from creating an invalid date range.days
and extend
. Both of them are pure (they are side effects free). Note that extend
returns a new instance of DateRage
instead of modifying the end_date
attribute.DateRage
is also relatively straightforward:import unittest
class DateRangeTestCase(unittest.TestCase):
def test_equality(self):
range1 = DateRange(start_date=date(2020,1,1), end_date=date(2021,1,1))
range2 = DateRange(start_date=date(2020,1,1), end_date=date(2021,1,1))
self.assertEqual(range1, range2)
def test_days(self):
range = DateRange(start_date=date(2020,1,1), end_date=date(2020,1,1))
self.assertEqual(range.days(), 1)
def test_days(self):
range1 = DateRange(start_date=date(2020,1,1), end_date=date(2020,1,1))
range2 = range1.extend(days=1)
self.assertEqual(
range2,
DateRange(start_date=date(2020,1,1), end_date=date(2021,1,2))
)
def test_cannot_create_invalid_date_range(self):
with self.assertRaises(BusinessRuleValidationException):
DateRange(start_date=date(2021,1,1), end_date=date(2020,1,1))
string
to represent emails. There is a high chance that you will need to validate those emails as well, and most likely you will need to do it in multiple places (i.e. user inputs, form data, serializers, business logic, etc.). Having a simple Email
value object will help you to stay DRY in the long run.