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.
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:
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
…
…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
…
… 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:
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:
With this request, I end up with a data structure that looks something like this
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.
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:
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:
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 2Clement 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:
…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.
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
Moral of the story: array_1
is modified, array_2
is unchanged.
Moral of the story: Neither array is modified.
Array#prepend
Moral of the story: array_1
is modified, array_2
is unchanged.
Array#<<
Moral of the story: array_1
is modified, array_2
is unchanged.
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.
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:
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:
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!
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:
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.
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
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 :)