Object-Oriented Programming Best Practices with Kotlin
Learn how to write better code following Object Oriented Programming Best Practices with Kotlin and SOLID principles by developing a Terminal Android app. By Ivan Kušt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Object-Oriented Programming Best Practices with Kotlin
25 mins
- Getting started
- Understanding Object-Oriented Programming?
- Classes and Objects
- Key Principles of OOP
- Understanding Encapsulation and Kotlin Classes
- Abstraction
- Inheritance and Polymorphism
- SOLIDifying your code
- Understanding the Single Responsibility Principle
- Understanding the Open Closed Principle
- Understanding the Liskov Substitution Principle
- Understanding the Interface Segregation Principle
- Understanding the Dependency Inversion Principle
- Kotlin Specific Tips
- Where to Go From Here?
Understanding the Liskov Substitution Principle
This principle states that if you replace a subclass of a class with a different one, the app shouldn’t break.
For example, if you’re using a List
, the actual implementation doesn’t matter. Your app would still work, even though the times to access the list elements would vary.
To test this out, create a new class named DebugShellCommandProcessor
in processor/shell
package.
Paste the following code into it:
package com.kodeco.android.kodecoshell.processor.shell
import com.kodeco.android.kodecoshell.processor.TerminalCommandProcessor
import com.kodeco.android.kodecoshell.processor.model.TerminalCommandErrorOutput
import com.kodeco.android.kodecoshell.processor.model.TerminalCommandInput
import com.kodeco.android.kodecoshell.processor.model.TerminalItem
import java.util.concurrent.TimeUnit
class DebugShellCommandProcessor(
override var outputCallback: (TerminalItem) -> Unit = {}
) : TerminalCommandProcessor {
private val shell = Shell(
outputCallback = {
val elapsedTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - commandStartNs)
outputCallback(TerminalItem(it))
outputCallback(TerminalItem("Command success, time: ${elapsedTimeMs}ms"))
},
errorCallback = {
val elapsedTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - commandStartNs)
outputCallback(TerminalCommandErrorOutput(it))
outputCallback(TerminalItem("Command error, time: ${elapsedTimeMs}ms"))
}
)
private var commandStartNs = 0L
override fun init() {
outputCallback(TerminalItem("Welcome to Kodeco shell (Debug) - enter your command ..."))
}
override fun process(command: String) {
outputCallback(TerminalCommandInput(command))
commandStartNs = System.nanoTime()
shell.process(command)
}
override fun stopCurrentCommand() {
shell.stopCurrentCommand()
}
}
As you may have noticed, this is similar to ShellCommandProcessor
with the added code for tracking how long each command takes to execute.
Go to MainActivity
and replace commandProcessor
property with the following:
private val commandProcessor: TerminalCommandProcessor = DebugShellCommandProcessor()
You’ll have to import this:
import com.kodeco.android.kodecoshell.processor.shell.DebugShellCommandProcessor
Now build and run the app.
Try executing the “ps” command.
Your app still works, and you now get some additional debug info — the time that command took to execute.
Understanding the Interface Segregation Principle
This principle states it’s better to separate interfaces into smaller ones.
To see the benefits of this, open TerminalCommandPrompt
. Then change it to implement CommandInputWriter
as follows:
class TerminalCommandPrompt(
private val commandProcessor: TerminalCommandProcessor
) : TerminalItem(), CommandInputWriter {
@Composable
@ExperimentalMaterial3Api
override fun View() {
CommandInputField(inputWriter = this)
}
override fun sendInput(input: String) {
commandProcessor.process(input)
}
}
Build and run the app to make sure it’s still working.
If you used only one interface – by putting abstract sendInput
function into TerminalItem
– all classes extending TerminalItem
would have to provide an implementation for it even though they don’t use it. Instead, by separating it into a different interface, only TerminalCommandPrompt
can implement it.
Understanding the Dependency Inversion Principle
Instead of depending on concrete implementations, such as ShellCommandProcessor
, your classes should depend on abstractions: interfaces or abstract classes that define a contract. In this case, TerminalCommandProcessor
.
You’ve already seen how powerful the Liskov substitution principle is — this principle makes it super easy to use. By depending on TerminalCommandProcessor
in MainActivity
, it’s easy to replace the implementation used. Also, this comes in handy when writing tests. You can pass mock objects to a tested class.
Kotlin Specific Tips
Finally, here are a few Kotlin-specific tips.
Kotlin has a useful mechanism for controlling inheritance: sealed classes and interfaces. In short, if you declare a class as sealed, all its subclasses must be within the same module.
For more information, check the official documentation.
In Kotlin, classes can’t have static functions and properties shared across all instances of your class. This is where companion objects come in.
For more information look at the official documentation.
Where to Go From Here?
If you want to know more about most common design patterns used in OOP, check out our resources on patterns used in Android.
If you need a handy list of design patterns, make sure to check this.
Another resource related to design patterns is Design Patterns: Elements of Reusable Object-Oriented Software, by the Gang of Four.
You’ve learned what Object-Oriented Programming best practices are and how to leverage them.
Now go and write readable and maintainable code and spread the word! If you have any comments or questions, please join the forum discussion below!