Implement design pattern trong Android với Kotlin phần I

I, Lời mở đầu

  • Điều gì quyết định đến chất lượng code của bạn ?
  • Chất lượng code được quyết định từ nhiều yếu tố như coding convention, comments, code structure…
    • Coding convention: giúp các developer viết code theo đúng quy chuẩn của một ngôn ngữ.
    • Comment: giúp bạn và những developer khác hiểu rõ hơn đoạn code của bạn đang thực hiện tác vụ gì.
  • Bài viết này mình đến đó chính là design pattern.
  • Android cũng đã cung cấp các công cụ (Jetpack, di, coroutine…) giúp các developer thực hiện Architecture component. Awesome !!!
  • Để code của bạn trở nên chuyên nghiệp, dễ hiểu và ngắn gọn, mình khuyên mọi người nên áp dụng cả design pattern vào trong code của mình.

II, Design pattern là gì

  • Design pattern là một giải pháp chung để giải quyết những vấn đề thông thường trong một số trường hợp nhất định.
  • Những lợi ích của việc sử dụng design pattern:
    • Code dễ hiểu: bởi vì bạn theo những guildline, quy tắc chuẩn nên các developer khác khi đọc code sẽ dễ dàng hiểu được bạn đang làm gì. Ví dụ như khi bạn sử dụng singleton pattern, mọi người sẽ hiểu bạn muốn tạo ra 1 instance duy nhất trong ứng dụng của mình.
    • Code dễ dàng tái sử dụng: Bạn có thể thực hiện 1 task vụ nhiều lần, áp dụng design pattern bạn sẽ giảm được việc lặp đi lặp lại đoạn code của minh.
    • Code sạch hơn: design pattern làm cho code của bạn trở nên ngắn gọn và module hoá hơn.
  • Trong software design, chúng ta có thể chia design pattern ra làm 3 nhóm:
    • Creational pattern.
    • Structural pattern.
    • Behavior pattern.
  • Phần đầu tiên, mình sẽ giới thiệu nhóm đầu tiên và cũng phổ biến nhất: creational pattern.

III, Creational pattern

  • Creational pattern được sử dụng để tạo những object mà không đòi hỏi bạn phải show ra logic hay các step để tạo ra object đó.
  • Do đó, mỗi lần bạn tạo ra 1 đối tượng, bạn không cần phải khởi tạo đối tượng thông qua việc sử dụng new operator nữa.
  • Mỗi ngôn ngữ hay system áp dụng design pattern một cách khác nhau.
  • Nhóm creational có rất nhiều cách implement tuỳ thuộc vào tình huống.
  • Mình sẽ đưa ra 3 pattern phổ biến nhát implement creational pattern: singleton pattern, dependency injection pattern và builder pattern.

1, Singleton pattern

  • Singleton pattern được sử dụng khi bạn muốn thực hiện 1 nhóm các tác vụ một lần.
  • Do đó trong ứng dụng chỉ có một instance được tạo ra và nó gắn với vòng đời của ứng dụng.
  • Singleton pattern thường được sử dụng trong các trường hợp: logging, database creation, thread pool…
  • Ví dụ: Với mỗi database, bạn chỉ cần tạo ra 1 Room instance duy nhất trong toàn bộ ứng dụng.
  • Singleton pattern rất được ưu ái, được Kotlin hỗ trợ việc implement thông qua việc sử dụng object classcompanion object class.
  • Trong kotlin, object class và companion object có một số đặc điểm đáng chú ý sau:
    • Không có contructor method.
    • Có thể có init{}, variables và functions.
    • Có thể extend 1 class khác nhưng không thể để class khác kế thừa.
  • Ví dụ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
object Singleton {

init {
println("Singleton class invoked.")
}

var variableName = "I am Var"

fun printVarName(){
println(variableName)
}

}

fun main(args: Array<String>) {
Singleton.printVarName()
Singleton.variableName = "New Name"
var a = A()
}

