Essential RubyOnRails patterns — part 3: Clients and Wrappers
as seen by RubyOnRails Developers @ Selleo
Clients and Wrappers (also referred to as “Facades”) are remarkably useful tools for working with external services, libraries or APIs. While both Clients and Wrappers are primarily used to facilitate usage of before-mentioned libraries or APIs, there are some significant differences in responsibilities each of these patterns have. In some cases a Wrapper can take over a Client’s responsibilities too.
When to use Clients ?
Clients are best suited for handling an external or internal services API — be it for JSON API, GraphQL, SOAP or any other custom format or protocol. Instead of communicating with such service using low level libraries directly, (i.e. Faraday or HTTParty) we do provide a thin interface around it in the form of a Client.
When to use Wrappers ?
Wrappers (Facades) work best as a layer encapsulating other library with complex interface or the one that is not very expressive, especially in the domain of our application. Wrappers are also very useful whenever default responses provided by aforementioned libraries are not very useful for us in their original form.
How to make most out of Client pattern?
# 1 Methods should reflect wrapped endpoints
If API we provide abstraction over exposes
/getNewProducts endpoint, then our Client should resemble it by exposing
get_new_products method. It is also a good idea to accept as many arguments to such method as original endpoint handles. In general, each Client method should allow as many capabilities of wrapped endpoint as reasonable.
# 2 Results should reflect responses of wrapped endpoints
Similarly to the previous rule, the results we return from methods wrapping external endpoint should resemble what this endpoint returns. It is ok to perform some lightweight transformations like JSON parsing or changing hash keys from camelized to underscored, but we should be cautious with adding any additional logic.
# 3 Provide references to wrapped endpoint
It might be good to provide a quick reference to documentation of wrapped endpoint above each methods that handles it. This way a user of such method can quickly review its specification and obtain information about potential capabilities (i.e. ways to filter results).
# 4 Provide expressive naming for Client classes
It is good convention to include name of service or API being wrapped in the class name. Also suffixing such class with
Client will clearly show what the responsibility of this class is.
AirbnbClient are examples of good Client class names.
# 5 Keep Clients isolated from your main app
Treat Clients as something that can be shared with different app without any additional effort. This basically means, that Clients should possess zero knowledge about the app itself and serve only as an interface to the external resources.
# 6 Provide an instance Client users can work with
As it is not uncommon to work with multiple Client methods simultaneously, refrain yourself from implementing those methods as static. For most of the time using an external service is correlated with performing some kind of authentication. Allowing users to instantiate a Client will make it possible to share authenticated session between multiple calls.
# 7 Do not reinvent the wheel
Last but not least, before implementing a Client, make sure that there is no existing library already providing a Client for given service. Most of popular services have Clients for various programming languages available and ready to use.
How to make most out of Wrapper pattern?
# 1 Prefer wrapping other libraries
While Wrappers can sort of replace Clients in cases we need one or two endpoints handled, it is usually a better idea to keep Wrapper’s and Client’s logic separate. A rule of a thumb here is to think about Wrapper as an encapsulation of other library to facilitate interaction with this library, making it more expressive and better suited for our purposes. For instance, when writing tests it is much easier to mock something like
S3Wrapper.get_file(“file_identifier”) instead of investigating which Client to choose and how to use it. This will need to happen anyway when writing Wrapper tests, but such approach works great when proceeding with Behavior Driven Development.
# 2 Provide expressive naming for public methods
In Client’s case it was important to keep naming of public interface methods as similar to handled endpoints as possible. For Wrappers it is the opposite — naming should reflect our needs and be as meaningful as possible, with the list of easy to understand arguments. In fact this is the main responsibility of Wrappers — simplification of interfaces.
# 3 Provide expressive naming for wrapper classes
Just as in case of Clients, it is good convention to include names of libraries or services being wrapped in the Wrapper’s class name and suffixing it with
Wrapper. Some adequate names would be
S3Wrapper. Another option is to use namespacing, i.e.
S3::Wrapper if we need a place for more classes correlated with wrapper, yet we need to be cautious not to get into naming-conflict with a library being wrapped.
# 4 Prefer using static methods
As Wrapper tends to be use-and-forget kind of tool it is a good idea to expose its capabilities as static methods.
S3Wrapper.new.get_file(file_identifier) can usually be easily replaced with
S3Wrapper.get_file(file_identifier). If we internally still want to work with an instance, we can use Singleton pattern to make our life easier (even though “Singleton” and “Easy life” can be rarely seen together in same sentence) and then delegate exposed methods to singleton’s instance on class level.
# 5 Design responses in the useful way
In opposition to Clients, where responses were to reflect actual responses from service being handled as closely as possible, Wrappers should return responses as close to our needs as possible. Any kind of data formatting or transformations can happen at this point. Thanks to that, when writing higher level tests, we can plan our solution using the data structures we feel the most comfortable working with.
# 6 Keep Wrappers isolated from your main app
Even though Wrappers are to be designed to simplify interfaces of other services / libraries and return highly usable responses, don’t yield to temptation of leaking your app details into Wrapper’s implementation (i.e. using models that are outside of Wrapper’s scope). If it happens incidentally, we can probably live with that, but it is better to follow the rule of not allowing that at all. Not introducing such dependencies will render wrappers more dependable upon.
If we ever need to simplify the use of any kind of library, be it for improving decomposition, testability or purely for making nice, readable API, Wrappers can help a lot in that effort. If it happens that there are no dedicated libraries for services we integrate with, Wrappers paired with Clients can be a great addition to our toolbox that will facilitate working with such services.