foo(bar) vs bar.foo()
How to code functions balancing scalability and usability
As our engineering team at Lime has grown, and our code base with it, it has become more difficult for any given engineer to know everything there is to know about the modules that they work on. We set out to try to solve this problem and dove into the code to see what we could do to fix it. In doing so we came across an interesting pattern that we’re using in our main codebase.
Let’s say we have some function
foo which takes as one of its parameters an instance of class
Bar. We have two ways of writing this code:
We firmly believe in taking a data-driven approach to adding new features, and when the data doesn’t exist we run A/B tests. When we’re running a test on scooters, we assign all of the scooters to a single test group. We need to make sure that whenever the scooter needs to exhibit the behavior under test, it must always exhibit the behavior it was originally assigned. If we thought about this as a drug test, our data would be bad if we randomly gave all participant placebos for one dose and the test drug for the next.
As we were looking for ways that we could improve our code, we saw that the way that we were implementing our A/B tests always preferred going the
bar.foo(), is essentially asking the object for information about itself, and
foo(bar) uses an outside expert to retrieve information about an object. When thinking about A/B testing as a drug test, should we ask the patient whether they’re getting the placebo or not? No, an external export should be keeping records based on the information they have about the patient.
As a coder though, just because doctors can’t do a thing doesn’t mean that it won’t work for us. Indeed this code does work and has powered some of our best feature decisions.
When given multiple ways to achieve the same goal, we should lean toward the solution that provides the most maintainability going forward. SOLID principles often serve as a good starting point to look for guidance on maintainability.
The first principle that stands out as providing insight is the Open/Closed Principle.
We want our modules to be closed to changes arising from new features being added to our code base and open to be used in a wide variety of ways. Essentially, all our core modules should be black boxes that give others powerful (but not unlimited) control over the objects. As developers want to use our modules, they should not be tinkering inside the black box.
Again, if we think about medical studies, we don’t go implanting subdermal tags to assign patients to groups, we use information that the patient has readily available, like their name. Similarly, we shouldn’t open up our Scooter class to add a new function to help the scooter identify which group it belongs to. Instead, we should make sure that a Scooter instance exposes enough information that an outside expert could determine whether it is in a test group.
ab_test_subject points to the fact that we probably do, by passing the full object to
AbTestSubject the code above doesn’t really point to how we determine what’s important to identify a scooter as a part of a group.
Unfortunately, the Open/Close principle only points to a problem without providing any solutions.
Interface segregation principle also seems to apply to this problem and also points to a possible solution. It states, no client should be forced to depend on methods it does not use. It recommends that our interface be split into multiple pieces.
In this case, we can think of the interface for the Scooter class as the list of all its public methods and variables. The split here is to take
Scooter#sidewalk_detection? and move it to a different interface. Where?
Turns out that
sidewalk_detection? is only used in two places in our code: when we start the logging and when we stop it.
We could move the definition of
sidewalk_detection? to Trip, but this is the same road with the same problems. Instead of opening
Scooter to add the experiment, we instead have to open
Trip. And if
Trip functionality isn’t part of the experiment, are we forced to put this back on
Scooter? Even then, many things that rely on
Trip don’t really rely on whether or not we’re trying to detect sidewalks, and the Integration Segregation principle says that we should split it out.
Enter the brand new module
We can also rewrite
Trip to do the bare minimum to support sidewalk detection:
Finally, all mention of sidewalk detection is removed from our
Scooter class allowing it to follow the Open/Closed Principle!
Making the new
SidewalkDetection module clearly groups code that cares about sidewalk detection in a single location, limiting the locations that need to change as the feature changes.
As we worked through this approach on all of the experiment code in the Scooter class, we found some experiments had already run their course, the associated code had already been deleted, and what remained in the
Scooter class was extremely dead. New experiments that follow the example of the
SidewalkDetection module, this should stop happening this problem, which is another nice bonus to this approach.
The last big plus to this approach is, once other vehicle technologies (like hoverboards) become consumer-ready, our
Hoverboard instance can also easily be passed to the module to enable sidewalk detection there too.
There’s a difference between writing functional code and writing good code. I like to think that functional code solves the problem you know about, while good code solves problems you didn’t even know you had.
- When coding a new feature, there is often a choice of adding it to an existing class or creating a new location for that feature and passing in the class as a parameter.
- If the feature is not core to the existing class, passing the class as a parameter helps to maintain the boundary between different pieces of code, and by extension different parts of the codebase.
- Make life easier for future developers by not expanding code interfaces that they’ll have to read through to understand how something works.
- Sometimes attaching a piece of code directly to a class can severely limit its reusability.
- SOLID principles are a great place to start to understand how to write more maintainable code.
If you are interested in working on challenging problems at Lime, check out our career page!