Week 1 | Lesson 3

OOP Principles & Data Models

Programming language and communication of intent
Encapsulation, Polymorphism, Inheritance, Abstraction

Programming language

and communication of intent

Communication of intent

Programming language provides means of expressing programmer's intent to a computer system.
But programming it is not just a way of giving instructions to a computer. It can also be a means of communication between humans, particularly in the context of team development, code reviews, and future maintenance of the software. Here are few points to keep in mind ...

Code Clarity

Code is more often read than it is written. Therefore, it is important to keep it clean and easily understood.


Code Consistency

Keeping your code consistent in terms of syntax, programming style and design patterns makes it easier to understand.


Documentation and Comments

Some code can become hard to understand despite our best effort. In these cases, comments and code documentation should be used to clarify the programmer's intent or communicate unintuitive information.

Object-oriented Programming

Principles in Kotlin

Object-oriented Programming Principles in Kotlin

Remember, there are four main OOP principles:

  1. Encapsulation
  2. Inheritance
  3. Polymorphism
  4. Abstraction


We will go more into detail of each of how these are handle in Kotlin on the following slides.

But first ...

Objects and Classes

Objects and Classes

We have already seen and worked with Kotlin objects and classes.
Let's explain what they are and how they work in more detail.

What is an object?

Object is a data structure in memory


What is a class?

Class is a template for how to create an object

Think of a class as a blueprint for creating objects.

Class Definition

Class is defined using the class keyword, followed by the class name, class header and the class body.
  • Class name - should begin with an initial letter (capitalized by convention).
  • Class header - class type parameters, the primary constructor, and other things, such as its superclass or interfaces it implements.
  • Class properties - are defined in the class header and are used to hold the state of the class and its objects.
  • Class body - containing properties, methods, constructors, initializer blocks, inner classes, and interfaces enclosed in curly braces.
    • Properties - additional properties that hold the state of the class and its objects.
    • Methods - functions that are part of the class and contain the executable code.
    • Constructors - special methods used to initialize the state of an object.
    • Initializer blocks - unnamed code blocks used for initializing shared variables and executing code that needs to run every time an instance of the class is created.
    • Inner classes and interfaces - class or interface definitions nested within the outer class body.
    • Companion objects - special objects that are tied to a class, rather than to an instance of a class.

Class Definition

Class is defined using the class keyword, followed by the class name, class header (optional), and the class body (optional) enclosed in curly braces.
Simple class definition without body.
							
								class UniversityCourse
							
						
More realistic class definition with parameters and body.
							
								class UniversityCourse(
									val subject: String,
									val startDate: LocalDate,
									val endDate: LocalDate,
									val students: MutableList<String> = mutableListOf() // initial value of empty list
								) {

									// class properties which is not part of the constructor
									private var isOpen: Boolean = false // initial value of false

									fun addStudent(studentName: String) {
										if (isOpen) {
											students.add(studentName)
										} else {
											error("Cannot add students to closed course.")
										}
									}

									fun open() {
										isOpen = true
									}

									fun close() {
										isOpen = false
									}
								}
							
						

Class Instantiation

You do not directly work with classes in Kotlin, you work with objects that are created from classes, called class instances.
Object == Instance of a class
  • The process that creates an object from class is called instantiation
  • Instantiation is done by calling a special constructor method
  • Class may have one or more constructors
  • If constructor is not explicitly defined, then the class will default constructor with no arguments
  • Instantiation can be used to set initial values for the object

Class Header

Class header defines its type parameters, the primary constructor, and other things, such it's superclass or interfaces it implements.
  • The class header specifies class properties.
  • Just like functions argument, class properties can have default values.
  • The declared properties also define the class primary constructor.
  • In this example, the class header specifies that the class has four properties, one with a default value, which means that it can be omitted when creating an instance of the class.
    									
    										class UniversityCourse(
    											val subject: String,
    											val startDate: LocalDate,
    											val endDate: LocalDate,
    											val students: MutableList<String> = mutableListOf() // initial value of empty list
    										) {
    											// class body
    										}
    									
    								

