samedi 29 mai 2021

Implementing traits/ advices for cross cutting concerns, the Golang way

I'm implementing my first microservice in golang. Services currently look something like this:

type UserRegistrationService struct {
    repo UsersRepository
}
func (ur UserRegistrationService) Execute(cmd RegisterUserCommand) User {
    usr := NewUser(cmd.Username)
    ur.repo.AddUser()
    return usr
}
func serviceClient() {
    usr := NewUserRegistration(NewPgUserRepo()).Execute(RegisterUserCommand{Username: "Mr. Crowly"})
    fmt.Printf("user %v was added successfully", usr)
}

Say I wanted to log at the start of every service and add an audit record after the execution. Here are the options I thought of:

Option1: Calling "advice" functions around the service

The simplest solution would be literally calling a before() and after() functions. In the following example, I'm injecting these functions as hooks into the service.

func (ur UserRegistrationService) Execute(cmd RegisterUserCommand, before func(RegisterUserCommand), after func(RegisterUserCommand)) User {
    before(cmd)
    defer after(cmd)
    usr := NewUser(cmd.Username)
    ur.repo.AddUser(usr)
    return usr
}
func serviceClient() {
    before := func(cmd RegisterUserCommand) {
        fmt.Printf("executing %v", cmd)
        //do more stuff before
    }
    after := func(cmd RegisterUserCommand) {
        AuditJournal(cmd)
        //do other stuff after
    }
    usr := NewUserRegistration(NewPgUserRepo()).Execute(RegisterUserCommand{Username: "Mr. Crowly"}, before, after)
    fmt.Printf("user %v was added successfully", usr)

}

As you can see, we have the 100 years old problem of cross cutting concerns, that is "scattering and tangling"

Option 2: Delegating cross-cutting concerns to a "service executor"

Instead of directly calling services, I can pass the (ready to be executed) service to an "executor" which can call before and after functions as follows:

type Command interface {
    //empty interface anti-pattern
}
type Entity interface {
    // again
}

type Service interface {
    Execute(Command) Entity
}

func (ur UserRegistrationService) Execute(cmd Command) Entity {
    typedCmd := cmd.(RegisterUserCommand)
    user := NewUser(typedCmd.Username)
    ur.repo.AddUser(user)
    return user
}
func serviceClient() {
    usr := ExecuteWithAdvice(RegisterUserCommand{Username: "Mr. Crowly"}, NewUserRegistration(NewPgUserRepo()))
    fmt.Printf("user %v was added successfully", usr.(User).Name)
}
func ExecuteWithAdvice(cmd Command, svc Service) Entity {
    before(cmd)
    defer after(cmd)
    return svc.Execute(cmd)
}
func before(cmd Command) {
    fmt.Printf("executing %v", cmd)
}
func after(cmd Command) {
    AuditJournal(cmd)
}

This is a better solution in my opinion, but it forces the use of empty interfaces (See the Command and Entity) and therefore, I need type assertions (see line 2 in the serviceClient and line 1 in the Execute method).

Option 3: Introducing an interface for the service, and returning a decorator from the constructor

type UserRegistrationServiceInterface interface {
    Execute(cmd RegisterUserCommand) *User
}
func NewUserRegistration(repo UsersRepository) UserRegistrationServiceInterface {
    return &UserRegistrationServiceWithAdvice{internalService:&UserRegistrationService{repo: repo} }
}
type UserRegistrationServiceWithAdvice struct {
    internalService UserRegistrationServiceInterface
}
func (u UserRegistrationServiceWithAdvice) Execute(cmd RegisterUserCommand) *User {
    before(cmd)
    defer after(cmd)
    return u.internalService.Execute(cmd)
}

The only problem with this approach is the need to introduce an interface and a decorator for each and every service and have the constructor return the nested decorator struct.

Is there a way to decorate my services with cross cutting concerns logic without:

  1. scattering and tangling
  2. having to introduce a wrapper/ decorator for every interface
  3. using empty interfaces with type assertions

If there's no solution that matches the above requirements, which of the previous solutions is the most idiomatic way in Go?

Aucun commentaire:

Enregistrer un commentaire