Let’s discuss an example of small refactoring that we recently did while reviewing one of the pull requests.
The advantages of refactoring can be easily explained using the example of Easter. When you have a larger family and need to peel 30 eggs, it can be a pain in the ass. But it doesn’t have to. It depends on how they were cooked. If the water was salted and the eggs were soaked in cold water – the whole job will not take more than 15 minutes, because the shell will come off easily. However, if you have to deal with small pieces of eggshell that stick to the egg whites, it will take an hour.
Before
Let’s think about a simple data model. We have document
which has one or more agreement
. In order not to cram everything into the model file we create a service class which deals with removing unsigned documents.
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def destroy
destroy_agreements
end
private
def destroy_agreements
MultiJson.load(@agreements.to_json).each do |key, value|
next if value.present?
@document.granted_form_agreements.where(form_agreement_id: form_agreement_ids(key.to_s)).destroy_all
end
end
def form_agreement_ids(agreement_type)
FormAgreement.where(agreement_type: agreement_type).pluck(:id)
end
end
What’s wrong with this code?
- Unnecessary JSON processing: In the first code example, the
@agreements
is already an array. This unnecessary processing may introduce additional performance costs. However, there is no need to serialize and deserialize the data in this case. - Unnecessary auxiliary methods: An auxiliary method
form_agreement_ids
helps us name what the key variable is but it does not make the task any easier. Additionally, there are a lot of methods crammed into line 17, which makes the code harder to read and debug. - No database query optimization: Each call to the
destroy_all
method causes a separate database query. This can lead to excessive database load, especially when a delete operation must be performed on many items. In addition, we have a query that is executed repeatedly onFormAgreement
. - Clueless code: The code is a bit unreadable in the loop that iterates over
@agreements
. Thenext if value.present?
condition is misleading. It is also unclear what value is because it is not named as an appropriate variable. - One line is too long: Notice that it is not visible even in the preview. Lines can almost always be shortened.
- Redundant naming: the main method is called
destroy
(when the class is aDestroyer
). Sometimes it happens when making builders that you writebuild
. It’s better to just docall
.
First refactoring
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def call
destroy_agreements
end
private
def destroy_agreements
@agreements.each do |agreement_type, is_checked|
next if is_checked
@document
.granted_form_agreements
.joins(:form_agreement)
.where(form_agreements: { agreement_type: agreement_type })
.destroy_all
end
end
end
What do we have here?
- Code simplification: Refactoring simplifies the structure of the code, leading to greater readability and understanding. After refactoring, the code is more concise and clear, making it easier to maintain and develop in the future.
- Remove unnecessary processing: The original code used the
MultiJson.load
method to serialize and deserialize the object@agreements
. In the refactored code, you can directly iterate over the passed array@agreements
, thus eliminating unnecessary processing. - Database query optimization: The refactored code uses joins (
.joins
) and condition (where
) directly in the database query, instead of calling an additional methodform_agreement_ids
. This allows you to reduce the number of database queries and process data more efficiently.
Second refactoring
Could you want more? Of course.
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def call
document
.granted_form_agreements
.joins(:form_agreement)
.where(form_agreements: { agreement_type: agreement_types_to_delete })
.destroy_all
end
private
attr_reader :document, :agreements
def agreement_types_to_delete
agreements.reject { |_type, is_checked| is_checked }.keys
end
end
Here are some advantages of this version:
- Simpler logic: In this version of the code, instead of iterating through
@agreements
and checking the value ofis_checked
for each contract type , the reject method was used to reject contracts that are not selected. This leads to more concise and readable logic. - Direct key retrieval: Instead of converting
@agreements
to an array of keys as in the second example, this version of the code directly retrieves the keys by using the keys on the returned result from the reject method. This leads to more concise code. - Database query optimization: This version of the code also uses database query optimization, combining
granted_form_agreements
withform_agreements
and executing one query with the where condition. This ensures that contracts are deleted more efficiently, especially if you need to delete them frequently and in large quantities. - Using methods that return instance attributes: Referring to instance variables via
@
is readable in the constructor of a class. However, doing this later on is problematic because a typo may returnnil
. Anyway, you could go a step further here and use theattr_accessor
method to set instance variables like this:self.document = document
. However, it is customary to use@
in the constructor - Call does the job: It often happens in our code that a call is almost an alias to some private method that contains the service logic. Calling individual methods in call makes sense if the algorithm of a given class is more extensive. In such a case, there is no point in multiplying entities. In turn, the private method
agreement_types_to_delete
makes sense and would unnecessarily lengthen the call method if its value was assigned to a variable.
Summary
Code refactoring of AgreementsDestroyer
is a good example of attention to detail and developing programming skills. While it may seem like small changes don’t matter, it’s details like these that make the difference between good and great code.
Attention to detail in your code is crucial to its readability, scalability, and performance. Small changes can significantly affect the final result and software quality.
When you are a beginner, paying attention to such details is extremely important. It’s not about pettiness, but about developing the skills of analytical thinking, code refactoring and optimization. Improving the structure of the code, removing unnecessary operations and optimizing queries are skills that will accompany you throughout your programming career.
Remember that as a programmer you create software that will be used by other people. Attention to detail is proof of your professionalism. Big things start with small details, so it’s worth paying attention to every aspect of the code and striving for perfection. Especially on Saturday mornings, when coffee tastes best.
(I love how ChatGPT sums up the refactoring message in its enthusiastic AI Spirit).