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...
For further actions, you may consider blocking this person and/or reporting abuse
User.find(user_id)will raise an exception if no user found. Maybe it's better to useUser.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!
Hi,
Yes you are totally right about the switch to
find_by. I also agree with making those methods private. Thanks for the feedback!Awesome post! Can you please add the complete final class to the post??
The only thing I didn't understand was this:
where is the definition of
:credit_cardand:purchase??Thanks again for sharing this!
Glad you found it helpful!
I included the final version of the
PurchaseHandlerservice class towards the end of the post. I removed the#credit_cardattr_reader as it wasn't being used. The#purchaseattribute is set in the#create_purchasemethod.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 :)
I'm curious why
run_callbacksisn't wrapping the contents of#create_purchaseas well?In the ProductQualityValidator there's
product.id; did you meanproduct_data[:id]? Thewhereshould befind_byto 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.
Hi there,
Ah yes
run_callbacksshould absolutely be used in the#create_purchasemethod! Thanks for bringing that up. The post has been update to reflect that, along with your suggestions for theProductQualityValidator. Thanks!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.
Hi Sophie, I am curious how you do the error handling in the service ?
Hi,
So the use of
ActiveModel::Validationsgives our instance ofPurchaseHandleraccess to the#errorsmethod. If you look at theProductQualityValidator, you can see that that it is operating onrecord. Thisrecordattribute of our validator refers to thePurchaseHandlerinstance that we are validating. We add errors to our instance viarecord.errors.add. We can callpurchase_handler.errorsto 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 ourPurchaseHandlerclass.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
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
PurchaseHandlerinstance, 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...Thanks!
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.
Great post. Just one thing that I miss, how callbacks can help to skip generating an invoice or email? Thanks.
Hi there,
What do you mean "skip generating an invoice or email"?
Oh, my fault. I read post one more time and now understood, thanks.
This is amazing! Thanks for sharing!
Excellent article!
Wow! Clappin' my hands!
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.
The
run_callbacksmethod needskindas argument apidock.com/rails/ActiveSupport/Ca.... So I've changed to it works as below:run_callbacks :initialize do ... endVery well-written! Thanks for the post.
Very good post!
Really Awesome!!
A+ 👏
This is awesome design which i looking for but i don't feel comfortable with too many callback.It makes code difficult to control .