Better way to turn a ruby class into a module than using refinements?
Asked Answered
S

2

26

Module#refine method takes a class and a block and returns a refinement module, so I thought I could define:

class Class
  def include_refined(klass)
    _refinement = Module.new do
      include refine(klass) {
        yield if block_given?
      }
    end
    self.send :include, _refinement
  end
end

and the following test passes

class Base
  def foo
    "foo"
  end
end

class Receiver
  include_refined(Base) {
    def foo
      "refined " + super
    end
  }
end

describe Receiver do
  it { should respond_to(:foo) }
  its(:foo) { should eq("refined foo") }
end

So, using refinements, I can turn a class into a module, refine its behaviour on the fly, and include it in other classes.

  • Is there a simpler way to turn a class into a module in Ruby (say in ruby < 2)?
  • In the C-implementation of rb_mod_refine we see

    refinement = rb_module_new();
    RCLASS_SET_SUPER(refinement, klass);
    

    Is this just setting the superclass of refinement to klass that copies the implementation of the class inside the refinement module?

  • I am aware that multiple inheritance IS done via Modules, but what would the community think of the above Class#include_refined? Would it be reasonable to extract this aspect out of refinements? "Locally" patching inside a Class instead of using "using" switches to activate refinements?
Sorrows answered 31/8, 2013 at 17:44 Comment(7)
Sounds like an unusual solution. Is there any practical use cases in Rails?Nierman
@BillyChan Imagine I want to use all actions defined in some controller class defined in some gem (or Rails::Engine) in my controller, which of course inherits from my ApplicationController. Take Tolk::LocalesController for instance.Sorrows
I'm confused as to why you would want to do this. Why not define it as a module, and if you need to instantiate something with just those methods you can create a class that does nothing but include the modules.Musty
Why would you not just inherit from the controller class inside of that gem?Joashus
look at #23945893 andSwetiana
A class is a subclass of a module so to me trying to make it back into its parent seems over complicated. Your original problem statement to me seems to be the exact use case of a module vs classWinch
There’s a very good article about this: new-bamboo.co.uk/blog/2014/02/05/refinements-under-the-knife.Bulimia
S
4

I am happy indeed with Ruby 2.1 (and later) class-level "private" scope of refinements. My example above can be rephrased as:

# spec/modulify_spec.rb
module Modulify
  refine(Class) do
    def include_refined(klass)
      _refined = Module.new do
        include refine(klass) { yield if block_given? }
      end
      include _refined
    end
  end
end

class A
  def a
    "I am an 'a'"
  end
end

class B
  using Modulify

  include_refined(A) do
    def a
      super + " and not a 'b'"
    end
  end

  def b
    "I cannot say: " + a
  end
end

RSpec.describe B do
  it "can use refined methods from A" do
    expect(subject.b).to eq "I cannot say: I am an 'a' and not a 'b'"
  end
end

and suits as solution for the original problem.

Sorrows answered 29/3, 2015 at 12:45 Comment(0)
N
1

Andrea, thank you for the info in comment. Excuse my lack of knowledge to understand this is really necessary though it sounds doable as per your research.

I don't think we need to go so low level to do something in Rails.

If I'm going to do similar on Engine, I will try the following ideas, from easy to hard.

  1. In routes.rb, mount the whole engine in right route.

    I'm afraid this most common usage can't fit your need

  2. In routes.rb, Customize engine's route for specific controllers in application route.

    Devise, as an engine, can do easily. But I know not every engine could do this.

  3. In routes.rb, redirect specific or whole set of routes to engine's routes

  4. In your application's action, redirect to specific engine's action in application's action.

    This should be customized enough for specific action

    class FoosController < ApplicationController
      def foo
        redirect_to some_engine_path if params[:foo] == 'bar'
      end
    
  5. Inherit the engine's controller - for a set of actions, and if all above can't fit

    *The engine's classes are available in all application, you can inherit a controller from them, instead of normal ApplicationController.

    # class FoosController < ApplicationController
    class FoosController < BarEngine::BarsController
    

    *Since most engine's controller inherit from ApplicationController, this inheritance still allows you to use your own things from ApplicationController, no bad effect at all.

  6. If all above can't do, I can try to serve a customized locally or from my github repo.

In conclusion, the above should be able to solve most of cases, and I myself prefer #5 when possible and needed.

Nierman answered 1/9, 2013 at 23:55 Comment(1)
thanks for the answer but I didn't really mean to start a thread about rails. By the way, I would also prefer 5. As you point out 'most' engines controller (see Devise) inherit from ApplicationController but Tolk::ApplicationController (which I mention above) doesn't.Sorrows

© 2022 - 2024 — McMap. All rights reserved.