avatarRoman Elizarov

Summary

The provided text discusses the evolution of object-oriented design towards extension-oriented design, emphasizing the benefits of extension methods in modern programming languages for enhancing code readability and maintainability.

Abstract

In traditional object-oriented programming, developers are limited to the methods provided by the authors of a class. However, with the widespread use of standard libraries and third-party classes, there's a need for extending the functionality of these pre-existing classes. The text highlights the limitations of older languages like Java and Objective-C, where utility functions like leftPad are not part of the standard library and must be implemented separately, leading to less cohesive code. Modern languages such as Kotlin, Scala, and Swift address this by introducing extension methods, allowing developers to add custom functionality to classes they don't control, resulting in more fluent and readable code. The text also illustrates how extension methods influence API design, promoting a clearer distinction between core properties and computed properties, thus improving the understandability of the code. The author advocates for the use of extension-oriented design as a powerful technique for creating cleaner and more expressive APIs.

Opinions

  • The author suggests that traditional object-oriented languages do not allow for sufficient extension of a class's vocabulary, which is problematic in a modern context where code reuse is common.
  • Guy Steele's critique of APL's lack of extensibility is extended to old object-oriented languages, emphasizing the need for language growth through user-defined vocabulary.
  • The author views extension functions as more than just syntactic sugar, considering them a fundamental shift in API design that leads to more intuitive and self-documenting code.
  • The text implies that the use of wildcard imports in Kotlin is justified due to the language's reliance on extension functions, which are often defined outside of classes but within the same package.
  • The author positively endorses the extension-oriented design approach used in Kotlin and other modern languages, encouraging its use for creating better software.

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.

  1. ^ How one developer just broke Node, Babel and thousands of projects in 11 lines of JavaScript, Chris Williams, 2016
  2. ^ Growing a Language, Guy Steele, 1998
  3. ^ Extensions in Kotlin Programming Language
Kotlin
Programming Languages
Extension Method
Api Design
Recommended from ReadMedium