Reputation: 20968
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?
Upvotes: 1
Views: 315
Reputation: 95308
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
Upvotes: 4