Class Constructors

Class constructor is a special method with the only purpose of class instantiation.
Class with zero arguments constructor
								
									class KotlinCourse {
										val subject: String = "Kotlin"
									}


									val kotlinCourse = KotlinCourse()
								
							
Class with one arguments constructor
								
									class Course(val subject: String)


									val kotlinCourse = Course("Kotlin")
								
							
Class with multiple arguments constructor
								
									class Course(
										val subject: String,
										val startDate: LocalDate,
										val endDate: LocalDate
									)


									val kotlinCourse = Course(
										"Kotlin",
										LocalDate.parse("2024-02-03"),
										LocalDate.parse("2024-02-21")
									)
								
							
  • Every class has a constructor, even if it is not explicitly defined.
  • Class may have multiple constructors (primary and secondary constructors).
  • Constructor may have zero, one, or many arguments
  • Class constructors support named arguments just like functions.

Alternate class constructors

Kotlin allows you to define multiple constructors for a class, also called secondary constructors. They provide alternative ways to instantiate the class.
Example of class definition, class alternate constructor, and constructor calls.
						
							// primary constructor defined by class header
							class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate
							) {

								// alternate constructor
								constructor(
									subject: String,
									startDate: String,
									endDate: String,
								) : this(subject, LocalDate.parse(startDate), LocalDate.parse(endDate))
							}
						
					
Primary constructor call with default values
						
							val kotlinCourse1 = UniversityCourse("Kotlin", LocalDate.parse("2024-02-03"), LocalDate.parse("2024-02-21"))
						
					
Alternate constructor call
						
							val kotlinCourse3 = UniversityCourse("Kotlin", "2024-02-03", "2024-02-21")
						
					

Default values & Named arguments

Just like functions, class constructors can have default values and support named arguments.
Class constructor really is just a special kind of function, so just like functions, class constructors can have default values and support named arguments.
						
							class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate = startDate.plusDays(30),    // default date
								val students: MutableList<String> = mutableListOf() // default value of empty list
							) {
								// class body
							}
						
					
								
									UniversityCourse("Kotlin", LocalDate.parse("2024-02-03"))
								
							

Calling the constructor with default values.
								
									UniversityCourse(
										"Kotlin",
										LocalDate.parse("2024-02-03"),
										students = mutableListOf("Alice", "Bob")
									)
								
							

Calling the constructor with one named argument for students.
The endDate will be set to the default value.
								
									UniversityCourse(
										subject = "Kotlin",
										startDate = LocalDate.parse("2024-02-03"),
										endDate = LocalDate.parse("2024-02-21"),
										students = mutableListOf("Alice", "Bob")
									)
								
							

Naming all arguments when calling the constructor.

Methods

Method is a function associated with an object.
In Kotlin, you can also call methods member functions.

Same rules and possibilities apply to methods as to functions not associated with a class.

						
							class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate = startDate.plusDays(30),
								val students: MutableList<String> = mutableListOf()
							) {

								fun addStudent(studentName: String) {
									students.add(studentName)
								}

								fun removeStudent(studentName: String) {
									students.remove(studentName)
								}

								fun printStudents() {
									students.forEach { println(it) }
								}
							}
						
					

The only difference is that methods are called on an instance of object.

						
							val kotlinCourse = UniversityCourse("Kotlin", LocalDate.parse("2024-02-03"))
							kotlinCourse.addStudent("Alice")
							kotlinCourse.addStudent("Bob")
							kotlinCourse.printStudents()
						
					

Initializer blocks

Initializer blocks are code that is run when an instance of a class is created.

For example, this can be used for additional argument validation or to set up some initial state.

Class may have one or more initializer blocks, which are executed in the order they are defined.

						
							class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate = startDate.plusDays(30),
								val students: MutableList<String> = mutableListOf()
							) {

								init {
									require(startDate < endDate) { "End date must be after start date" }
								}

								init {
									println("Course $subject created")
								}

							}
						
					

Scope and "this" reference

