[Ruby, Программирование, Ruby on Rails, Функциональное программирование] Метапрограммирование в реальной задаче

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
28-Янв-2021 03:30

Всем привет! В этой статье хочу рассказать про метапрограммирование на примере реальной часто встречающейся проблемы.Когда кто то говорит про метапрограммирование у олдскульного кодировщика случается приступ ярости.И на это есть причины так и на большом проекте может показаться безумием использовать метапрограммирование, так как код становится очень сложным для чтения. А если в проект включится специалист со стороны, то он и подавно ничего не разберет в этом мета-коде.Но все не так просто, как говорится - нет плохих инструментов. В этой статьей я постараюсь показать на реальном рабочем примере как метапрограммирование поможет сделать ваш код чище, избавит от рутинных повторений. И заставит порадоваться мета-магии.Справка из википедиМетапрограммирование — вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы (в частности, на стадии компиляции их исходного кода), либо программ, которые меняют себя во время выполнения (самомодифицирующийся код). Первое позволяет получать программы при меньших затратах времени и усилий на кодирование, чем если бы программист писал их вручную целиком, второе позволяет улучшить свойства кода (размер и быстродействие).На этом заканчивается вступление. Теперь хочу перейти к практической части и рассказать про суть проблемыДальше речь пойдёт о языке ruby и фраймворке Rails в частности. В руби есть рефлексия и большие возможности для метапрограммирования. Большинство гемов, модулей и фреймворков созданы при помощи мощных инструментов рефлексии.
Photo by Joshua Fuller on UnsplashКто писал что то на Rails скорее всего сталкивался с таким гемом как Rails Admin, если вы в своих проектах на рельсах до сих не пробовали использовать его или аналоги категорически рекомендую это сделать, так как практически из коробки вы получите полноценную CMS для вашей системы.Так вот там есть неприятная особенность проблема с has_one association.Они не обрабатываются автоматически и у вас не будет возможности редактировать вашу has_one связь. Выглядит это примерно как на рисунке 1. Вместо селекта, просто ссылка на чертеж.
рисунок 1Посмотрим на внутренности вот так выглядит модель нашего техпроцесса. У неё есть has_one связь с чертежом (draft) и хотелось бы иметь возможность редактировать её через 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 это что придется повторить эти гетеры сеттеры в каждом месте? Такое конечно можно сделать, но тогда нарушается очень важный принцип DRY. Ну и плюс природная лень не позволяет так расточительно писать много строк кода. Так что надо найти способ автоматизировать этот процесс и тут на помощь приходит метапрограммирование.Meta решениеЧто же приступим
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А теперь более подробно что тут происходит. Далее детально буду останавливаться только на аспектах которые касаются рефлексии и мета программирования.В руби всё является объектом, связь также является объектом и несет полную информацию о самой себе и всех своих отношениях. Первый интересный метод reflect_on_all_associations Который возвращает массив всех связей, но может принимать параметр "macro" в примере выше я передал туда :hasone и он вернул мне только has_one связи, прекрасно, даже не пришлось дальше селектить только нужные связи.Отлично, теперь есть список всех has_one связей и нужно автоматизировано определить гетеры и сетеры. Дальше идет, простите за тавтологию, метод define_method который динамически прямо во время исполнения определяет метод. В итоге этот код создает методы необходимые для корректной инициализации has_one в rails admin.Сушим до концаВсё это задумывалось для создания "сухого" кода, так что сейчас опишу последнюю деталь. Нужно всю эту мета-магию вынести в concern
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
ЗаключениеНадеюсь мне удалось показать практическую ценность использования приемов метапрограммирования. Использовать его или нет решать конечно вам. Если применять его слишком часто и не к месту, то проект превратиться в абсолютно не читаемый и трудно отлаживаемый, но в случае правильного использования, напротив, уменьшает количество кода, улучшает читаемость и избавляет от рутиной работы. Спасибо всем кто дочитал!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_ruby, #_programmirovanie (Программирование), #_ruby_on_rails, #_funktsionalnoe_programmirovanie (Функциональное программирование), #_metaprogrammirovanie (метапрограммирование), #_ruby_on_rails, #_dry, #_ruby, #_metaprogramming, #_avtomatizatsija (автоматизация), #_ruby, #_programmirovanie (
Программирование
)
, #_ruby_on_rails, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 20-Май 23:24
Часовой пояс: UTC + 5