Published on

OOP Mastery - Theory 02: Object-Oriented Characteristics in Kotlin

Authors

Kotlin is an Object-Oriented language, with all the Characteristics of Object-Oriented Programming. Kotlin can be used in developing Backend applications with libraries (Spring, Ktor) or it can be used to develop Mobile applications (Android, iOS). Mastering the Kotlin language opens up many job opportunities for you. Give it a try if you're interested in this language!

In this article and the OOP Mastery course series, I will completely use the Kotlin language for examples and project development.

Encapsulation in Kotlin

Encapsulation is the ability to control the level of access to properties within a Class. This is to ensure the integrity of the data and logic of a Class to serve the features of the Library or program.

  • When declaring without a specific modifier, the Kotlin compiler will default to understanding it as a public modifier.
class DefineKotlinAccessModifier {
  val publicModifier: String = "Public Modifier"
  internal val internalModifier: String = "Internal Modifier"
  protected val protectedModifier: String = "Protected Modifier"
  private val privateModifier: String = "Private Modifier"
}
  • Kotlin also provides Properties Getter, Setter features to allow programmers to be more flexible in managing the values ​​of variables.
class EncapsulationKotlin {
    var dynamicString: String = "Initialized Value"
        private set // limits the scope of the set function, dynamicString can be accessed from the outside, but setting must be done inside the Class
    var anotherDynamicString: String = "Another Initialized Value"
        set(value) {
            field = "Another $value"
        } // customize the logic of the set function for anotherDynamicString
}

Inheritance in Kotlin

Inheritance is the ability to help a Class reuse properties and functions (method, behavior) from another Class.

Unlike C++, Kotlin simplifies inheritance by not dividing it into different types of inheritance such as public or private. In Kotlin, when it comes to inheritance, there is only inheritance - nothing more.

  • For a Class to be inheritable, the programmer must add the open keyword before the Class definition.
open class SampleClass {
  var publicAttr: String = "public attr"
  internal internalAttr: String = "internal attr"
  protected protectedAttr: String = "protected attr"
  private privateAttr: String = "private attr"
}
class SampleSubClass : SampleClass() {}
  • You can also inherit from sealed class or abstract class
sealed class SampleSealedClass
abstract class SampleAbstractClass

class SubSealedClass : SampleSealedClass()
class SubAbstractClass : SampleAbstractClass()

sealed, abstract are special cases in Kotlin that you will learn about in later articles.

Polymorphism in Kotlin

Polymorphism in OOP is expressed through different Classes, inheriting a common Class, an Interface can call the same function (the term is called the same interface) but can perform different logic depending on each declaration location. Simply put, Polymorphism - the same form but different content inside.

Overload

Overload is defining methods (functions) in a Class that have the same name but differ in input params. The return data types of these methods can be the same or different as desired.

interface Encryptor {
  fun encrypt(input: String): String
  fun encrypt(input: Int): Int
  fun encrypt(input: Char): Char
}

Override

Override: Defining methods (functions) in the child Class inherited from the parent Class, and completely changing the logic inside it.

In Kotlin, to be able to override the functions or variables of the parent Class. You must add the open keyword before declaring that content.

open class BaseClass {
    open var name: String = "BaseClass"
    open fun print() {
        println("Hello from BaseClass1")
    }
}

class SubClass : BaseClass() {
    override var name: String = "SubClass" // kotlin allows you to override properties
        get() = super.name
        set(value) {
            field = value
        }
    override fun print() {
        super.print() // -> call the function of the BaseClass class
        println("Hello from SubClass")
    }
}

You can also use override for abstract class, or interface

abstract class MyAbstractClass {
    open val name: String = "Name"
    abstract val age: Int
    abstract fun myFunction()
    open fun myOpenFunction() {
        println("Hello from myEmptyFunction")
    }
    fun myNormalFunction() {
        println("Hello from myNormalFunction")
    }
}

