Digital Advisory Customer Experience Technology
Izabela Franke
Digital Advisory CX Strategy Retail
Jakub Nawrocki
Digital Transformation Retail
Paweł Wasilewski
Values People
Tomek Jurek
Digital Advisory M-commerce
Izabela Franke
Digital Advisory UX research
Jakub Nawrocki

Featured Insights

Explore all insights
Close
Digital Advisory Customer Experience Technology
Izabela Franke
Digital Advisory CX Strategy Retail
Jakub Nawrocki
Digital Transformation Retail
Paweł Wasilewski
Values People
Tomek Jurek
Digital Advisory M-commerce
Izabela Franke
Digital Advisory UX research
Jakub Nawrocki
Explore all insights

Featured Insights

EngineeringTechnology Mobile Development

The Good, the Bad and the Ugly of Interfaces in Mobile Dev

The Good, the Bad and the Ugly of Interfaces in Mobile Dev

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.

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 .

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

Simplified clean architecture schema
Simplified clean architecture schema

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.

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 " 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 every class 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.

Related insights

Arrange consultation with our Digital Advisory & Delivery Team

describe your challenge
Get in touch with our experts to learn more about the benefits of having us by your side.
We engineer
digital business

Subscribe to our insights

A better experience for your customers with Future Mind.

This field is required. Please fill it in, so we can stay in touch
This field is required.
© 2024 Future mind
all rights reserved