Taking the last post even farther, we learn about stackable traits
In the previous post, “Overriding traits in Scala” we saw code like this:
This allowed us to create a “BuckooMember” with “DoubleBuckooBucks” who then received double the points anyone else receives:
val doublePointsJohn = new BuckooMember(1, "John", "Public") with DoubleBuckooBucks
That’s important because now anytime within our existing application that we call “addBucks” on an instance of BuckooMember with DoubleBuckooBucks, that person will automatically accrue double the points. And where they are just an ordinary “BuckooMember” (without the DoubleBuckooBucks trait), they accrue the normal amount. In an application where there may be may be dozens of calls to “addBucks” we save a lot of time by not having to track down all of those calls and put an if/else statement around it to check whether or not the person is in the double points promotion. We also don’t have to add such a conditional statement into the “addBucks” method for all classes in the inheritance tree underneath the abstract class Member.
The need for Stackable Traits
But we still might run into the need for more than this. Let’s say a new promotion starts up for awhile and now anytime someone earns over 500 points they then earn a 50 point bonus above and beyond whatever point accrual they have. Some of the BuckooMembers will have just this promotion, some will have the double points promotion, and some will have both.
Scala’s “stackable traits” (or “stackable modifications”) feature will allow you to do this. First define the new promotion in the same way you defined the DoubleBuckooBucks trait:
Just to demonstrate the difference between a normal BuckooMember and a FivePercentBonus BuckooMember, we will create a normal BuckooMember and award 500 bucks and then compare the result with the same award to a BuckooMember with the FivePercentBonus trait, in the Scala REPL:
scala> val normalMember = new BuckooMember(1, "Normal", "Member") normalMember: BuckooMember = BuckooMember@551346c4 scala> normalMember.addBucks(500) scala> normalMember.buckooBalance res1: Int = 1500 scala> val bonusMember = new BuckooMember(2, "Bonus", "Member") with FivePercentBonus bonusMember: BuckooMember with FivePercentBonus = $anon$1@33c06b38 scala> bonusMember.addBucks(500) scala> bonusMember.buckooBalance res11: Int = 1525
As you can see, the “bonusMember” received the 5% bonus of 25 points.
Now let’s say we have someone who qualifies for the double points promotion and the five percent bonus promotion. As you might expect, you can easily “stack” traits together. Just chain the “with _____” calls:
scala> val doubleAndFiveMember = new BuckooMember(3, "Bonus Double", "Member") with DoubleBuckooBucks with FivePercentBonus doubleAndFiveMember: BuckooMember with DoubleBuckooBucks with FivePercentBonus = $anon$1@7ddbaff1 scala> doubleAndFiveMember.addBucks(500) scala> doubleAndFiveMember.buckooBalance res13: Int = 2025
Now the “doubleAndFiveMember” received double bonus points (500 * 2 = 1,000) and the 5% bonus of 25 points.
And you still don’t have to track down places in your code where “addBucks” is called, or introduce conditional statements anywhere.
It’s important to look at Scala’s rules for selecting the ordering of traits in a stacked construct like this. You might notice that for the FivePercentBonus, we chose not to calculate the bonus and apply it to the amount before passing the amount to super.addBucks(). Instead we applied the bonus, once calculated, to the balance, then passed the amount to super.addBucks(). This was a defensive measure taken in light of the context that there might be traits that FivePercentBonus is “stacked” on. If we calculated the bonus and added it to the amount that we passed to the super, the super (whatever it is) would be working on a base points amount that has been modified. In the manner we chose, though, we preserve the base points as they were passed to us to begin with.
This highlights an important rule. Do not change parameters when passing them around in stackable traits. Every trait in the “stack” should observe this rule, or the calculation will become wonky. Again, Scala wins a prize here, because its default behavior, in general, is to pass immutable variables around. So if you are “thinking in Scala” as you perform these stackable modifications, you should already be in a mindset where you are preserving correct behavior.
Unfortunately, someone downstream can still pass a modified amount to you if they are not thinking in Scala. There is no way I know of to enforce this in the code but it might have you begging the question of what order are the traits evaluated in. The answer is that traits to the right are evaluated first. In the following statement . . .
val doubleAndFiveMember = new BuckooMember(3, "Bonus Double", "Member") with DoubleBuckooBucks with FivePercentBonus
. . . FivePercentBonus is evaluated, and then DoubleBuckooBucks.
We could just as easily have swapped these:
val doubleAndFiveMember = new BuckooMember(3, "Bonus Double", "Member") with FivePercentBonus with DoubleBuckooBucks
And in that case, DoubleBuckooBucks would have been evaluated first, then FivePercentBonus.
So another way to approach this is to be aware of the ordering and make sure that as you “stack” traits, you place them in the correct order, if order becomes an issue in whatever context you are working in. Nevertheless, it should be possible in most cases to avoid having to worry about ordering, by insuring that you pass parameters up the “stack” as they were passed in to you. Actually, it is probably suggestive of a design problem if you end up stacking traits that alter the parameters being passed to super. A best practice might be to forbid such a choice as a way of highlighting design problems. In some rarer cases, you might want to pass modified parameters. Using our example, maybe you want these promotions to double on top of each other. If that’s the case, knock yourself out.
Stackable traits are a useful features Scala has introduced to those programming on the JVM. It’s somewhat akin to providing multiple inheritance, but it avoids a lot of the problems of multiple inheritance. The major advantage I see is that you can deal with individual methods (like in the example above) or a small set of methods without worrying about everything else in the class besides them. Finally, it gives you flexibility with the behavior of your objects that you otherwise wouldn’t have.