最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How do I keep a clean interface when including modules in Ruby - Stack Overflow

programmeradmin7浏览0评论

I'm creating a view helper in Rails in which I need to use the existing form_with helper from ActionView::Helpers::FormHelper. I've tried to call it using ActionView::Helpers::FormHelper.for_for which doesn't work because it's an instance method, so I need to include it.

I've found two ways which both seem wrong.

First: include the Rails module in my module.

module ApplicationHelper
  include ActionView::Helpers::FormHelper
  def helper_method
    form_for ...
   end
end

Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper with the methods of FormHelper when it's a module I'm using only internally for the implementation of my method.

Second: include the foreign module inside my method.

module ApplicationHelper
  def helper_method
    include ActionView::Helpers::FormHelper
    form_for ...
   end
end

This works for a single method, but seems bad from a maintenance perspective -- if I create a bunch of helper methods using form_for, they would all need to include the module separately. Also, I think that this means the module is included each time the method gets called, so that makes it slower.

What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for without exposing it to the interface of my module. (I would like to call it explicitly with the module name - FormHelper::form_for - but I can live with just form_for.) Is there a canonical way to do this in Ruby, or should I get used to it and just include the module?

(I've also come to this problem when trying to use the Rails' route helpers in a class that wasn't a controller.)

I'm creating a view helper in Rails in which I need to use the existing form_with helper from ActionView::Helpers::FormHelper. I've tried to call it using ActionView::Helpers::FormHelper.for_for which doesn't work because it's an instance method, so I need to include it.

I've found two ways which both seem wrong.

First: include the Rails module in my module.

module ApplicationHelper
  include ActionView::Helpers::FormHelper
  def helper_method
    form_for ...
   end
end

Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper with the methods of FormHelper when it's a module I'm using only internally for the implementation of my method.

Second: include the foreign module inside my method.

module ApplicationHelper
  def helper_method
    include ActionView::Helpers::FormHelper
    form_for ...
   end
end

This works for a single method, but seems bad from a maintenance perspective -- if I create a bunch of helper methods using form_for, they would all need to include the module separately. Also, I think that this means the module is included each time the method gets called, so that makes it slower.

What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for without exposing it to the interface of my module. (I would like to call it explicitly with the module name - FormHelper::form_for - but I can live with just form_for.) Is there a canonical way to do this in Ruby, or should I get used to it and just include the module?

(I've also come to this problem when trying to use the Rails' route helpers in a class that wasn't a controller.)

Share Improve this question edited Feb 5 at 10:24 jirka asked Feb 5 at 10:02 jirkajirka 311 silver badge7 bronze badges 2
  • Found a related question: <stackoverflow.com/questions/322470/…> - the inquirer apparently wants to do the same thing, but assumes the included method will be available in the public interface. Also, he owns the module that is being included, so he may change how its methods are defined. – jirka Commented Feb 5 at 10:26
  • 2 "this works for a single method" – not really. It will most likely just raise a NoMethodError because you're calling include from an instance method. But even for class methods, it would merely defer the include call until you invoke the method. It won't limit the inclusion to the method's scope. – Stefan Commented Feb 5 at 11:12
Add a comment  | 

1 Answer 1

Reset to default 5

You do not need / want to include ActionView::Helpers::FormHelper.

In Ruby you do not need to include another module to gain access to it's methods if both are being included in the same class. You can just assume that someone else is bringing it to the party:

module A
  def method_from_a
    "Hello world"
  end
end

module B
  def method_from_b
    method_from_a
  end
end

class Foo
  include A
  include B

  def bar
    method_from_b
  end 
end

puts Foo.new.bar # "Hello World"

While you could include A in B the end result is the same.

Since we know that all the helpers get mixed into the glorious mess that is the view context you can just assume that it and all the other Rails view helpers are going to be there.

module MyHelper
  def wrapper_method(...)
    form_for(...)
  end
end

What I'm trying to achieve is to do a sort of "private include" - use FormHelper::form_for without exposing it to the interface of my module.

This doesn't actually exist in Ruby. Include copies the methods of module to the recipient but they retain whatever visibility they where declared with. Ruby doesn't have a use x,y from Z package system type functionality so we just do it differently.

Coming from languages like Python/Kotlin, I don't like that this pollutes the interface of my ApplicationHelper with the methods of FormHelper when it's a module I'm using only internally for the implementation of my method.

It's not a language thing. It's a Rails thing. Many Rubyists frown just as heavily on the idea of mushing together all your modules and hoping you get the right one.

While you're right to worry your concern is misplaced as it all gets smooshed together anyways. Separating up modules into different modules/files is really just window dressing when they all get included in the same object in the end. I'm not sure how good of an interface you should expect from a junk drawer.

Remember that Rails is built for rapid prototyping and has a pretty heavy focus on "just works". Helpers are easy and convient and arguably better than having the complexity in the view but not uncontroversial.

You can easily also completely opt of writing helpers. The most common alternatives are variants of the decorator/presenter pattern.

If you want to add specific functionality to a form and use good old classical inheritance you can instead create a custom form builder class. Like this example I shamelessly stole from the docs that wraps a radio button in a <div>:

class MyFormBuilder < ActionView::Helpers::FormBuilder
  def div_radio_button(method, tag_value, options = {})
    @template.content_tag(:div,
      @template.radio_button(
        @object_name, method, tag_value, objectify_options(options)
      )
    )
  end
end
<%= form_with model: @person, builder: MyFormBuilder do |f| %>
  I am a child: <%= f.div_radio_button(:admin, "child") %>
  I am an adult: <%= f.div_radio_button(:admin, "adult") %>
<% end -%>

Here the view helpers are called on the view context (@template) which is passed in though the constructor. You can also configure Rails to use your form builder by default.

It's also recommended that you use the newer form_with method instead of the old form_for method which has a lot of quirks and a wonky pre-keywords signature.

发布评论

评论列表(0)

  1. 暂无评论