
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.