30 Best Practices and Tips for Ruby Users in 2024

30 Best Practices and Tips for Ruby Users in 2024

Sometimes, productivity for software developers involves writing code as quickly as possible. But, the quality of your code determines how productive you can be with a programming language.

I’ve picked and gathered some best practices and tips for Ruby users that I’ve discovered so far in this simple guide. They are the outcome of a year and a half of working on a big data and business intelligence project.

If I had heeded these suggestions right away, I think the creation of this capability would have been more successful—that is, quicker and of more excellent quality.

I have included a total of 30 tips. I divided the list into two sections so the post would stay manageable. In the first section, I am primarily concerned with optimizing the test specs and the queries’ bank calls.

Ruby is a special kind of language that works well for almost anything. Despite the fact that Ruby is easy to understand, its full potential requires in-depth knowledge and experience. To help you get the most out of Ruby, we’ve compiled a list of our favorite hacks, techniques, and best practices today. Let’s begin!

Testing (specs)

1. Make use of FactoryGirl

describe Car do

  subject(:car) { FactoryGirl.build(:car) }

end

With FactoryGirl, a software library, you can create tests that are easier to comprehend and have better syntax. It is also adaptable, allows for advanced modification, has a centralized modeling code, and optimizes the time needed to develop the requirements. In other words, it is a helping hand when creating time optimization and more sophisticated models.

2. Avoid making the Prefer build

FactoryGirl.build(:car)

# [Fast] Constructs the object in memory

FactoryGirl.create(:car)

# [Slow] Saves the template in the database and runs all validations and callbacks (eg after_create)

Using the same kind of bench in test and production settings is best practice. Even if the tests pass, this prevents surprises in production.

One effect of this is that the specs take longer to complete because there are excessive calls to the bench during the tests, especially when using ActiveRecord.

Try to use it whenever you can to lessen its influence.

3. Begin with the exception.

describe Car do

  subject(:car) { FactoryGirl.build(:car, color: color) }

  context 'when no color is given' do

    let(:color) { nil }

    it { is_expected.not_to be_valid }

  end

end

We frequently just test the joyful, error-free path. The issue is that we don’t consider all the test situations and instead save the exceptions for last as background. I advise considering the most improbable scenarios when beginning with the exceptions.

4. Explain the behavior

describe Car do

  subject(:car) { FactoryGirl.build(:car, fuel: fuel) }

  describe '#drive' do

    subject(:drive) { car.drive_for(distance) }

    context 'when driving a positive distance' do

      let(:distance) { 100 }

      context 'and there is not enough fuel' do

        let(:fuel) { 10 }

        it 'drives less than the wanted distance' do

          drive

          expect(car.walked_distance).to < distance

        end

        it 'consumes all fuel' do

          drive

          expect(car.fuel).to be 0

        end

      end

    end

  end

end

You only need to read the specs description to grasp how a model operates. But this isn’t always the case. Tests that do not accurately capture a model’s behavior are not uncommon. Everything in the example above is explained in detail. I know the car will not go as far as you would like when I ask to drive a specific route, and there needs to be more fuel. Halfway through, it stops, and the fuel runs out.

5. Examine the functionality rather than the implementation

def drive_for(distance)

  while fuel > 0 || distance > 0

    self.fuel.subtract(1)

    self.walked_distance += distance_per_liter

    distance -= distance_per_liter

  end

end

drive

expect(car.fuel).to eq 2

# [Good] tests functionality

expect(fuel).to receive(:subtract).with(1).exactly(5).times

# [Bad] tests implementation

If I change the logic of the method drive_forto this one below, the top test keeps going and the bottom one fails even with the correct logic.

def drive_for(distance)

  needed_fuel = distance.to_f / distance_per_liter

  spent_fuel = [self.fuel, needed_fuel].min

  self.fuel.subtract(spent_fuel)

  self.walked_distance += spent_fuel * distance_per_liter

end

It is uncommon for someone to be concerned about functionality when developing tests and conducting analysis throughout the code review process. It is a time waste to redo the tests each time you modify or change the implementation.

6. Rspec –profile

Top 20 slowest examples (8.79 seconds, 48.1% of total time):

  Lead stubed notify_lead_update #as_indexed_json #mailing events has mailing_events

    1.32 seconds ./spec/models/lead_spec.rb:209

  Lead stubed notify_lead_update .tags #untag_me untags the leads

    0.80171 seconds ./spec/models/lead_spec.rb:545

  Lead stubed notify_lead_update .tags #tag_me tags the leads

    0.778 seconds ./spec/models/lead_spec.rb:526

  Lead stubed notify_lead_update .tags #tag_me tags the leads

    0.75545 seconds ./spec/models/lead_spec.rb:531