class A {

init {
println("Class init method. Singleton variableName property : ${Singleton.variableName}")
Singleton.printVarName()
}

}
  • Do companion object và object class không cho phép tạo constructor, do đó chúng ta không thể truyền tham số vào trong chúng.
  • Trong trường hợp, bạn muốn tạo 1 class implement singletion pattern và có constructor. Bạn hãy làm theo ví dụ sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MovieRepository(private val movieDbApi: MovieDbApi, private val genreDao: GenreDao) {

// Do functions

companion object {

// Singletion prevents multiple ínstances.
@Volatile
private var INSTANCE: MovieRepository? = null

fun getInstance(movieDbApi: MovieDbApi, genreDao: GenreDao): MovieRepository {
var temp = INSTANCE
if(temp != null){
return temp
}
synchronized(this){
val instance = MovieRepository(movieDbApi, genreDao)
INSTANCE = instance
return instance
}
}

}

}
  • Trong ví dụ, mình muốn tạo ra 1 single instance MovieRepository gắn với class để nó tồn tại trong suốt ứng dụng.
  • Trong companion object, chúng ta khai báo INSTANCE là 1 private property. Do đó trong java compiled file sẽ sinh ra 1 private static INSTANCE chỉ có thể lấy được qua function getInstance().
  • Để có đảm bảo INSTANCE đồng nhất (ngay cả khi multiple thread), bạn phải sử dụng @Volatile.
  • Để không có bất kì ảnh hưởng nào trong quá trình tạo instance, bạn phải sử dụng synchronized().

2, Dependency injection pattern

  • Chúng ta đều biết rằng hầu hết các class đều cần đến các dependency.
  • Nếu không thực hiện denpendecy injection pattern, thông thường chúng ta sẽ hard-code và khởi tạo dependency trong chính class đó:
1
2
3
4
5
6
class Repository {

private val database = DatabaseManager() // dependent on DatabaseManager.
private val api = NetworkManager() // dependent on NetworkManager.

}
  • Vấn đề: bạn thực hiện hard-code như trên ở nhiều nơi sau đó lại muốn thay đổi dependecy đó bằng dependency khác, bạn sẽ phải thay thế ở tất cả mọi nơi có khai báo đó.
  • Do đó chúng ta không nên khởi tạo trực tiếp dependency ở trong 1 class mà hãy cung cấp nó từ bên ngoài.
1
2
3
4
5
class Repository (private val database: DatabaseManager, private val api: NetworkManager) {

// Do functions

}
  • Google hỗ trợ chúng ta thực hiện dependecy injection pattern cách sử dụng dagger, hilt

3, Builder pattern

  • Trong một tác vụ, đôi khi bạn chỉ quan tâm đến một số dữ liệu đầu vào và không quan tâm phần còn lại (có thì càng tốt :grinning:). Trong trường hợp như vậy bạn nên cân nhắc tới việc sử dụng builder pattern.
  • Ví dụ: bạn cần mua 1 cái laptop, trong laptop có nhiều bộ phận nhưng bạn chỉ quan tâm tới: processor. Những bộ phận khác như ram, battery, screen bạn không quan tâm đến. Sau đó, người bán hàng sẽ đưa ra cho bạn mẫu laptop thoả mãn yêu cầu của bạn.
  • Trong lập trình cũng thế, bạn tạo 1 class có nhiều variable, trong đó 1 số tham số là quan trọng và một số khác không quan trọng, hãy cân nhắc tới việc sử dụng builder pattern.
  • Khi bạn thực hiện builder pattern, nó không yêu cầu bạn thực hiện tất cả các method và bạn cũng không cần quan tâm đến thứ tự của các method set.
  • Ví dụ: mình sẽ tạo ra 1 class Laptop theo yêu cầu ở ví dụ trước đưa ra:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Laptop private constructor(
val processor: String,
val ram: String,
val battery: String,
val screenSize: String
) {

// Builder class
class Builder(private val processor: String) {

// optional features
var ram: String = "2GB"
var battery: String = "5000MAH"
var screenSize: String = "15inch"

fun setRam(ram: String): Builder {
this.ram = ram
return this
}

fun setBattery(battery: String): Builder {
this.battery = battery
return this
}

fun setScreenSize(screenSize: String): Builder {
this.screenSize = screenSize
return this
}

fun create(): Laptop = Laptop(processor, ram, battery, screenSize)

}
}
  • Khi bạn muốn tạo ra 1 instance của Laptop class:
1
2
3
4
Laptop.Builder("i5")
.setRam("16GB")
.setBattery("7000MAH")
.create()
  • Android cũng có rất nhiều class được implement theo builder pattern như AlertDialog…

IV, Tổng kết

  • Bạn không cần vội vã học các thứ kiến thức cao siêu đâu. Những thứ căn bản sẽ giúp bạn trở nên tốt hơn đó.
  • Trong lập trình cũng thế, nó đòi hỏi sự tỉ mỉ, kiên trì và sáng tạo nên đi chậm mà chắc nhé sau đó tăng tốc nhé.
  • Qua bài này, chúng ta lại có 1 công cụ mới để kết hợp đó là Architecture component + design pattern.
  • Hẹn mọi người vào bài chia sẻ sau nhé !!!