Search code examples
rubydryquirks-modeclass-extensions

What is the DRYest way to extend/patch a library in ruby?


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?


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