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.