1. Gioi thieu
Gan nhu ai cung bat dau giong nhau: lay main, ben trong goi readln(), vai if, mot danh sach chi tieu, vai lenh (add, list, remove) — va thay cuoc doi that de. Va do la giai doan binh thuong: ban dang hoc, ban muon thay ket qua nhanh, va println() cho mot chut dopamine de chiu. Nhung roi mot cau chuyen quen thuoc xay ra: lenh nhieu hon, kiem tra nhieu hon, dinh dang dau ra “chinh nhe mot chut” — va main bien thanh “trung tam chi huy tau vu tru”, noi vua la tay lai, vua la dong co, vua la bep, vua la con meo.
Hay minh hoa cach lam “tat ca o mot cho” thuong gap (khong phai “mot nguoi xau viet”, ma la “chung ta lon len theo cach do”):
fun main() {
val expenses = mutableListOf<String>()
while (true) {
print("> ")
val line = readln().trim()
if (line == "exit") break
if (line.startsWith("add ")) {
val title = line.removePrefix("add ").trim()
expenses.add(title)
println("Added: $title") // Added: Coffee
} else if (line == "list") {
println(expenses.joinToString())
} else {
println("Unknown command")
}
}
}
Luc dau co ve: “ok, doc duoc”. Roi ban se muon, vi du, moi chi tieu co so tien, so tien phai duong, khoang trang trong ten duoc xu ly dong nhat, dau ra dep hon, co the tinh tong. Va dot nhien ta phat hien ra rang:
- nhap lieu bi tron voi quy tac (validate/normalize),
- quy tac bi tron voi luu tru (ta truy cap truc tiep vao MutableList),
- xuat lieu bi tron voi tat ca nhung thu con lai,
- va bat cu thay doi nho nao cung dang so: nho la lam hong lenh add, va keo theo do — list, chi vi “ta co tai su dung mot bien mot chut chut”.
Y chinh: khi moi thu nam trong main, moi tinh nang moi khong chi them hon loan theo tuyen tinh, ma theo kieu lanh tuyet.
2. Ba vai tro: CLI, domain, storage
Bay gio ta se gioi thieu ba khai niem. Nghe co ve “chuyen nghiep”, nhung that ra chi la mot dieu rat don gian: ai chiu trach nhiem gi. Hay tuong tuong mot quan cafe nho:
- mot nguoi nhan don tu khach,
- mot nguoi che bien,
- nguoi thu ba quan ly kho/kiem ke nguyen lieu.
Neu mot nguoi lam tat ca moi viec cung luc, thi ho hoac la anh hung, hoac suc khoe se som can (thuong la cai thu hai). Trong chuong trinh cung tuong tu: ta can “phan cong lao dong”.
Trong du an console cua chung ta, cac vai tro nhu sau:
| Vai tro | Lam gi | Khong nen lam gi |
|---|---|---|
| CLI (Command Line Interface) | Doc dong chu, hieu lenh, in phan hoi | Khong chua quy tac nghiep vu, khong phan xu “dung/sai” ve ban chat |
| Domain (logic nghiep vu) | Biet quy tac: the nao la mot chi tieu, can kiem tra gi, tinh tong ra sao | Khong goi readln() va khong lam println() |
| Storage (luu tru) | Luu va tra du lieu (o v1 — chi trong bo nho) | Khong parse lenh va khong giai quyet quy tac nghiep vu |
Hay giu trong dau mot so do don gian ve luong du lieu:
flowchart LR
U[Nguoi dung] --> CLI[CLI: doc/parse]
CLI --> D[Domain: ap dung quy tac]
D --> S[Storage: luu/tra du lieu]
D --> CLI
CLI --> U
Va quy tac quan trong nhat hom nay: code domain phai co the duoc goi ma khong can console. Nghia la ve ly thuyet, ngay mai ta co the lam khong phai console ma la, vi du, GUI — va domain gan nhu khong can thay doi. (Ta se khong lam GUI, nhung y nay rat co ich.)
3. Vi du thuc hanh: quan ly chi tieu
Mo hinh domain: “du lieu sach”
Truoc khi tach trach nhiem, ta can thong nhat minh luu cai gi. Truoc day ta co the luu chi tieu nhu mot chuoi, hoac nhu Pair<String, Int>, hoac kieu “chuoi co ; o giua”. Hom nay ta tien mot buoc toi mo hinh dung dan: chi tieu la mot doi tuong voi cac truong ro rang.
Quan trong: mo hinh domain khong phai “logic”, ma la “du lieu cua linh vuc bai toan”. No can toi da don gian va “sach”: khong doc input, khong in, khong dinh dang.
data class Expense(
val title: String,
val amount: Int
)
O day co mot y nghia nho nhung then chot: khi ban thay Expense(title, amount), ban khong can nho “.first la gi, .second la gi”. Ban doc nhu mot con nguoi.
Neu ban muon them category, createdAt va cac thu khac — khoan da. Hom nay ta tap trung vao phan chia trach nhiem, khong phai lam “ke toan hoan hao”.
Storage: kho luu tru trong bo nho
Khi nghe “storage”, nhieu nguoi hay tuong tuong ve database, migration, transaction va mot chut hoang so. Hom nay don gian hon: lop storage o phien ban v1 la mot class an ben trong mot danh sach va cung cap method “them” va “lay tat ca”.
Tai sao can mot class rieng neu ta co the dung mutableListOf() ngay trong main? Vi ta muon “luu nhu the nao” tro thanh mot chi tiet rieng, khong bi trai khap chuong trinh.
Hay chu y mot diem quan trong: ben ngoai ta tra ve List<Expense>, khong phai MutableList<Expense>. Day la thoi quen cuu than: ai nhan duoc List thi khong the “am tham” xoa phan tu va de lai mot bug kho hieu. Trong Kotlin, dieu nay khop chinh xac voi y tuong ve read-only interface cua collection.
class ExpenseStorage {
private val items = mutableListOf<Expense>()
fun add(expense: Expense) {
items.add(expense)
}
fun all(): List<Expense> = items
}
Chu y: o day khong co readln() hay println(). Kho luu tru im lang nhu mot cai tu lanh tot: no khong binh luan ve quyet dinh cua ban.
Domain: service co quy tac, nhung khong in ra
Lop domain la noi “luat choi” song. Neu CLI la “noi chuyen voi nguoi dung nhu the nao”, thi domain la “the nao la mot chi tieu hop le”. Chinh o day moi hop ly de dung require, cac kiem tra va normalize chuoi.
Bay gio ta se tao ExpenseService, nhan vao cac gia tri da tach san (title, amount), dua chung ve dang chuan va luu thong qua storage.
class ExpenseService(
private val storage: ExpenseStorage
) {
fun addExpense(title: String, amount: Int) {
val normalizedTitle = title.trim()
require(normalizedTitle.isNotEmpty()) { "Title must not be empty" }
require(amount > 0) { "Amount must be > 0" }
storage.add(Expense(normalizedTitle, amount))
}
fun listExpenses(): List<Expense> = storage.all()
}
O day co hai y quan trong.
Thu nhat: domain nhan cac gia tri da san sang, khong phai “dong lenh thô”. Nghia la ExpenseService khong nen tu phan tich "add coffee 200". Do la viec cua CLI.
Thu hai: domain khong in “Added!” va khong hoi nguoi dung “hay nhap so tien”. No hoac thuc hien thao tac, hoac bao loi bang exception (hom nay la vay; ve sau ta se lam ket qua “co the dieu khien” hon, nhung do la phan tiep theo).
CLI: doc dong, parse lenh, goi domain
Lop CLI la “phien dich” giua con nguoi va chuong trinh. Nguoi dung viet chuoi, domain muon tham so dung kieu. Vay CLI phai lam ba buoc: doc, phan tich, goi.
readln() va println() song o day — do la dieu binh thuong va du kien.
Ta se lam mot model lenh nho va bo parse don gian nhat. Chu y: code nho va truc dien, khong “ao thuat” — ta uu tien tinh de doc.
data class ParsedCommand(
val name: String,
val args: String
)
fun parseCommand(line: String): ParsedCommand {
val trimmed = line.trim()
val space = trimmed.indexOf(' ')
return if (space == -1) {
ParsedCommand(name = trimmed.lowercase(), args = "")
} else {
val name = trimmed.substring(0, space).lowercase()
val args = trimmed.substring(space + 1).trim()
ParsedCommand(name = name, args = args)
}
}
Gio den phan logic CLI: ta doc dong, parse, lam when theo ten lenh, va sau do goi service.
Truoc mat ta lam ba lenh:
- add <title>;<amount>
- list
- exit
Tai sao tach bang ;? Vi trong ten co the co khoang trang, va ta can mot cach don gian de tach so tien. Day khong phai “dinh dang ly tuong”, ma la “tien cho vi du hoc tap”.
fun main() {
val storage = ExpenseStorage()
val service = ExpenseService(storage)
while (true) {
print("> ")
val cmd = parseCommand(readln())
if (cmd.name == "exit") break
when (cmd.name) {
"add" -> handleAdd(cmd.args, service)
"list" -> handleList(service)
else -> println("Unknown command: ${cmd.name}")
}
}
}
Luc nay main da giong mot kich ban: “doc → parse → chuyen tiep”. Va do la dau hieu rat tot.
Con lai la trien khai handleAdd va handleList. Chu y: day van la lop CLI, nen in ra o day la hop le.
fun handleAdd(args: String, service: ExpenseService) {
val parts = args.split(';', limit = 2)
val title = parts.getOrNull(0)?.trim().orEmpty()
val amount = parts.getOrNull(1)?.trim()?.toIntOrNull() ?: 0
try {
service.addExpense(title, amount)
println("OK: added") // OK: added
} catch (e: IllegalArgumentException) {
println("ERROR: ${e.message}") // ERROR: Amount must be > 0
}
}
Ta co y bat IllegalArgumentException, vi require(...) nem dung exception nay. Day la cach xu ly “mem”: chuong trinh khong sap, ma giai thich cho nguoi dung biet sai o dau.
handleList:
fun handleList(service: ExpenseService) {
val items = service.listExpenses()
if (items.isEmpty()) {
println("(empty)") // (empty)
return
}
for ((index, e) in items.withIndex()) {
println("${index + 1}. ${e.title} — ${e.amount}")
}
}
Dinh dang danh sach la nhiem vu cua CLI. Domain phai tra ve du lieu, con CLI quyet dinh cach hien thi cho dep.
Ranh gioi giua cac lop nam o dau
Khi ban bat dau tach code theo vai tro, se co mot van de tinh te: “cho toi pha le mot chut duoc khong, chi la mot println() thoi ma?”. Duoc. Cho den khi no thanh muoi cai “chi mot println”.
De ranh gioi khong bi loang, hay giu trong dau vai “tin hieu canh bao nguoc”.
- Neu ban thay readln() ben trong service — gan nhu chac chan la loi thiet ke. readln() thuoc ve console, va console thuoc ve CLI. Domain phai co kha nang hoat dong khi khong co nguoi ngoi ben canh.
- Neu ban thay println() ben trong storage — do cung la tin hieu: kho luu tru khong nen “binh luan ve cuoc doi”. No chi nen luu va tra du lieu.
- Neu ban tra ra ben ngoai MutableList, thi bat ky code ben ngoai nao cung co the goi clear(), va sau do ban se di tim “ai da an mat du lieu cua toi”. Kotlin collection phan biet ro read-only va mutable interface, va nen dung su khac biet nay nhu mot hang rao bao ve.
Doi khi ban nghi “co gi khac dau, du sao cung chi minh toi viet code”. Co khac. Nguoi khac do la ban sau hai tuan, mo lai du an, trong dau da co nhieu viec khac, nhieu muc tieu khac, va khong con chut hung thu nao de giai do cau do tu main.
4. Cac loi thuong gap khi tach CLI / domain / storage
Truoc khi ket thuc, hay di qua nhung “cai bay” ma gan nhu ai cung dap phai (toi cung tung dap, dung lo — day la truyen thong, giong nhu truyen thong cua lap trinh vien la tao them mot file “cuoi cung” nua co ten final_final2.kt).
Loi 1: domain bat dau in ra console.
Thuong trong rat vo hai: “toi muon in println("Added!") ngay trong addExpense”. Nhung nhu vay domain bi phu thuoc vao console; roi ban se muon dau ra khac (vi du bao cao mot dong, hoac khong in gi ca), va ban phai moi println ra khoi logic. Y dung: domain hoac tra ve du lieu, hoac bao loi; con viec in la trach nhiem cua CLI.
Loi 2: domain doc input (readln()) “cho tien”.
Rat de bi cam do: “de service tu hoi so tien”. Nhung khi do service khong the dung duoc ma khong co console tuong tac (vi du o mot giao dien khac), va cung khong ro noi nao dieu khien kich ban. readln() la cong cu danh rieng cho CLI, va no nen o dung cho.
Loi 3: storage bi lo ra ngoai nhu public val items: MutableList<Expense>.
Thuong nguoi ta lam “cho don gian”, roi mot doan code ngau nhien trong CLI goi items.removeAt(0) va logic bi vo. Storage phai an tinh mutable ben trong va ben ngoai chi tra List. Cac read-only interface cua collection ho tro y tuong nay va bao ve khoi mutating ngoai y muon.
Loi 4: CLI bat dau ap dung quy tac nghiep vu.
Vi du, trong handleAdd ban tu quyet dinh “so tien phai > 0” va kiem tra ngay o CLI, trong khi domain lai quen. Sau do se co mot con duong them du lieu khac (lenh khac, import, bat ky thu gi), va mot phan quy tac chi ton tai o mot noi. Quy tac nghiep vu phai song trong domain; CLI chi la “phien dich”, khong phai “tham phan”.
Loi 5: tron normalize voi hien thi.
Rat hay gap: chuoi title chi duoc cat bo khoang trang (trim()) luc in trong println vi “nhin dep hon”. Nhung dung hon la normalize du lieu ngay khi vao domain (nhu ta da lam), de ben trong chuong trinh du lieu luon o mot dinh dang. Khi do dau ra chi la dau ra, khong can “va” y nghia.
Loi 6: qua som co gang lam “kien truc hoan hao”.
Co mot cuc doan nguoc lai: doc thay tu “architecture” la lap tuc muon tao 25 class, 12 interface va mot so do ma chinh ban cung so. Hom nay muc tieu nho hon: tach nhap/xuat khoi quy tac va khoi luu tru. Neu ban giai thich duoc dau la CLI, dau la domain, dau la storage — ban da lam tot, va code se “binh tinh” hon ro ret.
GO TO FULL VERSION