A mixin is a type which can be mixed in to (i.e. included in) other types via multiple inheritance. This allows the encapsulation of certain behaviors which cut across the main type hierarchy. In C++ this is fairly standard, though it’s sometimes discouraged because of the potential for the “diamond problem”. In Java 8, mixins can be created using interfaces with default methods.
Why use mixins in Java?
Mixins give you design options which you did not have before. A class can inherit behavior that isn’t in its superclass. For example, you can have Goose implements Honker and Car implements Honker – with no need to implement honk() more than once. Some more practical uses include test fixtures (which I will address in more detail in my next article), and logging.
Default methods in mixins are also very useful for wrapping static method calls, especially in legacy code. This is what my interface-it tool automates. So you can replace static calls with calls to interface methods that can be mocked in tests, and/or overridden for special cases. It allows you to make a procedural design more object-oriented and pull hidden dependencies out into the open. It also lets you avoid using static imports, which can make the code less readable.
Finally, a benefit of mixins is polymorphism. You can override or extend the default behavior in special cases, which is something you can’t do with static calls.
Now that I’ve released version 1.1 of interface-it, I understand better why so many projects decide to do a big 2.0 release which includes breaking changes to API’s.
Once you’ve decided to support backwards compatibility for all the exposed API (all public and protected methods in the public classes, normally), minor releases hit a lot of technical debt because you can’t refactor everything you want to. TDD doesn’t save you from this .
With interface-it, I made a few assumptions about the API which changed very quickly when I discovered new requirements. For example, before the 1.0 release, I thought about encapsulating some of the arguments to key methods, but I didn’t see clear affiliations or behavior related to these arguments. It was just data needed for generating a mixin. For version 1.1, where you can generate a hierearchy of mixin types (just parent-child via the command-line interface, but the code API is more flexible), it was no longer data but behavior that I needed, because I had to manage multiple classes in the same call. Despite having excellent code coverage thanks to TDD, the change was a bit painful to implement, and I had to leave in place some methods which I would have preferred to delete.
“Animals are our friends!!”, comedian Bob Goldthwait used to shout, adding “But they won’t lend you money.”
One famous definition of legacy code (from Michael Feathers) is “code without tests”. I would extend that definition to say that it’s code which does not have tests for the behavior you want to have. TDD will get you code that is well-tested for the behavior you want when you develop it. If your code does anything innovative, some of the behavior you want will almost certainly change over time.
So TDD is our friend, but it won’t eliminate all legacy code. What it will do is make it easier to know when we accidentally change behavior. It doesn’t necessarily make it easy to change our code’s behavior, either, but it gives us confidence to tackle the change because we know that tests will catch and allow us to fix any little mis-steps before
they become more difficult and time-consuming to find and fix, and, importantly, it nudges us constantly to design our code in a way which allows it to be refactored with less trouble and risk.