iOS Development

Object-Oriented Programming Finest Practices with Kotlin

Spread the love


Object-Oriented Programming (OOP) is the preferred laptop programming paradigm. Utilizing it correctly could make your life, and your coworkers’, lives simpler. On this tutorial, you’ll construct a terminal app to execute shell instructions on Android.

Within the course of, you’ll be taught the next:

  • Key rules of Object-Oriented Programming.
  • SOLID rules and the way they make your code higher.
  • Some Kotlin particular good-to-knows.

Getting began

To start with, obtain the Kodeco Shell venture utilizing the Obtain Supplies button on the high or backside of this tutorial.

Open the starter venture in Android Studio 2022.2.1 or later by choosing Open on the Android Studio welcome display:

Android Studio Welcome Screen

The app consists of a single display much like Terminal on Home windows/Linux/MacOS. It permits you to enter instructions and present their output and errors. Moreover, there are two actions, one to cease a working command and one to clear the output.

Construct and run the venture. It’s best to see the primary, and solely, display of the app:

Main Screen

Whoa, what’s occurring right here? As you may see, the app presently refuses to run any instructions, it simply shows a non-cooperative message. Subsequently, your job will likely be to make use of OOP Finest Practices and repair that! You’ll add the flexibility to enter instructions and show their output.

Understanding Object-Oriented Programming?

Earlier than including any code, it is best to perceive what OOP is.

Object-Oriented Programming is a programming mannequin primarily based on knowledge. Every thing is modeled as objects that may carry out sure actions and talk with one another.

For instance, should you had been to signify a automotive in object-oriented programming, one of many objects could be a Automobile. It might comprise actions corresponding to:

  • Speed up
  • Brake
  • Steer left
  • Steer proper

Courses and Objects

Some of the essential distinctions in object-oriented programming is between lessons and objects.

Persevering with the automotive analogy, a category could be a concrete automotive mannequin and make you should buy, for instance — Fiat Panda.

A category describes how the automotive behaves, corresponding to its high velocity, how briskly it may speed up, and so on. It is sort of a blueprint for the automotive.

An object is an occasion of a automotive, should you go to a dealership and get your self a Fiat Panda, the Panda you’re now driving in is an object.

Class vs Object

Let’s check out lessons in KodecoShell app:

  • MainActivity class represents the display proven if you open the app.
  • TerminalCommandProcessor class processes instructions that you just’ll enter on the display and takes care of capturing their output and errors.
  • Shell class executes the instructions utilizing Android runtime.
  • TerminalItem class represents a bit of textual content proven on the display, a command that was entered, its output or error.

KodecoShell classes

MainActivity makes use of TerminalCommandProcessor to course of the instructions the consumer enters. To take action, it first must create an object from it, known as “creating an object” or “instantiating an object of a category”.

To realize this in Kotlin, you employ:


personal val commandProcessor: TerminalCommandProcessor = TerminalCommandProcessor()

Afterward, you might use it by calling its features, for instance:


commandProcessor.init()

Key Ideas of OOP

Now that the fundamentals, it’s time to maneuver on to the important thing rules of OOP:

  • Encapsulation
  • Abstraction
  • Inheritance
  • Polymorphism

These rules make it attainable to construct code that’s simple to grasp and preserve.

Understanding Encapsulation and Kotlin Courses

Knowledge inside a category may be restricted. Be sure different lessons can solely change the information in anticipated methods and stop state inconsistencies.

Briefly, the skin world doesn’t must know how a category does one thing, however what it does.

In Kotlin, you employ visibility modifiers to manage the visibility of properties and features inside lessons. Two of crucial ones are:

  • personal: property or perform is just seen inside the category the place it’s outlined.
  • public: default visibility modifier if none is specified, property or perform is seen all over the place.

Marking the inner knowledge of a category as personal prevents different lessons from modifying it unexpectedly and inflicting errors.

To see this in motion, open TerminalCommandProcessor class and add the next import:


import com.kodeco.android.kodecoshell.processor.shell.Shell

Then, add the next inside the category:


personal val shell = Shell(
    outputCallback = { outputCallback(TerminalItem(it)) },
    errorCallback = { outputCallback(TerminalItem(it)) }
)

You instantiated a Shell to run shell instructions. You possibly can’t entry it exterior of TerminalCommandProcessor. You need different lessons to make use of course of() to course of instructions through TerminalCommandProcessor.

Notice you handed blocks of code for outputCallback and errorCallback parameters. Shell will execute certainly one of them when its course of perform is known as.

To check this, open MainActivity and add the next line on the finish of the onCreate perform:


commandProcessor.shell.course of("ps")

This code tries to make use of the shell property you’ve simply added to TerminalCommandProcessor to run the ps command.

Nevertheless, Android Studio will present the next error:
Can not entry 'shell': it's personal in 'TerminalCommandProcessor'

Delete the road and return to TerminalCommandProcessor. Now change the init() perform to the next:


enjoyable init() {
  shell.course of("ps")
}

This code executes when the appliance begins as a result of MainActivity calls TerminalViews‘s LaunchEffect.

Construct and run the app.

Consequently, now it is best to see the output of the ps command, which is the checklist of the presently working processes.

PS output

Abstraction

That is much like encapsulation, it permits entry to lessons by means of a selected contract. In Kotlin, you may outline that contract utilizing interfaces.

Interfaces in Kotlin can comprise declarations of features and properties. However, the primary distinction between interfaces and lessons is that interfaces can’t retailer state.

In Kotlin, features in interfaces can have implementations or be summary. Properties can solely be summary; in any other case, interfaces may retailer state.

Open TerminalCommandProcessor and change class key phrase with interface.

Notice Android Studio’s error for the shell property: Property initializers aren't allowed in interfaces.

As talked about, interfaces can’t retailer state, and you can not initialize properties.

Delete the shell property to eradicate the error.

You’ll get the identical error for the outputCallback property. On this case, take away solely the initializer:


var outputCallback: (TerminalItem) -> Unit

Now you’ve an interface with three features with implementations.

Exchange init perform with the next:


enjoyable init()

That is now an summary perform with no implementation. All lessons that implement TerminalCommandProcessor interface should present the implementation of this perform.

Exchange course of and stopCurrentCommand features with the next:


enjoyable course of(command: String)

enjoyable stopCurrentCommand()

Courses in Kotlin can implement a number of interfaces. Every interface a category implements should present implementations of all its summary features and properties.

Create a brand new class ShellCommandProcessor implementing TerminalCommandProcessor in processor/shell bundle with the next content material:


bundle com.kodeco.android.kodecoshell.processor.shell

import com.kodeco.android.kodecoshell.processor.TerminalCommandProcessor
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalItem

class ShellCommandProcessor: TerminalCommandProcessor { // 1
  // 2  
  override var outputCallback: (TerminalItem) -> Unit = {}

  // 3
  personal val shell = Shell(
    outputCallback = { outputCallback(TerminalItem(it)) },
    errorCallback = { outputCallback(TerminalItem(it)) }
  )

  // 4
  override enjoyable init() {
    outputCallback(TerminalItem("Welcome to Kodeco shell - enter your command ..."))
  }

  override enjoyable course of(command: String) {
    shell.course of(command)
  }

  override enjoyable stopCurrentCommand() {   
    shell.stopCurrentCommand()
  }
}

Let’s go over this step-by-step.

  1. You implement TerminalCommandProcessor interface.
  2. You declare a property named outputCallback and use the override key phrase to declare that it’s an implementation of property with the identical identify from TerminalCommandProcessor interface.
  3. You create a personal property holding a Shell object for executing instructions. You go the code blocks that go the command output and errors to outputCallback wrapped in TerminalItem objects.
  4. Implementations of init, course of and stopCurrentCommand features name applicable Shell object features.

You want another MainActivity change to check the brand new code. So, add the next import:


import com.kodeco.android.kodecoshell.processor.shell.ShellCommandProcessor

Then, change commandProcessor property with:


personal val commandProcessor: TerminalCommandProcessor = ShellCommandProcessor()

Construct and run the app.

Welcome to Kodeco Shell

Inheritance and Polymorphism

It’s time so as to add the flexibility to enter instructions. You’ll do that with the assistance of one other OOP precept — inheritance. MainActivity is ready as much as present an inventory of TerminalItem objects. How will you present a distinct merchandise if an inventory is ready as much as present an object of a sure class? The reply lies in inheritance and polymorphism.

Inheritance allows you to create a brand new class with all of the properties and features “inherited” from one other class, often known as deriving a category from one other. The category you’re deriving from can also be known as a superclass.

Another essential factor in inheritance is you can present a distinct implementation of a public perform “inherited” from a superclass. This leads us to the following idea.

Polymorphism is said to inheritance and allows you to deal with all derived lessons as a superclass. For instance, you may go a derived class to TerminalView, and it’ll fortunately present it considering it’s a TerminalItem. Why would you try this? Since you may present your individual implementation of View() perform that returns a composable to point out on display. This implementation will likely be an enter discipline for coming into instructions for the derived class.

So, create a brand new class named TerminalCommandPrompt extending TerminalItem in processor/mannequin bundle and change its contents with the next:


bundle com.kodeco.android.kodecoshell.processor.mannequin

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import com.kodeco.android.kodecoshell.processor.CommandInputWriter
import com.kodeco.android.kodecoshell.processor.TerminalCommandProcessor
import com.kodeco.android.kodecoshell.processor.ui.CommandInputField

class TerminalCommandPrompt(
    personal val commandProcessor: TerminalCommandProcessor
) : TerminalItem() {

}

It takes one constructor parameter, a TerminalCommandProcessor object, which it’ll use to go the instructions to.

Android Studio will present an error. Should you hover over it, you’ll see: This kind is last, so it can't be inherited from.

It’s because, by default, all lessons in Kotlin are last, which means a category can’t inherit from them.
Add the open key phrase to repair this.

Open TerminalItem and add the open key phrase earlier than class, so your class seems like this:


open class TerminalItem(personal val textual content: String = "") {

  open enjoyable textToShow(): String = textual content

  @Composable
  open enjoyable View() {
    Textual content(
        textual content = textToShow(),
        fontSize = TextUnit(16f, TextUnitType.Sp),
        fontFamily = FontFamily.Monospace,
    )
  }
}

Now, again to TerminalCommandPrompt class.

It’s time to supply its View() implementation. Add the next perform override to the brand new class:


@Composable
@ExperimentalMaterial3Api
// 1
override enjoyable View() {
  CommandInputField(
      // 2
      inputWriter = object : CommandInputWriter {
        // 3
        override enjoyable sendInput(enter: String) {
          commandProcessor.course of(enter)
        }
      }
  )
}

Let’s go over this step-by-step:

  1. Returns a CommandInputField composable. This takes the enter line by line and passes it to the CommandInputWriter.
  2. An essential idea to notice right here is that you just’re passing an nameless object that implements CommandInputWriter.
  3. Implementation of sendInput from nameless CommandInputWriter handed to CommandInputField passes the enter to TerminalCommandProcessor object from class constructor.
Notice: Nameless objects are out of the scope of this tutorial, however you may examine extra about them within the official documentation.

There’s one last factor to do, open MainActivity and add the next import:


import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandPrompt

Now, change the TerminalView instantiation with:


TerminalView(commandProcessor, TerminalCommandPrompt(commandProcessor))

This units the merchandise used for coming into instructions on TerminalView to TerminalCommandPrompt.

Construct and run the app. Yay, now you can enter instructions! For instance, pwd.

Notice that you just gained’t have permission for some instructions, and also you’ll get errors.

Permission denied

SOLIDifying your code

