Загрузка...

Ruby & Rails: веб-разработка с удовольствием

Ruby on Rails — фреймворк для создания веб-приложений. Является открытым программным обеспечением (лицензия MIT). Здесь мы обсуждаем новости RoR, делимся учебными материалами и интересными находками С RoR даже сложные веб-приложения могут быть написаны за считанные дни. Это действительно разработка с удовольствием!
     

5. Metaprogramming patterns — 18 кю. Примеси

28.02.09, 19:27
Автор artem.voroztsov

В языке Ruby активно используются примеси.

Примеси - это модули, которые include'ятся в другие модули или классы. В результате выполнения метода include все методы модуля как бы становятся instance-методами класса (модуля), в который его заинклудили.

Предыдущие статьи:

1. Metaprogramming patterns — 25кю. Метод eval
2. Metaprogramming patterns — 22кю. Reuse в малом — bang!
3. Metaprogramming patterns — 20 кю. Замыкания
4. Metaprogramming patterns 19 кю. Спасение утопающих дело рук самих утопающих

По щучьему веленью, по моему хотенью ...

.. хочу чтоб все контейнеры получили метод sum!

Начнём с класса Array:

module Sum
  def sum
    inject {|s, element| s + element }
  end
end
class Array
  include Sum
end
[1,2,3,4,5].sum #=> 15

Вопрос: Почему бы не определить метод sum непосредственно для класса Array?

Ответ: Одна из причин - чтобы не добавлять этот метот N раз для нескольких классов. А так, мы можем написать модуль, а затем примешать его к нескольким классам.

require 'set'
[Array, Set, Range].each {|klass| klass.class_eval { include Sum} }

Здесь обычно проводят  аналогию с множественным наследованием.
Но аналогии лучше не проводить. Программирование с открытыми классами принципиально отличается от того, к которому привыкли программисты C++, Java, ... (как часто вы на С++ пишете итераторы по классам?).
Кроме того, модули можно как примешивать, так и отмешивать.
Примешивание в Ruby - явление, существующее независимо от наследования, параллельно с ним.

Надо сказать, что приведённый выше пример с модулем Sum не очень хорош. Дело в том, что все классы, соответствующие какому-либо контейнеру, и так включают в себя одну и ту же примесь - Enumerable, и если мы хотим добавить какой-либо метод ко всем контейнерам, то лучше просто добавить его к Enumerable:

require 'set'
# дописываем Enumerable
module Enumerable
  def sum
    inject {|m, element| m + element }
  end
end
# и тогда мы получаем sum для всех контейнеров:
Set[5,3,1].sum       # => 9
puts(('a'..'z').sum) # => 'abcdefghijklmnopqrstuvwxyz'
{1=>'a',2=>'b'}.sum  # => [1, "a", 2, "b"]

Отступление про inject

Здесь у многих возникает вопрос о том, что такое inject.

Если коротко, то inject - это метод контейнера, которому нужно передать лямбду, соответствующую бинарному оператору. Этот бинарный оператор как бы подставляется между элементами контейнера и вычисляется  результат.
 

[a1, a2, a3].inject {|a,b| a * b}   #=>  a1 * a2 * a3
 

Вместо звездочки может стоять всё что угодно. В том числе какая-нибудь неассоциативная операция f(a,b).

Если переданная лямбда есть сложение двух элементов, то результат inject'а будет суммой всех элементов контейнера. Если умножение - то произведение всех элементов. Если {|a,b| a > b ? a: b} - то максимум.

И ещё. Вы наверное слышали про Map&Reduce. Дак вот, на Ruby этот паттерн функционального программирования звучал бы так: Map&Inject!

Элементы интроспекции

Если Вы хотите узнать, в какие именно классы примешан модуль Enumerable, то выполните код:

require 'set' 
ObjectSpace.each_object(Class) do |klass| 
  next unless klass.include?(Enumerable) 
  puts klass 
end 
# [SortedSet, Set, Struct::Tms, Dir, File, IO, Range, Struct, Hash, Array, String]
 

И наоборот, если вы хотите знать все модули, которые примешены в класс Array, то выполните код:

puts Array.included_modules

Интересуетесь, что за методы предоставляет Enumerable? Тогда выполняйте

puts Enumerable.instance_methods.sort

Я не волшебник, я просто учусь использовать примеси ..

C помощью примешивания модулей можно сделать самые разные чудеса. Для примешиваемого модуля можно определить метод included, который вызывается в момент примешивания и получает в аргументе класс или модуль, в который он примешивается.

