Decoding Design: Exploring the Specification Pattern in Go for Powerful Code Composition

Photo by Karol D: https://www.pexels.com/photo/close-up-of-a-cable-car-323645/

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 Go

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.

package main

import (
	"fmt"
	"strings"
)

type EmailValidator interface {
	IsValid(EmailAddress) bool
}

We also create the EmailAddress struct, representing the email’s domain and username. A function is provided to create this struct from a given email string.

type EmailAddress struct {
	Domain   string
	UserName string
}

func createEmailFromString(email string) (EmailAddress, error) {
	elements := strings.Split(email, "@")
	if len(elements) != 2 {
		return EmailAddress{}, fmt.Errorf("Invalid email address")
	} else {
		newEmail := EmailAddress{
			Domain:   elements[1],
			UserName: elements[0],
		}
		return newEmail, nil
	}
}

Next, we introduce the UserNameValidator and DomainValidator, both implementing the EmailValidator interface. UserNameValidator checks if the username meets a minimum length requirement, while DomainValidator verifies the domain’s length and its presence in a list of allowed domains.

type UserNameValidator struct {
	MinLength int
}

func (unv UserNameValidator) IsValid(address EmailAddress) bool {
	return len(address.UserName) >= unv.MinLength
}

type DomainValidator struct {
	MinLength      int
	AllowedDomains []string
}

func (dv DomainValidator) IsValid(address EmailAddress) bool {
	if len(address.Domain) < dv.MinLength {
		return false
	}

	domainElements := strings.Split(address.Domain, ".")
	if len(domainElements) == 0 {
		return false
	}
	tld := domainElements[len(domainElements)-1]
	for _, domain := range dv.AllowedDomains {
		if domain == tld {
			return true
		}
	}
	return false
}

Combining everything, we create the EmailValidatorImpl struct, which holds an array of validators. The IsValid() method iterates over these validators, returning true only if all validations pass.

type EmailValidatorImpl struct {
	validators []EmailValidator
}

func (evi EmailValidatorImpl) IsValid(address EmailAddress) bool {
	for _, validator := range evi.validators {
		if !validator.IsValid(address) {
			return false
		}
	}
	return true
}

Testing

We demonstrate the code’s functionality by creating an email, setting up validators, and using EmailValidatorImpl to check if the email adheres to the specified rules.

func main() {
	myEmail, err := createEmailFromString("test@example.nl")
	if err != nil {
		fmt.Println(err)
		panic("Wrong email")
	}

	userValidation := UserNameValidator{
		MinLength: 3,
	}

	domainValidation := DomainValidator{
		MinLength:      6,
		AllowedDomains: []string{"nl", "com"},
	}

	validators := []EmailValidator{userValidation, domainValidation}

	emailValidator := EmailValidatorImpl{
		validators: validators,
	}

	if emailValidator.IsValid(myEmail) {
		fmt.Println("EmailAddress is OK")
	} else {
		fmt.Println("EmailAddress is not OK")
	}
}

Conclusion

Implementing business rules with the specification pattern in Go 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 *