-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Refactor 'new' action in AccountsController for better readability and efficiency #273
Conversation
…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
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, per
Line 4 in 48ade39
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
cd4d64f
to
67c6da4
Compare
I have changes based on what I think is better instead of |
Ah yeah, I don't think we should Can take a look after work! Thanks for putting this together |
head :not_found | ||
end | ||
@account = build_account | ||
head :not_found unless @account |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤷♂️
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I responded.
There was a problem hiding this comment.
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).
There was a problem hiding this 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.
There was a problem hiding this 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.
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. |
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