I joined Getaround, which was still named Drivy back then, two years ago. My previous and most extended professional experience had an internal organization that did not allow me to code full time, so many of my technical projects were actually side projects working alone.
Although I could choose my topics and constraints, working alone does not always help to learn good practices and tips that make a developer efficient and aware of the different technical challenges.
A few weeks ago, I took the time to understand how working in a (brilliant) team made me progress so much, not only as a Ruby developer but as a “Tech”. Here are a few topics that I learned or progressed on in the past two years.
tap
and then
I love Kernel#tap
because it lets me compose objects with conditions without having to add multiple conditional blocks.
We can argue that this is neither necessary nor more performant, but most of us love Ruby because it makes us write concise and straightforward code. I am feeling more comfortable with less procedural code.
On the same topic, we also have Kernel#then
which is comparable to tap
but returns the result of the block. This is very helpful when building conditional requests without having to add big if
blocks:
then
is just an alias for yield_self
introduced in Ruby 2.5.
Transactions enforce the integrity of the database by wrapping several SQL statements into one atomic action. I find them not only useful but sometimes even essential. In some cases, you have to ensure several changes were made successfully or to cancel them all.
The following example is quite explicit about how convinient transactions are:
If ticket creation were to fail, I am sure not to leave the car available or the order canceled, since the transaction will roll back.
A lot of articles exist about this topic on the web. We even wrote about it a while ago in our Code Simplicity series by Nicolas.
The command pattern is a great way to extract business logic from controllers or even models, stay tied to the Single-responsibility principle and share a common API for service objects.
Although as a pattern, this one must be used carefully because it cannot resolve every situation. Jason Swett has even an interesting point of view about using this pattern in the Rails community.
A form object is a simple class that handles logic from a form submission. This class can be associated with the command pattern to share a common API with multiple form objects in your app.
Not only does this pattern allow you to extract business code from the controller and make it more testable, but it is also a great way to have different validations for the same model. You cannot always share a common form or even common validations depending on your action, for instance, when handling a user account. The rules applied to form parameters in a user registration are not the same as an account update.
Take the terms of service for instance. You probably want to ensure a terms_of_service
parameter is present and true
when signing up, but this requirement is unnecessary for a user updating her account. Having multiple form objects depending on the feature is a great help for this.
Jean also wrote about it on our blog a few years ago.
The Facade pattern is proper when (but not only) decoupling business code from third-party code.
Let’s take the example of a third-party web API prividing its own gem.
Using it directly sometimes can be less maintainable as you don’t own its public API and are vulnerable to changes. What if you need to update this great_api
gem for security reasons, but the gem changed its Car::fetch
method to Vehicle::get
? You would need to change every occurrence of GreatApi::Car::fetch
in your business code to handle this breaking change.
Building a gateway around the gem ensures you to own it and encapsulate third-party code in one single place.
I try to remember the “Tell, don’t ask” principle when designing a brand new object to keep in mind what OOP is about: designing objects being able to interact. Therefore an object should describe itself its behavior rather than having a program asking it what it is composed of to predict its behavior.
This example of Thoughbot’s blog is quite explicit:
Sometimes we want to ship fast, but we still want to ship well. There are some cases where we want to release code that is meant to be temporary, or to be reminded to monitor some behaviors once a feature has been live for a few weeks.
Temporary code is often associated with forgotten code and then technical dept, if not bugs. But delayed deprecations are a great way to keep a codebase clean month after month.
Deprecations trigger notifications to their owners for both Ruby and JavaScript code. This can be useful to remind you to clean up a piece of code.
I enjoy using them because it helps me staying efficient while maintaining a clean codebase.
Another game-changer for our velocity and confidence when shipping new features is the feature flipper. It enables us to make some features available for a percentage of users or a percentage of time.
This is particularly useful to test changes and measure their impact without risking changing habits for all our users. If we need to urgently cancel a feature - because Murphy’s Law is always lurking - we can do so without deploying an urgent fix to hide it.
It doesn’t prevent us from being cautious and striving to ship high-quality tested code. Still, we are far more confident in ourselves when we know we can quickly handle unpredicted behaviors.
Quite often, we have to test behaviors in the past or the future. Sometimes we also want to test a feature without time variation, for example, to avoid test flakiness.
Timecop is the perfect tool for this with a simple and comprehensive API.
Zero downtime migrations are a pretty common thing, but to be honest, I never had not the chance to work with this process before working at Getaround.
The rule of thumb is to ensure any migration being deployed is compatible with the code already running. For instance, you cannot deploy at once a migration renaming a table’s column and the code handling the new column name. There is a very high chance that someone will run the app while the migrations haven’t been run yet and the column name doesn’t refer to anything yet.
Simple caution must be taken with multiple deployments such as:
Writing decoupled and reusable code is great. Ensuring this code will be properly used by others is even better. When I write code that can be shared or with variable data, I try to make sure nobody can add use cases that would break my code.
Let’s take an example, I am adding a state
attribute to Car
and I want to localize each state.
This is great, now I am able to use I18n.t("activerecord.attributes.car.states.#{car.state}")
.
But what if two months later, another developer adds a pending
state? It would break when somebody runs my code with a pending car, and I would only be warned about it when facing the bug itself.
To avoid this situation, when adding the new state
attribute, I also add specs to ensure all states have an associated translation:
Some people love it, some people don’t; in either case, we have to admit RSpec mocking is quite powerful. My perspective on the subject is to avoid mocking in feature specs as we want to stay as close as possible to a real-world example. When I have too much mocking to do to test a method, I probably need to think about the method/class dependencies.
Anyway, if you decide to use mocking, RSpec is a sweet candy. It helps you write difficult test cases with complex dependencies without instantiating tons of real objects and data.
Let’s take an example where I need an object to return a particular value. But this method has complex rules to return this value:
It is expensive to write and compute all requirements for this method, and I may even want it to return different results. I don’t want to be validating this pro?
method neither; this is not the purpose of my test. With mocking, I can allow
this object to receive
this method and return
the value I need for my test, instead of the value it would have returned with its default state.
Once again, let’s not forget that this powerful tool must be used cautiously; if an object is hard to unit test, maybe it is too much coupled with another object, or the abstraction is wrong.
NotImplementedError
Finally, ensuring the next developer, who could be yourself 6 months from now, is using your base class properly. When creating a base class meant for inheritance, you may need that its children implement a method.
Using the NotImplementedError
standard error is a good way to ensure the method is implemented and to document it as necessary for child instances.
I could add many more topics, even some simpler ones. With these tips, I am more confident now than I was 2 years ago, and I am looking forward to learning more in the years to come.
Of course, some may seem common sense to you, or even not necessary. I may also have forgotten good practices that look like a must have to you. If so, please feel free to reach us and debate.