Обертывание объекта методами из другого класса

#ruby-on-rails #ruby #methods

#ruby-on-rails #ruby #методы

Вопрос:

Допустим, у меня есть модель под названием Article:

 class Article < ActiveRecord::Base
end
  

И затем у меня есть класс, который предназначен для добавления поведения к объекту article (декоратору):

 class ArticleDecorator

  def format_title
  end

end
  

Если бы я хотел расширить поведение объекта article, я мог бы сделать ArticleDecorator модулем, а затем вызвать article.extend(ArticleDecorator), но я бы предпочел что-то вроде этого:

 article = ArticleDecorator.decorate(Article.top_articles.first) # for single object
  

или

 articles = ArticleDecorator.decorate(Article.all) # for collection of objects
  

Как бы я реализовал этот метод decorate?

Комментарии:

1. PS: Я знаком с драгоценным камнем draper, но не хочу использовать эту реализацию для проблемы, которую я пытаюсь решить.

2. Взгляните на github.com/karmi/tire/blob/master/lib/tire/model/search.rb . Он делает нечто подобное.

Ответ №1:

Что именно вы хотите от decorate метода? Должен ли он просто добавлять некоторые новые методы к переданным объектам или он должен автоматически обертывать методы этих объектов соответствующими методами формата? И почему вы хотите ArticleDecorator быть классом, а не просто модулем?

Обновлено:

Похоже, что решение от nathanvda — это то, что вам нужно, но я бы предложил немного более чистую версию:

 module ArticleDecorator

  def format_title
    "#{title} [decorated]"  
  end

  def self.decorate(object_or_objects_to_decorate)
    object_or_objects_to_decorate.tap do |objects|
      Array(objects).each { |obj| obj.extend ArticleDecorator }
    end
  end

end
  

Он делает то же самое, но:

  1. Позволяет избежать проверки типа аргументов, зависящих от Kernel#Array метода.
  2. Вызывает Object#extend напрямую (это общедоступный метод, поэтому нет необходимости вызывать его через send ).
  3. Object#extend включает только методы экземпляра, поэтому мы можем поместить их прямо в ArticleDecorator , не оборачивая их другим модулем.

Ответ №2:

Могу ли я предложить решение, которое не использует смешанные модули и тем самым предоставляет вам больше гибкости. Например, используя решение, немного похожее на традиционный GoF decorator, вы можете развернуть свою статью (вы не можете удалить mixin, если он применяется один раз), и это даже позволяет вам заменить обернутую статью на другую во время выполнения.

Вот мой код:

 class ArticleDecorator < BasicObject
  def self.[](instance_or_array)
    if instance_or_array.respond_to?(:to_a)
      instance_or_array.map {|instance| new(instance) }
    else
      new(instance_or_array)
    end
  end

  attr_accessor :wrapped_article

  def initialize(wrapped_article)
    @wrapped_article = wrapped_article
  end

  def format_title
    @wrapped_article.title.upcase
  end

  protected

  def method_missing(method, *arguments)
    @wrapped_article.method(method).call(*arguments)
  end
end
  

Теперь вы можете расширить одну статью, вызвав

 extended_article = ArticleDecorator[article]
  

или несколькими статьями путем вызова

 articles = [article_a, article_b]
extended_articles = ArticleDecorator[articles]
  

Вы можете восстановить исходную статью, вызвав

 extended_article.wrapped_article
  

Или вы можете обменять обернутую статью внутри следующим образом

 extended_article = ArticleDecorator[article_a]

extended_article.format_title
# => "FIRST"

extended_article.wrapped_article = article_b

extended_article.format_title
# => "SECOND"
  

Поскольку ArticleDecorator расширяет класс BasicObject, в котором почти нет уже определенных методов, даже такие вещи, как #class и #object_id, остаются неизменными для обернутого элемента:

 article.object_id
# => 123

extended_article = ArticleDecorator[article]

extended_article.object_id
# => 123
  

Обратите внимание, что BasicObject существует только в Ruby 1.9 и выше.

Комментарии:

1. Это сработает, поскольку Ruby следует TMTOWTDI, но этого можно достичь с помощью object.extend(module) и module.included(class)

2. @philpirozhkov как, если можно спросить? Удаление модуля? Изменение неотделанного объекта во время выполнения? Я так не думаю.

Ответ №3:

Вы бы расширили экземпляр класса article, вызвали alias_method и указали его на любой метод, который вы хотите (хотя это звучит как модуль, а не класс, по крайней мере, прямо сейчас). Новая версия получает возвращаемое значение и обрабатывает его как обычно.

В вашем случае, похоже, вы хотите сопоставить такие вещи, как «format_.*», с их соответствующими получателями свойств.

Какая часть сбивает вас с толку?

Комментарии:

1. Я немного прояснил свой вопрос.

Ответ №4:

 module ArticleDecorator
  def format_title
    "Title: #{title}"
  end
end

article = Article.top_articles.first.extend(ArticleDecorator) # for single object
  

Должно работать нормально.

 articles = Article.all.extend(ArticleDecorator)
  

Также может работать в зависимости от поддержки ActiveRecord для расширения набора объектов.

Вы также можете рассмотреть возможность использования ActiveSupport::Concern.