Object Oriented Design and Development

Topic 6: Further Kotlin language features: Lambda functions, interfaces

This week we will look at some further Kotlin language features, in particular lambda functions and interfaces.

More on Functions in Kotlin

In Kotlin, functions can be stored in variables and passed as arguments to other functions. In this respect, Kotlin is similar to JavaScript. This property of functions makes them first class (see the Kotlin documentation).

Anonymous Functions

Passing functions as arguments to other functions

Lambda functions

Making the lambda more concise - the implicit "it"

Real-world use of lambdas

Question

Other collection functions which use lambdas

Other collection functions - example

Interfaces

The Problem With Inheritance and the Need for Interfaces

Imagine we have a situation where a class could potentially inherit from more than one parent class. For example we could have a Bird could inherit from both Animal and FlyingThing. It could inherit the following methods and attributes from Animal:

... and the following from FlyingThing: How do we handle this?

problem with inheritance

Multiple Inheritance?

Introduction to Interfaces

Interfaces are used to define common behaviour across classes which might be from different inheritance hierarchies. For example, birds might be animals and planes might be vehicles, but they share the common behaviour of flying.

Generally you should follow the principle: if something "fundamentally is" something else, use inheritance. If it just shares common behaviour, use an interface. So for example a Bird is fundamentally an animal, so it makes sense to inherit Bird from Animal. But a Bird isn't fundamentally a "FlyingThing", it just has the behaviour of flying. So it makes sense to make FlyingThing an interface.

Interface Example

interface FlyingThing { 
    fun fly(): String 
}

How do we implement an interface

Interfaces with polymorphism

Real-world use of interfaces

The above was just a trivial example showing the concept of interfaces. However, how are they used in the real world? The advantage of using interfaces is that you can specify a list of methods which must always be present, without revealing what actual class will be used. As long as the class you use implements the interface and the list of methods stated in the interface, any class can be used where a method parameter is of an interface type. Why is this potentially more useful than inheriting from an abstract superclass which specifies a list of abstract methods?

It's probably best to illustrate real-world interface usage with an actual example. A common use of interfaces is to implement user event handling in a GUI application. Events occur when the user interacts with the UI, for example by clicking a button. We respond to events with event handlers - functions and methods which run when the event occurs. Typical event-handling code might look something like the example below (this is not the real code you would use in Kotlin GUI programming, it's just an example to illustrate the point). Here we are adding an event handler object (clickHandler) to a button:

button.addClickHandler(clickHandler)
The addClickHandler() method of the Button class might have the signature below, i.e. it takes one parameter of type ClickHandler:
fun addClickHandler(clickHandler: ClickHandler)
Here, ClickHandler could be an interface which might specify one method, onClick(), for example:
interface ClickHandler {
    fun onClick()
}

The advantage of this design over making ClickHandler an abstract superclass is that any class can implement the ClickHandler interface and provide an onClick() method to handle the user clicking on the button - including classes which already inherit from something else. If we created a ClickHandler class instead, the onClick() method would have to be placed inside a class inheriting from ClickHandler which makes the code less flexible as some of our classes might already be subclasses of another superclass. By defining an interface instead, it means that any object we like can act as the ClickHandler, provided it implements the ClickHandler interface and has an onClick() method.

This also allows us to make changes to our implementation more easily. If we want to change the class which handles our click events, we can easily do it by making the new class implement the interface and adding an onClick() method to it.

Later on in the module we will see how interfaces are often used in design patterns.

Anonymous classes

You can create objects which implement an interface on-the-fly without having to create a new named class by using an anonymous class. An anonymous class is an unnamed, single-instance class which typically inherits from an abstract class or (as here) implements an interface, and provides implementations of the required methods on-the-fly, and is specified using the syntax object: InterfaceName (literally, an object which implements the interface InterfaceName).

class MyGuiWindow : Window() {

    fun setupGui() {
        val b = Button("Click Me!")
        val eventHandler = object: ClickHandler {
            override fun onClick() {
                displayAlert("Button Clicked!")
            }
        }
        b.addClickHandler(eventHandler)
        b.addTo(this) // "this" means the current object, i.e. the window
    }
}
Looking at the code which creates the anonymous class in more detail:
val eventHandler = object: ClickHandler { ... }
This means that the variable eventHandler is an object of the anonymous class. The type of eventHandler is object: ClickHandler, i.e an object which implements the ClickHandler interface. Note how it includes an implementation of onClick(), as required by the interface.

Single Abstract Method (SAM) conversions

The above example works, but you could argue that creating the anonymous class with an implementation of onClick() is quite wordy and long-winded.

What Kotlin allows us to do instead, in cases where an interface only specifies a single method, is to specify the anonymous class using a lambda representing the interface's method. This technique is known as SAM (Single Abstract Method) Conversions.

So in this example we could rewrite our setupGui() as follows:

fun setupGui() {
    val b = Button("Click Me!")
    b.addClickHandler(ClickHandler {
        displayAlert("Button Clicked!")
    })
    b.addTo(this)
}
Note how the anonymous class, with its onClick() method, is no longer present. Instead the argument to addClickHandler() is now a ClickHandler object with a lambda as an argument. This lambda is the Single Abstract Method implementation, i.e. the implementation for the onClick() for our button.

For this to work you need to change your interface to be a functional interface. To do this, you just precede the keyword interface with the keyword fun:

fun interface ClickHandler {
    fun onClick()
}

Exercises

Exercise 1

At:

https://github.com/nwcourses/com534-topic6-music
is the starting point for a simple vinyl music management application. You will note that there are three classes present:

  1. First, add a method to the Album class to return only songs by a particular artist on the album. (As you probably know, multi-artist compilation albums exist, e.g. "Best of Rock", "Best of Dance", "Best of the 80s", etc). It should take an artist as a parameter, and use filter() to filter the list of songs and return the filtered list, i.e. songs by that specific artist.
  2. The main task in the exercise is to create an interface called Music which represents a piece of music (either single or album). It should contain two methods:
    • getPlayingTime() which returns the total playing time of the music in seconds as a Double.
    • getAllSongs() which returns a list of all the songs on that piece of music as a List<Song>.
    Add code to Single and Album to make them implement the Music interface. Implement the methods as follows:
    • In Single, getPlayingTime() should return the playing time of the A-side plus the playing time of the B-side. getAllSongs() should return a list containing the A-side and B-side.
    • In Album, getPlayingTime() should return the playing time of all the songs on the album added together. getAllSongs() should simply return the list of all songs on the album.
    Test out your system in main() by adding the Singles and Albums to a list, and writing code (using forEach()) to calling the two interface methods on each item of Music in the list.
  3. Now implement a RecordPlayer class. This represents a record player. This should contain just one method: a play() method which takes a Music object as a parameter, calls its getPlayingTime() and getAllSongs() methods, and prints all the details. (In the real world this would actually play the music, but obviously we can't do that here!)
    • How does this last question illustrate polymorphism?
    Test out your RecordPlayer in the main() by calling its play() method with each single and album.

Exercise 2 (more advanced)

This is a more advanced exercise on interfaces which illustrates their real-world use in user interface handling.

At:

https://github.com/nwcourses/com534-topic6-university
is another version of the University project. You are going to simulate event handling by using an event handler interface called MenuHandler which handles the "event" of the user selecting a menu option.