Metaprogramming in a real-world task

Hello everyone! In this article I want to talk about metaprogramming using an example of a real common problem.





When someone talks about metaprogramming, an old school coder gets a fit of rage.





And there are reasons for this, and on a large project it may seem crazy to use metaprogramming, as the code becomes very difficult to read. And if a specialist from the outside joins the project, then he even moreover will not understand anything in this meta-code.





But it's not as simple as they say - there are no bad tools. In this article I will try to show with a real working example how metaprogramming can help to make your code cleaner and get rid of routine repetitions. And will make you rejoice in meta-magic.





Help from wikipedia

Metaprogramming is a type of programming associated with the creation of programs that generate other programs as a result of their work (in particular, at the stage of compilation of their source code), or programs that change themselves during execution (self-modifying code). The first allows you to get programs with less time and effort on coding than if the programmer wrote them entirely by hand, the second allows you to improve the properties of the code (size and performance).





This concludes the introduction. Now I want to go to the practical part and tell you about the essence of the problem

Further we will focus on the ruby ​​language and the Rails framework in particular. Ruby has reflection and great opportunities for metaprogramming. Most gems, modules, and frameworks are built with powerful reflection tools.





Photo by Joshua Fuller on Unsplash





Whoever wrote something on Rails most likely came across such a gem as Rails Admin, if you have not tried to use it in your projects on rails, or if you have analogs, I strongly recommend doing this, since almost out of the box you will get a full-fledged CMS for your system.





So there is an unpleasant feature, the problem with the has_one association.





has_one . 1. , .





picture 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

      
      







I hope I was able to show the practical value of using metaprogramming techniques. Of course, you decide whether to use it or not. If you use it too often and out of place, then the project will turn into absolutely unreadable and difficult to debug, but if used correctly, on the contrary, reduces the amount of code, improves readability and eliminates routine work. Thanks to everyone who read it!








All Articles