Moreover, 5 extra design rules will enable you make sturdy, maintainable and easy-to-understand object-oriented code.

The SOLID rules are:

  • Single Accountability Precept: Every class ought to have one duty.
  • Open Closed Precept: It’s best to be capable of lengthen the habits of a part with out breaking its utilization.
  • Liskov Substitution Precept: In case you have a category of 1 sort, it is best to be capable of signify the bottom class utilization with the subclass with out breaking the app.
  • Interface Segregation Precept: It’s higher to have a number of small interfaces than solely a big one to stop lessons from implementing strategies they don’t want.
  • Dependency Inversion Precept: Parts ought to rely on abstractions relatively than concrete implementations.

Understanding the Single Accountability Precept

Every class ought to have just one factor to do. This makes the code simpler to learn and preserve. You may as well discuss with this precept as “decoupling” code.

In the identical means, every perform ought to carry out one job if attainable. measure is that it is best to be capable of know what every perform does from its identify.

Listed here are some examples of this precept from the KodecoShell app:

  • Shell class: Its job is to ship instructions to Android shell and notify the outcomes utilizing callbacks. It doesn’t care the way you enter the instructions or tips on how to show the outcome.
  • CommandInputField: A Composable that takes care of command enter and nothing else.
  • MainActivity: Reveals a terminal window UI utilizing Jetpack Compose. It delegates the dealing with of instructions to TerminalCommandProcessor implementation.

Understanding the Open Closed Precept

You’ve seen this precept in motion if you added TerminalCommandPrompt merchandise. Extending the performance by including new sorts of gadgets to the checklist on the display doesn’t break current performance. No further work in TerminalItem or MainActivity was wanted.

It is a results of utilizing polymorphism by offering an implementation of View perform in lessons derived from TerminalItem. MainActivity doesn’t should do any further work should you add extra gadgets. That is what the Open Closed Precept is all about.

PS command

For follow, check this precept as soon as extra by including two new TerminalItem lessons:

  • TerminalCommandErrorOutput: for displaying errors. The brand new merchandise ought to look the identical as TerminalItem however have a distinct coloration.
  • TerminalCommandInput: for displaying instructions that you just entered. The brand new merchandise ought to look the identical as TerminalItem however have “>” prefixed.

Right here’s the answer:

[spoiler title=”Solution”]


bundle com.kodeco.android.kodecoshell.processor.mannequin

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Textual content
import androidx.compose.runtime.Composable
import androidx.compose.ui.textual content.font.FontFamily
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType

/** Represents command error output in Terminal. */
class TerminalCommandErrorOutput(
    personal val errorOutput: String
) : TerminalItem() {
  override enjoyable textToShow(): String = errorOutput

  @Composable
  override enjoyable View() {
    Textual content(
        textual content = textToShow(),
        fontSize = TextUnit(16f, TextUnitType.Sp),
        fontFamily = FontFamily.Monospace,
        coloration = MaterialTheme.colorScheme.error
    )
  }
}

bundle com.kodeco.android.kodecoshell.processor.mannequin

class TerminalCommandInput(
    personal val command: String
) : TerminalItem() {
  override enjoyable textToShow(): String = "> $command"
}


Replace ShellCommandProcessor property initializer:


personal val shell = Shell(
  outputCallback = { outputCallback(TerminalItem(it)) },
  errorCallback = { outputCallback(TerminalCommandErrorOutput(it)) }
)

Then, course of perform:


override enjoyable course of(command: String) {
  outputCallback(TerminalCommandInput(command))
  shell.course of(command)
}

Import the next:


import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandErrorOutput
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandInput

[/spoiler]

Construct and run the app. Sort a command that wants permission or an invalid command. You’ll see one thing like this:

Permission denied with color

Understanding the Liskov Substitution Precept

This precept states that should you change a subclass of a category with a distinct one, the app shouldn’t break.

For instance, should you’re utilizing a Record, the precise implementation doesn’t matter. Your app would nonetheless work, regardless that the occasions to entry the checklist components would fluctuate.

