More tips and tricks for junior developers

September 18, 2019 – Emily Fiennes – 13-minute read

This article follows on from Clement’s post in which he details useful tips and tricks learnt while working at Drivy. Like Clement, I undertook Le Wagon’s intensive 9-week bootcamp. The program was great for a rapid overview of the key elements of full-stack engineering.

Since then, the learning curve has been a steep and stimulating one - at Drivy, I’m surrounded by real dev-warriors. As you can imagine, I relish every pull request I submit or review, for the opportunity to learn new things from my colleagues.

In this article, I will explore just a handful of the many useful tips and tricks that they have shared with me, in the hope that they will be useful to other junior developers.

POST or PUT?….or PATCH ?

At Drivy we implement RESTful api design, where CRUD actions match to HTTP verbs. I found the difference between POST, PUT and PATCH difficult to grasp, before I encountered concrete examples in the Drivy codebase:

POST

POST is used to create a new resource on the server, and maps to the create controller action. To create a car, i.e. a new row in the database, we need to gather and post to the server the data that corresponds to the columns in the cars table. This might be:

{
  is_open: false,
  make_id: 53,
  model_id: 11,
  plate_number: 'L87hYQJ',
  registration_year: 2019,
  user_id: @user.id,
}

We send this information, or payload, to the server at www.drivy.com/cars. The server then decides the location, or URI, for the resource - which will also correspond to the resource’s unique ID - and creates the row in our databse corresponding to that location. For example, www.drivy.com/cars/1. So far, so good…

PATCH and PUT

Hang on, both verbs correspond to the update in CRUD? Yes, but there is a subtle difference.

PUT overwrites the whole resource at an existing location. If we send the following payload to www.drivy.com/cars/123

{
  mileage: 9
}

…the entire resource at location /123 will be overwritten i.e. our car’s only attribute will now be its mileage. By the way, if a resource is not found on the server at the given location, a new one is created by the server.

On the other hand, PATCH overwrites only the attributes included in the payload. If the attribute is a new one, it is added to the resource. If we send this payload to our original resource, which now resides at www.drivy.com/cars/1

{
  mileage: 5,
  is_open: true
}

… the is_open attribute will be overwritten, and the mileage attribute added. So once these changes have been applied by the server, we will end up with a Car resource at location www.drivy.com/cars/1 that looks like this:

{
  is_open: true,
  make_id: 53,
  mileage: 5,
  model_id: 11,
  plate_number: 'L87hYQJ',
  registration_year: 2019,
  user_id: @user.id,
}

Manipulating data structures: benchmarking flat_map vs map.flatten

One of the first Ruby tools in the toolbox that I encountered during Le Wagon was Enumerable#map, and this method can be usefully combined with Array#flatten, to return a useable array of resources that might otherwise be nested. Take the following example:

# A `car` has many `car_photos`. Imagine we want to get all the photos of all the cars for one of our pro owners. This owner has 56 cars, and each car has at least 3 car_photos. I could:

@user.cars.map(&:car_photos)

With this request, I end up with a data structure that looks something like this

[[car_photo_1, car_photo_2, car_photo_3, car_photo_4, car_photo_5]]

It’s an array of an array of ruby objects. To be able to use it, I must first .flatten the array because the values are nested.

@user.cars.map(&:car_photos).flatten

But I know I’ll be using this request a lot, for users with a lot of cars. Maybe I’ll even be rendering all the photos at once. I’m going to need a faster way I can do this, so I’ll benchmark the performance of .flatten compared to .flat_map. I’ll do this in a temporary rake task (for easy access to database connection) but you might also run it as a script. My rake task requires the Benchmark module and looks like this:

  task benchmark_flat_map_vs_map: :drivy_environment do
    require 'benchmark'

    user = User.includes(cars: :car_photos).find(977)

    Benchmark.bmbm do |x|
      x.report('flatten') { user.cars.map(&:car_photos).flatten }
      x.report('flat_map') { user.cars.flat_map(&:car_photos) }
    end
  end

I’m loading all the user data into memory first, so we can just focus on the map comparison without including time needed for database roundtrips. I’ve chosen to use the bmbm method which does a “rehearsal” run to get a stable runtime environment and eliminate other factors.

(By the way, this RailsConf 2019 talk is a really useful and accessible intro to how and when to profile and benchmark your code.)

So, the results were:

Rehearsal --------------------------------------------
flatten    0.006082   0.001934   0.008016 (  0.010056)
flat_map   0.000158   0.000015   0.000173 (  0.000188)
----------------------------------- total: 0.008189sec

               user     system      total        real
flatten    0.000316   0.000003   0.000319 (  0.000313)
flat_map   0.000147   0.000002   0.000149 (  0.000145)

You’ll notice that flat_map is more than twice as fast than map. This latter will create an intermediary array, and so the code has to be iterated over twice.

This, my friends, is where flat_map is useful. Like map it takes a block:

@user.cars.flat_map(&:car_photos)

but doesn’t create that intermediary array.

I used to rely on these out-of-the-box methods, without ever interrogating what was going on. Imagine running the same request involving 1000 power users, each with 50 cars, and each car with 10 photos. Using flat_map can significantly help improve performance.

& Safe Navigation operator…part 2

Clement discussed the use of the & safe navigation operator to safely navigate through layers of object relations.

In the Owner Success squad, we learned the hard way that method chaining using the safe navigation operator can also be a sure-fire way to introduce bugs - and precisely because navigation is safe, i.e. no errors are raised, they can be excruciatingly difficult to debug.

When we install an Open box in a car, we use the provider_device_id given by the box provider. A device id is considered valid on the provider’s api if it is:

However, on our side, the device ids are entered manually in the backoffice. To cover our backs against human error, we format the device number at the time of form submission:

