Scala traits are interesting because they can be used for inclusion polymorphism and to mixin behaviour. I’ve found tension here though, as the former uses inheritance and the later is more about code re-use. So when a Scala class extends a trait with behaviour, it seems to go against the generally accepted view that using inheritance as a mechanism for code re-use is a bad idea.
It can be tricky not break the inheritance vs. composition principle when using traits with behaviour. Is it clear to you when you might be?
Mixins the Wrong Way
Odersky calls traits with behaviour “mixin traits”. To be a genuine mixin trait, it should be used to mixin behaviour and not just something you inherit from. But what’s the difference? Let’s look at an example.
Let’s say that you have a repository style class who’s API talks about business operations, a
Customers class for example. You might have an database backed version and you don’t want anything going behind your back and messing with the data; everything in production code should go through your business API.
Now let’s say that you want a test fixture to allow you to quickly setup test data in your
Customers without having to go through the production API. You can provide an implementation to a trait and collect some data together like this;
This says that extending classes must provide a value for
customers. It implements some coarse grained test setup against
customers. So when writing a test, it’s easy to just extend the trait and slot in an implementation of
customers. For example an
InMemoryCustomers or an Oracle implementation that by-passes any constraint checking the proper API might enforce.
But we’re saying here that an
OracleCustomerTest is a
BackdoorCustomers. That doesn’t even make sense. There’s no strong notion of a
BackdoorCustomers; it’s not a meaningful noun. Best case scenario, you’re upfront about the fact that it’s a fixture and rename
CustomersTestFixture but even then, the test is not a fixture, the two are independent. One is test apparatus that supports the test, the other is the test or experiment itself.
It’s tempting to use traits like this under the pretense of “mixing in” behaviour but you’re really inheriting behaviour from something (that in our case) isn’t related. You’re precluding any type of substitution or inclusion polymorphism. Now arguably, substitution isn’t of great value in test code like this but it’s still a laudable goal.
Using inheritance to mixin behaviour contradicts the inheritance vs. composition principle. So just when is a trait with behaviour a genuine mixin? The trick is in how we mix it in. Before, we made the types inherit the trait but we could have mixed the trait into a specific instance.
For example, we can rework our trait to be a self type.
It now enforces implementers to also be a sub-type of
Customers. This, in turn, forces us to rewrite the test
So now our test is not inheriting an orthogonal type. From an object-oriented perspective, it’s much cleaner. We use composition to give the test a
customers instance but this time, we treat it as two things. The actual type of the thing is;
So all the backdoor methods work along with the API methods but now we can clearer about which is which. For example,
Scala is both an object-oriented language and a functional language. So unless your team is entirely behind doing things functionally, you’re still going to come across object-oriented thinking and principles. Traits that have behaviour make it awkward because functionally-thinking, you could argue that nouns aren’t important and behaviour in traits is just behaviour. So why not extend that behaviour by whatever means (including inheritance)?
Because Scala has objects you can’t really just ignore object-oriented semantics and thinking. Not unless, like I say, the entire team buy into functional only code. If that were the case, then reusable behaviour should really be represented as functions on Scala singleton objects and not traits. You’d be forced to use composition anyway.
By that logic, it feels like extending traits for re-use in a functional programming context is just lazy. Mixing behaviour “the right way” seems much less contentious.