To check this out, create a brand new class named DebugShellCommandProcessor in processor/shell bundle.
Paste the next code into it:


bundle com.kodeco.android.kodecoshell.processor.shell

import com.kodeco.android.kodecoshell.processor.TerminalCommandProcessor
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandErrorOutput
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandInput
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalItem
import java.util.concurrent.TimeUnit

class DebugShellCommandProcessor(
    override var outputCallback: (TerminalItem) -> Unit = {}
) : TerminalCommandProcessor {

  personal 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"))
      }
  )

  personal var commandStartNs = 0L

  override enjoyable init() {
    outputCallback(TerminalItem("Welcome to Kodeco shell (Debug) - enter your command ..."))
  }

  override enjoyable course of(command: String) {
    outputCallback(TerminalCommandInput(command))
    commandStartNs = System.nanoTime()
    shell.course of(command)
  }

  override enjoyable stopCurrentCommand() {
    shell.stopCurrentCommand()
  }
}

As you might have seen, that is much like ShellCommandProcessor with the added code for monitoring how lengthy every command takes to execute.

Go to MainActivity and change commandProcessor property with the next:


personal val commandProcessor: TerminalCommandProcessor = DebugShellCommandProcessor()

You’ll should import this:


import com.kodeco.android.kodecoshell.processor.shell.DebugShellCommandProcessor

Now construct and run the app.

Strive executing the “ps” command.

PS command

Your app nonetheless works, and also you now get some further debug data — the time that command took to execute.

Understanding the Interface Segregation Precept

This precept states it’s higher to separate interfaces into smaller ones.

To see the advantages of this, open TerminalCommandPrompt. Then change it to implement CommandInputWriter as follows:


class TerminalCommandPrompt(
    personal val commandProcessor: TerminalCommandProcessor
) : TerminalItem(), CommandInputWriter {

  @Composable
  @ExperimentalMaterial3Api
  override enjoyable View() {
    CommandInputField(inputWriter = this)
  }

  override enjoyable sendInput(enter: String) {
    commandProcessor.course of(enter)
  }
}

Construct and run the app to verify it’s nonetheless working.

Should you used just one interface – by placing summary sendInput perform into TerminalItem – all lessons extending TerminalItem must present an implementation for it regardless that they don’t use it. As an alternative, by separating it into a distinct interface, solely TerminalCommandPrompt can implement it.

Understanding the Dependency Inversion Precept

As an alternative of relying on concrete implementations, corresponding to ShellCommandProcessor, your lessons ought to rely on abstractions: interfaces or summary lessons that outline a contract. On this case, TerminalCommandProcessor.

You’ve already seen how highly effective the Liskov substitution precept is — this precept makes it tremendous simple to make use of. By relying on TerminalCommandProcessor in MainActivity, it’s simple to exchange the implementation used. Additionally, this is useful when writing checks. You possibly can go mock objects to a examined class.

Kotlin Particular Ideas

Lastly, listed below are a number of Kotlin-specific suggestions.

Kotlin has a helpful mechanism for controlling inheritance: sealed lessons and interfaces. Briefly, should you declare a category as sealed, all its subclasses have to be throughout the similar module.

For extra info, examine the official documentation.

In Kotlin, lessons can’t have static features and properties shared throughout all situations of your class. That is the place companion objects are available.

For extra info have a look at the official documentation.

The place to Go From Right here?

If you wish to know extra about most typical design patterns utilized in OOP, take a look at our assets on patterns utilized in Android.

Should you want a useful checklist of design patterns, ensure that to examine this.

One other useful resource associated to design patterns is Design Patterns: Parts of Reusable Object-Oriented Software program, by the Gang of 4.

You’ve realized what Object-Oriented Programming greatest practices are and tips on how to leverage them.

Now go and write readable and maintainable code and unfold the phrase! In case you have any feedback or questions, please be part of the discussion board dialogue under!

Leave a Reply

Your email address will not be published. Required fields are marked *