DEV Community

Smarter Rails Services with Active Model Modules

Sophie DeBenedetto on December 01, 2017

MVC is Not Enough! We're familiar with the MVC (Model-View-Controller) pattern that Rails offers us––our models map to database tables a...
Collapse
 
skoddowl profile image
skoddowl • Edited

User.find(user_id) will raise an exception if no user found. Maybe it's better to use User.find_by(id: user_id), so if no user found it will set user variable to nil and user presence validation will handle this error?
Also what about to set visibility level of create_purchase, create_invoice and notify_user methods to private since we don't need to access these methods outside of the class?
Great post btw!

Collapse
 
sophiedebenedetto profile image
Sophie DeBenedetto

Hi,

Yes you are totally right about the switch to find_by. I also agree with making those methods private. Thanks for the feedback!

Collapse
 
felipemalinoski profile image
Felipe Matos Malinoski

Awesome post! Can you please add the complete final class to the post??

The only thing I didn't understand was this:

attr_reader :user, :credit_card, :product_params, :products, :purchase
Enter fullscreen mode Exit fullscreen mode

where is the definition of :credit_cardand :purchase??

Thanks again for sharing this!

Collapse
 
sophiedebenedetto profile image
Sophie DeBenedetto

Glad you found it helpful!

I included the final version of the PurchaseHandler service class towards the end of the post. I removed the #credit_card attr_reader as it wasn't being used. The #purchase attribute is set in the #create_purchase method.

Also keep in mind that the code for actually creating a purchase is not described here and isn't totally relevant--just an example to illustrate how we can use some of these super helpful Active Model modules :)

Collapse
 
paulmakepeace profile image
Paul Makepeace

I'm curious why run_callbacks isn't wrapping the contents of #create_purchase as well?

In the ProductQualityValidator there's product.id; did you mean product_data[:id]? The where should be find_by to get a single object back.

Great article! One of the more convincing takes on Ruby service objects for Rails. The callbacks do feel pretty clunky though.

Collapse
 
sophiedebenedetto profile image
Sophie DeBenedetto

Hi there,

Ah yes run_callbacks should absolutely be used in the #create_purchase method! Thanks for bringing that up. The post has been update to reflect that, along with your suggestions for the ProductQualityValidator. Thanks!

Collapse
 
ipmsteven profile image
Yunlei Liu

Hi Sophie, I am curious how you do the error handling in the service ?

Collapse
 
sophiedebenedetto profile image
Sophie DeBenedetto

Hi,

So the use of ActiveModel::Validations gives our instance of PurchaseHandler access to the #errors method. If you look at the ProductQualityValidator, you can see that that it is operating on record. This record attribute of our validator refers to the PurchaseHandler instance that we are validating. We add errors to our instance via record.errors.add. We can call purchase_handler.errors to see any errors that were added to the instance by the validator. You can use these errors however you want--render them in a view for example or use them to populate a JSON api response.

The validator is invoked by the after_initialize :valid? callback that we defined in our PurchaseHandler class.

Collapse
 
ipmsteven profile image
Yunlei Liu • Edited

Thanks for the reply.
I think what you mentioned is the validation error, or validate the input for service.

How about an exception is raised when service object is doing the work?
Like item is out of stock when try to purchase, no item found when db fetch, etc.

Some of the exceptions we can avoid by pre-validate the input parameter, but some can only happen when we execute the code

Thanks

Thread Thread
 
sophiedebenedetto profile image
Sophie DeBenedetto

Ah okay I see what you're saying. I think you can choose how to raise and handle exceptions as you would in any Rails model or service without violating the basic design outlined here. You have a few options available to you--use additional custom validators to add errors to the PurchaseHandler instance, or raise custom errors. I wrote another post on a pattern of custom error handling in similar service objects within a Rails API on my personal blog if you want to check it out: thegreatcodeadventure.com/rails-ap...

Thread Thread
 
ipmsteven profile image
Yunlei Liu

Thanks!

Collapse
 
davidalejandroaguilar profile image
David Alejandro

Great post!

By the way, the final code mysteriously removed the presence validators; and the first part of the post still has presence validators for credit_card, I think that should be payment_method instead.

Collapse
 
alekseyl profile image
Aleksey

Since all purchasing info stored in DB, this example actually will nicely deconstruct to MVC. Where model is a Purchase object, and controller is a PurchasesController.

So at the end you got yourself the good old MVC made in a good old Rails way.

Occam's razor is to the rescue, when you want to add some new essense try the old ones thoroughly.

It may work as an example of ActiveModel use, but it's a purely theoretic example.

Collapse
 
pauldruziak profile image
Paul Druziak

Great post. Just one thing that I miss, how callbacks can help to skip generating an invoice or email? Thanks.

Collapse
 
sophiedebenedetto profile image
Sophie DeBenedetto

Hi there,

What do you mean "skip generating an invoice or email"?

Collapse
 
pauldruziak profile image
Paul Druziak

Oh, my fault. I read post one more time and now understood, thanks.

Collapse
 
johnmeehan profile image
John Meehan

Excellent article!

Collapse
 
joshed profile image
Josh Dzielak 🔆

A+ 👏

Collapse
 
jiprochazka profile image
Jiří Procházka

Wow! Clappin' my hands!

Collapse
 
veetase profile image
王广星

Amazing post. But all methods share the same validations, they are alway not in real world. And callbacks will make code more difficult to read when code base get larger.

Collapse
 
ducnguyenhuy profile image
ducnguyenhuy

The run_callbacks method needs kind as argument apidock.com/rails/ActiveSupport/Ca.... So I've changed to it works as below:
run_callbacks :initialize do ... end

Collapse
 
betogrun profile image
Alberto Rocha

Very good post!

Collapse
 
pardhabilla profile image
pardha-billa

Really Awesome!!

Collapse
 
thecodetrane profile image
Michael Cain

Very well-written! Thanks for the post.

Collapse
 
martink_io profile image
Martin K

This is amazing! Thanks for sharing!

Collapse
 
huynhit9292 profile image
huynh-donuts

This is awesome design which i looking for but i don't feel comfortable with too many callback.It makes code difficult to control .