Refactoring Net::SSH: Part 2
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