What is a convention for reusing methods between presenters?
For example, say an app has the following presenters
class UserPresenter < BasePresenter
end
class AdminPresenter < BasePresenter
end
class EventPresenter < BasePresenter
end
User and Admin both have avatars. What is the correct way to share an avatar method between the User and Admin presenter?
One solution might be inheriting from an AvatarPresenter
class UserPresenter < AvatarPresenter
end
class AdminPresenter < AvatarPresenter
end
class EventPresenter < BasePresenter
end
class AvatarPresenter < BasePresenter
end
Which works OK in this simple example. But what if things become more complex in the future (e.g., an additional method shared between Admin and Event).
I suppose I'm looking to share Concerns between Presenters. Is this a conventional approach, and if so what would a template implementation look like? All my attempts are raising method not found errors.
What you are looking for is traits. In Ruby this takes the form of module mixins.
module Avatar
def self.included(base)
base.extend ClassMethods
base.class_eval do
# in this block you are operating on the singleton class
# where the module is included
end
end
def an_instance_method_from_avatar
end
module ClassMethods
def a_class_method_from_avatar
end
end
end
class UserPresenter
include Avatar
end
class AdminPresenter
include Avatar
end
This lets us create reusable components that can composed in many different ways. ActiveSupport::Concern takes the pattern above and simplifies it:
module Avatar
# modules can be extended by other modules
extend ActiveSupport::Concern
included do
# in this block you are operating on the singleton class
# where the module is included
end
class_methods do
def a_class_method_from_avatar
end
end
def an_instance_method_from_avatar
end
end
Inheritance (class based) on the other hand should only really be used if an object is a true subtype of its parent. While you could argue that an AdminPresenter is a presenter with an avatar this would lead to a really convoluted class diagram down the road if you need to add other functionality.
Related
I need to call a helper method within a model, from both a class and an instance method, e.g. Model.method(data) and model_instance.method. However, the class method always returns "NoMethodError: undefined method 'helper_method' for #<Class ...>"
model.rb:
class Model < ActiveRecord::Base
include ModelHelper
def method
helper_method(self.data)
end
def self.method(data)
self.helper_method(data)
end
end
model_helper.rb:
module ModelHelper
def helper_method(data)
# logic here
end
end
I even tried adding def self.helper_method(data) in the helper to no avail.
After quite a bit of seraching, I wasn't able to find anything on how to achieve this, or at least anything that worked.
The answer turned out to be pretty simple, and doesn't require any Rails magic: you just re-include the helper and define the class method within a class block:
class Model < ActiveRecord::Base
include ModelHelper
def method
helper_method(self.data)
end
# Expose Model.method()
class << self
include ModelHelper
def method(data)
helper_method(data)
end
end
end
No changes to the helper needed at all.
Now you can call method on both the class and an instance!
If there's no additional logic in method, then you can simply do:
class Model < ActiveRecord::Base
include ModelHelper
extend ModelHelper
end
And get both the instance (#model.helper_method) and the class (Model.helper_method) methods.
If, for legacy (or other) reasons, you still want to use method as an instance and class method, but method doesn't do anything different than helper_method, then you could do:
class Model < ActiveRecord::Base
include ModelHelper
extend ModelHelper
alias method helper_method
singleton_class.send(:alias_method, :method, :helper_method)
end
And now you can do #model.method and Model.method.
BTW, using modules to include methods in classes is seductive, but can get away from you quickly if you're not careful, leaving you doing a lot of #model.method(:foo).source_location, trying to figure out where something came from. Ask me how I know...
you need to define model_helper.rb as:
module ModelHelper
def self.helper_method(data)
# logic here
end
end
and call this method in model.rb as:
class Model < ActiveRecord::Base
include ModelHelper
def method
ModelHelper.helper_method(self.data)
end
def self.method(data)
ModelHelper.helper_method(data)
end
end
How does one override a class method defined in a model concern?
This is a bit tricky since you’re not really overriding a class method right? Because it’s using the concern api of definining class methods in the class_methods block.
so say I have a concern that looks like this:
module MyConcern
extend ActiveSupport::Concern
class_methods do
def do_something
#some code
end
end
end
In model.. how would I override that method so that I could call it like we do with super when using inheritance? So in my model I’d like to go:
def self.do_something
#call module do_something
end
?
If you've included MyConcern in the model that defines self.do_something, you should just be able to use super:
module MyConcern
extend ActiveSupport::Concern
class_methods do
def do_something
puts "I'm do_something in the concern"
end
end
end
class UsesMyConcern < ActiveRecord::Base
include MyConcern
def self.do_something
super
end
end
UsesMyConcern.do_something
# => "I'm do_something in the concern"
If you haven't or don't want to include MyConcern in the model and you want to invoke do_something on the module without creating any intermediary objects, you can change your model to:
class UsesMyConcern < ActiveRecord::Base
def self.do_something
MyConcern::ClassMethods.instance_method(:do_something).bind(self).call
end
end
UsesMyConcern.do_something
# => "I'm do_something in the concern"
ActiveSupport::Concern.class_methods defines a ClassMethods module in the concern if there isn't one already, and that's where we can find the do_something method.
Why not simply call the module's method: MyConcern.do_something?
I'm not sure if there's an easy of doing super for modules (though I can see why that may be useful).
The next best solution could be doing something like calling #included_modules and manually iterating with #responds_to?:
def self.do_something
self.super_module(__method__)
end
def self.super_module(method)
self.included_modules.find { |m| m.responds_to? method }.public_send(method)
end
The old way using alias_method_chain: https://ernie.io/2011/02/03/when-to-use-alias_method_chain/
The new way (requires > ruby 2.0.0) you really should use this, as there will be a DEPRECATION WARNING when using it in rails 5.0:
http://paweljaniak.co.za/2014/09/30/understanding-ruby-module-prepend-and-include/
With Rails concerns I can give my model class methods and instance methods through modules by including them. No blog entry or thread that I've found mentions how I can include variables in my model though.
Specifically I would like to give my including model a class instance variable #question, but I don't know where to put the declaration in the module so it is applied. I would also like the class instance variable to be overridden if the model itself declares that variable.
Does the ActiveSupport::Concern module actually care about variables at all?
module ContentAttribute
extend ActiveSupport::Concern
def foo
p "hi"
end
module ClassMethods
# #question = "I am a generic question." [doesn't work]
def bar
p "yo"
end
end
end
class Video < ActiveRecord::Base
include ContentAttribute
# #question = "Specific question"; [should override the generic question]
end
module ContentAttribute
extend ActiveSupport::Concern
included do
self.question = "I am a generic question."
end
module ClassMethods
attr_accessor :question
end
end
Then, in video...
class Video < ActiveRecord::Base
include ContentAttribute
self.question = "Specific question"
end
I have a model with the following two methods which are required in another model so I thought I'd try sharing them via a concern instead of duplicating the code.
class Region < ActiveRecord::Base
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
end
I have created a file in app/models/concerns/sets.rb and my new model reads:
class Region < ActiveRecord::Base
include Sets
end
sets.rb is:
module Sets
extend ActiveSupport::Concern
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
module ClassMethods
end
end
Question:
How do I share a method between models when the method references the model such as "Region.where..."
Either by referencing the class of the including model (but you need to wrap the instance methods in an included block):
included do
def ancestors
self.class.where(...) # "self" refers to the including instance
end
end
or (better IMO) by just declaring the method as a class method, in which case you can leave the class itself out altogether:
module ClassMethods
def ancestors
where(...)
end
end
I need some help with virtual attributes. This code works fine but how do I use it inside a plugin. The goal is to add this methods to all classes that uses the plugin.
class Article < ActiveRecord::Base
attr_accessor :title, :permalink
def title
if #title
#title
elsif self.page
self.page.title
else
""
end
end
def permalink
if #permalink
#permalink
elsif self.page
self.page.permalink
else
""
end
end
end
Thanks
You can run the plugin generator to get started.
script/generate plugin acts_as_page
You can then add a module which defines acts_as_page and extends it into all models.
# in plugins/acts_as_page/lib/acts_as_page.rb
module ActsAsPage
def acts_as_page
# ...
end
end
# in plugins/acts_as_page/init.rb
class ActiveRecord::Base
extend ActsAsPage
end
This way the acts_as_page method is available as a class method to all models and you can define any behavior into there. You could do something like this...
module ActsAsPage
def acts_as_page
attr_writer :title, :permalink
include Behavior
end
module Behavior
def title
# ...
end
def permalink
# ...
end
end
end
And then when you call acts_as_page in the model...
class Article < ActiveRecord::Base
acts_as_page
end
It will define the attributes and add the methods. If you need things to be a bit more dynamic (such as if you want the acts_as_page method to take arguments which changes the behavior) try out the solution I present in this Railscasts episode.
It appears that you want a Module for this
# my_methods.rb
module MyMethods
def my_method_a
"Hello"
end
end
The you want to include it into the classes you want to use it for.
class MyClass < ActiveRecord::Base
include MyMethods
end
> m = MyClass.new
> m.my_method_a
=> "Hello!"
Take a look here for more information on mixing in modules. You can put the module wherever in a plugin if you like, just ensure its named correctly so Rails can find it.
Create a module structure like YourPlugin::InstanceMethods and include it this module like this:
module YourPlugin
module InstanceMethods
# your methods
end
end
ActiveRecord::Base.__send__(:include, YourPlugin::InstanceMethods)
You have to use __send__ to make your code Ruby 1.9 compatible. The __send__ line is usually placed at the init.rb file on your plugin root directory.