Question

I wonder what's the best way to write a modular extension of an existing library in ruby that alters existing methods. It should not introduce repetition of code and should be used on demand only.

The specific task I'm trying to accomplish is extending ruby's Net::FTP module for support of some not so standards compliant servers. Such an extension should be completely seperated from the standards compliant library IMHO.

I thought requiring an additional file would be quite nice since that would not even pose the necessity for some kind of switch in the original code. So an additional require 'net/ftp/forgiving' would make the original library a bit more forgiving regarding our less gifted FTP server fellows.

The relevant file can then make use of ruby's open class and module architecture to patch the FTP class. For fixing the example of quirky behavior linked above I would need to patch Net::FTP#mkdir. which would look like this:

#content of net/ftp/forgiving
require 'net/ftp'

module Net
  class FTP

    # mkdir that will accept a '250 Directory created' as a valid response
    def mkdir(dirname)
      begin
        original_mkdir(dirname)
      rescue FTPReplyError => e
        raise unless e.message.start_with? '250 Directory created'
        return ""
      end
    end

  end
end

However this would require to somehow cache away the original Net::FTP#mkdir as Net::FTP#original_mkdir to keep the code DRY. Is this possible? Do you have any further suggestions on how to improve this method of patching/extending? Or maybe even completely different approaches?

Was it helpful?

Solution

This is called "monkeypatching" and is exactly the use case that alias_method was made for:

alias_method :original_mkdir, :mkdir
def mkdir(dirname)
  begin
    original_mkdir(dirname)
  rescue FTPReplyError => e
    raise unless e.message.start_with? '250 Directory created'
    return ""
  end
end

Although this is an often-seen "idiom" in Ruby, this will break existing code (maybe even code inside Net) that relies on mkdir raising an exception in this case. You can't limit these changes to files which require 'net/ftp/forgiving' only. Thus, it would be much cleaner to create a subclass rather than open up the original class:

module Net
  class ForgivingFTP < FTP
    # mkdir that will accept a '250 Directory created' as a valid response
    def mkdir(dirname)
      begin
        super(dirname)
      rescue FTPReplyError => e
        raise unless e.message.start_with? '250 Directory created'
        return ""
      end
    end
  end
end

Or even better, place it in a custom namespace! A good rule of thumb is:

subclass when possible, monkeypatch when necessary.

(Thanks to @tadman for this). In this case it doesn't seem to be necessary.

UPDATE: Following up on your comment, if you want to extend only a specific instance the Net::FTP class, you can extend their singleton classes:

obj = Net::FTP.new
class << obj
  alias_method :original_mkdir, :mkdir
  def mkdir(dirname)
    #...
    original_mkdir(dirname)
    #...
  end
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top