The provided content discusses delegating interface implementation in Kotlin, emphasizing the language's features that simplify the delegation pattern and promote composition over inheritance.
Abstract
The article introduces the concept of delegating interface implementation in Kotlin, highlighting the language's ability to streamline the delegation pattern and reduce boilerplate code. It contrasts composition with inheritance, advocating for the former due to its flexibility and maintainability. The author illustrates the verbosity of traditional delegation in Java, where methods must be manually forwarded to the underlying object. Kotlin, however, offers a more concise syntax for delegating interface implementations to other objects, thus preserving the benefits of composition without the associated verbosity. The article also touches on the use of companion objects in this context and suggests exercises for readers to practice implementing delegation patterns in Kotlin.
Opinions
The author suggests that composition should often be preferred over inheritance, indicating a common best practice in object-oriented design.
The author expresses that exposing underlying objects directly in composition defeats its purpose, implying that encapsulation is a key aspect of effective design.
Kotlin's delegation features are praised for reducing the tediousness of implementing the delegation pattern compared to languages like Java.
The article implies that Kotlin's language design facilitates better software engineering practices by making it easier to adhere to the principle of composition over inheritance.
The author emphasizes the importance of separating concerns in software design, as evidenced by the suggested improvements to the Record companion object implementation.
Delegating Interface Implementation
Delegation without the mystification — an introduction to composition, the delegation pattern, and how Kotlin makes both of them easy, with non-trivial examples
— — — — — — — — — — — — — — —
THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.
This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.
One of the problems with composition (which you should often prefer over inheritance) is the verbosity related to accessing the underlying interfaces.
For example, consider the following FileSystemManager and DatabaseManager interfaces:
Now imagine that we want to create a class that is able to implement exporting and importing, which requires interaction with both the file system and the database:
So far, so good.
However, since DbImportExportManagerImpl contains references to both a FileSystemManager and a DatabaseManager, it only makes sense that it should be able to take on those interfaces as well, right?
Unfortunately, doing that is pretty tedious. We basically have two options:
Expose the underlying objects and access them directly. That defeats the whole purpose of composition, because the container object doesn’t implement the corresponding interfaces, and so all we’ve really done is just added a layer that achieves nothing. Adding insult to injury, we need to specify the property every time we want to call a given method.
Have the container object implement the interfaces of the objects it contains, and reimplement every single method by delegating it to the underlying object (see bellow). This is called the Delegation pattern.
Take a look at DbFsBridge (a renamed version of DbImportExportManagerImpl) in the following example, and notice how many lines are wasted on boilerplate delegation to the underlying objects:
To save us from this, Kotlin allows delegating interface implementations to object instances:
The object to which we delegate does not need to be a property or constructor parameter — the value can come from any place which is accessible from the scope of the class definition. However, this means that member functions cannot be used, because those are only accessible from an instance of the class, and delegation is processed when the class definition is read.
You can, however, use anything defined in the companion object.
You can override delegated members in the same way you would if you were implementing them yourself. Keep in mind that if you do override some members, the instance you delegate to for the rest can’t access your overrides and will keep on using its own implementations. For an example, see the docs.
Exercises
Take a critical look at what we finished with in the exercise on companion objects.
Here it is for reference:
There are a couple of things wrong with this implementation:
The companion has to extend an abstract class, signifying an is-a relationship. However, we really want a contains-a or uses-a relationship for our purposes. The companion object is not supposed to be a Cache, it’s supposed to be a RecordFetcher.
The implementation above is tightly coupled to one specific cache implementation. What we would like is to have a Cache<K, O> interface, and pass whichever implementation we see fit.
Even then, while our companion object is no longer bound to a specific implementation of Cache, it is still tightly bound to Cache itself — a Cache needs to get passed in somehow. We don’t want the companion object of Records to know about caches and how to use them.
What we want is to separate the concerns in the following way:
RecordFetchers deal with record fetching. They don’t know about caches.
Caches deal with caching. They don’t know about record fetching.
CachingRecordFetcher is a RecordFetcher that takes a factory and a Cache instance. It implements the RecordFetcher contract by first checking the cache, and calls the factory only if nothing is found.
The companion object of a Record is a RecordFetcher which delegates its implementation to an instance of RecordFetcher. This allows us to reuse the RecordFetcher implementations.
Delegate the companion object implementations to an appropriate instance of CachingRecordFetcher.