📜 ⬆️ ⬇️

How to DRY model

In most rail projects, the main concentration of the code is on the model. Everyone probably read about the Slim controllers & fat models and try to cram as much into the model as possible, and as little as possible into the controllers. Well, this is commendable, but in an effort to thicken the model, many often forget about the principle of DRY - don't (fucking) repeat yourself.

I'll try to briefly describe how to eat fish in the area of ​​models and not forget about DRY.


')
So, we have a model - cut class. Like any Ruby class, the model consists of three things:

1) Instance Method Definitions

class User < ActiveRecord::Base def name [ first_name, last_name ].filter(&:presence).compact.map(&:strip) * ' ' end end 


2) class method definitions

 class User < ActiveRecord::Base def self.find_old where('created_at < ?', 1.year.ago).all end end 


3) "class" code

 class User < ActiveRecord::Base attr_accessor :foo has_many :authentications, :dependent => :destroy has_many :invitees, :class_name => 'User', :as => :invited_by delegate :city, :country, :to => :location attr_accessible :admin, :banned, :as => :admin mount_uploader :userpic, UserPicUploader scope :admin, where(:admin => true) validates :username, :uniqueness => true, :format => /^[az][az\d_-]+$/, :length => { :within => 3..20 }, :exclusion => { :in => USERNAME_EXCLUSION } end 


The easiest way to implement a model, like, in fact, any other class in Ruby, is to make the repeating parts in separate modules. In fact, modules are not only useful for this. Sometimes it is not a bad thing to simply take out a huge footballer of a code in a separate file so that the model looks cleaner and tons of irrelevant garbage do not get tangled.

Methods


So, with the definitions of instance methods everything is as simple and clear as possible:
 # user_stuff.rb module UserStuff def name [ first_name, last_name ].filter(&:presence).compact.map(&:strip) * ' ' end end # user.rb class User < ActiveRecord::Base include UserStuff end 


With class methods, slightly more interesting. That's not how it will work:
 # user_stuff.rb module UserStuff def self.find_old where('created_at < ?', 1.year.ago).all end end # user.rb class User < ActiveRecord::Base include UserStuff end 

Here we define the find_old method for the find_old module UserStuff , and it will not end up in the model.
Therefore, we must do something like this:
 # user_stuff.rb module UserStuff def find_old where('created_at < ?', 1.year.ago).all end end # user.rb class User < ActiveRecord::Base extend UserStuff # ,  include,  extend end 


Together, intans and class methods can be put into a module, for example, like this:
 # user_stuff.rb module UserStuff module InstanceMethods def name [ first_name, last_name ].filter(&:presence).compact.map(&:strip) * ' ' end end module ClassMethods def find_old where('created_at < ?', 1.year.ago).all end end end # user.rb class User < ActiveRecord::Base include UserStuff::InstanceMethods extend UserStuff::ClassMethods end 


Class code


The unresolved question is what to do with the “class” code. Usually it is just the most and he most often needs DRYing.

The problem is that it must be executed in the context of the class being declared (the model in our case). It is impossible to simply write it inside the module - it will try to execute immediately and most likely will break, because the simple module does not know anything about validation, or about numerous plugins, or about has_many etc. Therefore, it is necessary to shove the code into the module in such a way that it is executed only when this module is connected to the model. Fortunately, it’s very easy to cut.

The Module object has a defined method called included, which is called every time we include a module somewhere. Thus, we can get this code to execute the code we need. Like this:
 module UserValidations def self.included(base) # base    — ,   . base.instance_eval do #     base #      validates :username, :uniqueness => true, :format => /^[az][az\d_-]+$/, :length => { :within => 3..20 }, :exclusion => { :in => USERNAME_EXCLUSION } validates :gender, :allow_blank => true, :inclusion => { :in => %w(mf) } end end end 


Now the whole code of validations will not be executed at the moment of defining the module, but it will lie quietly and wait until we module this module somewhere.

All in a bunch


Now, how would this be combined with the definitions of the methods that were higher? That's how:

 # user_stuff.rb module UserStuff def self.included(base) #     base.extend ClassMethods #    # Module#include —  .          #      base.include(InstanceMethods),    : base.send :include, InstanceMethods #       base.instance_eval do validates :gender, :presence => true end end #   module ClassMethods def find_old where('created_at < ?', 1.year.ago).all end end # - module InstanceMethods def name [ first_name, last_name ].filter(&:presence).compact.map(&:strip) * ' ' end end end # user.rb class User < ActiveRecord::Base include UserStuff end 


So, we have a module in which we can safely take out any complex piece of the model, and then how many times to use this piece. It's almost awesome. Not only is the fact that the code is ugly and you can easily get confused in these endless included / send: include / extend.

You can prettier!


In Ruby community, the readability and beauty of the code, as well as the principle of hiding complexity, are highly valued - hide complex things behind some simple and beautiful API / DSL. If you look at the RoR code, it becomes immediately obvious that the above approach is used there almost everywhere. Therefore, naturally, the guys decided to make their lives easier and came up with ActiveSupport::Concern .

With this ActiveSupport::Concern our code can be rewritten as follows:
 module UserStuff extend ActiveSupport::Concern included do validates :gender, :presence => true end #   module ClassMethods def find_old where('created_at < ?', 1.year.ago).all end end # - module InstanceMethods def name [ first_name, last_name ].filter(&:presence).compact.map(&:strip) * ' ' end end end 


In fact, a more beautiful code is not the only feature of this helper. He has more useful features, but I may sometime write about them separately. Who can not wait, he let go to read raw active_support in the area of lib/active_support/concern.rb , there are very cool and spreading comments with code examples and all that.

Where to put?


Another important point that the programmer will immediately encounter, who for the first time will decide to razderbanit their models on separate modules - where to put the code and how to name the modules?

There is probably no single opinion, but I came up with the following scheme for myself:
 # lib/models/validations.rb module Models module Validations #   c     end end # lib/models/user/validation.rb module Models module User module Validations #    User end end end # app/models/user.rb class User < ActiveRecord::Base include Models::Validations include Models::User::Validations end 


The file names matter because they allow you not to load all the code at once, but using autoload rail is a mechanism that when it encounters an undefined constant, such as Models::User::Validations , first tries to search for the file models/user/validations.rb and try to load it , and only then, in case of failure, panic and throw a NameError exception.

Conclusion


I hope someone will make something useful for themselves from the article and the world will be a little less fucking-readable code and models for a half thousand times.

Update:
Here, privaloff noticed that I was stupid in the code, which without Soncern, namely, I forgot about instance_eval . Code corrected. Comrade plus karma for attentiveness.

Source: https://habr.com/ru/post/125822/


All Articles