Decomposition
February 23, 2017When I come up against a problem of any non-trivial nature, I spend a lot of time on the decomposition of that problem into smaller problems. One such example is the work I'm doing with my team's API gateway.
We have a 4-tier system:
- CMS (Database to XML)
- API gateway (multiple protocols to protobuf)
- Web/Mobile API (pb to JSON)
- Presentation layer (JSON to HTML/JSON)
I'm focusing on our API gateway with some re-organization of the request and back-end fetch.
Untangling requests
In our case, tight coupling between fetch and response logic complicates the API translation layer along the back and front-end of the system. Focusing on extracting these two concepts into separate packages would simplify both.
I started the extraction of the fetching logic into a 'fetcher' package along-side the xml definition and translation logic. Eventually, all of this package will be one unit, vertically oriented on content type.
I worked through a first example using structs.Section
. I was able to piece together the essential work to get a Section
back, and wrote a FetchSection
that looked like this:
type Fetcher interface {
FetchSection(ctx context.Context, id interface{}) (*structs.Section, error)
}
// Impl. is a, fetcher struct, which held nothing, and is cast to Fetcher at creation to seal it.
I iterated on the pattern with other types and found that the API liked to get request parameters from http.Request
, which wasn't available inside the fetchers. After a few attempts to include http.Request
or the important data in the signatures, I ended up with an inconsistent interface.
I decided to go in the opposite direction and push the context and request terms into the fetcher on a per-request basis, resulting in something like the following:
type ETag string
type Fetcher interface {
FetchSection(id interface{}) (*structs.Section, ETag, error)
}
The ETag
in the return values comes from the need to pass caching information to the caching layer and reply with an ETag header. We previously returned a full response object that contained the original body of the server's reply. This was thrown away most of the time.
The caller now generates the Fetcher
as needed, binding its context and request.
fetchr := fetcher.NewFetcher(ctx, req)
// Code to fetch a section by id no longer includes context:
section, etag, err := fetchr.FetchSection(id)
// section is *structs.Section
I'm now in the middle of the slow process to migrate all types to the new system. I think this will be a bit easier to test as we have a nice border within which we can test the conversion of xml to our structures, and we have a nice cut-point where we can substitute in a dummy fetcher.