The & allows us to safely chain the methods, so that in the event that strip returns nil, upcase will not raise an error. Great!

What we didn’t realise is that whilst strip returns the original receiver, even if no changes are made, strip! returns nil in the event that the receiver was not modified. Plus strip and strip! only deal with trailing and leading whitespaces, not internal ones.

So for a provider_device_id looking something like this:

'A dEVice ID 123'

strip! returned nil and upcase! was never run on the original object. We didn’t know about it because nil&.upcase! was not raising an error. We ended up with lots of device numbers in an invalid state, leading to errors on our external provider’s api, and had to correct them manually with a rake task.

It’s always worth checking the documentation for the subtle differences between methods with and without the !. We generally try to avoid chaining them to avoid introducing bugs like this one.

Arrays: concat, prepend, + and «

I find it useful to remind myself with clear examples of the precise output and side effects of each of these methods. Methods that modify the original receiver can also be a source of well-hidden bugs.

Array#concat

array_1 = ['Volkswagen', 'Vauxhall', 'Renault']
array_2 = ['Tesla', 'BMW']

array_1.concat(array_2)
#=> ['Volkswagen', 'Vauxhall', 'Renault', 'Tesla', 'BMW']

array_1
#=>  ['Volkswagen', 'Vauxhall', 'Renault', 'Tesla', 'BMW']

array_2
#=> ['Tesla', 'BMW']

Moral of the story: array_1 is modified, array_2 is unchanged.

Array#+

array_1 = ['Volkswagen', 'Vauxhall', 'Renault']
array_2 = ['Tesla', 'BMW']

array_1 + array_2
#=> ['Volkswagen', 'Vauxhall', 'Renault', 'Tesla', 'BMW']

array_1
#=>  ['Volkswagen', 'Vauxhall', 'Renault']

array_2
#=> ['Tesla', 'BMW']

Moral of the story: Neither array is modified.

Array#prepend

array_1 = ['Volkswagen', 'Vauxhall', 'Renault']
array_2 = ['Tesla', 'BMW']

array_1.prepend(array_2)
#=> [['Volkswagen', 'Vauxhall', 'Renault'], 'Tesla', 'BMW']

array_1
#=> [['Volkswagen', 'Vauxhall', 'Renault'], 'Tesla', 'BMW']

array_2
#=> ['Tesla', 'BMW']

Moral of the story: array_1 is modified, array_2 is unchanged.

Array#<<

array_1 = ['Volkswagen', 'Vauxhall', 'Renault']
array_2 = ['Tesla', 'BMW']

array_1 << array_2
#=>  ['Volkswagen', 'Vauxhall', 'Renault', ['Tesla', 'BMW']]

array_1
#=> ['Volkswagen', 'Vauxhall', 'Renault', ['Tesla', 'BMW']]

array_2
#=>  ['Tesla', 'BMW']

Moral of the story: array_1 is modified, array_2 is unchanged.

Delegating methods with Module#delegate

You can use delegate to expose the methods of objects on another class. For example, a cancellation belongs to a rental - as indeed you might expect it to in the real world.

class Cancellation

  belongs_to :rental

  #[...]

end

By the way, this database relationship is incidental and not a strict criteria for the use of delegate. It is a hint though, that there might be some overlap in the implementation of these two classes, and thus that there may be scope to delegate.

The cancellations table might look something like this in the database:

 t.string "state"
 t.integer "rental_id",
 t.integer "some_other_id_field",
 t.decimal "some_refund_field",
 t.decimal "some_other_refund_field"

As you can see, it does not have its own currency column.

So, what if you need to access the currency of a cancellation? You could:

cancellation.rental.currency.

But this means that the cancellation object has to know that a rental object has a currency column. That violates the Law of Demeter, which is the principle that objects should know as little as possible about each other. If our cancellation object knows too much about the rental object, or is coupled too closely, then any future changes to the implementation of the Rental class become hard to maintain.

You can use delegate to avoid chainging objects in this way. On the Cancellation class you can do:

class Cancellation

  belongs_to :rental

  delegate :currency, to: :rental

  #[...]

end

Then, you might call cancellation.currency elsewhere in the code. For example:

invoices = Invoice.where(currency: @cancellation.currency)

This helps keep your code DRYer, avoids object-chaining and respects the law of Demeter. Hoorah!

CapybaraScreenshot

The Capybara-Screenshot gem will automatically capture a screenshot for each failure in your test suite. But did you know that you can also manually capture photos? Just pop one of the following directly in your code:

Capybara::Screenshot.screenshot_and_save_page
Capybara::Screenshot.screenshot_and_open_image

and a lovely screenshot will be taken of the current step in your integration spec at that point in time. This has helped me countless times to debug my integration specs. You get to see what the user would see at that stage in the flow, and check that all information is correct and displaying as it should. Plus you’ll get a more digestible error output and stacktrace.

to_sql

9 weeks didn’t leave a whole lot of time to cover SQL in any detail. As I start to work on more complex projects, I need to make data-based decisions or include SQL in my requests for performance reasons. Chaining to_sql to an ActiveRecord relation returns the SQL statement run by the database adapter against the database to retrieve the results.

For example:

OpenDevice.where(“id < ?”, 500).to_sql

returns

"SELECT `open_devices`.* FROM `open_devices` WHERE (id > 500)"

Little by little, this is helping to improve my understanding of the underlying SQL syntax, rather than relying on the magical layer between me and the database that is provided by ActiveRecord.

Whether you are setting out on your full-stack adventure, or you already have a bit of experience, I hope this summary of some tips and tricks has been helpful. Don’t hestitate to reach out with comments or feedback :)

Did you enjoy this post? Join Drivy's engineering team!
View openings