Easy Mastery: Unleashing the Power of Python Specifications

Photo by Pixabay: https://www.pexels.com/photo/house-floor-plan-271667/

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.

Leave a Reply

Your email address will not be published. Required fields are marked *