Extension-oriented design
In a basic object-oriented programming you can directly call only methods of a class that were defined by the authors of this class. This is fine for user-defined classes. Moreover, 20–30 years ago, before the advent of massive code-reuse in the form of very large standard libraries and open-source, most of your code would have been working with classes from your own code anyway — with code maintained by your team or company. However, in a modern world we often use classes defined elsewhere.
Business-logic is typically full of strings and collections from the standard library, as well as other classes from 3rd party libraries we use. We are limited by the operations these classes provide. For example, when we need to replace spaces with dashes in a string, we write in our code:
string.replace(' ', '-')
But when we need to pad the string on the left to the specified length, we might not have this operation available as a method and are forced by an old language (like Objective-C, C++, Java, or JS) to write:
leftPad(string, ' ', length)
This leftPad
could be coming from a separate library¹, from a 3rd party collection of utility functions (like Apache Commons), or you can write it in your own project. Anyway, its call looks different than a built-in method on a string class.
Why is this a problem? I’ll quote one of the authors of Java — Guy Steele, from his 1998 “Growing a Language” paper²:
In most languages, a user can define at least some new words to stand for other pieces of code that can then be called, in such a way that the new words look like primitives. In this way the user can build a larger language to meet his needs.
He was criticising APL’s lack of such facilities, but the same critique applies to the old object-oriented languages in a modern setting. You are stuck with a vocabulary of operations on a class that designers of the original library had in mind. It cannot be extended by you. Moreover, it cannot be satisfyingly extended by the maintainer of a widely-used library either, because, quoting from the same paper again:
Some parts of the programming vocabulary are fit for all programmers to use, but other parts are just for their own niches. It would not be fair to weigh down all programmers with the need to have or to learn all the words for all niche uses.
Modern languages (like C#, Scala, Rust, Kotlin, and Swift) solve this issue by supporting extension methods. You can add domain-specific extensions to the classes you do not control, so that your own function could be called in a way that resembles a call of a built-in method and your code still reads nicely in a fluent left-to-right order as prose:
string.padLeft(' ', length)
This padLeft
extension could be, as well, defined anywhere. Nice story of programming languages evolution. But there is more to it.
Once a programming language supports extension functions, it changes the very approach to the classic object-oriented API design. This is a non-trivial revelation for a programmer switching from a older language like Java to a modern language like Kotlin, since extension functions are usually presented only as convenient syntactic sugar³. However, let us take a look at the following interface with a bunch of properties (or getter methods):
interface Obscure {
val foo: Int
val bar: Int
val sum: Int
val max: Int
val min: Int
}
It is not unlike an interface or a class that you might find in a typical business application — lots of properties and methods.
Can you quickly grasp what kind of entity this interface represents? What properties constitute its state space? It is not easy to figure out without additional documentation. But let us factor this interface into a core entity and convenience extension functions:
interface NotObscure {
val foo: Int
val bar: Int
}val NotObscure.sum: Int
val NotObscure.max: Int
val NotObscure.min: Int
Now, it becomes clear that this interface’s core concept consists of two integer properties foo
and bar
, while the remaining sum
, max
, and min
properties are simply provided for convenience and are computed on the basis of those core ones. There is no need to explicitly document this distinction anymore — it is obvious from the very structure of the code.
This extension-oriented design is extensively used in Kotlin standard library and in other Kotlin libraries. It is a powerful design technique. Use it for good.
There is a side-effect of this approach to design. You might notice that our Kotlin code usually uses wildcard imports like import com.example.*
. It is handy in Kotlin, because importing just a class in Kotlin is rarely enough. All the useful, convenient, utility functions are typically defined in the same package but outside of the class as extension functions.
- ^ How one developer just broke Node, Babel and thousands of projects in 11 lines of JavaScript, Chris Williams, 2016
- ^ Growing a Language, Guy Steele, 1998
- ^ Extensions in Kotlin Programming Language