I am working on a Go project where I am utilizing some rather big third-party client libraries to communicate with some third-party REST apis. My intent is to decouple my internal code API from these specific dependencies.
Decoupling specific methods from these libraries in my code is straightforward as I only need a subset of the functionality and I am able to abstract the use cases. Therefore I am introducing a new type in my code which implements my specific use cases; the underlying implementation then relies on the third-party dependencies.
Where I have a problem to understand how to find a good decoupling are configuration structs
. Usually, the client libraries I am using provide some functions of this form
createResourceA(options *ResourceAOptions) (*ResourceA, error)
createResourceB(options *ResourceBOptions) (*ResourceB, error)
where *ResourceA
and *ResourceB
are the server-side configurations of the corresponding resources after their creation.
The different options
are rather big configuration structs for the resources with lots of fields, nested structs, and so on. In general, these configurations hold more options then needed in my application, but the overall overlap is in the end rather big.
As I want to avoid that my internal code has to import the specific dependencies to have access to the configuration structs I want to encapsulate these.
My current approach for encapsulation is to define my own configuration structs which I then use to configure the third party dependencies. To give a simple example:
import a "github.com/client-a"
// MyClient implements my use case functions
type MyClient struct{}
// MyConfiguration wraps more or less the configuration options
// provided by the client-a dependency
type MyConfiguration struct{
Strategy StrategyType
StrategyAOptions *StrategyAOptions
StrategyBOptions *StrategyBOptions
}
type StrategyType int
const (
StrategyA StrategyType = iota
StrategyB
)
type StrategyAOptions struct{}
type StrategyBOptions struct{}
func (c *MyClient) UseCaseA(options *MyConfiguration) error {
cfg := &a.Config{}
if (options.Strategy = StrategyA) {
cfg.TypeStrategy = a.TypeStrategyXY
}
...
a.CreateResourceA(cfg)
}
As the examples shows with this method I can encapsulate the third-party configuration structs, but I think this solution does not scale very well. I already encounter some examples where I am basically reimplementing types from the dependency in my code just to abstract the dependency away.
Here I am looking for maybe more sophisticated solutions and/or some insights if my approach is generally wrong.
Further research from me:
I looked into struct embedding
and if that can help me. But, as the configurations hold non-trivial members, I end up importing the dependency in my calling code as well to fill the fields.
As the usual guideline seems to be Accept interfaces return structs
I tried to find a good solution with this approach. But here I can end up with a rather big interfaces as well and in the go standard library configuration structs seem not to be used via interfaces. I was not able to find an explicit statement if hiding configurations behind interfaces is a good practice in Go.
To sum it up:
I would like to know how to abstract configuration structs from third-party libraries without ending up redefining the same data types in my code.