module A
  def self.included(klass)
    puts "A примешивается к #{klass}"
  end
end
class X
  include A  # напечатается "A примешивается к X"
end

Наиболее популярный паттерн примеси выглядит так:

module A
  module InstanceMethods
    def a
      p "a"
    end
  end
  module ClassMethods
    def b
      p "b"
    end
  end
  def self.included(klass)
    klass.module_eval do
      include InstanceMethods
      extend ClassMethods
    end
  end
end
class X
  include A
end
X.b  
X.new.a

То есть внутри модуля определяются два подмодуля с именами InstanceMethods и ClassMethods. Первый подмодуль содержит методы, которыми нужно наделить все объекты класса, к которому происходит примешивание A. Второй - методы, которыми нужно наделить сам класс.

Здесь новый для нас метод - метод extend. Он есть у любого объекта. В частности, можно писать так

module A
  def hi
    puts "hi from object #{self}"
  end
end
x = "abc"
x.extend A
x.hi

И здесь всем становится понятно, что в руби не имеет смысла методы класса  называть "статическими методами класса". В руби и других  динамических языках классы есть тоже объекты, и методы класса есть просто персональные методы (singleton methods) этих объектов, экземпляров класса Class.

Рассмотрим более осмысленный пример этого паттерна, а именно рассмотрим примесь, которая позволяет проверять является ли массив арифметической последовательностью, а также определяет метод для конструирования арифметических последовательностей с заданными параметрами a, b  и n:

module ArithmeticSequence
  def self.included(klass)
    klass.class_eval do
      include InstanceMethods
      extend ClassMethods
    end
  end
  module ClassMethods
    def new_arithm(a,b,n)
      res = [a]
      (n-1).times { res < < res.last + b }
      res
    end
  end
  module InstanceMethods
    def arithm?
      return true if self.size < = 1
      b = self[1]-self[0]
      (2...self.size).all?{|i| self[i] == self[i-1] + b }
    end
  end
end
Array.class_eval { include ArithmeticSequence }
puts Array.new_arithm(100,3,4)      # [100, 103, 106, 109]
puts [10, 12, 14, 16].arithm?       # true
puts [1, 2, 5].arithm?              # false

 

Ещё одно важное замечание:

Если у модулей менять методы или добавлять новые, то эти изменения "почувствуют" все классы, в которые модуль был включен. Исключение составляют лишь изменения, связанные с включением в модуль новых подмодулей.

Кто главнее?

Пусть у некоторого объекта a можно вызвать некоторый метод x и это приводит к некоторому результату - успешному выполнению какого-то кода.  Где определён этот метод x? Есть несколько вариантов. Перечислим их в порядке убывания приоритета.

  1. Singleton метод объекта a (метод, определёный для singleton-класса Class:a)
  2. Метод последней примеси, примешенной в singleton-класс
  3. Метод определённый для класса
  4. Метод последней примеси, примешенной в класс
  5. Метод суперкласса (класса, от которого наследует класс), для которого выбор метода определяется согласно пунктам 3,4,5

 

1. Один и тот же метод x определён к классе B, а также в классе A, наследнике B. Какой метод будет использоваться при вызове A.new.x и B.new.x?

class B; def x; p "B"; end; end
class A< B; def x; p "A"; end; end
A.new.x # A
B.new.x # B

2. Один и тот же метод x определён в классе A, а также в примеси M. Какой метод будет использоваться при вызове A.new.x? Зависит ли результат от того, в каком месте написать include?
module M; def x; p "M"; end; end
class A; include M; def x; p "A"; end; end
A.new.x # A

module M; def x; p "M"; end; end
class A;  def x; p "A"; end; include M;  end
A.new.x # A, то есть не зависит от того, где написать include

3. Один и тот же метод x определён в примесях M и N, которые примешиваются к классу A. Какой метод будет использоваться при вызове A.new.x?

module M; def x; p "M"; end; end 
module N; def x; p "N"; end; end 
class A; include M; include N; end 
A.new.x # N

module M; def x; p "M"; end; end
module N; def x; p "N"; end; end
class A; include M, N;  end
A.new.x # N

4. Один и тот же метод x определён к классе A,  а также в примеси M, примешенной к B. Класс B наследник A. Какой метод будет использоваться при вызове B.new.x?
module M; def x; p "M"; end; end
class A; def x; p "A"; end; end
class B < A;  include M; end;
B.new.x # M

5. Один и тот же метод x определён к классе A, а также в примеси M. Класс B наследник A. Примесь примешена как в класс A, так и в класс B; Какой метод будет использоваться при вызове B.new.x?
module M; def x; p "M"; end; end
class A; include M; def x; p "A"; end; end
class B < A;  include M; end;
B.new.x # A