With this tool, we can easily optimize the duration for each test by getting fast feedback on spec time. It should take less than 0.02 seconds for a good unit test. Typically, functional ones require more time. You can add the line –profile to the file, so you don’t have to put in all the specs. The root of your project is rspec. Bank calls (queries)

7. Use find_each, do noteach

Car.each

# [Bad] Loads all elements in memory

Car.find_each

# [Good] Loads only the elements of that batch (1000 by default

Each causes the base to load all the contents simultaneously, increasing memory usage with base size. Find_each has the fixed consumption already. The only factor affecting it is the batch size, which is easily adjustable with the batch_size parameter.

8. Use pluck, do notmap

Car.where(color: :black).map { |car| car.year }

# [SQL] SELECT  "cars".* FROM "cars" WHERE "car"."color" = "black"

# [Bad] loads all attributes of cars and uses only one

Car.where(color: :black).map(&:year)

# [Bad] Similar to the previous example, only minor syntax

Car.where(color: :black).select(:year).map(&:year)

# [SQL] SELECT  "cars"."year" FROM "cars" WHERE "car"."color" = "black"

# [Good] Only loads the cars year attribute

Car.where(color: :black).pluck(:year)

# [SQL] SELECT  "cars"."year" FROM "cars" WHERE "car"."color" = "black"

# [Good] Similar to the previous example and has smaller and clearer syntax.

9. Use select, not pluckwhen cascading

owner_ids = Car.where(color: :black).pluck(:owner_id)

# [SQL] SELECT  "cars"."owner_id" FROM "cars" WHERE "car"."color" = "black"

# owner_ids = [1, 2, 3...]

owners = Owner.where(id: owner_ids).to_a

# [SQL] SELECT  "owners".* FROM "owner" WHERE "owner"."id" IN [1, 2, 3...]

# [Over] Execute 2 queries

owner_ids = Car.where(color: :black).select(:owner_id)

# owner_ids = #<ActiveRecord::Relation [...]

owners = Owner.where(id: owner_ids).to_a

# [SQL] SELECT  "owners".* FROM "owner" WHERE "owner"."id" IN SELECT ("cars"."owner_id" FROM "cars" WHERE "car"."color" = "black")

# [Good] Performs only 1 query with subselect

When you do this pluck, it executes the query and loads the entire results list into memory. Then, another query is performed with the result obtained previously.

ActiveRecord stores only one Relation with this selection and joins the two queries.

This saves the memory required to store the results of the first query. In addition, it eliminates the overhead to establish a connection to the bank.

The databases have been evolving for a long time, and if there is any optimization that it can do in this query, it will do better than ActiveRecord and Ruby.

10. Use exists? Do not have any?

Car.any?

# [SQL] SELECT COUNT(*) FROM "cars"

# [Bad] Count on the whole table

Car.exists?

# [SQL] SELECT 1 AS one FROM "cars" LIMIT 1

# [Good] Count with limit 1

The runtime of any grows according to the size of the base. This is because it makes one count the table and then compares if the result is zero. It already exists, puts one limit at the end, and always takes the same time regardless of the base size.

11. Load only what you will use

header = %i(id color owner_id)

CSV.generate do |csv|

  csv << header

  Car.select(header).find_each do |car|

    csv << car.values_at(*header)

  end

end

We often iterate over the elements and use only a few fields. This wastes time and memory because we have to load all the other fields that will not be used.

For example, I reduced memory usage by more than 88% in the project. Reviewing and selecting all the queries, I increased the competition with the same machine, and the execution time was 12 times faster.

All this optimization is gained in less than 1 hour of work. The additional cost is virtually zero if this concern is already in your head at the time of creation.

We will focus on treatment, data consistency, and tips and tricks when dealing with import and export (CSV) files.

Also Read: 5 Best Code Optimization Plugins For WordPress In 2024

Consistency of data

12. Set the time

Car.where(created_at: 1.day.ago..Time.current)

# [Bad] The result may change depending on the time it is run

selected_day = 1.day.ago

Car.where(created_at: selected_day.beginning_of_day..selected_day.end_of_day)

# [Acceptable] The result may still change but control of this change is relatively easy

selected_day = Time.parse("2018/02/01")

Car.where(created_at: selected_day.beginning_of_day..selected_day.end_of_day)

# [Good] The result does not change

Let’s say we have a routine that runs daily at midnight to generate a report from the previous day.

