Метапрограммирование В Реальной Проблеме

Всем привет! В этой статье я хочу поговорить о метапрограммировании на примере реальной, часто встречающейся проблемы.

Когда кто-то говорит о метапрограммировании, программист старой закалки приходит в ярость.

И для этого есть причины; в большом проекте использование метапрограммирования может показаться безумием, поскольку код становится очень трудно читать.

А если к проекту присоединится сторонний специалист, он уж точно ничего не поймет в этом метакоде.

Но не все так просто, как говорится – плохих инструментов не бывает. В этой статье я попытаюсь на реальном рабочем примере показать, как метапрограммирование поможет сделать ваш код чище и исключить рутинное повторение.

И это заставит вас порадоваться метамагии.

Помощь из Википедии Метапрограммирование - вид программирования, связанный с созданием программ, порождающих в результате своей работы другие программы (в частности, на этапе компиляции их исходного кода), или программ, изменяющих себя в процессе выполнения (самомодифицирующийся код).

Первый позволяет создавать программы с меньшими затратами времени и усилий на кодирование, чем если бы программист писал их полностью вручную, второй позволяет улучшить свойства кода (размер и скорость).



На этом вступление заканчивается.

Теперь хочу перейти к практической части и рассказать о сути проблемы.

Далее мы поговорим о языке 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 на рельсах #сухой #метапрограммирование

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.