When I’m bumping into an interface with only one implementation, it makes me think of reasons why is that interface even needed?
Common arguments
Interfaces are good for many things: promote code re-use by abstraction, help define boundaries in architecture, provide a form of multiple inheritance and so on, but usually a common argument for using them is that they make it easy to swap implementations, thus they also help with testing. The counter argument often is that when it’s very likely that the interface will not have other implementations, the interface becomes mere boilerplate code adding to cognitive complexity. Furthermore, if you use ProGuard then such an interface will get completely removed from the final artifact anyway due to merging of the class hierarchy (see the list of optimizations). With mocking libraries you don’t strictly need them either because it’s also possible to mock or fake classes just as conveniently as interfaces.
I would argue that many people tend to forget how interfaces in Kotlin also help with following the single-responsibility principle by making it so convenient to implement the decorator pattern.
Composition
Kotlin has a really nice feature among many: delegation using the by
keyword. Inheritance and composition
are the two main forms of reusing code. By using delegation you can take advantage of composition over inheritance in
a very convenient fashion, because delegation is a form of composition.
To favor composition over inheritance is a design principle that gives the design higher flexibility. It is more natural to build business-domain classes out of various components than trying to find commonality between them and creating a family tree.
Delegation in Kotlin only works with interfaces however, partly because classes have constructors and those would have to be delegated somehow too. Composition doesn’t require the involved classes to be open, hence it is also possible to use types in your composition from external dependencies. Inheritance would be impossible for external types which are declared as final.
Example
Let’s say you have a class which you expect to only ever have a single implementation, because it implements some kind of fundamental business logic that when changed must mean that the whole purpose of the application would be defeated.
class ChocolateFactory(
private val cocoaProvider: CocoaProvider,
private val milkProvider: MilkProvider,
) {
fun produce(): Chocolate {
val result = // ...
return result
}
// ...
}
For some strange business reason the client decides that The Chocolate Cartel must be notified every time some number of chocolates had been produced. An obvious change to the existing code could be:
class ChocolateFactory(
private val cocoaProvider: CocoaProvider,
private val milkProvider: MilkProvider,
private val hotline: Hotline,
) {
private var numOfProducts = 0
// Add extra behavior according to the new requirement
fun produce(): Chocolate {
val result = // ...
numOfProducts += 1
if (numOfProducts == CARTEL_THRESHOLD) {
numOfProducts = 0
hotline.call("Miss Bon-Bon")
}
return result
}
// ...
}
But there’s an obvious problem with the above solution; it violates the single-responsibility principle: ChocolateFactory
becomes responsible
for more than just producing chocolates - it could now potentially change both because of new notification requirements or
because of the way chocolates are produced becomes different. Making a shady phone call is unrelated to the operation of making chocolate,
so it can also be confusing why is Hotline
a hard dependency.
A better solution might be to inherit from the base ChocolateFactory
and extend the produce()
method to also include the notification behavior.
This could look like the following:
// Make the factory open
open class ChocolateFactory(/* .. */)
class ShadyChocolateFactory(
cocoaProvider: CocoaProvider,
milkProvider: MilkProvider,
private val hotline: Hotline,
) : ChocolateFactory(
cocoaProvider,
milkProvider,
) {
private var numOfProducts = 0
// Extend this method with the extra behavior
override fun produce(): Chocolate {
val result = super.produce()
numOfProducts += 1
if (numOfProducts == CARTEL_THRESHOLD) {
numOfProducts = 0
hotline.call("Miss Bon-Bon")
}
return result
}
// ...
}
In order to use inheritance, additional changes have to be made which can result in more difficult maintenance. First the ChocolateFactory
must be changed to mark it open
,
then the child class must call the super constructor with all the required arguments. The super constructor itself may also change in the future (perhaps add a PeanutProvider
?)
making it then mandatory to modify the constructor call at multiple locations in the code.
How to do add this feature using composition? Let’s try to introduce an interface for the class:
interface ChocolateFactory {
fun produce(): Chocolate
// ...
}
Let the original implementation be called OriginalChocolateFactory
and move all the tracking related logic out of it to its own class:
// Original implementation unchanged
class OriginalChocolateFactory(
private val cocoaProvider: CocoaProvider,
private val milkProvider: MilkProvider,
) : ChocolateFactory {
// ...
}
// This new factory doesn't need to repeat the constructor and
// it can have the extra dependency for its shady operation
class ShadyChocolateFactory(
private val decoratedFactory: OriginalChocolateFactory,
private val hotline: Hotline,
) : ChocolateFactory by decoratedFactory {
private var numOfProducts = 0
override fun produce(): Chocolate {
val result = decoratedFactory.produce()
numOfProducts += 1
if (numOfProducts == CARTEL_THRESHOLD) {
numOfProducts = 0
hotline.call("Miss Bon-Bon")
}
return result
}
}
The additional benefit of using decoration is that neither the original class, nor its unit tests have to be modified while adding new functionality.