In Kotlin, the this keyword is used to refer to the current instance of a class.
It is often used to distinguish between class properties and parameters or local variables with the same name.
						
							class Person(val name: String) {

								fun introduce(name: String) {
									println("${this.name} also goes by $name")
								}

							}
						
					
						
							fun main() {
								val person = Person("Monika")
								person.introduce("Moni") // prints "Monika also goes by Moni"
							}
						
					
Additionally, Kotlin provides other scope references using @label to refer to specific instances in nested scopes.
						
							class A {
								inner class B {
									fun Int.foo() {
										val a = this@A // refers to the instance of A
										val b = this@B // refers to the instance of B
										val c = this // refers to the receiver Int
										println("a: $a, b: $b, c: $c")
									}
								}
							}
						
					
						
							fun main() {
								val a = A()
								val b = a.B()
								b.run { 42.foo() }
							}
						
					

Class destruction

Java/Kotlin has no class destructor, because freeing up memory is entirely delegated to JVM through a process called garbage collection, which we will talk about in later in the course.
Some Java/Kotlin classes may have a special methods that should be called after we are done using the class in order for it to be eligible for garbage collection. I will also mention these later in the course.

Inner classes

Inner class (also called nested class), is a class defined within a body of another class.
						
							class OuterClass {

								private val outerVariable: String = "Outer Variable"

								// Inner class
								inner class InnerClass {
									fun accessOuterVariable(): String {
										return outerVariable
									}
								}
							}
							
					
						
							fun main() {
								val outer = OuterClass()
								val inner = outer.InnerClass()
								println(inner.accessOuterVariable()) // prints "Outer Variable"
							}
						
					

Use case for nested classes


Logical Grouping

Nested classes can help us keep our code organized by having related code together. For example, you may want to create an internal data class.


Encapsulation & Access Control

Inner classes can access members of the outer class, including those marked as private. This can benefit us in multiple scenarios, such us when creating helper classes without exposing private methods or properties of the outer class.


Increased Readability and Maintainability

Inner classes are used for code that is relevant to a small part of the outer class. Grouping them together improves code readability and maintainability.

Anonymous Classes

In Kotlin, anonymous classes are a way to create an instance of a class with certain modifications without having to actually declare a new subclass. They are often used to create instances of classes that have no name and are used only once.

Since it has no name, we have no way to instantiate such class.
Thus, an anonymous class must be declared and instantiated with a single expression.

An anonymous class must either implement an interface or extend an existing class (abstract or concrete).

Anonymous classes are useful for creating quick, one-off implementations of interfaces or abstract classes.

					
						fun main() {

							val runnable = object : Runnable {
								override fun run() {
									println("Running in an anonymous class!")
								}
							}

							val thread = Thread(runnable)

							thread.start()
						}
					
				

Singleton Objects

Static classes and constants in Kotlin

Singleton Objects

Singleton object is a class that can have only one instance in memory.

In Java and other languages, we used so called singleton pattern to create a class that can have only one instance.

Kotlin provides a convenient way to create singleton objects using the object keyword.

						
							object StringUtils {

								val DECIMAL_PLACES = 2

								fun formatNumber(number: Double): String {
									// code to format number
								}

							}
						
					
						
							fun main() {
								val formattedNumber = StringUtils.formatNumber(3.14159)
								println(formattedNumber) // prints "3.14"
							}
						
					

Some key features:

  • Singleton - Only one instance of the object is created.
  • Utility Methods - Commonly used for utility methods and constants.
  • Initialization - The object is initialized when it is first accessed.
  • No Constructors - Objects cannot have constructors.

Use case for static objects

Utility or Helper Classes

Sometimes we need some methods to be globally available in the application without needing to create class instance every time. Example of such is utility classs with static methods is String.valueOf() or Integer.toBinaryString().


Global Constants and Variables

Static keyword can be used to define class level variables hat are accessible throughout our application.


Singleton Pattern

Kotlin object is equivalent to Java's singleton pattern, but with the convenience of a simpler, error-proof syntax. Singleton class is a design pattern that restricts the instantiation of a class to a single instance.

Companion Objects

