OOP Mastery – Lý thuyết 02: Các đặc tính của Hướng đối tượng trong Kotlin

Kotlin là một ngôn ngữ Hướng đối tượng, có đầy đủ các Đặc tính của Lập trình Hướng đối tượng. Kotlin có thể được sử dụng trong phát triển Backend application với các thư viện (Spring, Ktor) hoặc có thể sử dụng để phát triển ứng dụng Mobile (Android, iOS). Việc thành thạo ngôn ngữ Kotlin giúp bạn mở ra nhiều cơ hội việc làm. Hãy thử nếu bạn cảm thấy hứng thú với ngôn ngữ này!

Trong bài viết này và chuỗi khóa học OOP Mastery mình sẽ hoàn toàn sử dụng ngông ngữ Kotlin để ví dụ và phát triển đồ án.

Tính đóng gói – Encapsulation trong Kotlin

Tính đóng gói là khả năng kiểm soát mức độ truy cập của các thuộc tính nằm trong 1 Class. Nhằm đảm bảo sự trọng vẹn dữ liệu, logic của 1 Class để phục vụ cho tính năng của Thư viện, hoặc chương trình.

Access ModifiersÝ nghĩa
publicCó thể được truy cập ở mọi nơi trong Source code
internalChỉ có thể truy cập được trong cùng module (khác package name nhưng cùng module vẫn truy cập được)
protectedChỉ có thể truy cập được trong phạm vi bên trong Class. Được kế thừa cho Sub Class.
privateChỉ có thể truy cập được trong phạm vi bên trong Class. Không được kế thừa cho Sub Class.
Bảng minh họa các Access Modifiers trong ngôn ngữ Kotlin
  • Khi khai báo không kèm modifier cụ thể, Kotlin compiler sẽ mặc định hiểu đó là 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 còn cung cấp thêm tính năng Properties Getter, Setter để Lập trình viên được tùy biến nhiều hơn trong việc quản lý giá trị của các biến.
class EncapsulationKotlin {
    var dynamicString: String = "Initialized Value"
        private set // giới hạn phạm vi của hàm set, có thể truy cập dynamicString từ bên ngoài, nhưng việc set phải được thực hiện bên trong Class
    var anotherDynamicString: String = "Another Initialized Value"
        set(value) {
            field = "Another $value"
        } // customize lại logic của hàm set cho anotherDynamicString
}

Tính kế thừa – Inheritance trong Kotlin

Kế thừa (Inheritance) là khả năng giúp 1 Class sử dụng lại các thuộc tính (properties) và hàm (method, behavior) từ 1 Class khác.

Khác với C++, Kotlin đơn giản hóa việc kế thừa bằng cách không phân chia ra các loại kế thừa khác nhau như public, hay private. Trong Kotlin, khi nhắc đến kế thừa, chỉ có kế thừa – nothing more.

Access Modifier Base ClassAccess Modifier Sub Class
privateNOT INHERITANCE
protectedprotected
internalinternal (Nếu Sub Class nằm khác module thì NOT INHERITANCE)
publicpublic
Bảng mô tả kế thừa trong Kotlin
  • Để một Class có thể kế thừa được Lập trình viên phải thêm từ khóa open trước định nghĩa Class
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() {}
  • Ngoài ra bạn còn có thể kế thừa từ sealed class, hoặc abstract class
sealed class SampleSealedClass
abstract class SampleAbstractClass

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

sealed, abstract là các trường hợp đặc biệt trong Kotlin bạn sẽ được học ở các bài viết phần sau.

Tính đa hình – Polymorphism trong Kotlin

Tính đa hình – Polymorphism trong OOP thể hiện qua việc các Class khác nhau, kế thừa chung 1 Class, Interface có thể cùng gọi đến 1 hàm (thuật ngữ gọi là cùng giao diện/Interface) nhưng lại có thể thực hiện các Logic khác nhau tùy vào từng nơi khai báo.
Hiểu đơn giản là Đa hình – cùng 1 hình thức nhưng nội dung bên trong khác nhau.

Overload

Overload là việc định nghĩa các phương thức (hàm) trong Class có cùng tên nhưng khác nhau về các input params. Các kiểu dữ liệu trả về của các phương thức này có thể giống hoặc khác nhau tùy ý.

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

Override

Override: Định nghĩa các phương thức (hàm) trong Class con được kế thừa từ Class cha, và thay đổi hoàn toàn logic bên trong nó.

Trong Kotlin để có thể override lại các function hoặc các biến của Class cha. Bạn phải thêm từ khóa open trước khai báo các nội dung đó.

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

