Lately I have been working with Abi in a way to separate the core of an application in small modules using rails just as a platform that uses those small modules/programs to build a real web application.
These modules are programs that mostly work for data transformation (although sometimes also to interact with external services).
I will use an example to explain a little more about the things that these small programs should do.
Use case specification
As a user I can edit my profile
- I click the link “Profile”
- I modify my name to “Benito”
- I select my country as “Mexico”
- I select my preferred language as “Español”
- I click the submit button
- I see my name as Benito
- I see my preferred language as “Español” and the page is in Spanish
- I see my country as México (with accent)
The system should validate the presence of name, country and preferred language.
To solve this problem we are going to build a
Profiles module that will be responsible for implementing the application logic of this use case.
The first part of the solution needs an edit action in the controller and a form. So we are going to write a method
Profiles.profile_for that will return the data that is needed in the form.
class ProfilesController < ApplicationController def edit @profile = Profiles.profile_for(current_user) end end
<%= simple_form_for @profile, as: :profile, url: profile_url do |f| %> <%= f.input :preferred_language, collection: @profile.language_options %> <%= f.input :name %> <%= f.input :country %> <%= f.button :submit %> <% end %>
A first specification for
Profiles.profile_for could be:
modules Profiles describe 'profile for user' do it 'has the current prefered language' it 'has the current name' it 'has the current country in the current language' it 'has the language options' end end
We need to show the country in the current language, so we are going to need something like:
class ProfilesController < ApplicationController def edit @profile = profile_for(current_user) end private def profile_for(user) Profiles.profile_for(user, locale: I18n.locale) end end
After that we can continue with the update action that will update the information in the profile, validates the updated profile and then store the new information in the database. We are going to create a method
Profiles.update_profile(profile, params) that will return a new profile with the new information that was given.
profile object should respond to
errors method should return something compatible with the form builder. The
profile should be able to return a data structure with the information to update the user record, we will call this method
So now we can have an
update action as:
class ProfilesController < ApplicationController def update @profile = Profiles.update_profile(profile_for(current_user), profile_params) if @profile.valid? current_user.update_attributes(@profile.to_record_attributes) redirect_to edit_profile_path else render :new end end end
Now the specification for the profile needs an update.
modules Profiles describe 'profile for user' do it 'has the current prefered language' it 'has the current name' it 'has the current country in the current language' it 'has the language options' it 'is not valid without a name' it 'is not valid without a country' it 'is not valid without a prefered language' it 'can be transformed to record attributes' end describe 'update profile' do it 'updates the name' it 'updates the preferred language' it 'updates the country' end end
How hard would be to implement this module?
As you can see, we are avoiding to work with
ActiveRecord inside this module. This is because is simpler to just focus on data transformation, and because we trust that
ActiveRecord will do its job updating the record attributes.
Extract this kind of modules will help you to make your Rails app simpler. And also will be easier to maintain because your test will run fast (really fast… maybe 600 tests in less than a second) and you will be able to make changes without fear of breaking something.