Object Declaration và Singleton trong Kotlin
🎯 Mục tiêu: Hiểu Object Declaration, Object Expression, và cách Kotlin hỗ trợ Singleton pattern ngay trong ngôn ngữ.
💡 Singleton Pattern là gì?
Singleton là design pattern đảm bảo một class chỉ có một instance duy nhất.
Trong Java, bạn phải tự implement:
// Java - verbose singleton
public class Database {
private static Database instance;
private Database() { }
public static synchronized Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
}Kotlin có syntax riêng cho Singleton - object declaration:
// Kotlin - simple singleton
object Database {
fun connect() = println("Connected")
}
fun main() {
Database.connect() // Connected
// Database là singleton - chỉ có 1 instance
}📝 Object Declaration
Cú pháp cơ bản
object MySingleton {
val property = "I'm a singleton property"
fun doSomething() {
println("Doing something")
}
}
fun main() {
println(MySingleton.property) // I'm a singleton property
MySingleton.doSomething() // Doing something
// MySingleton() ❌ Không thể tạo instance mới
}Ví dụ thực tế: Logger
object Logger {
private val logs = mutableListOf<String>()
fun log(level: String, message: String) {
val timestamp = java.time.LocalDateTime.now()
val entry = "[$timestamp][$level] $message"
logs.add(entry)
println(entry)
}
fun info(message: String) = log("INFO", message)
fun warn(message: String) = log("WARN", message)
fun error(message: String) = log("ERROR", message)
fun getLogs(): List<String> = logs.toList()
fun clearLogs() = logs.clear()
}
fun main() {
Logger.info("Application started")
Logger.warn("Low memory")
Logger.error("Connection failed")
println("Total logs: ${Logger.getLogs().size}") // 3
}Database Connection Singleton
object DatabaseConnection {
private var isConnected = false
private lateinit var connectionString: String
fun configure(connectionString: String) {
this.connectionString = connectionString
}
fun connect(): Boolean {
if (!::connectionString.isInitialized) {
throw IllegalStateException("Database not configured")
}
if (!isConnected) {
println("Connecting to: $connectionString")
isConnected = true
} else {
println("Already connected")
}
return isConnected
}
fun disconnect() {
if (isConnected) {
println("Disconnecting...")
isConnected = false
}
}
fun query(sql: String): String {
require(isConnected) { "Not connected to database" }
return "Result for: $sql"
}
}
fun main() {
DatabaseConnection.configure("localhost:5432/mydb")
DatabaseConnection.connect()
println(DatabaseConnection.query("SELECT * FROM users"))
DatabaseConnection.disconnect()
}🔧 Object Declaration với Inheritance
Object có thể inherit class hoặc implement interface:
interface Comparator<T> {
fun compare(a: T, b: T): Int
}
// Object implementing interface
object StringLengthComparator : Comparator<String> {
override fun compare(a: String, b: String): Int {
return a.length - b.length
}
}
// Object extending class
open class Animal(val name: String) {
open fun speak() = println("...")
}
object Dog : Animal("Dog") {
override fun speak() = println("Woof!")
}
fun main() {
val words = listOf("apple", "hi", "banana")
val sorted = words.sortedWith(StringLengthComparator)
println(sorted) // [hi, apple, banana]
Dog.speak() // Woof!
}🎭 Object Expression (Anonymous Objects)
Tạo object ẩn danh - thường dùng thay cho anonymous class trong Java:
Implement interface
interface OnClickListener {
fun onClick()
}
fun setClickListener(listener: OnClickListener) {
listener.onClick()
}
fun main() {
// Object expression - anonymous object
setClickListener(object : OnClickListener {
override fun onClick() {
println("Button clicked!")
}
})
}Implement nhiều interfaces
interface Runnable {
fun run()
}
interface Cancelable {
fun cancel()
}
fun main() {
val task = object : Runnable, Cancelable {
override fun run() {
println("Task running...")
}
override fun cancel() {
println("Task cancelled")
}
}
task.run()
task.cancel()
}Object expression với properties
fun createPoint(x: Int, y: Int) = object {
val xCoord = x
val yCoord = y
override fun toString() = "($xCoord, $yCoord)"
}
fun main() {
val point = createPoint(10, 20)
println(point) // (10, 20)
println("X: ${point.xCoord}") // X: 10
}Scope của anonymous object properties:
Properties chỉ accessible khi return type là object expression (inferred type), không phải named type.
// ✅ Works - return type inferred as object
private fun getPoint() = object { val x = 1; val y = 2 }
// ❌ Properties not accessible - return type is Any
fun getPointPublic(): Any = object { val x = 1; val y = 2 }🏷️ Companion Object vs Object Declaration
| Feature | Companion Object | Object Declaration |
|---|---|---|
| Liên kết với class | ✅ Có | ❌ Không |
| Số lượng per class | 1 | N/A |
| Access syntax | ClassName.member | ObjectName.member |
| Có thể có tên | ✅ Có | N/A |
| Dùng cho | Static-like members | Standalone singletons |
// Companion object - gắn với class
class User(val name: String) {
companion object {
fun create(name: String) = User(name)
}
}
// Object declaration - standalone
object UserRepository {
private val users = mutableListOf<User>()
fun add(user: User) = users.add(user)
fun getAll() = users.toList()
}
fun main() {
val user = User.create("Alice") // Companion object
UserRepository.add(user) // Object declaration
}📦 Object với State
object ShoppingCart {
private val items = mutableListOf<CartItem>()
val totalItems: Int get() = items.size
val totalPrice: Double get() = items.sumOf { it.price * it.quantity }
fun addItem(item: CartItem) {
val existing = items.find { it.productId == item.productId }
if (existing != null) {
existing.quantity += item.quantity
} else {
items.add(item)
}
}
fun removeItem(productId: String) {
items.removeAll { it.productId == productId }
}
fun clear() = items.clear()
override fun toString(): String {
return items.joinToString("\n") { "- ${it.name}: ${it.quantity} x ${it.price}" }
}
}
data class CartItem(
val productId: String,
val name: String,
val price: Double,
var quantity: Int = 1
)
fun main() {
ShoppingCart.addItem(CartItem("1", "Laptop", 999.0, 1))
ShoppingCart.addItem(CartItem("2", "Mouse", 29.0, 2))
ShoppingCart.addItem(CartItem("1", "Laptop", 999.0, 1)) // Cộng quantity
println(ShoppingCart)
println("Total: $${ShoppingCart.totalPrice}")
// - Laptop: 2 x 999.0
// - Mouse: 2 x 29.0
// Total: $2056.0
}🛠️ Thực hành
Bài tập: Tạo ConfigManager singleton
// TODO: Tạo ConfigManager object với:
// - load(configMap: Map<String, Any>)
// - get(key: String): Any?
// - getString(key: String, default: String): String
// - getInt(key: String, default: Int): Int
// - getBoolean(key: String, default: Boolean): BooleanLời giải:
object ConfigManager {
private val config = mutableMapOf<String, Any>()
fun load(configMap: Map<String, Any>) {
config.clear()
config.putAll(configMap)
println("Loaded ${config.size} config values")
}
fun get(key: String): Any? = config[key]
fun getString(key: String, default: String = ""): String {
return config[key]?.toString() ?: default
}
fun getInt(key: String, default: Int = 0): Int {
return (config[key] as? Number)?.toInt() ?: default
}
fun getBoolean(key: String, default: Boolean = false): Boolean {
val value = config[key]
return when (value) {
is Boolean -> value
is String -> value.lowercase() == "true"
else -> default
}
}
fun keys(): Set<String> = config.keys.toSet()
}
fun main() {
ConfigManager.load(mapOf(
"app.name" to "MyApp",
"app.version" to 1,
"debug" to true,
"max.connections" to 100
))
println(ConfigManager.getString("app.name")) // MyApp
println(ConfigManager.getInt("max.connections")) // 100
println(ConfigManager.getBoolean("debug")) // true
println(ConfigManager.getString("missing", "N/A")) // N/A
}📱 Trong Android
// Application-wide singleton
object AppPreferences {
private lateinit var prefs: SharedPreferences
fun init(context: Context) {
prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
}
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit().putBoolean("first_launch", value).apply()
var authToken: String?
get() = prefs.getString("auth_token", null)
set(value) = prefs.edit().putString("auth_token", value).apply()
}
// Network singleton with Retrofit
object ApiClient {
private const val BASE_URL = "https://api.example.com/"
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
inline fun <reified T> createService(): T {
return retrofit.create(T::class.java)
}
}
// Click listener with object expression
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
// Handle click
}
})
// Simplified with lambda (SAM conversion)
button.setOnClickListener { /* Handle click */ }⚠️ Lưu ý quan trọng
Initialization: Object declaration được khởi tạo lazy (lần đầu access) và thread-safe.
Testing concern: Singleton khó mock/test. Trong production code, cân nhắc:
- Dependency Injection thay vì global singletons
- Interface abstraction để dễ mock
interface ILogger {
fun log(message: String)
}
object DefaultLogger : ILogger {
override fun log(message: String) = println(message)
}
// Testing
class FakeLogger : ILogger {
val logs = mutableListOf<String>()
override fun log(message: String) = logs.add(message).let {}
}✅ Checklist - Tự kiểm tra
Sau bài học này, bạn có thể:
- Tạo singleton với
objectdeclaration - Hiểu object declaration được khởi tạo lazy và thread-safe
- Tạo object kế thừa class hoặc implement interface
- Sử dụng object expression cho anonymous objects
- Phân biệt companion object và object declaration
- Biết hạn chế của singleton trong testing
Tiếp theo: Companion Object