1

I have a ruby class which contains a lot of logic which will no longer be used but I need to keep around (for a while at least) for backwards-compatibility.

My plan is to move these methods to a module like LegacyStuff and include it in the class. I'm wondering if there's a neat way to add something such that when any method in the module is called, a warning is generated, without having to actually add warn statements to every individual method body.

I guess what I'm looking for is behaviour like a 'before call' or 'after call' hook on a whole module. I guess the sub-question here is "is this even a good idea?"

Ben Hull
  • 7,524
  • 3
  • 36
  • 56
  • 1
    *"is this even a good idea?"* -- The fact that you have one module so big that this question has even occurred to you is the real problem IMO! At least you're changing how it works, now... – Tom Lord Jun 19 '18 at 11:55
  • You could probably do it with some `method_added` magic. – Tom Lord Jun 19 '18 at 11:56
  • 1
    You can (heavily) adapt my answer for another question https://stackoverflow.com/a/50908899/681520 to do this, at least I think it should give you some tips. – Kimmo Lehto Jun 19 '18 at 12:20
  • "is this even a good idea?" - it's not a bad idea. The concrete implementation might be bad, but the idea is fine. – Sergio Tulentsev Jun 19 '18 at 12:40
  • It's a fair point, @TomLord, though it was mostly just me trying to avoid repeating `warn` throughout the module if I didn't have to. In this case, there're only a dozen methods or so in this legacy module. Also, can you believe I've been writing ruby for 10 years and never used `method_added`!? Very useful - thanks! – Ben Hull Jun 19 '18 at 13:19
  • 1
    @Beejamin "I've been writing ruby for 10 years and never used method_added" - me too. Unless you're into heavy MP, you don't need it at all. :) – Sergio Tulentsev Jun 19 '18 at 13:40
  • Yeah, you can do some [pretty crazy stuff with `method_added`](https://github.com/tom-lord/search_warrant/blob/6e144eb4c15a1aa2530f04bcb172b0cbdb4bb4ed/lib/search_warrant.rb#L38-L67) :) – Tom Lord Jun 19 '18 at 15:01
  • @TomLord yeah as long as you make sure to have an appropriate guard clause `return if @_adding_a_method` when using `define_method` inside `method_added` otherwise you end up with a horrible recursive call :) – engineersmnky Jun 19 '18 at 15:24
  • 2
    @engineersmnky Using `Module#prepend` makes it much easier to do that sort of thing. I intentionally wrote that code *without* `Module#prepend` as a key part of the challenge :) – Tom Lord Jun 19 '18 at 15:39

2 Answers2

4

While this is not exactly what you asked ("mark all methods at once") and there probably exists something like this in ruby itself or big frameworks like rails, it might still be useful to somebody.

module DeprecatedMethods
  def deprecated(method_name)
    prepend(Module.new do
      define_method method_name do |*args|
        puts "calling deprecated method #{method_name}"
        super(*args)
      end
    end)
  end

end

module AncientCode
  extend DeprecatedMethods

  deprecated def foo # selectively mark methods as deprecated
    puts "doing foo"
  end

  def bar
    puts "doing bar"
  end
end

class Host
  include AncientCode
end

host = Host.new
host.foo
host.bar
# >> calling deprecated method foo
# >> doing foo
# >> doing bar

Currently this injects a module per deprecated method, which is kinda wasteful. You can put all deprecated methods in one module, by doing something like this:

deprecated_methods :foo, :bar
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
2

Code

First create a module containing a single module method, setup. This module could be put in a file that is required as needed. This module method will be invoked from a module containing instance methods to be included in a given class. Using aliases, it modifies those instance methods to print a message (containing the method name) before executing the remainder of its code.

Module AddMessage

module AddMessage
  def self.setup(mod, msg_pre, msg_post)
    mod.instance_methods(false).
        each do |m|
          am = "_#{m}".to_sym
          mod.send(:alias_method, am, m)
          mod.send(:private, am)
          mod.send(:define_method, m) do |*args, &block|
            puts "%s %s %s" % [msg_pre, m, msg_post] 
            send(am, *args, &block)
          end
        end
  end
end

Module to be included in class

module LegacyStuff
  def old1
    "hi"
  end
  def old2(a, b)
    yield(a, b)
  end
  AddMessage.setup(self, "Warning:", "is deprecated")
end

AddMessage::setup is passed three arguments, the name of the calling module and a message prefix and message suffix that are used to form the warning message. When an instance method m in this module is executed by a class instance the message "Warning: #{m} is deprecated" is printed (e.g., "Warning: old1 is deprecated") before the remaining calculations are performed.

Use

LegacyStuff is simply included by a class.

class C
  include LegacyStuff
  # <constants, methods and so forth go here>
end

c = C.new
c.old1
  # Warning: old1 is deprecated
  #=> "hi"
c.old2(1,2) { |a,b| a+b }
  # Warning: old2 is deprecated
  #=> 3
c.cat
  #=> NoMethodError: undefined method `cat' for #<C:0x000000008ef0a8>

Explanation of the module method AddMessage:setup

The following (of the generally less-familiar) methods are used by this method: Module#instance_methods, Module#alias_method, Module#private and Module#define_method.

The following three steps are performed for each instance method m defined in module mod (e.g., elements of the array LegacyStuff.instance_methods(false)).

mod.send(:alias_method, am, m)

Create an alias am of the method (e.g., _:old1 for old1).

mod.send(:private, am)

Make the alias am a private method (e.g., _old1).

mod.send(:define_method, m) do |*args, &block| ... end

Redefine the method m (e.g., old1) to print the indicate string and then execute the alias am (e.g., _old1).

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100