A Ruby library for building and implementing interfaces.
Ruby Shell
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.githooks/pre-commit
lib
spec
.gitignore
.travis.yml
Gemfile
LICENSE.txt
README.md
Rakefile
implements.gemspec

README.md

Implements

Implements is a tool for building modular libraries and tools as interfaces, for implementing those interfaces, and ensuring that consumers are able to load the best available implementation at runtime.

Installation

Add this line to your application's Gemfile:

gem 'implements'

And then execute:

$ bundle

Or install it yourself as:

$ gem install implements

Usage

Implements was created as a dependency of my redis-copy gem, which provides multiple implementations of each of multiple interfaces in order to provide support for new features in redis, while falling back gracefully (sometimes multiple steps) to less-optimal implementations when the underlying support is not present.

The goal of the implements gem in particular is to provide an implementation registry that is attached to the interface, and can be used to provide the best-possible implementation for a given scenario. It also allows third-party libraries to provide their own implementations of an interface without having to touch the library that contains their upstream interface.

Below you will find a simplified example:

require 'implements/global'

module RedisCopy
  module KeyEmitter
    extend Implements::Interface

    # @param redis_connection [Object]
    # @return [void]
    def intialize(redis_connection)
      @redis = redis_connection
    end

    # @param keys [String] - ('*') a glob-ish pattern
    # @return [Enumerable<String>]
    def keys(pattern = '*')
      raise NotImplementedError
    end

    # ...

    class Default
      implements KeyEmitter

      def keys(pattern = '*')
        @redis.keys(pattern)
      end
    end

    class Scanner
      # note how a block is given to `implements`.
      # this block is called with the class' initialize arguments
      # to determine whether or not this implementation is compatible
      # with the input and its state before initializing the object.
      implements KeyEmitter do |redis_connection|
        bin_version = Gem::Version.new(redis_connection.info['redis_version'])
        bin_requirement = Gem::Requirement.new('>= 2.7.105')

        break false unless bin_requirement.satisfied_by?(bin_version)

        redis_connection.respond_to?(:scan_each)
      end

      def keys(pattern = '*')
        @redis.scan_each(match: pattern, count: 1000)
      end
    end
  end
end

The consumer of this interface, then, can get the best available implementation, given their environment and the object(s) passed to #initialize, without having to know anything about the implementations themselves:

source_redis = Redis.new(port: 9736) # a scanner-compatible redis process (>= 2.7.105)
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
# => <RedisCopy::KeyEmitter::Scanner: ... >
key_emitter.keys('schedule:*').to_enum
# => <Enumerator ...>

source_redis = Redis.new(port: 9737) # a scanner-incompatible redis process (< 2.7.105)
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
# => <RedisCopy::KeyEmitter::Default: ... >
key_emitter.keys('schedule:*').to_enum
# => <Enumerator ...>

The consumer can choose to favor a particular implementation by name:

key_emitter = RedisCopy::KeyEmitter.implementation(:scanner).new(source_redis)
# => <RedisCopy::KeyEmitter::Scanner: ... >

And if a compatible implementation cannot be found, an appropriate exception is raised:

key_emitter = RedisCopy::KeyEmitter.implementation(:scanner).new(source_redis)
# Implements::implementation::NotFound: no compatible implementation for RedisCopy::KeyEmitter>

The implementation finder assumes that implementations loaded later are somehow better than those loaded before them, but a consumer can specify first preference and fallback groups:

key_emitter = RedisCopy::KeyEmitter.implementation(:scanner, :auto).new(source_redis)
# => <RedisCopy::KeyEmitter::Default: ... >

And implementations can be added which are not in the auto load-order and have to be explicitly asked for:

# Like this insane whack-a-mole implementation,
# Which we wouldn't want anyone to accidentally use:
class RedisCopy::KeyEmitter::WhackAMole
  Implements RedisCopy::KeyEmitter, auto: false

  def keys(pattern = '*')
    return enum_for(__method__, pattern) unless block_given?
    while(key = redis.randomkey)
      yield key if glob_match?(pattern, key)
    end
  end

  # ...
end
key_emitter = RedisCopy::KeyEmitter.implementation(:whack_a_mole).new(source_redis)
# => <RedisCopy::KeyEmitter::WhackAMole: ... >
# But it doesn't come back unless you ask for it.
key_emitter = RedisCopy::KeyEmitter.implementation.new(source_redis)
# => <RedisCopy::KeyEmitter::Scanner: ... >

TODO:

  • Provide tools for testing all implementations of an interface.
  • Finalize syntax for the check. A block alone is convenient, but not clear.
  • Finalize scope of check. Allocate and instance_exec? Run all as hooks before #initialize?

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request