Сподіваюсь, ви прочитали Першу частину про метапрограмування з Ruby eval перед тим, як дістатись до цієї. Якщо ні, ви можете перейти за посиланням і почитати її першою. Це, можна сказати, як передумова.
Метапрограмування з Ruby Eval — Частина 1
У першій частині ми розглянули основи 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)