6. Что бывает, если включить один и тот же модуль в класс и в его наследника?
module M; def x; p "M"; end; end
module N; def x; p "N"; end; end
class A; include M; include N;  end
class B< A; include M; end;
B.new.x # N

7. Что бывает, если включить один и тот же модуль в класс и в singleton-класс?
module M; def x; p "M"; end; end
class A; def x; p "A"; end; end
a = A.new
a.x
class < <a; include M; end;
a.x

Наследование класс-методов

Методы класса наследуются:

class A
  def A.x
    p "#{self}.x"
  end
end
class B < A
end
A.x # "A.x"
B.x # "B.x"

Методы класса, полученные в результате примешивания модуля, также наследуются без проблем:

module A
  def self.included(klass)
    klass.module_eval do
      def self.x
        p "#{self}.x"
      end
    end
  end
end
class U
  include A
end
class W
  include A
end
U.x # "U.x"
W.x # "W.x"
class Y < W
end
Y.x # "Y.x"

Проблемы возникают лишь с наслеованием через модуль:

class A
  def A.x
    p "#{self}.x"
  end
end
module B
  include A
end
class Z
  include B
end
Z.x # !!! undefined method `x' for Z:Class (NoMethodError)

Но и эта проблема разрешима.

Как обеспечить наследование класс-методов через модуль

Почитайте об этом в

Наследование класс методов можно обеспечить, написав следующий стандартный модуль:

class Module
  private
  module MixinClassMethods
    def included_by_module(klass)
      #check to see if klass is already set up
      if not klass.instance_variables.include? '@class_method_module'
        klass.send(:mixin_class_methods)
      end
      klass_method_module = klass.instance_variable_get('@class_method_module')
      klass_method_module.send(:include, @class_method_module)
    end
    def included(klass)
      @extra_include_block.call(klass) if @extra_include_block
      case klass
      when Class
        klass.extend(@class_method_module)
      when Module
        #more work to include in a module
        included_by_module(klass)
      end
    end
    def define_class_methods(&block)
      @class_method_module.module_eval &block
    end
  end
  def mixin_class_methods(&block)
    #ensure the existence of the ClassMethods module
    if not (Module === (@class_method_module ||= Module.new))
      fail "@class_method_module is not a module!"
    end
    @extra_include_block = block
    extend MixinClassMethods
  end
end

 Использовать его довольно просто:

require 'mixin_class_methods'
module A
  mixin_class_methods
  
  def my_instance_method
    # ...
  end
  define_class_methods do
    def my_class_method
      # ...
    end
  end
end
MyClass.module_eval do
  include A
end

А вот реальный (хоть и несколько надуманный и упрощённый) пример из жизни рельсоукладчиков:

require 'mixin_class_methods'
module BaseExt
  mixin_class_methods
  def update_selected_attributes(*attributes)
    attributes = attributes.first if attributes.first.is_a?(Array)
    self.class.update_all(
      attributes.map{|a|    
        a = a.to_s
        if self.class.serialized_attributes[a]
          a + "='" + self[a].to_yaml.gsub("\n",'\n').gsub("'"){"\\'"} + "'"
        else
          self.class.send(:sanitize_sql_hash_for_assignment, a=>self[a])
        end
      }.join(', '),
      {:id => self.id}
    )
  end
  define_class_methods do
    def max_id
      x = self.find_by_sql("SELECT MAX(id) AS id FROM #{self.table_name}").first
      if x
        x.id||0
      else
        0
      end
    end
    def each
      (1..self.max_id+10).each do |i|
        o = self.find_by_id(i)
        if o
          yield o
        end
      end
      self
    end
    def random(field = 'id')
      count = self.count
      return nil if count == 0
      self.find(:first, :conditions => ["`#{field}` > ?", rand(max_id) ])
    end
  end
end
ActiveRecord::Base.module_eval do
  include BaseExt
end

 Ссылки

1. Metaprogramming patterns — 25кю. Метод eval
2. Metaprogramming patterns — 22кю. Reuse в малом — bang!
3. Metaprogramming patterns — 20 кю. Замыкания
4. Metaprogramming patterns 19 кю. Спасение утопающих дело рук самих утопающих
http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html
http://www.ruby-forum.com/topic/68638

Комментарии

“…метод extend. Он есть у любого объекта…”
Для символов (Symbol) extend не работает, хотя метод безусловно есть :-)

Войдите, чтобы оставить комментарий