Thumb marko Marko Ćilimković Thursday, July 7, 2016

Have you ever been stuck debugging a piece of code for hours, only to realize you were looking in the wrong place? Have you ever needed to take more than minutes to figure out why is something acting as it is? Do you remember how it feels to go back and work on a 2-year old application with the features you don't remember? Sounds like you need a new approach in developing Rails applications - outside the convention, but still inside Ruby and Rails. Take a look at the old new layer of abstraction that isn't used in Rails apps by default and learn how to be better at what you're already good at.

History Lesson

Improvement, maintainability, speed, performance, security. Since we can remember, people are obsessed with the mentioned notions. We are always striving to be better in anything we put our hands on. From stone-tipped spears to automatic corner shotguns with self-guided bullets, from horses to solar fueled airplanes, from straw houses to almost 1km tall buildings where you can live and never have to go outside.

The same principle is seen in the programming world where people are debating for decades about the best approach for a given set of problems and requests. That is a beautiful thing, but it can sometimes be cumbersome or confusing. In the end, the decision on what approach to choose is on you, based on your priorities and beliefs.

Personally, my stone-tipped spear was PHP and the procedural style of coding where everything was clumped together in one big script that contained SQL code, HTML, CSS, and PHP. I evolved my development skills with years of training, learning from mistakes, teaching others and listening to more experienced developers. But...I still feel like I don't have that cool automatic corner shot gun. It's a Colt AR-15 at the most! Say hello to my little friend, Rails! You're going to be introduced to service objects, which will break the overly simple architecture of models, views, and controllers and upgrade you to a more sophisticated and enjoyable framework.

Breaking the Conventions

Thin controllers - fat models, Law of Demeter, Tell-Don't-Ask principles and a whole lot of other design guidelines are great to know and use, but sometimes some of them are not, depending on what your goal is and how the application needs to be tailored.

A lot of beginner programmers make the same mistake and keep a lot of business logic in the controllers. There's also messier beginners with no programming background that have one and one goal only - to make things work. They often have the business logic in the views which resembles the old PHP procedural way of programming. A total nightmare that can be cured with the thin controllers - fat models principle, which is probably the first I have heard in the Rails community and it's an awesome one because it helps you clean up all the mess and make the code more readable and maintainable.

Here's the deal, that same principle is openly showing his weakness in its name. Fat models...as your application grows, features are added, old methods are being patched, new resources created and the lines of code grow to unmaintainable proportions. Imagine an application in which you can sign up, log in, log out, generate a new password and by doing so, see that it also sends emails, maybe SMS messages etc. A lot of Rails apps use devise for authentication, but if yours isn't one of them, the User model might look like this

app/models/user.rbclass User < ActiveRecord::Base
validates_presence_of :username, :email
validates_uniqueness_of :username
validates_confirmation_of :password

has_secure_password

def self.authenticate(username, password)
user = find_by_username(username)
user if user && user.authenticate(password)
end

def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = auth[:provider]
user.uid = auth[:uid]
user.username = auth[:info][:nickname]
user.save!
end
end


def send_invitation(email)
UserMailer.invitation(self, email).deliver_now
increment! :invitation_count
end

def reached_invitation_limit?
invitation_count.to_i > 5
end

def send_password_reset
generate_password_reset_token
self.password_reset_sent_at = Time.zone.now
save!
UserMailer.password_reset(self).deliver
end

def generate_password_reset_token
begin
self.password_reset_token = SecureRandom.hex
end while User.exists?(password_reset_token: password_reset_token)
end

def password_reset_expired?
password_reset_sent_at < 2.hours.ago
end
end

And this is only the beginning of the application features, right? Users will be able to do all sorts of things and all of the methods will end up down the stack, pushed from the controllers into that single model. Naturally, that won't feel right and it's going to be hard to maintain because of all the unrelated methods. So, the fat model needs to go.

Concerns - models on diet

Rails 4 applications come with a nifty tool called concerns that are automatically loaded when launching a Rails app. These are basically modules that inherit the ActiveSupport:Concern module, which gives you access to the model they are included in and enable you to use all the ActiveRecord magic like validations, associations, etc.

This solves not only the problem with having a bunch of unrelated methods in one model, but also reduces code duplication and puts together all related code in modules. Ta-da! You're keeping your code DRY. The extracted code will go in the authentication.rb concern

app/models/concerns/authentication.rbmodule Authentication
extend ActiveSupport::Concern

included do
has_secure_password
end

module ClassMethods
def authenticate(username, password)
user = find_by_username(username)
user if user && user.authenticate(password)
end

