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
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
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
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_NAMEwill be picked up in the local context of the
Pipelineis included so you might need to code a mechanism for setting and retrieval that does but you get the idea.
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.