In this example we are going to study two strategy pattern implementations but both of them achieve the same outcome in a slightly different way. They simply create token for different banks.


Both strategy models are valid in real-world scenarios so the "duplicated solution" doesn't necessarily mean it is a bad design at all. It is just how you can handle such business case. Also both models have their strengths and weaknesses.



Both examples rely on the ProviderName parameter in request objects. Based on the given value:



All strategies handle same job in a different way


Structure


├── main.go
└── obie
├── access_token.go
├── barclays
│   ├── config.go
│   └── provider.go
├── natwest
│   ├── config.go
│   └── provider.go
└── provider.go

Files


main.go

package main

import (
"context"
"net/http"

"github.com/you/obie"
"github.com/you/obie/barclays"
"github.com/you/obie/natwest"
)

func main() {
barcPrv := barclays.NewProvider(barclays.Config{
TokenEndpoint: "https://barclays/token/endpoint",
ClientID: "barclays-client",
ClientSecret: "barclays-secret",
}, http.DefaultTransport)

natwPrv := natwest.NewProvider(natwest.Config{
TokenEndpoint: "https://natwest/token/endpoint",
ClientID: "natwest-client",
ClientSecret: "natwest-secret",
Scope: "natwest scope",
}, http.DefaultTransport)

provider := obie.NewProviderStrategy()
provider.Add(barclays.Name, barcPrv)
provider.Add(natwest.Name, natwPrv)

ctx := context.Background()

_, _ = accessToken(ctx, provider, obie.AccessTokenRequest{ProviderName: barclays.Name})
_, _ = accessToken(ctx, provider, obie.AccessTokenRequest{ProviderName: natwest.Name})
}

func accessToken(ctx context.Context, prv obie.Provider, req obie.AccessTokenRequest) (obie.AccessToken, error) {
return prv.AccessToken(ctx, req)
}

obie/access_token.go

package obie

type AccessTokenRequest struct {
ProviderName Name
}

type AccessToken struct {
AccessToken string
RefreshToken string
TokenType string
Scope string
ExpiresIn int
}

obie/provider.go

package obie

import (
"context"
"fmt"
)

type Name string

type Provider interface {
AccessToken(context.Context, AccessTokenRequest) (AccessToken, error)
}

type ProviderStrategy struct {
providers map[Name]Provider
}

func NewProviderStrategy() *ProviderStrategy {
return &ProviderStrategy{
providers: make(map[Name]Provider),
}
}

func (p *ProviderStrategy) Add(name Name, prv Provider) {
p.providers[name] = prv
}

func (p *ProviderStrategy) AccessToken(ctx context.Context, req AccessTokenRequest) (AccessToken, error) {
if _, ok := p.providers[req.ProviderName]; !ok {
return AccessToken{}, fmt.Errorf("access token: unknown provider: %s", req.ProviderName)
}

return p.providers[req.ProviderName].AccessToken(ctx, req)
}

obie/barclays/config.go

package barclays

type Config struct{
TokenEndpoint string
ClientID string
ClientSecret string
}

obie/barclays/provider.go

package barclays

import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"

"github.com/you/obie"
)

var Name obie.Name = "Barclays"

type Provider struct {
config Config
transport http.RoundTripper
}

func NewProvider(conf Config, trans http.RoundTripper) Provider {
return Provider{
config: conf,
transport: trans,
}
}

func (p Provider) AccessToken(ctx context.Context, req obie.AccessTokenRequest) (obie.AccessToken, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", p.config.ClientID)
data.Set("client_secret", p.config.ClientSecret)

httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)

body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))

// Send the request and handle the response
// b.transport.RoundTrip(httpReq)

return obie.AccessToken{}, nil
}

obie/natwest/config.go

package natwest

type Config struct{
TokenEndpoint string
ClientID string
ClientSecret string
Scope string
}

obie/natwest/config.go

package natwest

import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"

"github.com/you/obie"
)

var Name obie.Name = "Natwest"

type Provider struct {
config Config
transport http.RoundTripper
}

