#398 Service Objects pro
Models can quickly become a grab-bag of unrelated methods if behavior is constantly being pushed from the controller. Here I show how to refactor this using Concerns and Service Objects.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
This episode is great. There is something weird however : the class authentication is calling authenticate on the user class. I think password authentication should be handle by the authentication class
This is due to the
has_secure_password
behavior. We could extract this into the Authentication class however I don't know if that tradeoff is worth it.I really like this approach. I hate when the models get to fat ("ball of methods"). Many times they have to deal with tings that should not concern them. I use the lib folder and add workers for the complex controllers. One advantage is that you can run tests isolated form rails and tests get really fast.
If you like to see more screencasts on refactoring, go to destroyallsoftware
We call it rails porn (Yes, it's really that good!)
Sorry, Ryan for promoting another paid rails screencast series here, but I think that it's more complementing than competing with you.
+1 for destroyallsoftware
agree that is more complementing, though i would love to have more "true pro" episodes like this on railscasts!
I agree. But for me railscasts is more a "Hi, check this new gem and learn how to use it in 10 minutes" series. While Gary Bernhardt's screencasts is more like "So, you think you know how to do standard rails stuff, well let me blow your mind" Alert => mindblowing: burn your controllers. They both are great but for different reasons.
Just for fun wat
I agree! Gary's Destroy All Software is awesome and a good compliment to RailsCasts. :)
I usually do not cover theory but try to focus more on practical solutions to common problems. This episode is more of the former - I may do more of these occasionally.
These are also good on the subject of refactoring
http://www.confreaks.com/videos/1115-gogaruco2012-go-ahead-make-a-mess
http://www.confreaks.com/videos/1233-aloharuby2012-refactoring-from-good-to-great
One of my favorites is Katrina Owen's "Therapeutic Refactoring": http://www.youtube.com/watch?v=J4dlF0kcThQ
I love both approaches. I relate well to Gary's DAS site because I've used Unix, Vim, and other scripting languages for a really long time, and am especially interested in how to improve my workflow using those tools. However, your approach is usually just what I need to get started using a new gem or technique for my Rails projects.
I greatly appreciate you spending more time on Refactoring, though, especially with a more Ruby or OO-centric approach. It has become the central theme at my work, where we've had enormous technical debt accrue over a 2 year period. IMHO, the traditional approach that many developers have learned initially for Rails is the root cause of why many codebases have grown to a barely maintainable level. Anything you can do to help developers learn better habits and produce more readable, elegant and stable code will certainly pay huge dividends when others inherit projects.
Please continue this work, if not for the free RailsCasts side, at least for the Pro subscribers.
Thank you for all of your hard work!
Thank you for highlighting the use of Service Objects, it's a technique that I have adopted and felt has improved the codebase for the projects I have worked on. There's probably one piece of code I would push back down into the model:
This query method exposes the ActiveRecord Query API and I would rather have that encapsulated in the ActiveRecord model.
find_by_username
is debatable, but I typically let those slip.Thoughts?
Can you explain why find_by_user name is different than than the User.where example? I guess I don't understand why one should belong there and the other one is debatable...
When unit testing the Service Object I can easily stub the method
find_by_username
. However thewhere
would required an ugly stub chain. If the API changes thewhere
clause is more costly than thefind_by_username
.Having said that, there maybe a day when
find_by_username
is deprecated like it's cousin,find_all_by_
in Rails 4 and I would regret that choice :-)If working on a team and they wanted to move the method
find_by_username
to the User I would be be totally cool with that, perhaps using the ActiveRelation syntax to build the query.I know some folks advocate even hide the
all
method within their model, for example:Anyway, I hope that gives you some idea into my thought process behind that :-)
ActiveSupport Concern is nice way to extend some class with module mixins, but to keep my controllers clean I rather use kindergarten. It is more convenient way to deal with controllers cleanup. Kindergarten additionally use CanCan for easier authorization (no more
Ability.rb
) and some more useful features.Thanks for giving some samples to the recent Tweet-Stream about this topic.
Concerns let you group functionality - nice. But in the end it all ends up mixed into the consumers class (User). I know it's nice Ruby magic - and there might be reasons or use cases to use Concerns. But somehow I like the service approach more. It's more explicit, better testable and more OO style - less 'Ruby meta style'.
Grails which is inspired by Rails and written in Groovy, explicitly accounts for service objects just as Ryan explains here. I think for a few methods here and there, the "concern" or module mix-in approach is fine. But for something more substantial, a service object is better. It is a judgment call really.
+1
Yes, I also came to Rails from Grails. Grails has a strong service layer concept, and encourages to push a lot of business logic into stateless (and transactional) service layer. Something I've missed in Rails, so I started doing something like this implicitly by creating non-db backed model classes.
http://grails.org/doc/latest/guide/services.html
Also, I would suggest if you are going to use service objects, to use similar convention as with controllers, i.e. call an object authentication_service vs service. That will help to know where the original definition lives - i.e. is it a model or a service.
I recently looked into Grails and liked the service object approach. May I ask - which framework do you prefer at this point? (Apologies for this being off topic)
Great episode Ryan!
Could you tell us your thoughts about what to do when an app has too many models?
Do you think namespacing or subfolders are good ideas?
Thanks!
Id be very interested in an answer to this. We run a large data warehouse and the amount of models talking to it is becoming to much. I have name spaced it under a Dw class which handles the connection to the DB and any other shared logic. But I wonder if there is a better way to handle such a large quantity of models without cluttering the app models..?
+1!
+1!
I love the service object approach. Your episode on Presenters from scratch really expanded my mind on how to simplify view code using models. I've come to really hate helper modules because they are so hard to test and easy to make a mess with.
For controllers I've been using a set of classes I call ControllerAssistants. I set them up through a before filter and give them info like the params, etc. They are really easy to test and debug.
My models are huge and could really use some help.
Views need Presenters
Controllers need Assistants
Models need Services
How great is that?
+1
Using service objects definitely seems cleaner than concerns. As it is, if you have many concerns your model will become fat with include statements :).
And what is wrong with that?
If the business logic is related to that model class and only that model class, then it should go there.
A fat model is not a bad model, so long as your model isn't fat because it's including functionality that belongs to another class.
Need help understanding Ryan's code for the services implementation!
I am reading Ryans code for Service Objects, specifically the PasswordReset class code. It has both the class methods and instance methods plus one instance variable @user which can be accessed via attr_reader class method, so far so good.
Consider the following class method in this service:
It shows that the class method is returning a new instance of the "User" class, correct? See how he calls and uses it further in the PasswordResetsController class as shown below:
it seems like his call to PassworReset's class method from_email is returning a new instance of the PasswordReset class itself. The method implementation shows that it is returning a new instance of the "User" class however. He then accesses the associated instance variable user via the newly created instance as shown in "if password_reset.user" line of the create method shown above. I am totally confused as to what is going on here. Is this an error? or some "quack quack" (duck typing mumbo jumbo) stuff going on here?
Kindly explain.
Thanks.
Bharat
The contents of the from_email method is short-hand for
Ryan uses implicit parentheses and the fact that you can call other class-level methods without a prefix in a class-level method.
Thanks. That makes sense.
I have never seen it done this way before so threw me as well. But one thing still gets me why in the other class methods you are not using the attribute @user ie,
Is it because of the attr_reader?
Great episode!
However, has anyone considered the use of "protocols" like Smalltalk does? Any thoughts on pros and cons about using it?
On point (+1) reminds me of my java days.. soon we could be singing value objects!!
My first thought also.
Coming from .NET/Java this was the first concern I had with Rails. Where does business logic go? I was baffled to find out there's no standard way of doing that. Sorry, but putting it into model code always seemed like a hack to me.
Service layer has been the industry standard of coding complex business logic for quite a while now. This vid is great, but it just scratched the surface. Imagine dealing with trees of business logic or testing parts of it in complete isolation by stubbing out the context. Exceptions, transactions, logging all managed automagically using AOP. Sigh... those were the days :)
Next up - moving the instantiation and lifetime management of these service objects to config or DSL using some dependency injection framework.
I see that Uncle Bob's talk about architecture in ruby apps is giving an impact on our community !
http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years
If you execute your test library with the rake command the tests in the service directory are automatically included if you are using RSpec, but not if you are using Test::Unit. Is this right?
If yes, what is the best way to auto include the tests in the service directory if you are using Test::Unit?
I'm not sure if this is the best practice. But I moved the services tests from 'test/services' to 'test/unit/services' to make them part of 'rake test:units'. This works good enough for me.
Really nice article Ryan. I remember I in the asp.net world the concept of services is really strong. Beyond nicer readability Services are considered a fundamental component of an application for handling business logic.
In Ruby achieving great code readability is often harder due to developers having multiple possible ways of separating responsibility based on taste. But separating business logic isn't exactly emphasised as well as it could be which leads often hard to read code. Although mostly a nice feature I sometimes see the flexibility offered by Ruby language as its curse too.
How does a Service Object differs from a Decorator ?
The article Ryan linked to might shed some light. It gives an overview of all the different strategies for dealing with fat models (including Service Objects and Decorators).
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
Got bug when using #120 Thinking Sphinx and models what include concerns like in #398-service-objects
card.rb
concerns/userable.rb
Shouldn't that be
include Concerns::Userable
andmodule Concerns::Userable
? Rails autoloading assumes directories correlate to namespaces.I am namespacing my concerns as demonstrated in the tutorial.
I have a class
Portrait
and a concernPortrait::ToHash
defined asbut I get this error:
superclass mismatch for class
The error goes away if I do this:
I have 5 models and I do not need to inherit the class from
ActiveRecord::Base
when defining the concern in any of the others. Just this one.Any clues what could be happening?
You're defining a class name Portrait as descending from ActiveRecord::Base.
Then you redefining it without ActiveRecord. You can't do that.
Suggestion: wrap your Portrait class (non ActiveRecord) in a module with the name of you app. If you app is called Store, so
Also, don't put a module inside the class like this. Module are not meant to be used as classes.
Thanks. The
module
insideclass
was suggested by @rbates himself.Is it true most of time service objects has only one public method for the action and class name could be verb? Any resources to learn more about it?
Q1
True. But for the most part it doesn't make sense from a standpoint of clarity and natural language. Let's say you have model Product and you want a service object that will handle pricing on some complex criteria (geolocation, color, shipping, moonphase, etc). It would be a good candidate for a service object ProductPricingService. You wouldn't just call it Price or Pricing. First of all it's ambigious. Pricing of what? Products that you sell? Pricing you get from vendors? Pricing of gas today? And if you were to argue that this Service object could be used by many models, then you are defeating the purpose of Service Object. I'm exaggerating of course. But the whole point is to create clear and maintainable code. So while you can save some keystrokes by naming it Price, it's much more clear to another person ( and you in 5 months) if you name it ProductPricingService.
Q2
For instance let's say you have model Product. you have methods like :
If this is the only method that deals with labels you have and it's that simple than creating a Service object is an overkill. However if your method becomes complex or you find that you need multiple method for related things:
You might want to move that into a service like so:
And now just do this
Hope that clears things up a bit
I had already read the CodeClimate blog entry about fat models. It is a great reference for OO refactoring, but lacks of examples on how to organize the code the Rails way. This RailsCast just covered all things that weren't clear for me. Thanks, Ryan!
Rafael Barbolo from Loja Virtual Kauplus
Hi folks, what kind of naming convention do you propose for service objects?
I usually use nouns for class names. I used deverbal nouns for service names like 'ManagerInviter'. But in a review it was told to me to use verb as service names... like 'InviteManager'...
Which one do you propose and why?
Orban, try this out http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html
It is definitely been enlightening, service object approach can be much more simplified with the help of this episode..
Working around unrelated objects can be tad tricky at times. Just the right info to get it done, fast.
Fantastic
This is useful for few people only.