class MySubClass : MyAbstractClass() {
    override val age: Int = 10
    override fun myFunction() {
        println("Hello from myFunction")
    }

    override fun myOpenFunction() {
        super.myOpenFunction()
    }

    override val name: String
        get() = super.name
}

interface MyInterface {
    val name: String // Kotlin interface can contain member variables
    fun myFunction() {
    // Kotlin interface can contain pre-defined functions
        println("Hello from MyInterface")
    }
    // Kotlin interface can contain undefined functions
    fun myEmptyFunction()
}

class MyClass : MyInterface {
// how to override in Interface Implementation
    override val name: String = "MyClass"
    override fun myFunction() {
        super.myFunction()
        println("Hello from MyClass")
    }

    override fun myEmptyFunction() {
        println("Hello from myEmptyFunction")
    }
}

In summary, Kotlin provides very versatile features for programmers to use. Depending on the case and the purpose of the feature, we may choose abstract, interface or open class, sealed class to define Parent. This knowledge will definitely be explained fully, specifically, with examples in the following articles.

Abstraction in Kotlin

Abstraction - an interesting characteristic in the object-oriented programming approach.

Abstraction is expressed through the fact that when designing software features, programmers will focus on designing the workflow of behaviors / methods and then divide them into Classes without going into specific details. Classes designed at this stage are called Abstract Classes, or can also be called Interfaces in some cases.

In Kotlin, polymorphism is expressed through Interface, Abstract Class.

interface MyInterface {
    val name: String // Kotlin interface can contain member variables
    fun myFunction() {
    // Kotlin interface can contain pre-defined functions
        println("Hello from MyInterface")
    }
    // Kotlin interface can contain undefined functions
    fun myEmptyFunction()
}

abstract class MyAbstractClass {
    open val name: String = "Name"
    abstract val age: Int
    abstract fun myFunction()
    open fun myOpenFunction() {
        println("Hello from myEmptyFunction")
    }
    fun myNormalFunction() {
        println("Hello from myNormalFunction")
    }
}

Practical application of Abstraction in the software development process.

Example: Design a solution to store and read music files for the Spotify application. It is required to encrypt to ensure data security.

In this case, we have not found a specific solution for which encryption algorithm to use, if we continue searching for an encryption algorithm, the development will continue to be suspended. The solution at this time is Abstraction.

  • Step 1: Design a common Interface for encrypting and decrypting music files
interface ICryptography {
  fun encrypt(arr: ByteArray): ByteArray
  fun decrypt(arr: ByteArray): ByteArray
}
  • Step 2: Implement a simple solution and integrate it into the source code
class DummyCryptography : ICryptography {
  fun encrypt(arr: ByteArray): ByteArray {
    return arr
  }
  fun encrypt(arr: ByteArray): ByteArray {
    return arr
  }
}
  • Step 3: Develop advanced, detailed solutions
class CryptographySolution1 : ICryptography {
    fun encrypt(arr: ByteArray): ByteArray {
      // implement logic for the Solution1
    }
    fun encrypt(arr: ByteArray): ByteArray {
      // implement logic for the Solution1
    }
}

class CryptographySolution2 : ICryptography {
    fun encrypt(arr: ByteArray): ByteArray {
      // implement logic for the Solution 2
    }
    fun encrypt(arr: ByteArray): ByteArray {
      // implement logic for the Solution 2
    }
}
  • Step 4: Test independent solutions
class CryptographySolution1Test {
  // write unit test for CryptographySolution1
}
class CryptographySolution2Test {
  // write unit test for CryptographySolution2
}
  • Step 5: Select and Release the product

You can see that thanks to the nature of Abstraction and Polymorphism, we can easily replace DummyCryptography with CryptographySolution1 or CryptographySolution2.

This is the strength of OOP. In addition, throughout the development process, the Solutions take place independently of each other, so Testing also becomes easier, which makes the feature highly reliable when Release.