The maze book for programmers!
mazesforprogrammers.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

Refactoring Net::SSH: Part 2

11 October 2004 — 3-minute read

This is the second in a series of articles that deal with refactoring the Net::SSH library to take advantage of dependency injection. The first was Net::SSH Refactoring Adventure.

Because I am using Syringe, and because the Syringe API has not yet solidified, refactoring Net::SSH to use Syringe is something of an iterative process. For example, I defined the services using one version of the Syringe API. Then, I implemented a suggestion from Eivind Eklund, adding a few new API’s to Syringe. This caused me to go back and change how the services were defined in Net::SSH, and so forth.

Original Interface

The original interface (which is still supported) was to call #register on the container every time one wanted to install a new service definition:

  container.register( :something ) { SomeImplementation.new }

This works great. It is actually the same syntax that Jim Weirich proposed in his article on dependency injection in Ruby, which inspired me to write Syringe in the first place.

However, Eivind pointed out that if you are registering many services in one place (which will often be the case), the “container.register” part starts detracting from the readability of your code. You wind up with something like this:

  container.register( :something ) { ... }
  container.register( :another ) { ... }
  container.register( :yet_another ) { ... }
  container.register( :foo ) { ... }
  container.register( :bar ) { ... }
  container.register( :baz ) { ... }
  ...

New Interface

So, Eivind proposed using an instance_eval‘d block, within which the service names are functions to which their construction blocks are attached directly. This seems to follow the DRY rule, as well.

This new interface is introduced via the register! method (note the bang at the end of the name).

  container.register! do
    something {...}
    another {...}
    yet_another {...}
    foo {...}
    bar {...}
    baz {...}
    ...
  end

Likewise, there are corresponding methods for registering libraries:

  Syringe::register_library! {...}
  Syringe::register_library_namespace! {...}

And also for declaring a namespace on a container:

  Container#namespace! {...}

Note the convention: bang methods in Syringe will denote the instance_eval‘d version of the method.

I should also note that the instance_eval is done on a proxy object, to prevent pollution of the Container namespace. The proxy object also defines convenience methods for service interception, requiring libraries, and namespace declaration:

  container.register! do
    something { ... }
    namespace :foo do
      something {...}
      ...
    end

    intercept( :something ).with { ... }
    require "some/syringe-ified/library'
  end

Application of the New Interface

So, in Net::SSH, there is a namespace called “net.ssh.transport.ossl”. This namespace (and its subnamespaces) includes all of the OpenSSL-specific functionality in Net::SSH. As such, it declares a lot of services—various factories, some Hashes for storing configuration information, etc. It was functional and fairly readable in the old syntax:

  Syringe.register_library_namespace( "net/ssh/transport/ossl/package", :ossl ) do |c|
    c.register( :hmac_algorithm_sources ) { Array.new }
    c.require( "/net/ssh/transport/ossl/hmac/package" )

    c.register( :cipher_names ) do
      Hash.new "3des-cbc" => "des-ede3-cbc",
               "blowfish-cbc" => "bf-cbc",
               ...
    end

    ...

    c.register( :cipher_factory, :model => :singleton_deferred ) do |c|
      require 'net/ssh/transport/ossl/cipher-factory'
      svc = Net::SSH::Transport::OSSL::CipherFactory.new( c.cipher_names )
      svc.identity_cipher = c.identity_cipher
      svc
    end

    c.register( :hmac_factory, :model => :singleton_deferred ) do |c|
      require 'net/ssh/transport/ossl/hmac-factory'
      Net::SSH::Transport::OSSL::HMACFactory.new( c.hmac_algorithm_sources )
    end

    ...
  end

However, converting it to the new syntax made it even better:

  Syringe.register_library_namespace!( 'net/ssh/transport/ossl/package', :ossl ) do

    hmac_algorithm_sources { Array.new }
    require 'net/ssh/transport/ossl/hmac/package'

    cipher_names do
      Hash.new "3des-cbc"     => "des-ede3-cbc",
               "blowfish-cbc" => "bf-cbc",
               ...
    end

    ...

    cipher_factory( :model => :singleton_deferred ) do |c|
      require 'net/ssh/transport/ossl/cipher-factory'
      svc = Net::SSH::Transport::OSSL::CipherFactory.new( c.cipher_names )
      svc.identity_cipher = c.identity_cipher
      svc
    end

    hmac_factory( :model => :singleton_deferred ) do |c|
      require 'net/ssh/transport/ossl/hmac-factory'
      Net::SSH::Transport::OSSL::HMACFactory.new( c.hmac_algorithm_sources )
    end

    ...
  end