Introduction
So, One of the biggest issues when designing a library/framework Is when you need to make a call to an external dependency.
Specifically in the context of this post, when that dependency is no longer usable for any reason, what do you do? Most would then need to implement a new interface for a new dependency, and then as a result potentially make breaking changes to your API to accommodate which isn't good.
I bring this up because I see all to often libraries that directly call the external dependency in there API methods (I'm guilty of this too -_-), then have to completly change there API when the dependency changes that requires data to be structured differently.
This then breaks things for many people.
This is why I advocate separating the API and the external calls into separate classes, it does add complexity and increases the amount of code but the advantage is that when you're external dependency changes, your API doesn't, and nor does it for anyone using you're libraries.
When I think to do this (ahem) I like to separate the logic into:
API: The API of your library
Provider: The logic that calls your an external dependency
Formatter: If needed, the logic that handles data formatting between the API and the Provider
Basic Provider Logic
Now you can do this any way you like, so long as there is a clear separation of concerns. Using ruby as an example, I like to take advantage of the fact you can define def self.[]()
to make my provider API easy to use so I can do somthing like this:
Module Providers
class << self
attr_writer :listing
def [](provider)
@listing[provider]
end
end
class Provider_Base
def self.inherited(child, name = child.class.to_s.downcase.to_sym)
# Normally I'd add logic to ensure that only this method can add to
# `Providers.listing`, but I can't be arsed for this example
Providers.listing[name] = child
end
end
end
The reason I do name = child.class.to_s.downcase.to_sym
in the function declaration is so that if you want you can easily change the way the name is set by doing:
Module Providers
class OtherProviderBase < Provider_Base
def self.inherited(child)
super(child, :some_other_naming_mechaism)
end
end
end
Now you can inherit from OtherProviderBase
instead.
Using Your provider
Using the providers in you're api becomes super easy now:
class API
PROVIDER_NAME = :provider_name
def some_method
Providers[PROVIDER_NAME].some_method
end
end
Why do it like this? Lets say you build a new dependency provider, you can just change the provider to this:
class API
PROVIDER_NAME = :other_provider_name
def some_method
Providers[PROVIDER_NAME].some_method
end
end
End result is that the API doesn't change and won't break for you of your users.
And using the above providers example, you can add formatters like this:
class API
PROVIDER_NAME = :provider_name
def some_method
Formatters[PROVIDER_NAME].some_method(
Providers[PROVIDER_NAME].some_method)
end
end
You can even go further and do this:
class API
PROVIDER_NAME = :provider_name
def some_method(*args)
method = :some_method
output = Providers[PROVIDER_NAME].send(method, (Formatters::Input[PROVIDER_NAME].send(method,*args))
Formatters::Output[PROVIDER_NAME].send(method, output)
end
end
Provider Piplines
You could even provide a mechanism (where appropriate of course) to automatically pipeline, like this maybe:
module Pipelines
attr_reader :listing
def pipline_for(action, *pipes)
@listing[action] = pipes
define_method(action, do) |*args|
run_pipline_for(action, *args)
end
end
def self.extended(child)
child.include Instance_Methods
end
module Instance_Methods
def run_pipline_for(action, *args)
Pipelines.listing[action].inject(args) do |result, (item, custom_action)|
item[PROVIDER_NAME].send(custom_action || action, *result)
end
end
end
end
class API
extend Pipeline
PROVIDER_NAME = :provider_name
pipline_for :action, Formatters::Input, [Providers, :get_remote_data], Formatters::Output
pipline_for :other_action, Formatters::Input, Providers, [Processors, :differing_name], [Formatters::Output, :custom_output]
end
Sorry, this probably out of scope for this post but couldn't help myself when I saw somthing interesting to code. Also I'm not sure if
PROVIDER_NAME
will be picked up in the local context of theAPI
class whenPipeline
is included so you might need to code a mechanism for setting and retrieval that does but you get the idea.
conclusion
By separating your external dependency into it's logic that is then called by your API, you can then keep your API constant between versions and then the only difference between versions becomes the dependencies, without any breaking changes between them.
p.s. I completly admit to not having checked or tested any of this code as it was just examples to get the ball rolling.
Top comments (2)
If someone is interested in a functional pipline example:
You're not wrong, I would normally add insurance to insure that the output or the next output is returned but as I said at the bottom of my post, I never actually tested the code and was never intended to be properly used, merely ideas and examples to get the ball rolling, which is why I made it simple!