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)
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']
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.
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
...
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
I recommend reading this ebook Mastering Ruby Exceptions by Honey Badger
Links:
Top comments (1)
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.