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:
- scattering and tangling
- having to introduce a wrapper/ decorator for every interface
- 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