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

Refactor 'new' action in AccountsController for better readability and efficiency #273

Conversation

AfolabiOlaoluwa
Copy link

  • Used '.presence' instead of '.blank?' for handling both nil and empty string in 'params[:type]'
  • Removed redundant condition for 'params[:type].blank?' by using 'type' directly
  • Combined 'Account.new' calls into a single line

The distinction between .blank? and .presence lies in how they handle nil and empty strings.

.blank?: This method returns true if the object is nil, empty, or consists only of whitespace characters. However, it does not handle nil directly; instead, it relies on the truthiness of the object.

.presence: This method is particularly useful when you want to obtain a non-nil value. It returns nil if the object is blank (empty or consists only of whitespace characters), and it returns the object itself if it is present.

In the context of the action code, using .presence allows handling both nil and an empty string in a concise manner. If params[:type] is nil, it will be assigned to type as nil, and if it's an empty string, it will be assigned to type as nil as well. This makes the subsequent check (type.blank? || ...) more straightforward, as it covers both cases.

For more details, refer to the link https://api.rubyonrails.org/classes/Object.html#method-i-presence

…d efficiency

- Used 'presence' instead of 'blank?' for handling both nil and empty string in 'params[:type]'
- Removed redundant condition for 'params[:type].blank?' by using 'type' directly
- Combined 'Account.new' calls into a single line
@AfolabiOlaoluwa AfolabiOlaoluwa marked this pull request as ready for review February 3, 2024 11:48
Comment on lines 5 to 11
type = params[:type].presence

if type.blank? || Account.accountable_types.include?("Account::#{type}")
@account = Account.new(accountable_type: "Account::#{type}")
else
head :not_found
end
Copy link
Contributor

@josefarias josefarias Feb 3, 2024

Choose a reason for hiding this comment

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

Nice thinking! I think we can take it a step further.

Consider the following:

maybe(dev)> params = {}
maybe(dev)> params[:type] = "Foo"
maybe(dev)> "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
#=> (irb):23:in `<main>': undefined method `constantize' for nil (NoMethodError)
#=> "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
maybe(dev)> params[:type] = nil
maybe(dev)> "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
#=> (irb):27:in `<main>': undefined method `constantize' for nil (NoMethodError)
#=> "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
maybe(dev)> params[:type] = "Credit"
maybe(dev)> "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
=> Account::Credit(id: uuid, created_at: datetime, updated_at: datetime)

We could use that to refactor as follows:

class AccountsController < ApplicationController
  def new
    @account = Account.new accountable_type: account_type_class
  end

  private

  def account_type_class
    "Account::#{params[:type]}".presence_in(Account.accountable_types).constantize
  rescue NameError
    Account
  end
end

We're presumably okay with defaulting to Account per

Account # Default to Account if type is not provided or invalid

But if not, we can just raise the NameError — fine to throw an exception if we got this far without a valid type param. I'll let @Shpigford confirm.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, per

delegated_type :accountable, types: %w[ Account::Credit Account::Depository Account::Investment Account::Loan Account::OtherAsset Account::OtherLiability Account::Property Account::Vehicle], dependent: :destroy

I don't think rescuing and returning Account makes sense. I think we should just raise.

Copy link
Contributor

Choose a reason for hiding this comment

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

We just need to be careful as the new action renders a different partial depending on the presence of a valid type param. I think I'll a quick test to make that doesn't break with refactors. (But agree that this could be made cleaner)

Copy link
Author

Choose a reason for hiding this comment

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

Well, it's a question of where the .constantize fails. If it won't fail, then if..else statement or a tenary operator is fine.

Copy link
Author

Choose a reason for hiding this comment

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

Perhaps I need to look at this as a controller of its own rather than refactoring action by action. I also think a possible solution is to use || instead of raise even if .constantize happens to return a nit and fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

How is the type param obtained? Sorry I haven't looked myself. It shouldn't be open to freetext input. If we get to this point with an invalid type then someone's attempting something shady and it's fine to raise an exception. Arguably, even better to raise and capture in Sentry so we're aware of which attack vectors are being attempted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Via links rendered when the type parameter is blank

…d efficiency and clarity

This commit refines the 'new' and 'create' actions in the AccountsController to achieve better code organization, readability, and maintainability.

1. 'new' action:

   - Introduced a new method 'build_account' to encapsulate the logic for building a new account based on the provided type.

   - The 'head :not_found unless @account' line ensures that a not-found response is returned when the account creation fails.

2. 'create' action:

   - Consolidated the account creation logic into the 'build_and_associate_account' method for improved code structure.

   - Utilized 'render "new", status: :unprocessable_entity unless @account.save' to streamline the response handling in case of a save failure.

   - Implemented 'redirect_to accounts_path, notice: "New account created successfully"' for a successful account creation, providing a clear redirect path and notice message.