func NewProvider(conf Config, trans http.RoundTripper) Provider {
return Provider{
config: conf,
transport: trans,
}
}

func (p Provider) AccessToken(ctx context.Context, req obie.AccessTokenRequest) (obie.AccessToken, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", p.config.ClientID)
data.Set("client_secret", p.config.ClientSecret)
data.Set("scope", p.config.Scope)

httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
p.config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)

body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))

// Send the request and handle the response
// b.transport.RoundTrip(httpReq)

return obie.AccessToken{}, nil
}

All strategies handle same job in a same way


Structure


├── main.go
└── obie
├── access_token.go
├── client.go
├── config.go
└── refresh_token.go

Files


main.go

package main

import (
"context"
"net/http"

"github.com/you/obie"
)

func main() {
barclays := obie.ProviderConfig{
TokenEndpoint: "https://barclays/token/endpoint",
ClientID: "barclays-client",
ClientSecret: "barclays-secret",
}
natwest := obie.ProviderConfig{
TokenEndpoint: "https://natwest/token/endpoint",
ClientID: "natwest-client",
ClientSecret: "natwest-secret",
}

config := &obie.Config{
Providers: map[obie.ProviderName]obie.ProviderConfig{
obie.ProviderName("barclays"): barclays,
obie.ProviderName("natwest"): natwest,
},
}

ctx := context.Background()

client := obie.NewClient(config, http.DefaultTransport)

_, _ = client.AccessToken(ctx, obie.AccessTokenRequest{ProviderName: "barclays"})
_, _ = client.RefreshToken(ctx, obie.RefreshTokenRequest{ProviderName: "natwest", RefreshToken: "ref-tok"})
}

obie/config.go

package obie

type ProviderName string

type Config struct {
Providers map[ProviderName]ProviderConfig
}

type ProviderConfig struct {
TokenEndpoint string
ClientID string
ClientSecret string
}

obie/client.go

package obie

import (
"context"
"net/http"
)

type Client interface {
AccessToken(context.Context, AccessTokenRequest) (AccessToken, error)
RefreshToken(context.Context, RefreshTokenRequest) (AccessToken, error)
}

type ClientStrategy struct {
config *Config
transport http.RoundTripper
}

func NewClient(config *Config, transport http.RoundTripper) ClientStrategy {
return ClientStrategy{
config: config,
transport: transport,
}
}

obie/access_token.go

package obie

import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
)

type AccessTokenRequest struct {
ProviderName ProviderName
}

type AccessToken struct {
AccessToken string
RefreshToken string
TokenType string
Scope string
ExpiresIn int
}

func (c ClientStrategy) AccessToken(ctx context.Context, req AccessTokenRequest) (AccessToken, error) {
config := c.config.Providers[req.ProviderName]

data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", config.ClientID)
data.Set("client_secret", config.ClientSecret)

httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)

httpReq.Header.Set("Cache-Control", "no-store")
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Pragma", "no-cache")

body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))

// Send the request and handle the response
// c.transport.RoundTrip(httpReq)

return AccessToken{}, nil
}

obie/refresh_token.go

package obie

import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
)

type RefreshTokenRequest struct {
ProviderName ProviderName
RefreshToken string
}

func (c ClientStrategy) RefreshToken(ctx context.Context, req RefreshTokenRequest) (AccessToken, error) {
config := c.config.Providers[req.ProviderName]

data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", config.ClientID)
data.Set("client_secret", config.ClientSecret)
data.Set("refresh_token", req.RefreshToken)

httpReq, _ := http.NewRequestWithContext(
ctx,
http.MethodPost,
config.TokenEndpoint,
bytes.NewReader([]byte(data.Encode())),
)

httpReq.Header.Set("Cache-Control", "no-store")
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Pragma", "no-cache")

body, _ := httputil.DumpRequest(httpReq, true)
fmt.Println(string(body))

// Send the request and handle the response
// c.transport.RoundTrip(httpReq)

return AccessToken{}, nil
}