I have a service object similar to the one shown below which is exposed through HTTP:
type ComputeService struct {
}
func (svc ComputeService) Compute(userType string, data Data) (Result, error) {
// rate limit based on userType (both check and increment counter)
// if not rate limited, compute and return result
}
Now, as you can see rate-limiting needs to be done based on userType
. Implementation of rate limiter itself changes according to userType
. I am thinking of 2 approaches of resolving the rate limiter using userType
.
// First approach: for each request new ComputeService object is
// created after resolving RateLimiter using userType in the handler
// itself
type ComputeService struct {
RateLimiter RateLimiter
}
// Second approach: ComputeService object is initialized during startup
// and only RateLimiter is resolved with every Compute() call by calling
// the provider function
type ComputeService struct {
RateLimiterProvider func(userType string) RateLimiter
}
Both are testable. Which one would be preferable ? I'm leaning towards the 2nd approach since using it would mean that the handler will be purely read request, delegate to service, write out the response whereas in 1st approach, handler would contain additional step of resolving the rate limiter implementation.
If you are using a DI system like Dargo you can use its Provider injection to dynamically choose the implementation at runtime.
In that case your services would look something like the following:
import "github.com/jwells131313/dargo/ioc"
type RateLimiter interface {
}
type UserType1RateLimiter struct {
}
type UserType2RateLimiter struct {
}
type ComputeService struct {
RateLimiterProvider ioc.Provider `inject:"RateLimiter"`
}
func (svc ComputeService) Compute(userType string, data Data) (Result, error) {
// rate limit based on userType (both check and increment counter)
// if not rate limited, compute and return result
raw, err := svc.RateLimiterProvider.QualifiedBy(userType).Get()
if err != nil {
return nil, err
}
limiter := raw.(RateLimiter)
//...
}
This is how you would bind these into the ServiceLocator:
func initializer() {
serviceLocator, _ = ioc.CreateAndBind("ExampleLocator", func(binder ioc.Binder) error {
binder.Bind("RateLimiter", UserType1RateLimiter{}).QualifiedBy("UserType1")
binder.Bind("RateLimiter", UserType2RateLimiter{}).QualifiedBy("UserType2")
binder.Bind("ComputeService", ComputeService{})
return nil
})
}
This only applies when you are using something like Dargo, but it still might be useful in your case.
If you are not using Dargo it seems to me to be a matter of opinion, although I personally would choose the second approach