Companion object is an object that is tied to a class, rather than to an instance of a class.
  • There can be only one companion object per class, and it is defined using the companion keyword.
  • It may have a name, but it is optional. Otherwise, it is referred to as a default companion object.
  • It is often used to define static methods and constants.
  • Other than that, it is just like any other object.
  • Companion objects are by convention placed at the bottom of the class.
						
							class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate
							) {

								// regular class methods

								companion object UniversityCourseFactory { // companion object name is optional
									const val KOTLIN = "Kotlin"

									fun kotlinCourse(startDate: String, endDate: String): UniversityCourse {
										return UniversityCourse(KOTLIN, LocalDate.parse(startDate), LocalDate.parse(endDate))
									}
								}
							}
						
					
						
							fun main() {

								val kotlinCourse = UniversityCourse.kotlinCourse("2024-02-03", "2024-02-21")

								println(UniversityCourse.KOTLIN) // prints "Kotlin"

							}
						
					

Data Models

Data Models

A data model is a conceptual representation of how data is structured and related in your application.

A data model defines the shape and structure of data in terms of classes, properties, and relationships.

  • In object-oriented programming, data models are typically expressed using classes that reflect real-world entities (e.g., User, User, Product).
  • A good data model serves as a bridge between:
    • the business logic (how the system behaves),
    • the data storage (like a database),
    • and the API layer (how other systems communicate with it).

Data Classes

Kotlin provides a special kind of class called a data class to make data modeling more concise and expressive.

For a data class, Kotlin compiler automatically generates convenience methods for copying, comparing, and printing objects and more.

Data classes are marked with the data keyword.

						
							data class UniversityCourse(
								val subject: String,
								val startDate: LocalDate,
								val endDate: LocalDate = startDate.plusDays(30),
								val students: MutableList<String> = mutableListOf()
							)
						
					

There are a few rules for data classes:

  • The primary constructor must have at least one parameter.
  • Primary constructor must have val or var keyword.
  • Data classes cannot be abstract, open, sealed, or inner.
  • Data classes cannot inherit from other classes.

Enums

Enum (enumeration) is a special type of class, which contains a fixed set of constants.
  • Enum is created with the enum class keyword.
  • Enum constants are static and final implicitly.
  • By convention, the enum values should be in upper case.
  • enums can also have properties, methods, and can be initialized with a constructor.
						
							enum class Days {
								MONDAY,
								TUESDAY,
								WEDNESDAY,
								THURSDAY,
								FRIDAY,
								SATURDAY,
								SUNDAY
							}
						
					
						
							fun main() {
								val day = Days.MONDAY
								println(day) // prints "MONDAY"
							}
						
					

Enums

Since enum is a class, it may have fields, constructors and methods.
						
							enum class Days(val isWorkDay: Boolean) {
								MONDAY(true),
								TUESDAY(true),
								WEDNESDAY(true),
								THURSDAY(true),
								FRIDAY(true),
								SATURDAY(false),
								SUNDAY(false);

								fun isWeekend(): Boolean {
									return !isWorkDay
								}

								fun isWeekday(): Boolean {
									return isWorkDay
								}
							}
						
					
						
							fun main() {
								val day = Days.MONDAY
								println(day) // prints MONDAY
								println(day.isWorkDay()) // prints true
							}
						
					

Enums

Enums are particularly useful for evaluating a finite number of states.

Especially in combination with when expression.

Whenever you use when with enums, the compiler will check if all possible values are covered.

						
							fun getHoursInClass(day: Days): Int {
								return when (day) {
									Days.MONDAY,
									Days.TUESDAY,
									Days.THURSDAY -> 4
									Days.WEDNESDAY,
									Days.FRIDAY -> 3
									Days.SATURDAY,
									Days.SUNDAY -> 0
								}
							}
						
					

In case you forget to cover all possible values, the compiler will throw an error. You may also use else branch to cover all other cases.

						
							fun getHoursInClass(day: Days): Int {
								return when (day) {
									Days.MONDAY,
									Days.TUESDAY,
									Days.THURSDAY -> 4
									Days.WEDNESDAY,
									Days.FRIDAY -> 3
									else -> 0
								}
							}
						
					