The first implementation may generate erroneous results if run at a different time.

In the second, just ensure that it runs on the correct day and that the result is as expected.

Third, it is possible to execute on any day. Just inform the correct target date.

My tip for periodic routines is to leave the target date parameter with the default for the previous period.

13. Ensure ordering

Car.order(:created_at)

# [Bad] Can return in different order if you have more than one record with the same created_at

Car.order (: created_at,: id)

# [Good] Even with repeated created_at the ID is unique then the returned order will always be the same

Car.order(:license_plate, :id)

# Not necessary because license_plate is already unique

When order is necessary, using a single attribute as a tie-breaking criterion is always essential.

When we’re not careful about it, we’re usually only going to find the production error by using dummy data in the tests and rarely cover cases like this.

My tip for this item is to invest in prevention.

14. Beware of where by updated_at and batches

Car.where(updated_at: 1.day.ago.Time.current).find_each(batch_size: 10)

# [SQL] SELECT * FROM "cars" WHERE (updated_at BETWEEN '2017-02-19 11:48:51.582646' AND '2017-02-20 11:48:51.582646') ORDER BY "cars"."id" ASC LIMIT 10

# [SQL] SELECT * FROM "cars" WHERE (updated_at BETWEEN '2017-02-19 11:48:51.582646' AND '2017-02-20 11:48:51.582646') AND ("cars"."id" >  3580987) ORDER BY "cars"."id" ASC LIMIT 10

# [SQL] SELECT * FROM "cars" WHERE (updated_at BETWEEN '2017-02-19 11:48:51.582646' AND '2017-02-20 11:48:51.582646') AND ("cars"."id" > 21971397) ORDER BY "cars"."id" ASC LIMIT 10

# [SQL] ...

#

# [Bad] Records may be missing

Car.where('updated_at > ?', 1.day.ago).find

# [SQL] SELECT * FROM "cars" WHERE (updated_at BETWEEN '2017-02-19 11:48:51.582646' AND '2017-02-20 11:48:51.582646')

#

# [Less Bad] There are no batches so the records are correct, however it can consume a lot of memory

ids = Car.where('updated_at > ?', 1.day.ago).pluck(:id)

Car.where(id: ids).find_each(batch_size: 10)

# [SQL] SELECT id FROM "cars" WHERE (updated_at BETWEEN '2017-02-19 11:48:51.582646' AND '2017-02-20 11:48:51.582646')

# [SQL] SELECT * FROM "cars" WHERE (id IN [31122, 918723, ...]) ORDER BY "cars"."id" ASC LIMIT 10

# [SQL] SELECT * FROM "cars" WHERE (id IN [31122, 918723, ...]) AND ("cars"."id" > 3580987) ORDER BY "cars"."id" ASC LIMIT 10

# [SQL] ...

#

# [Best] Only the IDs of the records are preloaded and all records will be processed correctly in batch, although it can consume a lot of memory if the table is GIANT

Unfortunately, I have not found an optimal solution for this case, but pre-selecting the IDs and then iterating in batches on the IDs (which is fixed) ensures that all records are processed correctly only 1 time.

Understanding all the working and possible exceptions of these queries is tough, so if in doubt, avoid batches with updated_at.

15. TimeZone

Time.parse ("2018/02/01")

Time.now

# [Bad] Do not consider timezone

Time.zone.parse ("2017/02/01")

Time.zone.now

Time.current # same thing as above line

# [Good] Consider timezone

You thought you were not going to have anything to do with time?

The most common mistake is not to consider timezone in operations with date and time.

Even if your system does not need to handle time zones, always use time zones so you do not waste time after fixing everything. Hourglass:

16. Be very careful with Query Timezones

Car.where("created_at > '2018/02/01'")

# [Bad] Do not consider timezone

Car.where('created_at > ?', Time.zone.parse("2018/02/01"))

But sometimes, we need to make more “manual” queries; in those cases, timezone care is yours.

Use queries with parameters and o Time. Zone to ensure: +1:

17. DB always works with UTC

sql = <<-SQL

INSERT INTO 'cars' (id, created_at, updated_at)