def from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = auth[:provider]
user.uid = auth[:uid]
user.username = auth[:info][:nickname]
user.save!
end
end
end
end

which gives us a more readable user model, because now we can remove all of the authentication-related code from the user class and replace it with just one line where we include the concern. Applying this logic to all of the other related methods can make debugging more difficult because we won't always know where an error occurred and which concern to tackle first.

app/models/user.rbclass User < ActiveRecord::Base
validates_presence_of :username, :email
validates_uniqueness_of :username
validates_confirmation_of :password

include Authentication
include Similarity
include Searching
include Tagging
include PasswordForgetting
include CsvConversion
include Liking
end

Some people find cover in concerns, but a lot of people consider that any application with  app/concerns the directory is concerning. It's basically keeping it under your hat...you don't see it, but it's still there. It's ok to have one, maybe two concerns for one model (especially if you're using concerns only on that model). In that case, it's even better to wrap the concern in a namespace of the model you're using it on

I'd much rather maintain and upgrade an app that has everything higher up the stack, and by that, I mean the controllers. And this thought made me wonder: where do I need to look to understand what a piece of code is doing and how much stuff unrelated to the code is there.

So, should we leave it in the controllers? Not really...controllers should be thin...it's harder to test the behavior when all the business logic is there, and they already have enough responsibility handling requests and responses. If only there was something else...

My Little Friend - Service Object

A service object is a method that is wrapped in an object. Service Objects separate the business logic of applications in reusable components, which are basically methods, wrapped in an object. They are used to reduce coupling in systems, make it obvious to other developers what an application does and simplify testing the system.

One service - one job principle is going to change your code architecture from having a few objects with many methods for the business logic to many classes, each having a single purpose. With that new layer in our MVC pattern, all developers in the team will know where lies which functionality and all new teammates will quickly learn what the application can do with just a glance at the new services directory: CreateUser, InviteUser, VoteUpComment, VoteDownComment, LikeComment etc...

The best thing about service objects is that they don't require any new gems and no new language except for the knowledge you already have - Ruby, which makes service objects Plain Old Ruby Objects (PORO).

app/services/authentication.rbclass Authentication
def initialize(params, omniauth = nil)
@params = params
@omniauth = omniauth
end

def user
@user ||= @omniauth ? user_from_omniauth : user_with_password
end

def authenticated?
user.present?
end

private

def user_from_omniauth
User.where(@omniauth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = @omniauth[:provider]
user.uid = @omniauth[:uid]
user.username = @omniauth[:info][:nickname]
end
end

def user_with_password
user = User.find_by_username(@params[:username])
user && user.authenticate(@params[:password])
end
end

Some developers are strict with the tell-don'-ask and Law of Demeter principles, but try and resist it, because you'll be much faster in code refactoring when jumping from the controller to the service class and its methods.

If we create POROs with all of our concerns we can delete the concerns and also delete the include calls in the user model, but there's still one thing to change, and that's in our controllers. Specifically, the SessionsController is going to look like this

app/controllers/sessions_controller.rbclass SessionsController < ApplicationController

def new

end

def create
auth = Authentication.new(params, env["omniauth.auth"])
if auth.authenticated?
session[:user_id] = auth.user.id
redirect_to root_path, notice: 'Successfully logged in!'
else
flash[:alert] = 'Invalid username or password'
render :new
end
end

end

Conclusion

The benefits of using services are tenfold:

  • simpler controllers
  • skinnier models - now they only deal with associations, scopes, validations, and persistence
  • linear flow of code - no callbacks in models, which are one of the worst code smells in Rails
  • faster at figuring out how a specific business logic is implemented
  • makes obvious what an app does by looking at app/services

One of the first "problems" developers encounter when using services is much more of unnecessary code in Rails apps, because they are mostly used to use the magic and all the hidden things Active Record provides, which is mostly just CRUD operations on resources. In that case, don't use service objects for ALL actions, especially if it's simple CRUD operations. Just interact directly with the model and use the AR functionalities. Use it more often on complex controller action that needs to interact with a complex model.

Service objects are better for medium to large applications that have a decent amount of logic beyond creating, reading, updating and deleting resources. If you have only one or two occurrences where you're placing code somewhere it doesn't belong to, you're ok. But I still encourage you to use services even in those cases, because apps tend to grow, and developers are mostly lazy and don't refactor code enough.

Write better code and be proud of it! :)



Cookies help us deliver our services. By using our services, you agree to our use of cookies.