Introduction
Most applications require business rules, such as data validation. It’s crucial to implement these rules in a way that’s flexible, clear, and easy to maintain. The Specification pattern offers a solution, allowing the creation of reusable business rules that can be combined using boolean logic.
Implementation in Python
In this example, we’ll build a simple and adaptable email validator. First, we import necessary packages and define the EmailValidator interface, which includes a method to check the validity of an email address.
We will start by defining the EmailAddress
class:
class EmailAddress:
_domain: str = None
_user_name: str = None
def __init__(self, email: str):
elements = email.split('@')
if len(elements) != 2:
raise Exception("Invalid email")
self._user_name = elements[0]
self._domain = elements[1]
@property
def user_name(self) -> str:
return self._user_name
@property
def domain(self) -> str:
return self._domain
In this the constructor takes a string and splits it into a username, the bit before the @-sign, and the domain, the bit after the @-sign.
The EmailValidator
interface looks like this:
class EmailValidator:
def is_valid(self, address: EmailAddress) -> bool:
pass
Now we can work on our first validator, where we check whether our username has the minimumlength:
class UserNameValidator(EmailValidator):
_min_length: int = 0
def __init__(self, min_length: int):
self._min_length = min_length
def is_valid(self, address: EmailAddress) -> bool:
return len(address.user_name) >= self._min_length
Next we come to the DomainValidator
where check both the length of the domain and whether it is an allowed domain:
class DomainValidator(EmailValidator):
_min_length: int = None
_allowed_domains: list[str] = None
def __init__(self, min_length: int, allowed_domains: list[str]):
self._min_length = min_length
self._allowed_domains = allowed_domains.copy()
def is_valid(self, address: EmailAddress) -> bool:
return (len(address.domain) >= self._min_length) and (address.domain in self._allowed_domains)
Next we bring it all together in the EmailValidatorImpl
:
class EmailValidatorImpl:
_validators: list[EmailValidator] = None
def __init__(self, validators: list[EmailValidator]):
self._validators = validators.copy()
def is_valid(self, address: EmailAddress) -> bool:
for v in self._validators:
if v.is_valid(address):
continue
else:
return False
return True
All validators need to return true for our email-address to be valid. If one fails, we return false.
Testing
Let’s test our code:
if __name__ == "__main__":
my_email: EmailAddress = EmailAddress("test@example.com")
user_validator: EmailValidator = UserNameValidator(3)
domain_validator: EmailValidator = DomainValidator(3, ["example.com", "google.com"])
email_validator: EmailValidatorImpl = EmailValidatorImpl([user_validator, domain_validator])
print(f"Email is valid {email_validator.is_valid(my_email)}")
All we do here is create an emailadres, and a validator. Finally we check whether our email-address is valid according to the two validators we have.
Conclusion
Implementing business rules with the specification pattern in Python is straightforward and flexible. Adding new validators is easy; just write the validator and include it in the array. While our example focuses on simplicity, future enhancements could include more informative error messages, a topic for another post.