How to DRYit models
In most rail projects, the main concentration of code falls on the model. Everyone probably read about Slim controllers & fat models and try to shove as much as possible into models, and as little as possible into controllers. Well, this is commendable, however, in an effort to thicken models, many often forget about the DRY principle - don't (fucking) repeat yourself.
I’ll try to briefly describe how to eat fish in the area of models, and not to forget about DRY.
So, the model we have is a cut class. Like any cut class, a model consists of three things:
The easiest way to reprogram a model, like, in fact, any other class in ruby, is to take out the repeating parts in separate modules. In fact, modules are not only useful for this. Sometimes it’s not bad, it’s just putting a huge trash of code into a separate file so that the model looks cleaner and tons of irrelevant garbage do not get under your feet.
So, with the definitions of instance methods, everything is as simple and clear as possible:
Class methods are slightly more interesting. This doesn’t work:
Here we define the method for
Therefore, you need to do something like this:
Together, intans and class methods can be put into a module, for example, like this:
The unresolved question is what to do with the "class" code. Usually he is just the most and he most often needs DRYing.
The problem is that it must be executed in the context of the declared class (the model in our case). It is simply impossible to write it inside the module - it will try to execute immediately and most likely will break, because a simple module does not know anything about validation, nor about numerous plugins, nor about
The Module object has an included method, which is called every time we include the module somewhere. Thus, we can force the code we need to execute under this matter. Like this:
Now all the validation code will not be executed at the time the module is defined, but will lie quietly and wait until we get this module somewhere inactive.
Now how would this be combined with the method definitions that were above? That's how:
In total, we have a module in which you can safely take out any arbitrarily complex piece of the model, and then how many times you need to use this piece. Everything is pretty cool. It’s not cool that the code is ugly and you can easily get confused in these endless included / send: include / extend.
In the ruby community, the readability and beauty of the code is greatly appreciated, as well as the principle of hiding complexity - to hide complex things behind some simple and beautiful API / DSL. If you look at the RoR code, it immediately becomes clear that the above approach is used there almost everywhere. Therefore, of course, the guys decided to make their life easier and came up with
Using this very
In fact, a more beautiful code is not the only feature of this helper. It still has some useful features, but I may someday write about them separately. Who can’t wait, let him go read the raw active_support in the area
Another important point that the programmer will immediately encounter, who for the first time decides to tear his models into separate modules - where to put the code and how to name the modules?
There probably will not be an unambiguous opinion, but for myself I came up with the following scheme:
File names matter because they allow you to not load all the code at once, and use rail autoload - a mechanism that when it encounters an undefined constant, such as
I hope someone brings out something useful from the article for themselves and in the world it becomes a little less than a shitty-readable code and models for one and a half thousand time.
Update:
Here privaloff noticed that I was dumb in the code, which without concern, namely, I forgot about
I’ll try to briefly describe how to eat fish in the area of models, and not to forget about DRY.
So, the model we have is a cut class. Like any cut class, a model consists of three things:
1) definition of instance methods
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 => /^[a-z][a-z\d_-]+$/,
:length => { :within => 3..20 },
:exclusion => { :in => USERNAME_EXCLUSION }
end
The easiest way to reprogram a model, like, in fact, any other class in ruby, is to take out the repeating parts in separate modules. In fact, modules are not only useful for this. Sometimes it’s not bad, it’s just putting a huge trash of code into a separate file so that the model looks cleaner and tons of irrelevant garbage do not get under your feet.
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
Class methods are slightly more interesting. This doesn’t 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 method for
find_old
the module itself UserStuff
, but it will not end up in the model. Therefore, you need to 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 he is just the most and he most often needs DRYing.
The problem is that it must be executed in the context of the declared class (the model in our case). It is simply impossible to write it inside the module - it will try to execute immediately and most likely will break, because a simple module does not know anything about validation, nor about numerous plugins, nor about
has_many
etc. Therefore, you need to put the code into the module so that it is executed only when this module is connected to the model. Fortunately, in ruby this is very easy to do.The Module object has an included method, which is called every time we include the module somewhere. Thus, we can force the code we need to execute under this matter. Like this:
module UserValidations
def self.included(base)
# base в данном случае — то, куда инклудится модуль.
base.instance_eval do
# выполняем код в контексте base
# только в момент подключения модуля
validates :username,
:uniqueness => true,
:format => /^[a-z][a-z\d_-]+$/,
:length => { :within => 3..20 },
:exclusion => { :in => USERNAME_EXCLUSION }
validates :gender,
:allow_blank => true,
:inclusion => { :in => %w(m f) }
end
end
end
Now all the validation code will not be executed at the time the module is defined, but will lie quietly and wait until we get this module somewhere inactive.
All in a bunch
Now how would this be combined with the method definitions that were above? 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
In total, we have a module in which you can safely take out any arbitrarily complex piece of the model, and then how many times you need to use this piece. Everything is pretty cool. It’s not cool that the code is ugly and you can easily get confused in these endless included / send: include / extend.
You can be beautiful!
In the ruby community, the readability and beauty of the code is greatly appreciated, as well as the principle of hiding complexity - to hide complex things behind some simple and beautiful API / DSL. If you look at the RoR code, it immediately becomes clear that the above approach is used there almost everywhere. Therefore, of course, the guys decided to make their life easier and came up with
ActiveSupport::Concern
. Using this very
ActiveSupport::Concern
code, we can rewrite it like this: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. It still has some useful features, but I may someday write about them separately. Who can’t wait, let him go read the raw active_support in the area
lib/active_support/concern.rb
, there are very cool and weighty comments with code examples and all that.Where to put it?
Another important point that the programmer will immediately encounter, who for the first time decides to tear his models into separate modules - where to put the code and how to name the modules?
There probably will not be an unambiguous opinion, but for myself I came up with the following scheme:
# 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
File names matter because they allow you to not load all the code at once, and use rail autoload - 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 download it, and only then, in case of failure, panic and throw an exception NameError
.Conclusion
I hope someone brings out something useful from the article for themselves and in the world it becomes a little less than a shitty-readable code and models for one and a half thousand time.
Update:
Here privaloff noticed that I was dumb in the code, which without concern, namely, I forgot about
instance_eval
. The code corrected. Comrade plus in karma for mindfulness.