Rails refactoring guru Andrzej Krzywda shares his experience and best practices during this session. Andrzej shows how to apply typical refactoring recipes to put your code in order, with examples of service objects and other patterns, all using RubyMine refactorings extended with custom shortcuts:
We’ve got a lot of interesting questions during the webinar. Unfortunately there wasn’t enough time to answer all of them online, but Andrzej has now kindly answered the rest. Here are the most interesting ones:
Q: Is a good idea to move services to a controller concern?
A: To be honest, no. I know that I’m against the main rule of DHH’s way, but I really see no point in controller concerns. The reasons I’m using a new class is because it makes it easier to test. It’s also forcing me to decouple the controller state a bit. I don’t really have much experience in using Rails concerns, as I never felt it would be of any help. I prefer more OOP approaches.
Q: Does this kind of pattern reduce/isolate problems of Rails upgrades?
A: This webinar was mostly about the service objects, but I also mentioned repositories, adapters, and form objects.
Repositories help you with encapsulating data access. Inside them it’s OK to use ActiveRecord objects/classes. Repositories have a well-defined scope and are easy to test. This means that after a Rails upgrade you can easily check if db access is still working.
I use adapters for encapsulating all kind of external world access. (BTW, in a way, repositories are adapters too.) You can wrap sending email with a NotificationAdapter, even though it’s part of Rails. The same benefits as above apply: easy to test and easy to verify in case of upgrades.
In its final state, you can use the service objects together with domain objects and test it all without even loading Rails. You just test the business logic and the integrations with other parts (which you can replace with InMemory objects).
All in all, those patterns make it easier to upgrade Rails, as you can check every small bit if it still works with the new Rails version.
Q: How to convince other team members to start the refactoring, especially more “senior” ones focused on classic-Rails-apps-way?
A: Oh, that’s hard. Senior Rails-way people are probably the hardest to convince to refactoring. Rails-way people are great for making the first versions of the product. Maybe suggest that your senior team members work on those phases so that they’re not involved in the long-term work 😉
Q: Do you have some rule which params you pass to constructor of service object, and which to execute/call method?
A: I put the collaborators to the constructor and other params to the execute/call method. The main reason is that I often want to replace the collaborators with some in-memory fakes during testing and the constructor feels better for it. However, in the end, it’s not really an important difference.
Q: What do you think is better, refactoring on the way and trying to optimize the code, or refactoring after you are completely done with the feature/iteration?
A: Theoretically, we can do refactorings before/during/after each ticket/feature/iteration. My favorite way is to do it during the current ticket. Sometimes it’s better to start with some preparatory refactorings and then do the ticket. In other cases it’s better to do the refactoring along the way. It always depends on what needs to be done, how much new code is involved, etc. I’m not a big fan of doing refactorings at the end of the iteration, as it’s difficult to define the scope and the goal in such case, IMO.
Q: What benefit do we get for expressing CreateTimeEntry as a class? It has two methods, initialize and execute, so it is basically just a functor. How is this better than a private method of the controller?
A: That’s a very good question! Overall, the difference is not that huge. Extracting a service objects is often just a cosmetic change. I’d say that the two main benefits are the following:
1. Making it its own class makes it easier to test. We can construct the object with some fake collaborators, once they appear. A common pattern we do is to introduce the concept of repositories at some point. Then, we can use an InMemoryRepo in the tests, saving a lot of time in not hitting the database. All of this is also possible while having it in the controller, but it’s a bit easier this way.
2. Extracting the service object forces us to untangle the global state of the controller. We now need to be explicit with what we pass as the params. No longer can we rely on the proper @ivars being set. I believe that it’s a good thing in the longer run.
We also have a small gift to Wojciech Piekutowski for his question about convincing senior Rails-way people to refactoring – a free RubyMine personal license! Wojciech, we will send you all the instructions on how to get your license shortly.
If you still have any questions, please ask them here in comments. Stay tuned to catch our new webinars!
Develop with pleasure!
JetBrains RubyMine Team