OOP Principle #1

Encapsulation

What is Encapsulation

Encapsulation is a concept of controlling access to the internal state of an object, protecting it from unauthorized access and ensuring data integrity.
  • In Java/Kotlin, this is typically achieved using access modifiers (private, protected, internal) and getter/setter methods.
  • By using getter/setter methods, the class can enforce its own data validation rules to ensure it's internal state remains valid and consistent.
						
							data class Assignment(
								val dueDate: LocalDate,
								val assignee: String,
							) {
								private var finalGrade: Int? = null

								fun getFinalGrade(): Int? {
									return finalGrade
								}

								fun setFinalGrade(finalGrade: Int) {
									require(finalGrade in 0..100) { "Final grade must be between 0 and 100" }
									this.finalGrade = finalGrade
								}
							}
						
					
						
							fun main() {
								val assignment = Assignment(LocalDate.now(), "John Doe")
								assignment.setFinalGrade(90)
								println(assignment.getFinalGrade())
							}
						
					
OOP Principle #2

Inheritance

What is Inheritance

Inheritance> establishes an "is-a" relationship between two classes, where one class inherits properties and methods of the other class.
The class that inherits is called subclass and the class inherited from is called superclass.
  • The class to be inherited from must be marked as open.
  • To define inheritance, the : symbol is used followed by the superclass name, for example class Dog : Animal().
  • If a superclass has a non-default constructor, you must call super() method in the subclass constructor.
  • You can mark methods and attributes of a superclass as protected. This will make them only accessible within the same package or within subclass.
  • You can reference fields and methods in the superclass class using the super keyword.
  • To prevent inheritance, you can mark the class with final modifier.

Let's have a look at this in detail ...

Inheritance

						
							fun main() {
								val cat = Cat("meow")
								cat.makeSound()

								val dog = Dog()
								dog.makeSound()

								// this would not compile, because makeRawSound is protected
								// dog.makeRawSound();

								val bird = Bird("tweet")
								bird.makeSound()
							}
						
					
Animal is base class for all animals.
									
										open class Animal(private val sound: String) {

											fun makeSound() {
												println(sound)
											}

											protected fun makeRawSound() {
												println(sound)
											}
										}
									
								
BarkingAnimal extends Animal and adds a bark method.
									
										open class BarkingAnimal : Animal("woof") {

											fun bark() {
												makeRawSound()
											}
										}
									
								
Cat extends Animal and overrides the sound.
									
										class Cat(sound: String) : Animal(sound)
									
								
Dog extends BarkingAnimal and does not override the sound.
									
										class Dog : BarkingAnimal()
									
								
Bird extends Animal and adds an alternative constructor. Notice the use of super.
									
										class Bird : Animal {

											constructor(song: String) : super(song)

										}
									
								

Inheritance

You can use the final modifier to prevent method overriding or class inheritance.
						
							open class Cat {

								final fun meow() {
									System.out.println("meow");
								}

							}
						
					

We are trying to override the meow method, the compiler will throw an error.

						
							class MyCat: Cat() {


								// This will not compile
								override fun meow() { }

							}
						
					

Pros of inheritance

Promotes code reuse Inheritance allows subclasses to inherit methods and fields from superclasses which leads to a reduction in code duplication.

Promotes polymorphism Subclasses can redefine certain methods based on their requirement.

Hierarchy and organization Inheritance helps to design the software in a hierarchical manner where classes with general characteristics are at a higher level and classes with specific characteristics are at lower level.

Cons of inheritance

Tight coupling A subclass is tightly coupled with its superclass. If the superclass is modified, subclasses could be affected, as they inherit methods and fields from the superclass.

Inheritance chain Inheritance often leads to long chains which could make tracking down errors in the code difficult.

Issues with multiple inheritance Kotlin does not support multiple inheritance (a class can’t extend more than one class).
However, it supports multiple interface implementation, which is a partial workaround for this issue.

