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_card
and:purchase
??Thanks again for sharing this!
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 :)
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 meanproduct_data[:id]
? Thewhere
should befind_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.
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 theProductQualityValidator
. Thanks!Hi Sophie, I am curious how you do the error handling in the service ?
Hi,
So the use of
ActiveModel::Validations
gives our instance ofPurchaseHandler
access to the#errors
method. If you look at theProductQualityValidator
, you can see that that it is operating onrecord
. Thisrecord
attribute of our validator refers to thePurchaseHandler
instance that we are validating. We add errors to our instance viarecord.errors.add
. We can callpurchase_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 ourPurchaseHandler
class.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
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...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.
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.
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.
Excellent article!
A+ 👏
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_callbacks
method needskind
as argument apidock.com/rails/ActiveSupport/Ca.... So I've changed to it works as below:run_callbacks :initialize do ... end
Very good post!
Really Awesome!!
Very well-written! Thanks for the post.
This is amazing! Thanks for sharing!
This is awesome design which i looking for but i don't feel comfortable with too many callback.It makes code difficult to control .