Are you sure “reducing coupling” is what you‘re after?
Kotlin and Swift may have introduced a lot of functional magic to the game, but being natural successors to Java and Obj-C, they have strong OOP foundations. Inheritance is still a fundamental concept and interfaces / protocols play an important role in a mobile developer’s toolset.
But, as with everything, there are right reasons to use them and there are wrong ones. When it comes to code design, we tend to follow good practices even if we are not 100% sure why they are good. And while it’s not a bad thing in general, it may lead to subtle abuses. For example, when we get the hang of Protocol Oriented Programming, we might feel the urge to apply it to every single problem— it’s a common bias, everything looks like a nail when you’re holding a hammer.
Photo by Theo Crazzolara https://unsplash.com/photos/k8mRwVA4MpA
So let’s explore different reasons for using interfaces and protocols.
The good
Polymorphism
Polymorphism is the very reason why interfaces came to be. The most common example is model classes, e.g. let’s say you have a chat app and handle different types of messages.
interface Message { | |
val timestamp: DateTime | |
} | |
class TextMessage( | |
val text: String, | |
override val timestamp: DateTime | |
) : Message | |
class ImgMessage( | |
val imgUri: URI, | |
override val timestamp: DateTime | |
) : Message |
This way you can handle a list of Message
objects and e.g. order them by their timestamp
but still be able to differentiate between text and image message types. Without interfaces, you would need to resort to ugly workarounds like a multi-purpose class with lots of nullable fields.
Multiple inheritance
Neither Kotlin nor Swift allows for multi-inheritance of classes (as a solution to the diamond-problem), but its benefits can be achieved with interfaces.
On Android, if you find yourself adding lots of unrelated responsibilities to some Base
classes, like BaseActivity
or BaseViewModel
, you might want to consider upgrading from simple inheritance to composition via an interface with default implementation.
interface ToastShowable { | |
fun showToast(context: Context, text: String) = ... | |
} | |
class MainActivity : ComponentActivity(), ToastShowable { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
//... | |
showToast(this, "Le toast") | |
} | |
} |
You can do something similar on iOS by leveraging protocol extensions.
protocol Shakeable { | |
func shake() | |
} | |
extension Shakeable where Self: UIView { | |
func shake() { | |
UIView.animate(... | |
} | |
} |
In those examples, you could use an extensions function in Kotlin (fun Activity.showToast(...)
) and protocol extensions on the UIView
directly (extension UIView ...
), but the introduction of interface / protocol gives you more control — the function is scoped only to classes that inherit from it, thus not leaking the extension to the whole codebase.
Boundaries between layers
In his Clean Architecture, Uncle Bob proposes a structure comprised of onion-like layers. The very foundation of this paradigm is the dependency rule — outer layers can have knowledge of the inner layers, but inner layers should know nothing of the outer ones.
Job Offers
Clean architecture is very widespread in mobile development, but one thing that is often misunderstood is that the data layer is outer to the domain layer. Business logic doesn’t need to know how the data is delivered, it should only define its inputs, usually via the means of a Repository
interface, which can be implemented by the data layer.
In clean architecture, the domain layer defines Repository interface which is implemented by data layer
There can be great benefits to decoupling the business logic and making it completely independent from view and data. Anyone who created a white-label app with different targets using different data sources will tell you that. However, we should strongly consider the cost of over-engineering — in many mobile applications there really isn’t much business logic at all and we might end up with a bunch of sad one-liner UseCase classes relaying data from Repository to ViewModel.
This point will be expanded in The Ugly section a bit more.
The bad
Test Doubles
Historically, interfaces were inevitable if we wanted to have test doubles like mocks and fakes in unit tests. One could argue, though, that it was more of an abuse of this language structure than legitimate use. In Java it was simply a limitation of mocking frameworks.
In Swift, a test using a protocol-based mock could look like this:
protocol Grinder { | |
func grind(_ coffee: Coffee) | |
} | |
class GrinderMock: Grinder { | |
private(set) var timesUsed: Int = 0 | |
func grind(_ coffee: Coffee) { | |
timesUsed += 1 | |
} | |
} | |
class CoffeeMakerTest: XCTestCase { | |
func test_grinder_used_once_when_coffee_made() { | |
let grinder = GrinderMock() | |
let sut = CoffeeMaker(grinder: grinder) | |
sut.makeCoffee() | |
XCTAssertEqual(grinder.timesUsed, 1) | |
} | |
} |
However, this is not necessary. We can use a simple class
for the Grinder
and the GrinderMock
can inherit from it. A possible downside is that it would not compile if Grinder
was a struct
, which is a final type and cannot be inherited from.
Similarly in Kotlin, there is no need to use interfaces in that case. Even though all classes are final by default, mocking frameworks like mockk or mockito can easily handle a final type.
Callbacks
A common pattern in the early Android — Java world, was to introduce interfaces for callbacks to be anonymously implemented like this:
public interface OnClickListener { | |
void onClick(View v); | |
} | |
button.setOnClickListener(new OnClickListener() { | |
public void onClick(View v) { | |
//handle click | |
} | |
}); |
This is no longer idiomatic in Kotlin. Functions are types in Kotlin, so you can easily pass them around as variables
class ListAdapter(val itemClickListener : () -> Unit) { ...
Or use a dedicated functional (SAM) interface type, like this:
fun interface OnClickListener { fun invoke() }
The ugly
“Because I can easily replace it with another implementation”
Okay, I admit, this is probably the main reason I am writing this article at all. It’s extremely common to receive this answer when asking about the reason for writing an interface with a single implementation. Sometimes it’s rephrased as “reducing coupling”, which sure does sound clever.
Don’t get me wrong, reducing coupling might be a very thoughtful thing to do, e.g. the aforementioned Boundaries between layers example is legitimate. But even Uncle Bob’s advice should not be taken as gospel, every potential solution should be considered in the context of the problem we are trying to solve.
We have to remember, that everyclass
already has its interface — it’s the public methods it exposes and their results. The key to designing a good interface is thinking in terms of its inputs and outputs, the responsibility of that class. Side note: unit testing is a great tool to encourage this kind of thinking.
At the end of the day, we just want to write maintainable code. As summed up by the quote from The Pragmatic Programmer:
When you come across a problem, assess how localized the fix is. Do you change just one module, or are the changes scattered throughout the entire system? When you make a change, does it fix everything, or do other problems mysteriously arise?
Disconnecting the interface from implementation formally, by just having two separate types, is not a silver bullet to achieve this. Having a separate interface
or protocol
always comes at a cost — increasing cognitive complexity, making code navigation more cumbersome etc. We should always consider two things:
- if it really reduces coupling — our interfaces might still be badly designed, even when separated from implementation;
- if reducing coupling is worth the cost.
If you’ve read this far and want a simple summary of this article, take this red flag 🚩: if you design an interface with just one class inheriting from it, you might want to stop and think about it.
This article has been originally published on Future Mind blog. We are hiring!