Memory overhead When a subclass object is created, a separate memory space is reserved for it in addition to the separate memory space reserved for the superclass object. This might result in memory wastage if the subclass makes limited use of the superclass's features.

Composition

Composition provides a "has-a" relationship. It allows you to use object instances as fields within the other classes.

Pros

  • Results in loose coupling and improves encapsulation, because the contained objects can be easily swapped without changing the code that uses them.
  • Can be used to overcome lack of multiple inheritance in Kotlin.
  • Usually allows for better testability as well.

Cons

  • It can result in bloated classes if overused, and requires more code setup than inheritance.
  • it can be more difficult to use when requests must be delegated to the appropriate class.

Composition

						
							fun main() {
								val cat = Cat("Garfield")
								cat.meow()
							}
						
					
Composed class
									
										class Cat(val name: String) {

											private val sound = Sound("meow")

											fun meow() {
												sound.makeSound()
											}
										}

									
								
Composition class
									
										class Sound(private val sound: String) {

											fun makeSound() {
												println(sound)
											}
										}
									
								

Composition and Inheritance

Both inheritance and composition have their strengths and weaknesses. Deciding when to use each can be instrumental for designing cleaner and more effective code.

The two techniques can be, and often are, combined.
						
							fun main() {
								val cat = Cat("Garfield", Sound("meow"))
								cat.makeSound()
							}
						
					
Superclass - adding Sound through composition
									
										open class Animal(private val sound: Sound) {
											fun makeSound() {
												sound.makeSound()
											}
										}
									
								
Composed class - has no dependencies
									
										class Sound(private val sound: String) {
											fun makeSound() {
												println(sound)
											}
										}
									
								
Subclass extending Animal
									
										class Cat(val name: String, sound: Sound) : Animal(sound)
									
								
OOP Principle #3

Polymorphism

What is polymorphism

In programming, polymorphism allows us to define one interface or method that can have multiple implementations. It means that the same method or property could exhibit different behavior in different instances of object implementing given interface.

There are two types of polymorphism

  • Compile-Time polymorphism
  • Run-Time polymorphism

Compile-time polymorphism

Also known as static polymorphism

Compile-time polymorphism is achieved through method overloading. The correct method to call is determined by the compiler at compile time based on the method signature.

						
						fun main() {

							val result1 = Calculator.add(10, 20)
							val result2 = Calculator.add(10, 20, 30)

							println(result1)    // prints 30
							println(result2)    // prints 60
						}
						
					
						
						object Calculator {

							// method with 2 parameters
							fun add(a: Int, b: Int): Int {
								return a + b
							}

							// overloaded method with 3 parameters
							fun add(a: Int, b: Int, c: Int): Int {
								return a + b + c
							}
						}
						
					

Method overloading = defining two or more methods in a class with the same name but different signature.

Method signature = combination of the method name, return type and the parameters.

Runtime polymorphism

Also known as dynamic method dispatch

Runtime polymorphism is a process in which a call to an overridden method is resolved at runtime rather than at compile-time. This mechanism allows the Java Virtual Machine (JVM) to decide which method to invoke from the class hierarchy at runtime, based on the type of object.

						
							fun main() {
								val animal0 = Animal()

								val animal1: Animal = Dog() // Animal reference but Dog object
								val animal2: Animal = Cat() // Animal reference but Cat object

								animal0.makeSound() // prints "(silence)"
								animal1.makeSound() // prints "woof"
								animal2.makeSound() // prints "meow"
							}
						
					
Superclass
									
										open class Animal {
											open fun makeSound() {
												println("(silence)")
											}
										}
									
								
Subclass 1
									
										class Dog : Animal() {
											override fun makeSound() {
												println("woof")
											}
										}
									
								
Subclass 2
									
										class Cat : Animal() {
											override fun makeSound() {
												println("meow")
											}
										}
									
								
OOP Principle #4

Abstraction

Abstract class

Abstract is defined using the abstract keyword
and are used to define common behavior that can be inherited by subclasses.

