There’ve been a lot of discussions for several months about “The Rails Way” and problems associated with it - huge models with several thousands lines of code and no distinguishable responsibilities (User class which does everything related to users doesn’t tell much what the app does), unmaintainable callback hell and no domain objects at all. Fortunately, service objects (or usecases) and other forms of extracting logic from models have become quite mainstream recently. However, it’s not always clear how to use them all together without creating huge classes with tens of private methods and too many responsibilities. Here are some strategies you can use to solve these problems.
Pass form objects as data aggregates to service objects
Imagine situation where your usecase involves more than one model, some virtual attributes are needed etc., basically the usecase where form object is necessary (unless you want to have a serious mess in models). Furthermore, you need to implement pretty complex features - besides proper mapping of attributes to models in form objects you want to send some notifications, log an activity and other stuff. How to tackle such a problem? Often I see a code where all custom logic, apart from simple saving data, is put in form objects. That’s not really a way to go. Why start from separating responsibilities and end up with form object which does everything? You can easily handle it by passing form objects with populated data to service objects. Take a look at the controller action below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
UsersController is responsible here for control flow, form aggregates and validates data and
User::Registration does some domain business logic and assumes it operates on valid data. Looks and feels great, it’s easy to test and the responsibilities are clear. I also inject some dependencies in the constructor of
User::Registration to make it even more testable and extensible.
The only problem with such an approach is lack of compatibility or difficulties with customization with some gems (like
inherited_resources). But gems shouldn’t force you to write code in a particular way and it would be probably better to give up on them and enjoy a clean code.
Create flexible service objects with listeners
Let’s consider previous usecase: registration. Imagine situation where you need three types of user registration: “normal” one like when you enter the page and want to create an account, registration by admin and registering new users by non-admin within some kind of groups. The core of registration process remains the same in all cases. However, there will be slight differences between the usesaces, eg. there won’t be any notification sent to admin when user is being registered by admin. One way would be to create service object for each usecase and encapsulate entire logic within them. But it may lead to code that is not DRY and these classes may be unnecessary. If the core of the registration process doesn’t change, we can create interface flexible enough to handle all cases by passing listeners that are being notified after registration process. Our service object could look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
We can pass any number of listeners to the constructor and inject dependencies if required. All listeners have to implement the same interface:
notify method which takes one argument. The controller action for registering new user may look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
We’ve encapsulated additional logic in separate classes in form of listeners and created quite flexible interface - we need another action in registration process but only in one type of registration, not all of them? Just pass another listener into the constructor in controller.
Extract context classes
It often happens that you need an entity to play different roles, eg. the user can be a seller or a buyer, depending on the usecase. Instead of adding plenty of methods to the
User class that are related only to the particular role, you can create context classes. Both sellers and buyers will probably share the same methods. One way to solve this problem could be inheritance but it would mean single table inheritance in this case (ActiveRecord model) and that’s not what we need. However, we can use
SimpleDelegator which would delegate all the method calls to the decorated object (user) if the decorator doesn’t implement these methods:
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10
We can also implement some convenience methods in
User class to get user in approperiate context:
1 2 3 4 5 6 7 8 9 10 11
Trade long if/case statements for declarative dispatch
Rather a structural implementation detail but still quite interesting. Imagine situation when you have to return proper status based on some conditions. You can of course use
case statement but the problem with this approach is that the code is not really pretty, especially when you have multiple conditions for each status. For complex logic it might be better to handle such usecases with more declarative approach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
detect enumerator (aliased also as
find) will return the first element for which the block returns true. In this case all the conditions must be satisfied to return true.
Instead of using bunch of private methods, you can move these methods to policy objects and change the receiver of the message to be the policy, not
1 2 3 4 5 6 7
I like the clarity of this approach - you can immediately say what are the necessary conditions for each status without multiple
&& operators in case statement which would make it harder to read.
I’ve shown some simple and common techniques I use in almost everyday coding. I hope that you will find them useful and will keep the code clean.