Many people are learning Ruby by starting with Rails framework and, unfortunately, this is the worst possible way of learning this language. Don’t get me wrong: Rails is great, it helps you to build web applications quickly and efficiently without having to get into many technical details.
It’s nice to meet you!
Many people are learning Ruby by starting with Rails framework and, unfortunately, this is the worst possible way of learning this language. Don’t get me wrong: Rails is great, it helps you to build web applications quickly and efficiently without having to get into many technical details. They provide a lot of “Rails magic” that makes things simply work. And for a newbie programmer this is really great, because the most pleasant moment of the process is when you can say “it’s alive!”, and see that all parts fit together and people use your app. We like to be “creators” 🙂 But there is one thing that distinguishes good programmers from the average: the good ones understand how the tools they use work. And by “understanding your tools” I don’t mean knowing all the methods and modules provided by a framework, but understanding how it works, understanding how the “Rails magic” happens. Only then you can feel comfortable with using objects and programming with Rails. The foundation of the object-oriented programming, and the secret weapon which makes the complicated Rails application easier, is the already mentioned in the title PORO, that is Plain Old Ruby Object
What really lies beneath this name? What is this great secret weapon? It is a simple Ruby class that does not inherit from anything. Yes, just that, and so much.
class AwesomePoro
end
How can I help you?
You are continuously developing your application and adding new functionalities as the number of users and their expectations are growing. You get to the point where you encounter more and more dark places of extremely twisted logic, the places which are avoided like the plague by even the bravest developers. The more such places, the more difficult to manage and develop the application. A standard example is the action of registering a new user, which triggers a whole group of other actions associated with this event:
- checking the IP address in a spam database,
- sending an email to the new user,
- adding a bonus to an account of a recommending user,
- creating accounts in related services,
- and many more…
A sample code responsible for user registration might look like this:
class RegistrationController < ApplicationController
def create
user = User.new(registration_params)
if user.valid? && ip_valid?(registration_ip)
user.save!
user.add_bonuses
user.synchronize_related_accounts
user.send_email
end
end
end
Okay, you’ve got it coded, everything works, but… is all of this code really okay? Maybe we could write it better? First of all, it breaks the basic principle of programming – Single Responsibility, so surely we could write it better. But how? This is where the already mentioned PORO comes to help you. It’s enough to separate a class RegistrationService, which will be responsible for only one thing: notifying all the related services. By services we will consider the individual actions which we have already singled out above. In the same controller all you need to do is create an object RegistrationService and call on it the “fire!” method. The code has become much clearer, our controller is taking up less space, and each of the newly created classes is now responsible for only one action, so we can easily replace them should a need arise.
class RegistrationService
def fire!(params)
user = User.new(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
after_registered_events(user)
end
user
end
private
def after_registered_events(user)
BonusesCreator.new.fire!(user)
AccountsSynchronizator.fire!(user)
EmailSender.fire!(user)
end
def ip_validator
@ip_validator ||= IpValidator.new
end
end
class RegistrationController < ApplicationController
def create
user = RegistrationService.new.fire!(registration_params)
end
end
However Plain Old Ruby Object may prove useful not only for controllers. Imagine that the application you are creating uses a monthly billing system. The exact day of creating such a billing is not important to us, we only need to know that it concerns a specific month and year. Of course you can set the day for the first day of each month and store this information in the object of the “Date” class, but neither is it a true information, nor do you need it in your application. By using PORO you can create a class “MonthOfYear”, the objects of which will store the exact information you need. Moreover, when applying in it the module “Comparable”, it will be possible to iterate and to compare its objects, just like when you are using the Date class.
class MonthOfYear
include Comparable
attr_reader :year, :month
def initialize(month, year)
raise ArgumentError unless month.between?(1, 12)
@year, @month = year, month
end
def <=>(other)
[year, month] <=> [other.year, other.month]
end
end
Introduce me to Rails.
In the Rails world, we are used to the fact that each class is a model, a view or a controller. They also have their precise location in the directory structure, so where can you put our little PORO army? Consider a few options. The first thought that comes to mind is: if the created classes are neither models, nor views nor controllers, we should put them all in the “/lib” directory. Theoretically, it is a good idea, however if all of your PORO files will land in one directory, and the application will be large, this directory will quickly become a dark place which you fear to open. Therefore, undoubtedly, it is not a good idea.
AwesomeProject
├──app
│ ├─controllers
│ ├─models
│ └─views
│
└─lib
└─services
#all poro here
You can also name some of your classes non-ActiveRecord Models and put them in the “app/models” directory, and name the ones which are responsible for handling other classes the services and put them in the “app/services” directory. This is a pretty good solution, but it has one drawback: when creating a new PORO, each time you will have to decide every whether it is more of a model or a service. This way, you may reach a situation where you have two dark places in your application, only smaller ones. There is yet a third approach, namely: using namespaced classes and modules. All you need to do is create a directory which has the same name as a controller or a model, and put all PORO files used by the given controller or model inside.
AwesomeProject
├──app
│ ├─controllers
│ │ ├─registration_controller
│ │ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─models
│ │ ├─settlement
│ │ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─views
│
└─lib
Thanks to this arrangement, when using it, you don’t have to precede the name of a class with a namespace. You have gained shorter code and more logically organized directory structure.
Check me out!
It is a pleasant surprise that when using PORO, the unit tests of your application are faster and easier to write and later on more likely to be understood by others. As each class is now responsible for only one thing, you can recognize the boundary conditions sooner, and easily add appropriate test scenarios to them.
describe MonthOfYear do
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
describe "creating new instance" do
it "initializes with correct year and month" do
expect { described_class.new(10, 2015) }.to_not raise_error
end
it "raises error when given month is incorrect" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
I hope we’ll meet again!
The examples we presented clearly show that using PORO improves readability of applications and makes them more modular, and, in consequence, easier to manage and expand. Embracing the principle of the Single Responsibility facilitates the exchange of particular classes if necessary, and doing so without interfering with other elements. It also makes testing them a simpler and faster procedure. Moreover, this way keeping Rails models and controllers short is much easier, and we all know that they tend to get unnecessarily big in the process of development.