Scala Salad Anti Pattern
One of Scala's best features is traits. Mixins to Ruby peeps. Interfaces with implementations to Java peeps. A class can implement as many traits as you want and so its very easy to compose a class of reusable traits (stacked to be technical), and that's the power behind it. However as a system grows larger I've seen traits cause problems in some areas. And here's my thoughts on why and how to avoid it.
It's very tempting to create as much of your code as possible as traits. Have traits extend traits. After all, this means more your code is then by definition reusable right? Simply create a new class that mixes in all the functionality you need, override a value here and there. In principle this is sound and is used to reduce code duplication down to almost zero. This is used great effect in the scala collection library.
However a particular anti-pattern concerning traits is what I call the salad pattern.
In the familar cake pattern we have ingredients (traits) that are layered together to form a coherent whole. No one ever ate a cake and became confused where the chocolate ends and the jam starts.
But in the salad pattern, all kinds of methods and variables are tossed into a giant bowl. The mayonnaise gets all over the rocket. You can't find the chopped peppers for all the lettuce. In a bad salad you can't easily determine the individual ingredients.
When developers go trait crazy, there are soon traits composed of traits composed of traits. Traits that provide a single abstract method with another single implementing trait. Traits used to provide easy scoping to dependencies instead of imports and variables. I've seen classes that when linearised have 50 or more ancestors (I really have).
This leads to the what on earth does this class do effect. Tracing a cyclic graph of dependencies to understand a class can quickly become a brain melt. Especially when a class ends up mixing in the same trait over and over due to trait inheritance.
When mentoring junior developers in Scala I try to provide a few simple rules that I think give a good rule of thumb. Of course rules are meant to be broken, and it's easy to find counter examples, but I've found these give a decent starting point.
A trait often provides an is-a or can-do relationship or similar. If
Duck extends Swimming with Quacking
then that is because a duck can swim and a duck can quack. AList extends Traversable
because List is a Traversable. The functionality added by the trait forms a part of the whole and naturally sits with the composed object.Avoid traits as a dependency shortcut. If your TimelineService needs access to methods from a HTTP client then don't mix in the client. Mixin a dependency on the client. constructor parameters, abstract members, or self types with the cake pattern. An obvious exception is DSLs such as the Scala test traits.
Avoid very fine grained traits. When there are many small cogs composed together, it becomes difficult to rationalize all the cogs as a whole. I previously said the Scala Collection API is excellent. And that's because it's easy to use and extremely powerful. But it's not easy to decipher internally. That's a decent trade off for a core SDK but probably not for your code.
If a class inherits the same trait through many parents, then it's worth investigating if declaring self types on the parents is a more appropriate relationship.
Abstract only where useful. This isn't Java. It's acceptable to have services without interfaces (abstract traits). If you find yourself naming things Authorization and AuthorizationImpl then that's a good indication it's overkill. Otherwise the naming is obvious because its based on the distinction. For example, with SearchIndex, ElasticsearchSearchIndex and SolrSearchIndex it is immediately clear why there is a common trait.
Remember: No one really enjoys a salad.
Written by sam
Related protips
1 Response
Thanks for the great article! If I might I want to add another aspect, unless the relationship is "class A" is "trait B", someone might add some other "B" functionality that will make no sense for the programmer that will go over the methods of "a: A" in his awesome IDE.