AlterEGO Labs official blog

About ruby, rails and another amazing stuffs…

Ruby метапрограмминг и наследование

Ruby не перестает меня удивлять! Как по мне, то самое удивительное и мощное в нем - метапрограмминг. После статического языка, в котором твой арсенал ограничен заложенными ключевыми словами и конструкциями, просто невозможно вообразить насколько развязаны твои руки и что ты можешь со всем этим делать! Используя динамику и выразительность Ruby можно превратить кусок кода практически в понятную фразу на английском.

Элементы конструкций не ограничены! Можно создавать их самому сколько угодно, получая в результате высокоуровневый и понятный почти любому человеку DSL.

Самое главное вовремя остановится… Но не сейчас)

Варианты объявления класса

Все мы привыкли объявлять наши классы используя элемент синтаксиса ruby, а именно ключевое слово class:

1
2
class User
end

Но существует еще один вариант. Т.к. класс является экземпляром класса Class, то справедлива следующая запись:

1
2
3
User = Class.new do
  attr_accessor :name
end

Используя способ выше также можно указать суперкласс, просто передав его как параметр в метод Class.new:

1
2
3
4
5
6
7
class User
end

RegisteredUser = Class.new(User) do
end

RegisteredUser.ancestors # => [RegisteredUser, User, Object, Kernel, BasicObject]

Но есть одна интересная особенность!

Когда вы несколько раз объявляете класс используя ключевое слово class, то реально класс инициализируется только один раз. Все следующие разы класс открывается для внесения изменений. Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User
end

User.object_id # => 70299452133440

User.new.name # => NoMethodError: undefined method `name' for #<User:0x007fd35b40a930>

class User
  def name
    'Sergey'
  end
end

User.obect_id # => 70299452133440

User.new.name # => Sergey

В случае с объявлением класса, используя метод Class.new, ситуация немного другая. На самом деле слово User, которое мы используем для инициализации экземпляров, является практически такой же переменной, которая ссылается на экземпляр класса Class. Поэтому код ниже дает, может быть, вполне очевидные результаты:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class User
  def name
    'Sergey'
  end
end

User.object_id # => 70162158343080

User.new.name # => Sergey

User = Class.new do
  def second_name
    'Gernyak'
  end

  def full_name
    "#{second_name} #{name}"
  end
end

User.object_id #=> 70162144228540

User.new.full_name # => NameError: undefined local variable or method `name' for #<User:0x007fdb3b028b18>

User.new.second_name # => Gernyak

Получается мы взяли и заменили значение нашей переменной User на другое. Об этом также свидетельствует значение object_id. Поэтому наш новый класс User ничего не знает о методе #name.

Правда и тут можно сделать хитрость: добавить наш существующий класс в цепочку наследования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User
  def first_name
    'Sergey'
  end
end

User.new.name # => Sergey

User = Class.new(User) do # <= Вот сюда
  def second_name
    'Gernyak'
  end

  def full_name
    "#{second_name} #{first_name}"
  end
end

User.new.full_name # => Gernyak Sergey

User.ancestors # => [User, User, Object, Kernel, BasicObject]

Немного магии

А почему бы не объявить динамически класс, как суперкласс для другого класса?

1
2
3
4
class User < Class.new
end

User.ancestors # => [User, #<Class:0x007fd241dae998>, Object, Kernel, BasicObject]

Данную фичу я использовал в одной из наших библиотек exracted_validator.

У меня есть некоторый базовый класс, который содержит общую логику для возможности вынесения валидаций из модели:

1
2
3
4
5
module ExtractedValidator
  class Base < SimpleDelegator
    # Some logic here
  end
end

Но с некоторыми видами валидаций могут возникнуть проблемы, т.к. они жестко завязаны на именование класса, в котором они объявлены. Например, валидация uniqueness. Для того, чтобы иметь возможность использовать такую валидацию в кастомном валидаторе, необходимо переопределить метод .model_class:

1
2
3
4
5
class SignUpUserValidator < ExtractedValidator::Base
  def self.model_class
    User
  end
end

Используя последний код, все будет работать как и ожидается и все останутся довольны.

Ну почти все… Я не доволен тем, что мне принудительно нужно добавлять еще один метод каждый раз, когда буду объявлять новый валидатор. Поэтому я решил сделать небольшой рефакторинг используя метапрограмминг :-)

И вот что получилось:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module ExtractedValidator
  class Base < SimpleDelegator
    def self.[](model)
      Class.new(self) do
        define_singleton_method :model_class do
          model
        end
      end
    end
  end
end

class SignUpUserValidator < ExtractedValidator::Base[User]
end

В данном примере динамически создается класс-обертка, в котором объявляется требуемый метод .model_class и этот метод возвращает переданный класс, целевой для данного валидатора, модели. Цепочка наследования может выглядеть следующим образом:

1
SignUpUserValidator.ancestors # => [SignUpUserValidator, #<Class:0x007f8ec9b604b8>, ExtractedValidator::Base, SimpleDelegator, Delegator, #<Module:0x007f8ec9c1a138>, BasicObject]

Вот #<Class:0x007f8ec9b604b8> как раз и есть тот самый класс-обертка!

На этом у меня все. Спасибо за внимание!

Comments