VALUES (#{id},#{ created_at.utc },#{Sequel::CURRENT_TIMESTAMP})

SQL

18. Spread the time

class CarController

def create

CarCreateJob.perform_async(car_params.merge(created_at: Time.current))

render :ok

end

end

Also Read: How To Optimize Your WordPress Code Using Minification And Concatenation

Importation exportation

Transferring data across systems is always challenging.

Even though we developed APIs, services, and other methods, we still use straightforward tables with comma-separated (.csv) data for data transportation.

19. Don’t allow one case to throw off the entire procedure.

# Ignore invalid rows in export

Car.find_each do |car|

row = to_csv_row(car)

if valid_row?(row)

csv << row

else

notify_me_the_error_so_i_can_fix_it(row)

end

end

# Begin-rescue to ensure creation on import

CSV.parse(file) do |row|

begin

Car.create(row).save!

rescue e

errors.add(row, e)

end

end

# Background jobs in import treats each row separately

# The code is very clean, uses little memory and performance is better

CSV.parse(file) do |row|

CarCreateJob.perform_async(row)

end

Has there ever been an instance where you were executing the migration script, which locked up in the middle because it threw an exception or something, and you had to restart the processing?

To prevent this, ensure everything else usually functions, even if one entry has an error.

Having a few incomplete entries is preferable to having none at all.

Set up mechanisms so that you are informed of these exceptions and have a means of resolving them.

Don’t just keep adding to the logs—you need to be alerted by something, like an email or a notification on your dashboard.

If the issue is specific to one case, it can be simpler for you to handle it alone; however, if it has the potential to impact other cases, act quickly to fix it!

20. In CSV, use “tab” as a separator.

CSV.generate(col_sep: TAB) do |csv|

...

csv << values.map { |v| v.gsub(TAB, SPACE) }

end

We will eventually need to import or export data in CSV format for several reasons.

If a user inserts a value with a comma and we select “comma” as the separator in the export, we must handle this carefully to avoid corrupting the CSV.

The most popular method is to enclose the fields of the type open with quotation marks (“); if the value contains quotation marks, we also need to handle it, and so on.

This has complicated code, which raises the possibility of errors, unanticipated situations, and potentially poor performance.

The scenario is altered if we utilize the “tab” character as a divider.

When a user inserts a “tab,” replacing it with “space” makes the change almost undetectable in most situations. We also don’t need to worry about other characters because the resulting “CSV” is easily readable and well-organized.

Naturally, the user’s “tab” matters in some situations, so we must constantly consider our options before deciding.

I promise you, the “tab” is on your side.

21. Treat the data

date.strftime("%Y/%m/%d")

string.strip.delete("\0")

tag_string.parameterize

ANYTHING_BETWEEN_PLUS_AND_AT_INCLUSIVELY = /\+.*@/

email.lower.delete(" ").gsub(ANYTHING_BETWEEN_PLUS_AND_AT_INCLUSIVELY, "@")

THREE_DIGITS_SEPARATOR_CAPTURING_FOLLOWING_THREE_NUMBERS = /[.,](\d{3})/

DECIMAL_SEPARATOR_CAPTURING_DECIMALS = /[.,](\d{1,2})/

number_string

.gsub(THREE_DIGITS_SEPARATOR_CAPTURING_FOLLOWING_THREE_NUMBERS, '\1')

.gsub(DECIMAL_SEPARATOR_CAPTURING_DECIMALS, '.\1')

Who has never experienced suffering because “equal” texts are regarded differently due to differences in capital and lowercase letters, beginning and ending spacing, etc.?

While most other countries use the day, month, and year format for dates, the United States uses the month, day, and year format. Changing the day and month is easy when you just read accidentally.

And the user who uses two distinct users for the same individual while filling out a form with “so-and-so + 2example.com”?

These mistakes lead to difficulties, so ensure they are intact before filling out an export CSV or reading from an import.

22. Validate the data

time > MIN_DATE && time <= Time.current ?

object.present? && object.valid? ?

!option.blank? && VALID_OPTIONS.include?(option) ?

We occasionally receive dates that are from before BC or the even future. There are multiple exceptions for null or incorrect objects. 

23. Encoding is Evil

require 'charlock_holmes'

contents = File.read('test.xml')

detection = CharlockHolmes::EncodingDetector.detect(contents)

# {:encoding => 'UTF-8', :confidence => 100, :type => :text}

encoding = detection[:encoding]

CharlockHolmes::Converter.convert(content, encoding, 'UTF-8')

I do not think I need to convert anyone whose encoding is evil.

This will help you in this battle.

Even so, it’s not a silver bullet, so if it’s not too loud, it’s worth telling the user that the file could not be read and asked to convert to UTF-8 or some accepted format.

24. CSV with header: true

CSV.parse(file) do |row|

# row => ['color', 'year', ...]

next if header?(row)

# row => ['black', '2017', ...]

Car.new(color: row[0], year: row[1])

end

# [Bad] Need to handle the first line (header)

# [Bad] It will be an error if the CSV column order changes

options = { headers: true, header_converters: :symbol }

CSV.parse(file, options) do |row|

# row => { color: 'black', year: '2017', ... }

Car.new(row)

OpenStruct.new(row).color # => 'black'

end

# [Good] CSV will already handle the header

# [Good] Independent implementation of CSV column order

values = CSV.parse(file, options).map.to_a

Car.create(values)

# Insert all at once

Ensure the user has sorted the file’s columns in the desired order and ignore the first line when reading a CSV with a header.

We know this never occurs because a CSV file with the columns is always arranged incorrectly.

As a result, we are forced to read the header and understand each line as such.

This is already taken care of for you by a parameter in the CSV.

You can iterate over a CSV file with an object resembling a hash using the parameter headers: true.

25. CSV Lint

dialect = {

header: true,

delimiter: TAB,

skip_blanks: true,

}

Csvlint::Validator.new(StringIO.new(content), dialect)

It already returns every error with the error line; the explanation is fantastic.

Incredibly quick and simple. This removes a large portion of the import errors.

26. Show user errors: Be transparent!

Your system is well-protected because you verified the encoding, looked for syntax flaws in the file, and excluded rows that contained mistakes.

Even though everything on your system seems perfect, the user must import all the crucial data.

Inform your users gently of any bugs you find and choose to ignore, along with suggestions for resolving the issue.

Even better, create a fresh CSV containing just the lines with mistakes.

27. Parameterize

path = 'path/to/Filé Name%_!@#2017.tsv'

extension = File.extname(path)

File.basename(path, extension).parameterize

'file-name-_-2017'

Users and servers store files on different operating systems.

Accents and unusual characters can be exceptionally bothersome for us.

Thankfully, parameterization is a helpful treatment for this and doesn’t have adverse effects. 

28. Avoid prominent names in files

Prominent file names can be easily trimmed off, depending on the FileSystem or communication protocol. This is done, for instance, using the FTP protocol.

29. Compact the CSV

Compacting CSV files reduces their size significantly because they frequently include repeated characters. It’s worthwhile because I’ve seen 32 MB files shrink to 6 MB.

30. zipRuby, not RubyZip

RubyZip allocates 700 times more objects in memory than zipRuby and is two times slower. Hence, avoiding the former and using more of the latter is wise!

Conclusion: Tips for Ruby Users

You can write more reliable Ruby code using these best practices and techniques to make your code more transparent and easier to read.

With its many peculiar syntactical twists, Ruby is a fantastic language. It would help if you learned as much as possible about this language to benefit from it to the fullest.

I hope these 30 Ruby Tips will help you better navigate your coding endeavor. Let me know in the comments below which was your favorite Ruby tip of all.

Consider looking into the following with Ruby:

  • Techniques for concurrency and multithreading
  • When to apply nil
  • Base number conversion
  • Monitoring running processes

I hope this simple guide on 30 Best Practices and Tips for Ruby users in 2024 helped you better understand what practices to follow and things to keep in mind while writing the code. Feel free to comment below with any questions; I will happily answer them.

FAQs: Tips for Ruby Users

Which are the best ways to write legible and well-organized Ruby code?

Observe the Ruby Style Guide, give your variables and methods sensible names, and keep your indentation constant. Divide difficult jobs into smaller, easier-to-manage components.

When it comes to strings in Ruby, should I use single or double quotes?

For straightforward string literals, use single quotes; for string interpolation or escape sequences, use double quotes. This enhances performance.

How can I ensure proper error handling in Ruby code?

Use the rescue and begin blocks to handle exceptions. If you will rescue exceptions, be transparent so as not to hide unforeseen mistakes.

How should symbols be utilized in Ruby, and what is their importance?

Symbols are simple, unchangeable means of identification. Use symbols for method names and keys in hashes to make code more readable and performant.

In Ruby, should I utilize global variables?

Global variables should be avoided wherever possible. They can cause unexpected behavior and complicate code maintenance. Instead, make use of constants or local variables.

What are the advantages of managing dependencies with RubyGems?

Installing and managing external libraries (gems) in Ruby projects is more accessible with RubyGems. Collaboration is facilitated, and dependencies within a project are maintained.

Want faster WordPress?

WordPress Speed Optimization

Try our AWS powered WordPress hosting for free and see the difference for yourself.

No Credit Card Required.

Whitelabel Web Hosting Portal Demo

Launching WordPress on AWS takes just one minute with Nestify.

Launching WooCommerce on AWS takes just one minute with Nestify.