Abstract class cannot be instantiated directly. The main purpose of an abstract class is encapsulating common behavior that can be shared among multiple subclasses, while allowing each subclass to implement its own behavior either by overriding abstract methods, adding new methods or fields, or overriding non-abstract methods.

  • Abstract classes can have constructors, but they cannot be directly instantiated.
  • They can contain both abstract and non-abstract methods.
  • Abstract methods must be implemented by subclasses.
  • Non-abstract methods can be optionally overridden by subclasses.

Abstract class

							
								fun main() {
									val cat: Animal = Cat("meow") // Animal reference but Cat object
									cat.makeSound()
								}
							
						
Abstract class definition
								
									/**
									 * Abstract class definition.
									 */
									abstract class Animal(
										protected val sound: String // notice the protected modifier
									) {

										/*
										Abstract method definition, which a subclass must implement.
										*/
										abstract fun makeSound()
									}
								
							
Abstract class implementation
								
									/**
									 * Subclass of Animal.
									 * Compiler will force us to call superclass constructor!
									 */
									class Cat(sound: String): Animal(sound) {

										/*
										Compiler will force us to use override keyword!
										*/
										override fun makeSound() {
											println(this.sound) // referencing sound in 'this' instance
										}
									}
								
							

Interfaces

Interface is a reference type (like class) defined with the interface modifier.

In Kotlin, an interface is a reference type similar to a class. It can contain abstract methods and properties, as well as default method implementations. Interfaces cannot store state and cannot have constructors. They are used to define a contract that classes can implement.

Therefore, interface cannot be directly instantiated, just like abstract class. You could say interface is a 100% abstract class.

  • Interfaces are declared using the interface keyword.
  • All methods in an interface are abstract by default, but they can also have default implementations.
  • Interfaces can contain properties, but these properties must be abstract or have default implementations.
  • A class implements an interface using the : symbol followed by the interface name.
  • A class can implement multiple interfaces, allowing for a form of multiple inheritance.

Interfaces

Example:
							
								fun main() {
									val cat: Animal = Cat() // Animal reference but Cat object

									cat.makeSound()

									val distance = 3.2
									val movementTime = cat.move(distance)

									println("Cat move $distance m in $movementTime s")
								}
							
						
Interface
								
									interface Animal {

										fun makeSound()

										fun move(double distance): Double

									}
								
							
Implementation
								
									class Cat : Animal {

										override fun makeSound() {
											println("meow")
										}

										fun move(double distance): Double {
											double speed = 2.0
											return distance / speed
										}
									}
								
							

Interfaces

You can also implement multiple interfaces at once.
								
									fun main() {
										val cat: Animal = Cat() // Animal reference but Cat object

										cat.makeSound()

										val distance = 3.2
										val movementTime = cat.move(distance)

										println("Cat move $distance m in $movementTime s")
									}
								
							
Moving interface
								
									interface Moving {

										fun move(double distance): Double

									}
								
							
Vocalizing interface
								
									interface Vocalizing {

										fun makeSound()

									}
								
							
Implementation of both Moving and Vocalizing
								
									class Cat : Moving, Vocalizing {

										override fun makeSound() {
											println("meow")
										}

										fun move(double distance): Double {
											double speed = 2.0
											return distance / speed
										}
									}
								
							

Interfaces

You can also extend interface with other interfaces. The concrete class that implements such interface will be required to implement all abstract methods.
Animal interface
								
									interface Animal extends Moving, Vocalizing {

										fun eat(String food)

									}
								
							
Moving interface
								
									interface Moving {

										fun move(double distance): Double

									}
								
							
Vocalizing interface
								
									interface Vocalizing {

										fun makeSound()

									}
								
							
Implementation of Animal
								
									class Cat : Animal {

										override fun makeSound() {
											println("meow")
										}

										override fun move(double distance): Double {
											double speed = 2.0
											return distance / speed
										}

										override fun eat(String food) {
											println("eats " + food)
										}
									}
								
							

Next Lesson

Next Lesson

In the following lesson, we will learn the fundamentals of testing.

We will cover:

  • What is testing?
  • Why do we test?
  • Types of tests
  • Levels of tests
  • Test design techniques

We will also write some tests using Kotest.