class SubClass : BaseClass() {
    override var name: String = "SubClass" // kotlin cho phép bạn override properties
        get() = super.name
        set(value) {
            field = value
        }
    override fun print() {
        super.print() // -> call hàm của lớp BaseClass
        println("Hello from SubClass")
    }
}

Ngoài ra bạn cũng có thể sử dụng override cho abstract class, hoặc 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 // interface của Kotlin có thể chứa member variable
    fun myFunction() {
    // interface của Kotlin có thể chứa hàm đã được định nghĩa sẵn
        println("Hello from MyInterface")
    }
    // interface của Kotlin có thể chứa hàm chưa được định nghĩa
    fun myEmptyFunction()
}

class MyClass : MyInterface {
// cách override trong Implementation của Interface
    override val name: String = "MyClass"
    override fun myFunction() {
        super.myFunction()
        println("Hello from MyClass")
    }

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

Tổng kết lại thì Kotlin cung cấp rất đa dụng tính năng cho Lập trình viên sử dụng. Tùy từng trường hợp, và mục đích tính năng mà ta sẽ có thể chọn abstract, interface hoặc open class, sealed class để định nghĩa Parent. Những kiến thức này chắc chắn sẽ được giải thích đầy đủ, cụ thể, kèm ví dụ trong các bài viết tiếp theo.

Tính Trừu tượng – Abstraction trong Kotlin

Tính trừu tượng – 1 đặc tính thú vị trong cách tiếp cận của lập trình hướng đối tượng.

Tính trừu tượng – abstraction được thể hiện thông qua việc khi thiết kế các tính năng của phần mềm các Lập trình viên sẽ tập trung vào việc thiết kế luồng làm việc của các hành vi (behaviour / method) rồi từ đó phân chia ra các Class mà chưa đi sâu vào các chi tiết cụ thể. Các Class được thiết kế ở giai đoạn này gọi là Abstract Class, hoặc cũng có thể gọi là Interface trong 1 vài trường hợp.

Trong Kotlin, tính đa hình thể hiện qua Interface, Abstract Class.

interface MyInterface {
    val name: String // interface của Kotlin có thể chứa member variable
    fun myFunction() {
    // interface của Kotlin có thể chứa hàm đã được định nghĩa sẵn
        println("Hello from MyInterface")
    }
    // interface của Kotlin có thể chứa hàm chưa được định nghĩa
    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")
    }
}

Ứng dụng thực tế của Tính Trừu Tượng – Abstraction trong quá trình phát triển phần mềm.

Ví dụ: Thiết kế giải pháp lưu trữ và đọc file nhạc cho ứng dụng Spotify. Yêu cầu phải mã hóa để đảm bảo tính an toàn dữ liệu.

Trong trường hợp này chúng ta chưa tìm ra một giải pháp cụ thể cho việc sẽ sử dụng thuật toán mã hóa nào, nếu cứ tiếp tục tìm kiếm thuật toán mã hóa thì việc development sẽ tiếp tục bị treo. Giải pháp lúc này là Abstraction.

  • Bước 1: Thiết kế ra Interface chung cho việc mã hóa, giải mã file âm nhạc
interface ICryptography {
  fun encrypt(arr: ByteArray): ByteArray
  fun decrypt(arr: ByteArray): ByteArray
}
  • Bước 2: Implement một giải pháp đơn giản và tích hợp nó vào mã nguồn
class DummyCryptography : ICryptography {
  fun encrypt(arr: ByteArray): ByteArray {
    return arr
  }
  fun encrypt(arr: ByteArray): ByteArray {
    return arr
  }
}
  • Bước 3: Phát triển các phương án nâng cao, chi tiết
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
    }
}
  • Bước 4: Test các giải pháp độc lập
class CryptographySolution1Test {
  // write unit test for CryptographySolution1
}
class CryptographySolution2Test {
  // write unit test for CryptographySolution2
}
  • Bước 5: Lựa chọn và Release sản phẩm

Bạn có thể thấy nhờ tính chất Trừu Tượng (Abstraction) và Đa Hình (Polymorphism) chúng ta có thể dễ dàng thay thế được DummyCryptography bằng các CryptographySolution1 hoặc CryptographySolution2.

Đây chính là điểm mạnh của OOP. Ngoài ra trong suốt quá trình phát triển, các Solution đều diễn ra độc lập với nhau nên việc Testing cũng trở nên dễ dàng hơn, điều đó khiến cho tính năng được tin cậy cao khi Release.