DEV Community

Antonio J.
Antonio J.

Posted on

This week in Rails

This week I've made use of different ruby tools that help me make my tests, controllers and business application clean and DRY. I will show you a bit of what you can do with custom contexts for active record validations, service objects and custom exceptions to reduce your controller's methods AbcSize and use RSpec shared example groups to keep your tests code DRY.

Active Record Validations

We can specify when the validation should happen with the :on option. With custom context those validations are triggered explicitly by passing the contexts name to valid? or save

#app/models/person.rb
validates :age, presence: true, on: :submit

#app/somewhere_else.rb
Person.valid?(:submit) 

Person.save(context: :submit)
Enter fullscreen mode Exit fullscreen mode

More info.

Service Objects

Service objects are helpful to extract your business logic from your controllers. They must perform one action, e.g under app/services/app_manager folder you can group your needed actions per file as app_creator.rb or app_submitter.rb. Then, you can use those classes in your controllers.

app = AppManager::AppCreator.call(params)

AppManager::AppSubmitter.call(app) if params['submit']
Enter fullscreen mode Exit fullscreen mode

I recommend reading this ebook Fearless Refactoring.

Testing

shared_example_group RSpec feature lets you group specifics behaviors that are repeated in your tests. This was useful to test API's responses with it_behaves_like 'a success response' within many API controllers.

More info.

Exceptions

You can create and raise your custom exceptions to reduce if's and else's cases in your app business logic. Instead of dealing with nil or false values returned from your methods, you can raise your custom exceptions to keep your code clean and maintainable.
So you can transform your code from:

def create
...
unless current_user.can_book_from?(agency)
  redirect_to trip_reservations_page, notice: "You're not allowed to book from this agency."
end
unless trip.has_free_tickets?
  redirect_to trip_reservations_page, notice: "No free tickets available"
end
unless reservation.save
  logger.info "Failed to save reservation: #{reservation.errors.inspect}"
  redirect_to trip_reservations_page, notice: "Reservation error."
end
...
Enter fullscreen mode Exit fullscreen mode

To:

def create
  TripReservationService.call(current_user, params[:trip_reservation])
rescue TripReservationService::NotAllowedToBook
  redirect_to trip_reservations_page, notice: "You're not allowed to book from this agency."
rescue TripReservationService::NoTicketsAvailable
  redirect_to trip_reservations_page, notice: "No free tickets available."
end
Enter fullscreen mode Exit fullscreen mode

I recommend reading this ebook Mastering Ruby Exceptions by Honey Badger

Links:

Top comments (1)

Collapse
 
rhymes profile image
rhymes

Great points!

I'm always conflicted about the last one, even though we use it in some places, as it uses exceptions as a control flow mechanism instead of... well, reacting to exceptional situations. Ruby's exceptions also are quite slow.