3. Private helper methods:

   - 'build_account': Handles the building of a new account with the specified type, considering type presence and accountable types inclusion.

   - 'build_and_associate_account': Constructs a new account associated with the current family, utilizing 'build_accountable' for accountable object creation.

   - 'build_accountable': Creates a new accountable object based on the provided accountable type.

   - 'account_type_class': Determines the account type class based on the provided type, ensuring validity.

These changes enhance the overall structure and readability of the AccountsController, promoting a more maintainable codebase.
@AfolabiOlaoluwa AfolabiOlaoluwa force-pushed the refactor-accounts-controller-create-action branch from cd4d64f to 67c6da4 Compare February 5, 2024 16:19
@AfolabiOlaoluwa
Copy link
Author

I have changes based on what I think is better instead of rescue NameError. You might want to check.

@josefarias
Copy link
Contributor

I have changes based on what I think is better instead of rescue NameError. You might want to check.

Ah yeah, I don't think we should rescue NameError either, per #273 (comment)

Can take a look after work! Thanks for putting this together

head :not_found
end
@account = build_account
head :not_found unless @account
Copy link
Contributor

Choose a reason for hiding this comment

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

I would advise against using modifier clauses like this. Standard Rails is to not use them in controller actions

For example here - a user will at first see that it returns :not_found. Its not until the end of the line they realise its conditional.

I know this is subjective and both ways work and it saves a couple of lines, but for readability and consistency with other controller actions we should defer to the more verbose if...else...

Copy link
Author

Choose a reason for hiding this comment

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

I agree it is subjective. And I buy your point regarding consistencies with other controllers, which is rather also subjective in my opinion.

Perhaps we can decide what things should look like but I don't think that "rhyme" is totally given in a large codebase mostly with if..else conditional statements or tennary operators or modifier clause. That uniformity is not given that we'll have only if..else statements accross the board. It is not achievable!

On the other hand, if I take it a bit further to avert concerns in your example and pass in some ..else statement, this is what I think:

def new
  @account = build_account

  if @account.blank?
    head :not_found
  end
end
def create
  @account = build_and_associate_account

  unless @account.save
    render "new", status: :unprocessable_entity
  else
    redirect_to accounts_path, notice: "New account created successfully"
  end
end

Afterall, it's Ruby under the hood with dashes of Rails additions. So we don't have to follow Rails standards in such a situation. I really just want to clean these controllers one after the other and set some precedence we follow in this repo. Let me know what you think using the @account, but I certainly take preference in what I provided and I'll accept it in any repository since it works.

Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this

unless @account.save
  render "new", status: :unprocessable_entity
else
  redirect_to accounts_path, notice: "New account created successfully"
end

just flipping the standard

 if @account.save
  redirect_to accounts_path, notice: "New account created successfully"
else
  render "new", status: :unprocessable_entity
end

I'm not seeing the benefits 🤷‍♂️

Copy link
Author

Choose a reason for hiding this comment

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

Absolutely not seeing the benefits too. Just trying to regularize the uniformity in your opinion.

else
render "new", status: :unprocessable_entity
end
render "new", status: :unprocessable_entity unless @account.save
Copy link
Contributor

@robzolkos robzolkos Feb 5, 2024

Choose a reason for hiding this comment

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

See comment above re modifier clauses.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I responded.

Copy link
Contributor

@robzolkos robzolkos Feb 5, 2024

Choose a reason for hiding this comment

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

The other benefits of the if...else style is that it is clearer that the primary function of the action is to save the new account. By placing the render clause first it reads left to right like the rendering of "new" if the primary function (which it only true in the exceptional case).

Furthermore it is prone to AbstractController::DoubleRenderError exceptions (as would be in this particular case if the @account did not save).

Copy link
Contributor

@robzolkos robzolkos left a comment

Choose a reason for hiding this comment

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

This is a nice cleanup. I left a comment re my views on modifier clauses.

Copy link
Contributor

@josefarias josefarias left a comment

Choose a reason for hiding this comment

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

So this PR has grown in scope since the last time I commented on it.

The last time I saw it, the refactoring was pretty close to what I would've done. So I thought it'd be good to comment and suggest what I felt was final polish.

Now the scope has grown and it makes multiple stylistic choices I wouldn't make. I don't think expounding on my thinking would be productive here as I don't own the codebase and there's no defined code style yet.

Debated whether I should comment at all but since I was asked for a review I didn't want to leave y'all hanging. It's good work, just too far removed from what I would recommend and I don't want to bikeshed here in the PR. Happy to pontificate elsewhere, though! Hit me up! 😄

I'll let the maintainers take it from here.

@Shpigford
Copy link
Member

My thoughts are a similar version of what @josefarias mentioned. Specifically, a lot of discourse over a relatively small amount of code change.

At this stage of the product, my general feeling is it's not worth spending this much time/energy on code that very well could completely get ripped out or rewritten over the next few weeks.

The focus at the moment should be primarily on adding new functionality and less on refactoring.

Going to close this for now.

If there are specific bits of this that have a substantial impact on features, then feel free to submit those smaller code changes.

@Shpigford Shpigford closed this Feb 6, 2024
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.

4 participants