Если вы работаете с Ruby, скорее всего, вы уже слышали слово «метапрограммирование». Возможно, вы даже использовали метапрограммирование, но не до конца поняли истинную силу или полезность того, что он может сделать. К концу этой статьи вы должны иметь твердое представление не только о том, что это такое, но и о том, на что оно способно, и о том, как вы можете использовать одну из «убойных функций» Руби в своих проектах.
Что такое «метапрограммирование»?
Метапрограммирование лучше всего объясняется как программирование программирования. Не позволяйте этому абстрактному определению вас пугать, потому что Ruby делает метапрограммирование таким же легким для понимания, как и для работы.
Метапрограммирование можно использовать как способ добавления, редактирования или изменения кода вашей программы во время ее работы. Используя его, вы можете создавать новые или удалять существующие методы для объектов, повторно открывать или изменять существующие классы, перехватывать несуществующие методы и избегать повторяющегося кодирования, чтобы сохранить вашу программу СУХОЙ.
Понимание того, как Ruby вызывает методы
Прежде чем вы сможете понять весь объем метапрограммирования, вам необходимо понять, как Ruby находит метод при его вызове. Когда вы вызываете метод в Ruby, он должен найти этот метод (если он существует) во всем коде, который находится в цепочке наследования.
class Person def say "hello" end end john_smith = Person.new john_smith.say # => "hello"
Когда метод say()
вызывается в приведенном выше примере, Ruby сначала ищет родителя объекта john_smith
; поскольку этот объект является экземпляром класса Person
и у него есть метод с именем say()
, этот метод вызывается.
Однако все становится сложнее, когда объект является экземпляром класса, унаследованного от другого класса:
class Animal def eats "food" end def lives_in "the wild" end end class Pig < Animal def lives_in "farm" end end babe = Pig.new babe.lives_in # => "farm" babe.eats # => "food" babe.thisdoesnotexist # => NoMethodError: undefined method `thisdoesnotexist' for #<Pig:0x16a53c8>
Когда мы вводим наследование в смесь, Ruby необходимо рассмотреть методы, определенные выше в цепочке наследования. Когда мы вызываем babe.lives_in()
, Ruby сначала проверяет класс Pig
на lives_in()
метода lives_in()
; потому что это существует, это называется.
Это немного другая история, когда мы вызываем метод babe.eats()
. Ruby проверяет этот метод, запрашивая класс Pig
может ли он ответить на eats()
, и в отсутствие такого метода, существующего в Pig
, он продолжит, спрашивая родительский класс Animal
может ли он ответить; может в нашем случае так и будет называться.
Когда мы вызываем babe.thisdoesnotexist()
, поскольку метод не существует, мы получаем исключение NoMethodError
. Вы можете думать об этом как о некоем каскаде: какой бы метод ни был определен ниже в цепочке наследования, он будет вызван методом; если метод вообще не существует, возникает исключение.
Основываясь на том, что мы обнаружили до сих пор, мы можем суммировать, как Ruby рассматривает каждый метод как шаблон, примерно так:
- Спросите родительский класс объекта, может ли он ответить на метод, вызывая его, если он найден.
- Спросите следующий родительский класс, может ли он ответить на метод, и вызовите его, если он найден, продолжая этот шаг к вершине цепочки наследования столько времени, сколько необходимо.
- Если ничто в цепочке наследования не может ответить на вызываемый метод, метод не существует, и должно быть создано исключение.
Обратите внимание, что, поскольку каждый объект наследует от Object
(или BasicObject
в Ruby 1.9) на самом верхнем уровне, этот класс всегда будет последним, который будет запрошен, но только в том случае, если он сделает это так далеко по цепочке наследования, не найдя метод, который может отвечать.
Представляем класс Singleton
Ruby дает вам все возможности объектно-ориентированного программирования и позволяет создавать объекты, которые наследуются от других классов и вызывают их методы; но что, если только один объект требует добавления, изменения или удаления?
«Синглтон-класс» (иногда известный как «собственный класс») разработан именно для этого и позволяет вам делать все это и даже больше. Простой пример по порядку:
greeting = "Hello World" def greeting.greet self end greeting.greet # => "Hello World"
Давайте разберемся, что только что произошло здесь, построчно. В первой строке мы создаем новую переменную с именем greeting
которая представляет простое String
значение. Во второй строке мы создаем новый метод с greeting.greet
и даем ему очень простое содержимое. Ruby позволяет вам выбрать, к какому объекту прикрепить определение метода, используя формат some_object.method_name
, который вы можете распознать как тот же синтаксис для добавления методов класса к классам (т. def self.something
). В этом случае, как мы сначала greeting
, метод был присоединен к нашей переменной greeting
. В последней строке мы вызываем новый метод greeting.greet
который мы только что определили.
greeting.greet
имеет доступ ко всему объекту, к которому он был прикреплен; в Ruby мы всегда называем этот объект « self
. В этом случае self
ссылается на значение String
к String
мы его добавили. Если бы мы прикрепили его к Array
, self
вернул бы этот объект Array
.
Как вы увидите, добавление методов с использованием some_object.method_name
не всегда является лучшим способом решения таких задач. Вот почему Ruby предоставляет еще один гораздо более полезный способ динамической работы с объектами и их методами. Познакомьтесь с классом Singleton:
greeting = "i like cheese" class << greeting def greet "hello! " + self end end greeting.greet # => "hello! i like cheese"
Синтаксис очень странный, но результат тот же, что и у нашего some_object.method_name
добавления методов some_object.method_name
. Этот метод синглтон-класса позволяет вам добавлять много методов одновременно, без префикса всех имен ваших методов. Этот синтаксис также позволяет добавлять все, что вы обычно добавляете при объявлении класса, включая attr_writer
, attr_reader
и attr_accessor
.
Как это работает?
Так как же это на самом деле работает? Название «синглтон-класс», возможно, немного выдало это, но Руби хитрый и добавляет еще один класс в нашу цепочку наследования. Когда вы пытаетесь работать с одноэлементным классом, Ruby нужен способ добавить методы к объекту, к которому мы добавляем, что не допускается языком. Чтобы обойти это, он создает новый секретный класс, который мы называем «синглтон-класс». Этот класс получает методы и изменения, а также становится родителем объекта, над которым мы работаем. Этот одноэлементный класс также сделан экземпляром предыдущего родителя нашего объекта, так что цепочка наследования остается в основном неизменной:
some object instance > singleton class > parent class > ... > Object/BasicObject
Возвращаясь к тому, что мы знаем о поиске метода Ruby, мы ранее решили, что при поиске метода интерпретатор Ruby выполнил три простых шага. Классы Singleton добавляют еще один шаг к этому процессу поиска:
- Спросите объект, может ли его одноэлементный класс ответить на метод, вызывая его, если найден.
- Спросите родительский класс объекта, может ли он ответить на метод, вызывая его, если он найден.
- Спросите следующий родительский класс, может ли он ответить на метод, и вызовите его, если он найден, продолжая этот шаг к вершине цепочки наследования столько времени, сколько необходимо.
- Если ничто в цепочке наследования не может ответить на вызываемый метод, метод не существует, и должно быть создано исключение.
Как мы уже обсуждали ранее, вы можете думать об этом как о каскаде, и это очень важно влияет на наше представление об объектах в Ruby: объекты могут не только получать методы из своих унаследованных классов, но теперь они могут получать индивидуально уникальные методы в качестве Программа запущена.
Использование метапрограммирования для работы с instance_eval
и class_eval
Наличие одноэлементных классов — это хорошо, но чтобы по-настоящему динамически работать с объектами, вы должны иметь возможность повторно открывать их во время выполнения в других функциях. К сожалению, Ruby синтаксически не позволяет вам иметь какие-либо операторы class
внутри функции. Вот где instance_eval
входит в картину.
Метод instance_eval
определен в стандартном модуле Kernel
Ruby и позволяет добавлять методы экземпляра к объекту так же, как наш синтаксис синглтон-класса.
foo = "bar" foo.instance_eval do def hi "you smell" end end foo.hi # => "you smell"
Метод instance_eval
может принимать блок (в котором для self
установлено значение объекта, с которым вы работаете) или строку кода для оценки. Внутри блока вы можете определить новые методы, как если бы вы писали класс, и они будут добавлены в одноэлементный класс объекта.
Методы, определенные instance_eval
будут методами экземпляра. Важно отметить, что область действия — это методы экземпляра, потому что это означает, что в результате вы не можете делать такие вещи, как attr_accessor
. Если вам это нужно, вам нужно работать с классом объекта, вместо этого используя class_eval
:
bar = "foo" bar.class.class_eval do def hello "i can smell you from here" end end bar.hello # => "i can smell you from here"
Как видите, instance_eval
и class_eval
очень похожи, но их область применения и применение немного отличаются. Вы можете вспомнить, что использовать в каждой ситуации, помня, что вы используете instance_eval
для создания методов экземпляра и class_eval
для создания методов класса.
Почему меня это волнует?
В этой точке вы, вероятно, думаете: «Это здорово, но почему мне это нужно? Какое значение имеет реальный мир? »Простой ответ« да, вы должны »и« много ».
Метапрограммирование позволяет вам создавать более гибкий код, будь то с помощью красивых API или легко тестируемого кода. Более того, он позволяет вам делать это, используя мощные и элегантные методы программирования и синтаксис. Метапрограммирование позволяет создавать код, который является СУХИМЫМ, многократно используемым и чрезвычайно лаконичным.
Во второй части этой серии статей мы рассмотрим, как применять метапрограммирование к повседневным проблемам, и увидим, как его элегантность и мощь могут навсегда изменить способ разработки решений.