Abstracting out adapters - Dependency Injection

Is that not just shards? Granted I’m not super familiar with Ruby, but my assumption was it’s just their package manager for libs.

Not impossible, but deff requires extra boilerplate to properly handle. I.e. all the adapter logic. It also wouldn’t be out of the question to require the user to implement their own adapter/type based on some interface. This would remove the burden from you, but would ofc make it a touch harder to use. This could be a viable solution for cases where there are too many possible libraries you want to integrate with, or if they want to use a lib that you don’t provide a built in solution for.

Is this not the same thing as your library providing a default implementation of an interface also defined by your library? E.g.

def initialize(redis_client : Redis? = nil)
  @redis_client = redis_client || DefaultRedisClient.new
end

My main point of friction is [RFC] Standardized Abstractions, which could be solved right now by adding more interface modules to the stdlib, or even having a dedicated shard to contain them, and using them in certain places. This would not require any new compiler/language features.

As I mentioned in that other thread, PHP handles this via PHP Standards Recommendations - PHP-FIG , which define interfaces/requirements for various common libraries/implementations, such as eventing, caching, and HTTP clients, factories, and messages. They also have it somewhat easier for Redis in particular since there is only 1 implementation, the C extension.

Depending on the use case, it may not actually be a problem if we do something similar to PSR in that there is a more general CacheInterface that libraries could depend on, with each Redis implementation, implementing that interface. Granted this wouldn’t solve the issue of easily switching Redis implementations, but it might if you’re just using it power some more generic caching layer.

Close, but instead of monkey patching the external redis library yourself; you would (or require the user to) provide additional types(s) that wrap the external lib implementation to transform its methods to your interface.

Something like:

class Library::Adapter::RedisA
  include Library:RedisInterface

  def initialize(@redis : External::Redis::A); end

  # Define methods that map the API of the external shard to your API
  # Could also maybe leverage `delegate` if they're the same
  def del(key)
    @redis.delete key
  end

  # ...
end

As you pointed out, the main benefit of this is you are totally decoupled from both what underlying redis client you’re using, and insulated from changes within that lib. I.e. you can be as type strict as much as you want, as long as you do the required transformations to make each external redis shard adhere to your interface. E.g. using .as as needed or what have you.

The point in all of this is making it so your code only depends on the abstractions/interfaces it defines. Then it become super easy to plug in implementations other than redis if you really wanted as nothing says, unless you want it to be, that your Library::Config type depends on redis, but maybe just ConfigInterface or CacheInterface as i mentioned earlier.

The only gotchas with this approach is 1) the extra boilerplate required in your (or the user’s code), and 2) dealing with breaking changes in the underlying library that affect its public API and by extension your adapter. Tho, in this case you could either do version comparisons with macros, or just define another adapter for that version :person_shrugging:.

Also checkout [RFC] Standardized Abstractions, as it’s also related to decorating types.