Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract services to reduce logic in Active Record models #387

Closed
wants to merge 13 commits into from

Conversation

joemasilotti
Copy link
Owner

@joemasilotti joemasilotti commented Apr 26, 2022

This PR extracts a few services to reduce the amount of business logic in Active Record models, namely notifications and welcome emails. It also helps reduce noise when running bin/check locally because emails are not being triggered when seeding the database.

Pull request checklist

  • My code contains tests covering the code I modified
  • I linted and tested the project with bin/check
  • I added significant changes and product updates to the changelog

Copy link

@mcansky mcansky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly questions about your approach, curious to read about your approach
I do agree there is a nice improvement in separation of responsibility over all but you use two different approaches (object based vs exception catching) for flow control; curious to why

before_action :require_new_conversation!
before_action :require_active_subscription!

rescue_from ColdMessage::BusinessBlank, with: :redirect_to_new_business
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using exceptions for control flow is a debated practice : sure it makes the code quite clearer and indeed makes this quite nice to read.

yet, in https://github.com/joemasilotti/railsdevs.com/pull/387/files#diff-1fffcf1a4effb60cbfb062145b6ececaffc723517e646b154034d46a0f447e46R12 you relied on a more object approach : return an object with easy to use status methods to check what happened.
why not use this approach here ? this could be less "magical", more explicit and focused to the places where it's actually needed

related :

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Thanks for the push! I refactored to use a third state on Result and have the controller redirect dynamically. Overall I like this approach much better!

app/services/cold_message.rb Outdated Show resolved Hide resolved
@@ -0,0 +1,18 @@
class InvisibleDeveloper

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you instantiate a new invisible developer, the name sort of implies it's already invisible regardless of its invisibility status.

Maybe this could be written in a more job/command style:

InvisibilizeDeveloper.new(developer).perform


InvisibilizeDeveloper.new(developer).execute

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you could keep it as is but rename mark as create.

It's not a create in the creating-a-record-in-a-database sense but it does imply that something that didn't exist now exists

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe option 1 reads better. What do you think?

@fractaledmind
Copy link

Just a passing thought:

ActiveJobs can be run both asynchronously and asynchronously, which makes them (IMO) a wonderful "standard Rails" way to build "service objects that do things".

@joemasilotti
Copy link
Owner Author

ActiveJobs can be run both asynchronously and asynchronously, which makes them (IMO) a wonderful "standard Rails" way to build "service objects that do things".

Sure, these POROs could be shoved under Active Job. But what does that buy me? I still have the same problems of where to put authorization and such.

result = ColdMessage.new(message_params, developer_id:, user: current_user).send_message

if result.success?
redirect_to result.message.conversation
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use a guard clause ? such as redirect_to result.message.conversion and return if result.success? (the one liner isn't the nicest way to do.
why : it's mostly esthetic, the guard clause would look more independent and separate

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before

def create
  result = ColdMessage.new(message_params, developer_id:, user: current_user).send_message

  if result.success?
    redirect_to result.message.conversation
  elsif result.redirect?
    redirect_to_result(result)
  else
    @context = context(result.message)
    render :new, status: :unprocessable_entity
  end
end

After

def create
  result = ColdMessage.new(message_params, developer_id:, user: current_user).send_message

  redirect_to result.message.conversation if result.success?
  redirect_to_result(result) if result.redirect?

  @context = context(result.message)
  render :new, status: :unprocessable_entity
end

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving 4 lines is nice, but I don't think this read any cleaner than what was written before. I'd actually argue it is harder to understand at a glance.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you went with the "after" version, you'd need to prepend return to both redirect calls.

As it's written, Rails wouldn't know if it should redirect or render.

For example, if the operation is successful, redirect_to would be executed but the program (i.e the controller action) would keep on running. It'd still execute the action until it hit the render :new call. At this stage Rails would get confused and error out.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, forgot about those. Another reason why I don't think this approach reads cleaner!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in this particular case it gets a little messy-looking with guard classes, but in general I agree with @mcansky. I'm nearly always refactoring conditional statements out of the bottom of methods because that feels like an anti-pattern (and one of RuboCop's potential configs is to recommend guard clauses which I like enabling).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could that be a sign that this action is trying to do too much ?

  if result.success?
    redirect_to result.message.conversation
  elsif result.redirect?
    redirect_to_result(result)

both are, in the end, redirects, couldn't your result object just respond to ''redirect" and have the url in an attr_reader ?, and if you need a notice too have it as another attr_reader or method ?

that way the "logic" is bundled in the result object and the action just has to wonder if it has to redirect or jump back to the form render

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. That is an excellent point. The redirect already includes an optional notice, why not use it here, too?

@joemasilotti
Copy link
Owner Author

Closed in favor of #404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants