Мета-програмування з Ruby Eval: Посібник (Частина 2)

Сподіваюсь, ви прочитали Першу частину про метапрограмування з Ruby eval перед тим, як дістатись до цієї. Якщо ні, ви можете перейти за посиланням і почитати її першою. Це, можна сказати, як передумова.

Метапрограмування з Ruby Eval — Частина 1

pic

У першій частині ми розглянули основи eval і як динамічно створювати клас та додавати методи до нього. Тепер давайте подивимось, як ми можемо розширювати клас, а також навчимось створювати модулі і використовувати їх у класах під час виконання. Готові? Давайте зануримось.

Розширення класу

Припустимо, у нас вже є файл класу, ми можемо динамічно розширити його під час виконання, щоб додати більше методів для екземплярів і класу.

Розглянемо наступний приклад класу Animal:

class Animal  
 def initialize(name)  
 @name = name  
 end  
end

Щоб додати більше методів до нього, ми можемо просто використати наступне:

## Додавання методів через class_eval  
Animal.class_eval do  
 def speak  
 “#{@name} видає звук”  
 end  

 def self.species  
 @species ||= []  
 end  

 # Додаємо атрибут читача динамічно  
 attr_reader :name  
end

Наведен код додає метод екземпляра "speak", класовий метод "species" і також атрибут читача для отримання значення @name.

Перевизначення методів у класі або зміна існуючої поведінки

Тепер, припустимо, що у класу Animal є метод "name", ось так:

class Animal  
 def initialize(name)  
 @name = name  
 end  

 def name  
 print “Ім’я — #{@name}”  
 end  
end

Я можу перевизначити цей метод на льоту, використовуючи наступний код:

Animal.class_eval do  
 # Зберігаємо оригінальний метод  
 alias_method :original_name, :name  

 def name  
 puts “Можете кликати мене #{@name}!”  
 end  
end

Я можу використовувати class_eval, щоб перевизначити будь-який метод в будь-якому класі під час виконання. Але потрібно бути обережним, щоб не змінити очікувану поведінку, що може призвести до непорозумінь і проблем для інших розробників.

Динамічне створення атрибутних методів

Якщо ви хочете створити методи для атрибутів: читач і записувач, ось як це зробити:

[‘age’, ‘color’, ‘breed’].each do |attribute|  
 Animal.class_eval do  
 # Створюємо геттер  
 define_method(attribute) do  
 instance_variable_get(“@#{attribute}”)  
 end  

 # Створюємо сеттер  
 define_method(“#{attribute}=”) do |value|  
 instance_variable_set(“@#{attribute}”, value)  
 end  
 end  
end
# Приклад використання  
dog = Animal.new(“Рекс”)  
puts dog.speak # => “Гучно: Рекс видає звук”  
dog.age = 5  
dog.color = “коричневий”  
puts “#{dog.name} — це #{dog.age} років і #{dog.color}”

Створення модулів за допомогою class_eval

У першій частині ми побачили, як створювати класи, а чи можна створювати модулі? І, може, включати їх у класи? Так, можемо. Ось код, який це робить:

# Створення модуля   
module_code = <<-RUBY  
 module Swimmable  
 def swim  
 “\#{@name} плаває!”  
 end  
 end  
RUBY

Ось код модуля, але спершу потрібно його оцінити, а потім включити в клас, правильно? Тож як це зробити?

# Оцінюємо код модуля і включаємо його  
Object.class_eval(module_code)  
Animal.class_eval { include Swimmable }

У наведеному коді, я спочатку оцінюю код модуля за допомогою Object.class_eval, а потім для включення його в клас використовую class_eval для самого класу. Ось і все. Тепер я можу перевірити це за допомогою наступного коду:

animal = Animal.new(“панда”)  
puts animal.swim

Це повинно працювати. Тепер ви знаєте, як створювати модулі на льоту і включати їх у будь-який існуючий клас або можна також створити клас на льоту та включити в нього модуль.

Додавання методів до модуля

Давайте створимо реальний випадок використання.
мова перевірки:

module Validators  
 def self.create_validator(field, &validation_logic)  
 module_eval do  
 define_method(“validate_#{field}”) do |value|  
 if validation_logic  
 instance_exec(value, &validation_logic)  
 else  
 value.nil? ? false : true  
 end  
 end  
 end  
 end  
end

Ви можете використовувати модуль Validators для створення базових перевірок на присутність, як це:

class User  
 include Validators  

 attr_accessor :email, :age, :username  

 # Створення базових перевірок на присутність  
 create_validator :email  
 create_validator :age  
 create_validator :username  
end  

user = User.new  
puts user.validate_email(nil) # => false  
puts user.validate_email(“[email protected]”) # => true

Якщо ви хочете створити кастомну перевірку для перевірки значення ціни і т.д., ви можете зробити це ось так:

create_validator(:price) { |value| value.to_f > 0 }

Подивіться, як це може бути корисно?

Розширення модулів

module Extensions  
 module_eval do  
 def self.included(base)  
 base.extend(ClassMethods)  
 end  

 module ClassMethods  
 def class_method  
 puts "Це метод класу"  
 end  
 end  
 end  
end

Оцінка класів і модулів

moduleeval (також має псевдонім moduleexec) — це метод, який дозволяє оцінювати код в контексті модуля. Як і class_eval, він дає змогу визначати або змінювати методи, які будуть методами екземплярів класів, що включають модуль.

module Greeting  
 def self.add_greeting(name)  
 module_eval(<<-RUBY)  
 def greet_#{name}  
 puts "Привіт, #{name}!"  
 end  
 RUBY  
 end  
end  

class Person  
 include Greeting  
end  

Greeting.add_greeting("alice")  
person = Person.new  
person.greet_alice # Виведе: Привіт, alice!

Хоча module_eval і class_eval дуже схожі, є кілька ключових відмінностей:

Контекст: moduleeval використовується саме для модулів, тоді як classeval — для класів.
Однак, оскільки класи в Ruby також є модулями (Class наслідує від Module), ви можете використовувати module_eval і для класів.

# Це еквівалентно для класів  
MyClass.class_eval do  
 def some_method  
 puts "Hello"  
 end  
end  

MyClass.module_eval do  
 def some_method  
 puts "Hello"  
 end  
end

module_eval часто використовується в методах модулів для динамічного визначення методів, які будуть доступні всім класам, що включають цей модуль.

Динамічне визначення методів

define_method — потужний спосіб для динамічного створення методів:

class APIWrapper  
 ['get', 'post', 'put', 'delete'].each do |http_method|  
 define_method(http_method) do |url, params = {}|  
 # Загальне оброблення HTTP запитів  
 puts "Making #{http_method.upcase} request to #{url}"  
 end  
 end  
end  

api = APIWrapper.new  
api.get('/users') # => "Making GET request to /users"  
api.post('/users') # => "Making POST request to /users"

Видалення методів

Так само, як ми можемо динамічно визначати методи, ми можемо їх видаляти:

class Example  
 def temporary_method  
 "I won't be here long"  
 end  

 remove_method :temporary_method  
end

Динамічне управління константами та змінними

Встановлення констант

module Configuration  
 const_set(:API_VERSION, “v1”)  
 const_set(:MAX_RETRIES, 3)  
end  

puts Configuration::API_VERSION # => “v1”

Операції з змінними

class StateManager  
 class_variable_set(:@@state, {})  

 def self.state  
 class_variable_get(:@@state)  
 end  

 def update_state(key, value)  
 instance_variable_set(“@#{key}”, value)  
 end  
end

Найкращі практики та попередження

Розгляд безпеки

  • Ніколи не використовуйте eval з ненадійними даними
  • Вибирайте більш конкретні методи оцінки замість базового eval
  • Використовуйте define_method замість eval для динамічного визначення методів

Вплив на продуктивність

  • Методи оцінки повільніші за статичні визначення
  • Кешуйте результати при багаторазових оцінках
  • Розгляньте можливість використання метапрограмування під час завантаження класу, а не під час виконання

Читабельність коду

  • Документуйте, чому ви використовуєте метапрограмування
  • Тримайте генерацію динамічного коду простою та очевидною
  • Розгляньте, чи може більш простий підхід працювати краще

Методи оцінки в Ruby надають потужні інструменти для метапрограмування, дозволяючи писати більш динамічний та гнучкий код. Хоча ці інструменти слід використовувати обережно, розуміння їх відкриває нові можливості для елегантного вирішення складних задач.

Пам’ятайте, що з великою силою приходить велика відповідальність — завжди розглядайте, чи є метапрограмування найкращим рішенням для вашого конкретного випадку, і добре документуйте свій код, коли використовуєте його.

Це все на зараз. Дякую, що прочитали до кінця.

Перекладено з: Meta programming with Ruby Eval: A guide (Part 2)

Leave a Reply

Your email address will not be published. Required fields are marked *