Всем привет! В этой статье я хочу поговорить о метапрограммировании на примере реальной, часто встречающейся проблемы.
Когда кто-то говорит о метапрограммировании, программист старой закалки приходит в ярость.
И для этого есть причины; в большом проекте использование метапрограммирования может показаться безумием, поскольку код становится очень трудно читать.
А если к проекту присоединится сторонний специалист, он уж точно ничего не поймет в этом метакоде.
Но не все так просто, как говорится – плохих инструментов не бывает. В этой статье я попытаюсь на реальном рабочем примере показать, как метапрограммирование поможет сделать ваш код чище и исключить рутинное повторение.
И это заставит вас порадоваться метамагии.
Помощь из Википедии Метапрограммирование - вид программирования, связанный с созданием программ, порождающих в результате своей работы другие программы (в частности, на этапе компиляции их исходного кода), или программ, изменяющих себя в процессе выполнения (самомодифицирующийся код).
Первый позволяет создавать программы с меньшими затратами времени и усилий на кодирование, чем если бы программист писал их полностью вручную, второй позволяет улучшить свойства кода (размер и скорость).
На этом вступление заканчивается.
Теперь хочу перейти к практической части и рассказать о сути проблемы.
Далее мы поговорим о языке Ruby и фреймворке Rails в частности.
В Ruby есть рефлексия и большие возможности для метапрограммирования.
Большинство драгоценных камней, модулей и фреймворков создаются с использованием мощных инструментов отражения.
фото Джошуа Фуллер на Unsplash Тот, кто что-либо писал на Rails, скорее всего сталкивался с таким гелем, как Rails Admin, если вы еще не пробовали использовать его или аналоги в своих проектах на Rails, настоятельно рекомендую это сделать, так как практически из коробки вы получите полноценная CMS для вашей системы.
Так вот есть неприятная особенность — проблема с ассоциацией has_one. Они не обрабатываются автоматически, и вы не сможете редактировать отношения has_one. Выглядит это примерно как на рисунке 1. Вместо выделения просто ссылка на рисунок.
картинка 1 Давайте заглянем внутрь, вот так выглядит модель нашего техпроцесса.
У него есть_одна связь с чертежом (черновиком) и хотелось бы иметь возможность редактировать его через CMS.
Ситуация удивительная, но решение этой проблемы простое, как палка.class TechProcess < ApplicationRecord include MdcSchema has_many :executor_programs, inverse_of: :tech_process, foreign_key: :barcode_tech_process, primary_key: :barcode validates :barcode, presence: true validates :barcode, uniqueness: true has_one :draft2tech_process, dependent: :destroy has_one :draft, through: :draft2tech_process has_many :tech_process2tech_operations has_many :tech_operations, through: :tech_process2tech_operations end
Как следует из вики ассоциация has_one не будет правильно инициализирована, пока не будут указаны методы установки и получения идентификатора.
Грубо говоря, сеттеры и геттеры.
Добавьте их в модель def draft_id
self.draft.try :id
end
def draft_id=(id)
self.draft = Draft.find_by_id(id)
end
И в результате мы получаем полноценный интерфейс редактирования техпроцесса с возможностью выбора чертежа, создания нового или редактирования прикрепленного, в общем все прелести CMS.
Ничего сложного, но что если у нас будет 15 моделей с одним или даже несколькими соединениями has_one, нам придется повторять эти геттеры и сеттеры в каждом месте? Это, конечно, можно сделать, но тогда нарушается очень важный принцип.
СУХОЙ .
Ну и плюс природная лень не позволяет так расточительно писать много строк кода.
Поэтому нам нужно найти способ автоматизировать этот процесс, и здесь на помощь приходит метапрограммирование.
Мета-решение
Итак, давайте начнем self.reflect_on_all_associations(:has_one).
each do |has_one_association|
define_method("#{has_one_association.name}_id") do
self.send(has_one_association.name).
try :id
end
define_method("#{has_one_association.name}_id=") do |id|
self.send("#{has_one_association.name}=",has_one_association.klass.find_by_id(id))
end
end
Вот как выглядит код, который сделает has_one и Rails Admin друзьями А теперь более подробно, что здесь происходит. Далее я подробно остановлюсь только на аспектах, которые касаются рефлексии и метапрограммирования.
В Ruby все является объектом, соединение также является объектом и несет полную информацию о себе и всех своих отношениях.
Первый интересный метод отражать _on_all_associations Возвращает массив всех соединений, но может принимать параметр «макрос» в примере выше, который я туда передал :has один и он вернул мне только есть _one соединения, отлично, даже не пришлось дополнительно выбирать только нужные соединения.
Отлично, теперь есть список всех соединений has_one и вам нужно автоматически определить геттеры и сеттеры.
Дальше идет, простите за тавтологию, метод define_method который динамически определяет метод во время выполнения.
В результате этот код создает методы, необходимые для корректной инициализации has_one в админке рельсов.
Сушить до конца
Все это было предназначено для создания «сухого» кода, поэтому сейчас опишу последнюю деталь.Нам нужно принять во внимание всю эту метамагию.
require 'active_support/concern'
module HasOneHandler
extend ActiveSupport::Concern
included do
self.reflect_on_all_associations(:has_one).
each do |has_one_association| define_method("#{has_one_association.name}_id") do self.send(has_one_association.name).
try :id
end
define_method("#{has_one_association.name}_id=") do |id|
self.send("#{has_one_association.name}=",has_one_association.klass.find_by_id(id))
end
end
end
end
И в результате класс, в который нужно добавить геттеры и сеттеры, добавляется всего одна строка с включением контейнера.
Вот таким лаконичным способом нам удалось подружить соединения CMS и has_one. Если применить стандартный подход, нам пришлось бы писать определения гетеров и сеттеров для каждого соединения в каждой модели, а их может быть довольно много.
Финальная версия модели class TechProcess < ApplicationRecord
include MdcSchema
include HasOneHandler
has_many :executor_programs, inverse_of: :tech_process, foreign_key: :barcode_tech_process, primary_key: :barcode
validates :barcode, presence: true
validates :barcode, uniqueness: true
has_one :draft2tech_process, dependent: :destroy
has_one :draft, through: :draft2tech_process
has_many :tech_process2tech_operations
has_many :tech_operations, through: :tech_process2tech_operations
end
Заключение
Надеюсь, мне удалось показать практическую ценность использования методов метапрограммирования.Используете ли вы его или нет, конечно, зависит от вас.
Если использовать его слишком часто и неуместно, то проект превратится в совершенно нечитабельный и трудный для отладки, а при правильном использовании он, наоборот, уменьшит объём кода, улучшит читабельность и избавит от рутинной работы.
Спасибо всем, кто прочитал! Теги: #программирование #Функциональное программирование #автоматизация #ruby #метапрограммирование #ruby на рельсах #сухой #метапрограммирование
-
Как Ускорить Работу Вашей Системы Windows
19 Oct, 24 -
Моисм
19 Oct, 24 -
Яндекс.открытки Ищут Партнеров
19 Oct, 24