Object-Oriented Programming (OOP) is the preferred pc 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 mission utilizing the Obtain Supplies button on the high or backside of this tutorial.
Open the starter mission in Android Studio 2022.2.1 or later by choosing Open on the Android Studio welcome display:
The app consists of a single display just like Terminal on Home windows/Linux/MacOS. It enables 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 mission. You need to see the principle, and solely, display of the app:
Whoa, what’s occurring right here? As you possibly can see, the app at present refuses to run any instructions, it simply shows a non-cooperative message. Due to this fact, your job might 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’s best to perceive what OOP is.
Object-Oriented Programming is a programming mannequin primarily based on information. Every thing is modeled as objects that may carry out sure actions and talk with one another.
For instance, when you have been to signify a automotive in object-oriented programming, one of many objects can be a Automobile. It could include actions resembling:
- Speed up
- Brake
- Steer left
- Steer proper
Lessons and Objects
One of the essential distinctions in object-oriented programming is between courses and objects.
Persevering with the automotive analogy, a category can be a concrete automotive mannequin and make you should buy, for instance — Fiat Panda.
A category describes how the automotive behaves, resembling its high pace, how briskly it could possibly speed up, and many others. It is sort of a blueprint for the automotive.
An object is an occasion of a automotive, when you go to a dealership and get your self a Fiat Panda, the Panda you’re now driving in is an object.
Let’s check out courses in KodecoShell app:
-
MainActivity
class represents the display proven once 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.
MainActivity
makes use of TerminalCommandProcessor
to course of the instructions the person 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 utilize:
non-public val commandProcessor: TerminalCommandProcessor = TerminalCommandProcessor()
Afterward, you might use it by calling its features, for instance:
commandProcessor.init()
Key Ideas of OOP
Now that you already know the fundamentals, it’s time to maneuver on to the important thing rules of OOP:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
These rules make it potential to construct code that’s straightforward to know and preserve.
Understanding Encapsulation and Kotlin Lessons
Information inside a category will be restricted. Be certain different courses 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 utilize visibility modifiers to manage the visibility of properties and features inside courses. Two of crucial ones are:
-
non-public
: property or perform is barely 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 interior information of a category as non-public
prevents different courses 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:
non-public 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 courses to make use of course of()
to course of instructions by way of TerminalCommandProcessor
.
Notice you handed blocks of code for outputCallback
and errorCallback
parameters. Shell
will execute certainly one of them when its course of
perform 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.
Nonetheless, Android Studio will present the next error:Can't entry 'shell': it's non-public 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.
In consequence, now it’s best to see the output of the ps
command, which is the listing of the at present working processes.
Abstraction
That is just like encapsulation, it permits entry to courses by way of a selected contract. In Kotlin, you possibly can outline that contract utilizing interfaces.
Interfaces in Kotlin can include declarations of features and properties. However, the principle distinction between interfaces and courses 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 substitute 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 get rid of 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 got an interface with three features with implementations.
Substitute init
perform with the next:
enjoyable init()
That is now an summary perform with no implementation. All courses that implement TerminalCommandProcessor
interface should present the implementation of this perform.
Substitute course of
and stopCurrentCommand
features with the next:
enjoyable course of(command: String)
enjoyable stopCurrentCommand()
Lessons 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
package deal with the next content material:
package deal 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
non-public 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.
- You implement
TerminalCommandProcessor
interface. - You declare a property named
outputCallback
and use the override key phrase to declare that it’s an implementation of property with the identical title fromTerminalCommandProcessor
interface. - You create a personal property holding a
Shell
object for executing instructions. You move the code blocks that move the command output and errors tooutputCallback
wrapped inTerminalItem
objects. - Implementations of
init
,course of
andstopCurrentCommand
features name acceptableShell
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, substitute commandProcessor
property with:
non-public val commandProcessor: TerminalCommandProcessor = ShellCommandProcessor()
Construct and run the app.
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 are you going to present a unique merchandise if an inventory is ready as much as present an object of a sure class? The reply lies in inheritance and polymorphism.
Inheritance lets you create a brand new class with all of the properties and features “inherited” from one other class, also referred to as deriving a category from one other. The category you’re deriving from can be known as a superclass.
Yet one more essential factor in inheritance is which you can present a unique implementation of a public perform “inherited” from a superclass. This leads us to the following idea.
Polymorphism is expounded to inheritance and lets you deal with all derived courses as a superclass. For instance, you possibly can move a derived class to TerminalView
, and it’ll fortunately present it pondering it’s a TerminalItem
. Why would you try this? Since you may present your individual implementation of View()
perform that returns a composable to indicate on display. This implementation might be an enter subject for getting into instructions for the derived class.
So, create a brand new class named TerminalCommandPrompt
extending TerminalItem
in processor/mannequin
package deal and substitute its contents with the next:
package deal 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(
non-public val commandProcessor: TerminalCommandProcessor
) : TerminalItem() {
}
It takes one constructor parameter, a TerminalCommandProcessor
object, which it’ll use to move the instructions to.
Android Studio will present an error. In case you hover over it, you’ll see: This sort is last, so it can't be inherited from
.
It is because, by default, all courses 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 appears to be like like this:
open class TerminalItem(non-public 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 offer 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:
- Returns a
CommandInputField
composable. This takes the enter line by line and passes it to theCommandInputWriter
. - An essential idea to notice right here is that you just’re passing an nameless object that implements
CommandInputWriter
. - Implementation of
sendInput
from namelessCommandInputWriter
handed toCommandInputField
passes the enter toTerminalCommandProcessor
object from class constructor.
There’s one last factor to do, open MainActivity
and add the next import:
import com.kodeco.android.kodecoshell.processor.mannequin.TerminalCommandPrompt
Now, substitute the TerminalView
instantiation with:
TerminalView(commandProcessor, TerminalCommandPrompt(commandProcessor))
This units the merchandise used for getting 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.
SOLIDifying your code
Moreover, 5 extra design rules will provide help to 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: You need 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 kind, it’s 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 courses from implementing strategies they don’t want.
- Dependency Inversion Precept: Elements ought to depend upon abstractions moderately 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. It’s also possible to check with this precept as “decoupling” code.
In the identical manner, every perform ought to carry out one job if potential. A great measure is that it’s best to be capable of know what every perform does from its title.
Listed below 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 the right way to show the end result. -
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 toTerminalCommandProcessor
implementation.
Understanding the Open Closed Precept
You’ve seen this precept in motion once you added TerminalCommandPrompt
merchandise. Extending the performance by including new kinds of gadgets to the listing on the display doesn’t break current performance. No additional work in TerminalItem
or MainActivity
was wanted.
This can be a results of utilizing polymorphism by offering an implementation of View
perform in courses derived from TerminalItem
. MainActivity
doesn’t need to do any additional work when you add extra gadgets. That is what the Open Closed Precept is all about.
For observe, check this precept as soon as extra by including two new TerminalItem
courses:
-
TerminalCommandErrorOutput
: for exhibiting errors. The brand new merchandise ought to look the identical asTerminalItem
however have a unique coloration. -
TerminalCommandInput
: for exhibiting instructions that you just entered. The brand new merchandise ought to look the identical asTerminalItem
however have “>” prefixed.
Right here’s the answer:
[spoiler title=”Solution”]
package deal 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(
non-public 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
)
}
}
package deal com.kodeco.android.kodecoshell.processor.mannequin
class TerminalCommandInput(
non-public val command: String
) : TerminalItem() {
override enjoyable textToShow(): String = "> $command"
}
Replace ShellCommandProcessor
property initializer:
non-public 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:
Understanding the Liskov Substitution Precept
This precept states that when you substitute a subclass of a category with a unique one, the app shouldn’t break.
For instance, when you’re utilizing a Checklist
, the precise implementation doesn’t matter. Your app would nonetheless work, although the instances to entry the listing components would differ.
To check this out, create a brand new class named DebugShellCommandProcessor
in processor/shell
package deal.
Paste the next code into it:
package deal 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 {
non-public 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"))
}
)
non-public 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’ll have observed, that is just like ShellCommandProcessor
with the added code for monitoring how lengthy every command takes to execute.
Go to MainActivity
and substitute commandProcessor
property with the next:
non-public val commandProcessor: TerminalCommandProcessor = DebugShellCommandProcessor()
You’ll need to import this:
import com.kodeco.android.kodecoshell.processor.shell.DebugShellCommandProcessor
Now construct and run the app.
Strive executing the “ps” command.
Your app nonetheless works, and also you now get some extra 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(
non-public 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.
In case you used just one interface – by placing summary sendInput
perform into TerminalItem
– all courses extending TerminalItem
must present an implementation for it although they don’t use it. As a substitute, by separating it into a unique interface, solely TerminalCommandPrompt
can implement it.
Understanding the Dependency Inversion Precept
As a substitute of relying on concrete implementations, resembling ShellCommandProcessor
, your courses ought to depend upon abstractions: interfaces or summary courses 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 straightforward to make use of. By relying on TerminalCommandProcessor
in MainActivity
, it’s straightforward to switch the implementation used. Additionally, this is useful when writing exams. You possibly can move mock objects to a examined class.
Kotlin Particular Suggestions
Lastly, listed below are just a few Kotlin-specific ideas.
Kotlin has a helpful mechanism for controlling inheritance: sealed courses and interfaces. Briefly, when you declare a category as sealed, all its subclasses should be inside the identical module.
For extra data, examine the official documentation.
In Kotlin, courses can’t have static features and properties shared throughout all situations of your class. That is the place companion objects are available in.
For extra data take a look at the official documentation.
The place to Go From Right here?
If you wish to know extra about commonest design patterns utilized in OOP, try our sources on patterns utilized in Android.
In case you want a helpful listing of design patterns, be sure to examine this.
One other useful resource associated to design patterns is Design Patterns: Components of Reusable Object-Oriented Software program, by the Gang of 4.
You’ve discovered what Object-Oriented Programming greatest practices are and the right way 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!