Extension Functions
There are lots of functions that we can call on pretty much any object. Some, like toString()
, are the sorts of things that you might expect any object to support. Others, like many of the scope functions (e.g., let()
, apply()
), might seem a bit odd that you can call on anything.
Still others might not even be obvious that they are functions. Back in the chapter on collections, we saw creating a Map
using mapOf()
:
println(mapOf("foo" to "bar", "baz" to "goo"))
to()
is actually a function that we are calling on "foo"
and "baz"
, where to()
returns a Pair
made up of what we are calling it on and the parameter to the function (the value after to
).
The root class of the Kotlin class hierarchy is Any
. All classes extend from Any
. If you look at the Kotlin reference to Any
, you might expect to see lots of these functions listed. Instead, there are only three functions on Any
:
equals()
hashCode()
toString()
Those map to their Java equivalents in Kotlin/JVM and have other implementations in Kotlin/JS and Kotlin/Native.
So… where are all these other things, like let()
and apply()
and to()
coming from, if they are not functions on Any
?
Those are defined as extension functions, where you can provide an implementation of a function for a class that you did not create. In this chapter, we will explore why we have these things, how we set them up, and whether they are really a good idea or not.
The Case of the Utility Function
A question often arises in object-oriented programming: “where should this function go?”
Often, the answer is obvious, as the work associated with the function is tied to some class that you are working on. Putting that function on that class is a logical move to make.
But sometimes we have functions that do not necessarily fit that pattern.
For example, suppose that you want to be able to validate a user-entered String
to see if it looks like a valid email address. As it turns out, lots of really strange things can be valid email addresses, as the email address standard is very loose and forgiving. However, if you hunt around on the Web, you will find various regular expressions that can be used to validate addresses, where those regular expressions can work for most commonly-seen address structures.
Kotlin supports regular expressions via a Regex
class. You can create one using a regular-expression pattern and, among other things, call matches()
on it to see if a particular String
matches the pattern:
val EMAIL_PATTERN = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+[.]+[a-zA-Z0-9-.]+(\s)*"
fun main() {
val emailRegex = Regex(EMAIL_PATTERN)
println(emailRegex.matches("martians-so-do-not-exist@commonsware.com"))
println(emailRegex.matches("this is not an email address"))
}
(note: this is not a particularly good regular expression for evaluating email addresses, but it is fairly short and is adequate for a book)
If we only needed this logic in one spot in the app, we could just have a function in that one spot that handles this code. But we might need to validate email addresses in a few places:
- On the user registration screen, as we are using email addresses as user IDs
- On the login screen, because our user IDs are email addresses
- On the “add a friend” screen
- On the “share this content” screen
- And so on
So, where does this “is this a valid email address?” code go?
Pointless Superclasses
A classic object-oriented programming solution is to note that all four cited uses are in screens, so there should be some base class that has this function that everyone else can inherit from. For example, in Android, that could be:
- A
BaseActivity
that extends fromActivity
and has this function - A
BaseFragment
that extends fromFragment
and has this function - A
BasePresenter
for some Model-View-Presenter (MVP) framework that has this function - Or whatever
However, in general, forcing a particular inheritance model, just to provide access to some simple function, is not a good OO design approach.
Utility Classes
The other typical OO approach is to have some utility class with this function. That could be somewhat specialized, such as a DataValidator
that knows how to perform various types of validation. Or, it could be a Util
class that serves as a home for all sorts of wayward bits of code that have no other likely home.
This works. However, the more generic the utility class, the more likely that it becomes a “junk drawer”, gradually collecting more and more code, until you have this massive class that nobody likes to maintain.
Global Functions
Yet another approach, particularly in Kotlin, is to use a top-level function. You could just have an isValidEmail()
function floating around somewhere that performs this validation.
This is not much of an improvement over the utility class, though. In fact, it strongly lends itself towards “junk drawer” models, where you have some Kotlin source file with a random collection of top-level functions that needed to live somewhere.
Worse, all top-level functions share a namespace. Suppose that you have an isValidEmail()
function, and now you add some library where it has its own isValidEmail()
top-level function. Now you will have a build error, as the compiler will find duplicate functions with the same name.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.