Clean Architecture — สถาปัตยกรรมหลายชั้นที่เสนอโดย Robert Martin (Uncle Bob) ในปี 2012 ซึ่งแบ่งแอปพลิเคชันออกเป็นชั้นอิสระ: Domain (Entities, Use Cases), Data (Repositories, DataSources) และ Presentation (ViewModels, Views) หลักการสำคัญคือ Dependency Rule: การพึ่งพาชี้เข้าด้านใน ชั้นภายนอกพึ่งพาชั้นภายใน แต่ไม่ใช่ในทางกลับกัน Clean Architecture ถูกใช้ในการพัฒนาแอปพลิเคชันมือถือสำหรับโปรเจกต์ที่มีความซับซ้อนทางตรรกะทางธุรกิจสูง รายละเอียดเพิ่มเติมในหนังสือ The Clean Architecture
ประเด็นสำคัญ
Clean Architecture — รูปแบบสถาปัตยกรรมที่กำหนดโดย Robert Martin (Uncle Bob) ในปี 2012 แนวคิดหลักคือการแบ่งแอปพลิเคชันออกเป็นชั้น ๆ ด้วยกฎการพึ่งพาที่เข้มงวด: โค้ดภายในชั้นไม่รู้เกี่ยวกับโค้ดภายนอก ชั้นภายนอก (UI, เฟรมเวิร์ก, DB) เป็นรายละเอียดการนำไปใช้ ชั้นภายใน (ตรรกะทางธุรกิจ, กฎขององค์กร) เป็นแก่นแท้ของแอปพลิเคชัน
ชั้นของ Clean Architecture ในการพัฒนาแอปพลิเคชันมือถือ: 1) Domain — Entities (วัตถุทางธุรกิจ) และ Use Cases (สถานการณ์การใช้งาน); 2) Data — RepositoryImpl (การนำ Repository ไปใช้), DataSources (เครือข่าย, DB, แคช); 3) Presentation — ViewModels, Views (Compose/SwiftUI) Domain เป็นชั้นในสุดที่ไม่มีการพึ่งพา Data พึ่งพา Domain (นำอินเทอร์เฟส Repository ไปใช้) Presentation พึ่งพา Domain (เรียก Use Cases, สมัครรับผลลัพธ์)
| ชั้น | ประกอบด้วย | การพึ่งพา |
|---|---|---|
| Domain | Entities, Use Cases, Repository Interfaces | ไม่มี (Kotlin/Swift บริสุทธิ์) |
| Data | RepositoryImpl, DataSources (API, DB, แคช) | Domain, Retrofit, Room, Ktor |
| Presentation | ViewModels, Views, Composables | Domain, Jetpack, SwiftUI |
Dependency Rule — กฎที่เข้มงวดเพียงข้อเดียวของ Clean Architecture โค้ดต้นฉบับสามารถอ้างอิงได้เฉพาะชั้นภายในตัวเองหรือชั้นที่ต่ำกว่า (ใกล้ศูนย์กลางกว่า) Presentation นำเข้า Domain Domain ไม่ได้นำเข้า Data หรือ Presentation สิ่งนี้ทำได้ผ่านหลักการกลับด้านการพึ่งพา (Dependency Inversion Principle): Domain กำหนดอินเทอร์เฟส Repository, Data นำไปใช้ Presentation พึ่งพานามธรรมของ UseCase ไม่ใช่ Repository เฉพาะ
Domain — ชั้นที่เสถียรที่สุด ของแอปพลิเคชัน Entities เป็นวัตถุทางธุรกิจที่ไม่ขึ้นกับเฟรมเวิร์ก: User, Product, Order Use Cases เป็นคลาสที่มีเมธอด invoke เดียว (หรือ operator fun invoke ใน Kotlin) ซึ่งนำสถานการณ์เดียวไปใช้: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase Repository Interfaces เป็นนามธรรมการเข้าถึงข้อมูลที่กำหนดใน Domain และนำไปใช้ใน Data Domain ไม่มี Android SDK, iOS UIKit, Retrofit, Room — มีเพียง Kotlin หรือ Swift บริสุทธิ์
// Entity — วัตถุทางธุรกิจ (Domain)
struct User: Equatable {
let id: Int
let name: String
let email: String
}
// Repository Interface — นามธรรมข้อมูล (Domain)
protocol UserRepository {
func getUser(id: Int) async throws -> User
func getUsers() async throws -> [User]
}
// Use Case — หนึ่งสถานการณ์ (Domain)
final class GetUserUseCase {
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func execute(id: Int) async throws -> User {
return try await repository.getUser(id: id)
}
}
Use Case — «คลาสที่มีเมธอดเดียว» ไม่ใช่หลักปฏิบัติแต่เป็นคำแนะนำในทางปฏิบัติ เมื่อ Use Case ซับซ้อนขึ้น (การตรวจสอบ + การบันทึก + การเรียก Repository) เมธอดของมันจะถูกจัดกลุ่มตามความหมาย: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser สิ่งสำคัญคือ Use Case ไม่ควรรู้ว่าข้อมูลมาจากไหน (เครือข่าย, DB, แคช) หรือใครเป็นผู้แสดงผล (Compose, SwiftUI) ที่ IT Sectr เราจัดสรร Use Case สำหรับทุกการดำเนินการที่มีกฎทางธุรกิจ การตรวจสอบ หรือการรวมข้อมูลจากสองแหล่ง
ความบริสุทธิ์ของ Domain ทำได้ผ่านการแมป DTO ที่ขอบเขตของชั้น ชั้น Data รับโมเดล JSON (DTO) แมปไปยัง Domain Entity ชั้น Presentation รับ Domain Entity แมปไปยัง ViewModel (DisplayItem) Domain Entity ไม่เคยมีคำอธิบายประกอบของ Retrofit, Room หรือ Codable — สิ่งนี้รับประกันว่าชั้นจะไม่ต้องเปลี่ยนแปลงเมื่อเปลี่ยน DB จาก Room เป็น Realm หรือเมื่อเปลี่ยน Retrofit เป็น Ktor
ชั้น Data — การนำอินเทอร์เฟสที่กำหนดใน Domain ไปใช้ ประกอบด้วย RepositoryImpl (คลาสที่นำ UserRepository ไปใช้) และ DataSources (RemoteDataSource — API, LocalDataSource — DB, CacheDataSource — SharedPreferences/NSUserDefaults) ชั้น Data พึ่งพา Domain (นำเข้าอินเทอร์เฟส Repository และ Entities) และเฟรมเวิร์ก (Retrofit, Room, Ktor, CoreData) RepositoryImpl ซ่อนแหล่งข้อมูลจาก Domain — Use Case ไม่รู้ว่าข้อมูลมาจากเครือข่ายหรือแคช
// DTO — โมเดลสำหรับเครือข่าย (Data)
data class UserDto(
@SerializedName("id") val id: Int,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("email") val email: String
)
// RepositoryImpl — การนำไปใช้ (Data)
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): User {
// กำลังพยายามดึงจากแคช
localDataSource.getUser(id)?.let { return it.toDomain() }
// ถ้าไม่มี — โหลดจากเครือข่าย
val dto = remoteDataSource.fetchUser(id)
val user = dto.toDomain()
localDataSource.saveUser(user)
return user
}
override suspend fun getUsers(): List<User> {
return remoteDataSource.fetchAllUsers().map { it.toDomain() }
}
}
// Mapper — การแปลง DTO ↔ Domain
fun UserDto.toDomain() = User(
id = id,
name = "$firstName $lastName",
email = email
)
กลยุทธ์การแคช ในชั้น Data: RepositoryImpl ตรวจสอบพื้นที่จัดเก็บในเครื่องก่อน หากไม่พบข้อมูล ให้โหลดจากเครือข่ายและบันทึกในเครื่อง หากเครือข่ายไม่พร้อมใช้งาน ให้ส่งคืนข้อมูลที่ล้าสมัยพร้อมแฟล็ก isStale Use Case ใน Domain ไม่รู้เกี่ยวกับกลยุทธ์ — มันรับ User ผ่าน Repository.getUser(id) การเปลี่ยนกลยุทธ์ (เช่น การทำให้แคชไม่ถูกต้องทุก 15 นาที) ไม่ส่งผลกระทบต่อ Domain และ Presentation
ความเป็นโมดูลใน Android — Kotlin Multiplatform อนุญาตให้แยก Domain เป็นโมดูล KMP แยกต่างหากโดยไม่มีการพึ่งพา Android SDK Data เป็นโมดูลแยกต่างหากที่มีการพึ่งพา Domain Presentation เป็นโมดูล Android ที่มีการพึ่งพา Domain การพึ่งพา Gradle: domain (Kotlin บริสุทธิ์), data (domain + Retrofit + Room), app (domain + presentation + Hilt) ความเป็นโมดูลดังกล่าวจำเป็นสำหรับโปรเจกต์ขนาดใหญ่ — CI สร้าง Domain แยกต่างหาก การทดสอบหน่วยของ Domain ไม่ต้องการโปรแกรมจำลอง Android
ชั้น Presentation — ชั้นนอกสุดของ Clean Architecture ประกอบด้วย ViewModels (Android) / ObservableObject (iOS) และ Views (Compose/SwiftUI) ViewModel เรียก Use Case รับผลลัพธ์และแปลงเป็นสถานะ UI View สมัครรับสถานะและแสดงผล Presentation พึ่งพา Domain — นำเข้า Use Cases และ Entities Presentation ไม่ได้นำเข้าชั้น Data — ข้อมูลมาผ่าน Use Case ซึ่งภายในใช้ Repository
ViewModel ใน Clean Architecture ไม่มีตรรกะทางธุรกิจ — มันเรียก Use Case หาก Use Case ส่งคืน User ViewModel จะแปลงเป็น UserDisplayItem (name, emailFormatted, avatarUrl) — โมเดลการนำเสนอล้วน ๆ Use Case ไม่รู้เกี่ยวกับ DisplayItem — มันส่งคืน Entity การแยกนี้ช่วยให้ทดสอบ Use Case โดยไม่มี UI และทดสอบ ViewModel โดยไม่มี UseCase (ผ่าน mock) ที่ IT Sectr เราปฏิบัติตามอย่างเคร่งครัด: Use Case — ตรรกะทางธุรกิจ, ViewModel — การนำเสนอเท่านั้น, View — การแสดงผลเท่านั้น
// Use Case (Domain) — ตรรกะทางธุรกิจบริสุทธิ์
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: Int): User {
return repository.getUser(id)
}
}
// ViewModel (Presentation) — การนำเสนอเท่านั้น
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = UserScreenState.Loading
val user = getUserUseCase(id)
val displayItem = UserDisplayItem(
name = user.name,
email = user.email,
initials = user.name.split(" ").joinToString("") { it.first().toString() }
)
_state.value = UserScreenState.Success(displayItem)
}
}
}
data class UserDisplayItem(
val name: String,
val email: String,
val initials: String
)
sealed interface UserScreenState {
data object Loading : UserScreenState
data class Success(val displayItem: UserDisplayItem) : UserScreenState
data class Error(val message: String) : UserScreenState
}
การนำทาง ในชั้น Presentation ก็เป็นส่วนหนึ่งของวงแหวนภายนอกเช่นกัน Clean Architecture ไม่ได้กำหนดกลไกการนำทาง — อาจเป็น NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) หรือ Router (VIPER) สิ่งสำคัญคือการตัดสินใจนำทางทำโดย Presentation แต่การนำทางไม่ควรแทรกซึมเข้าไปใน Use Case Use Case ส่งคืนผลลัพธ์ ViewModel ตัดสินใจว่าจะไปที่หน้าจอใด ใน Clean Architecture การนำทางเป็นรายละเอียดที่สามารถเปลี่ยนได้โดยไม่ต้องเปลี่ยน Domain
Clean Architecture บน Android ถูกนำไปใช้ผ่านโมดูล Gradle: domain (Kotlin บริสุทธิ์), data (domain + Retrofit + Room), presentation (domain + Compose) โครงสร้างโฟลเดอร์: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt DI (Hilt) เชื่อมต่อชั้น: UserRepositoryImpl ถูกผูกกับอินเทอร์เฟส UserRepository ในโมดูล domain
Clean Architecture บน iOS ใช้ SPM หรือกลุ่ม Xcode โดยไม่มีโมดูลแยก (เนื่องจากข้อจำกัดของ Xcode) Domain — โฟลเดอร์ที่มีไฟล์ที่ไม่นำเข้า UIKit หรือ SwiftUI Data — โฟลเดอร์ที่มี APIClient, CoreDataStack, RepositoryImpl Presentation — โฟลเดอร์ที่มี ViewModels และ SwiftUI Views DI ผ่านคอนสตรัคเตอร์หรือการประกอบในแอป การเรียกหลักคือ async/await ผ่าน UseCase.execute() พร้อมการตรวจสอบ MainActor สำหรับการอัปเดต UI
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
private let apiClient: APIClient
func fetchUser(id: Int) async throws -> UserDTO {
return try await apiClient.get("/users/\(id)")
}
}
// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUser(id: Int) async throws -> User {
if let cached = try await local.getUser(id) {
return cached
}
let dto = try await remote.fetchUser(id)
let user = dto.toDomain()
try await local.saveUser(user)
return user
}
}
// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserScreenState = .loading
private let getUserUseCase: GetUserUseCase
func loadUser(id: Int) {
Task {
state = .loading
if let user = try? await getUserUseCase.execute(id: id) {
state = .success(user)
} else {
state = .error("Failed to load")
}
}
}
}
Clean Architecture ในโปรเจกต์ IT Sectr — มาตรฐานของเราสำหรับโปรเจกต์ตั้งแต่ 30 วันขึ้นไป เราใช้สถาปัตยกรรมสามชั้นกับ Kotlin Multiplatform สำหรับ Android/iOS ตั้งแต่ปี 2022 Domain — โมดูล KMP ที่ใช้ร่วมกัน, Data — โมดูลแพลตฟอร์ม (Retrofit บน Android, URLSession บน iOS), Presentation — UI ดั้งเดิม สิ่งนี้ให้โค้ดตรรกะทางธุรกิจที่ใช้ร่วมกัน 60–80% ระหว่าง iOS และ Android ลดเวลาในการพัฒนา 30–40% เมื่อเทียบกับการนำไปใช้สองแบบแยกกัน
คำถามที่พบบ่อย
อย่างน้อยสามชั้น: Domain, Data, Presentation สำหรับโปรเจกต์ขนาดใหญ่ จะเพิ่ม Framework (การพึ่งพา Android SDK/iOS UIKit) และ Device (GPS, กล้อง, เซ็นเซอร์) จำนวนชั้นไม่ใช่กฎที่ตายตัว แต่เป็นเรื่องของความสะดวก สิ่งสำคัญคือปฏิบัติตาม Dependency Rule: การพึ่งพาชี้เข้าด้านในสู่ Domain คุณสามารถเริ่มต้นด้วยสามชั้นและเพิ่มชั้นเมื่อโปรเจกต์เติบโตขึ้น
ใช่ — เพิ่มขึ้น 30–50% เมื่อเทียบกับ MVVM เนื่องจากการแยกอินเทอร์เฟส Repository, Use Cases และแมปเปอร์ ซึ่งมากเกินไปสำหรับแอปพลิเคชัน CRUD ธรรมดา Clean Architecture เหมาะสำหรับโปรเจกต์ที่มีตรรกะทางธุรกิจซับซ้อนซึ่งความสามารถในการทดสอบและการแยกชั้นสำคัญกว่าความเร็วในการพัฒนา สำหรับ MVP หรือต้นแบบ ให้ใช้ MVVM — Clean Architecture จะทำให้การเริ่มต้นช้าลง
ได้ นี่คือแนวทางปฏิบัติทั่วไป Use Cases ยังคงอยู่ใน Domain ในขณะที่ Presentation ใช้วงจร MVI (Intent → Reducer → State) ชั้น Data ยังคงเหมือนเดิม Domain ยังคงเหมือนเดิม MVI ใน Presentation ให้สถานะหน้าจอที่คาดเดาได้ Clean Architecture ให้การแยกตรรกะทางธุรกิจ การรวมกันนี้ใช้ในโปรเจกต์ขนาดใหญ่ที่มีนักพัฒนาหลายสิบคน
จำเป็นต้องมี Use Case เมื่อการดำเนินการเกี่ยวข้องกับกฎทางธุรกิจ: การตรวจสอบ การรวมข้อมูลจากสองแหล่ง การคำนวณ การบันทึก การตรวจสอบสิทธิ์การเข้าถึง คำขอ getUser(id) อย่างง่ายโดยไม่มีตรรกะเพิ่มเติมสามารถเรียก Repository โดยตรงจาก ViewModel อย่างไรก็ตาม เพื่อความสอดคล้องทางสถาปัตยกรรม ทีมงานหลายแห่งสร้าง Use Case สำหรับทุกเมธอดสาธารณะของ Repository — สิ่งนี้เพิ่มโค้ด 5–10% แต่ทำให้การอ่านง่ายขึ้น
Domain: การทดสอบหน่วยของ Use Cases ด้วย Mock Repository — Kotlin/Swift บริสุทธิ์โดยไม่มี Android SDK Data: การทดสอบการรวมของ RepositoryImpl ด้วย DataSource จำลอง/ปลอม Presentation: การทดสอบ ViewModel ด้วย UseCase ปลอม ด้วย Dependency Rule แต่ละชั้นจะถูกทดสอบแยกกัน ที่ IT Sectr ความครอบคลุมของ Domain สูงถึง 95%, Data — 70–80%, Presentation — 60–70%
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม