Separation of Concerns เป็นหลักการที่แต่ละโมดูลหรือเลเยอร์ของแอปพลิเคชันรับผิดชอบหนึ่งขอบเขตของความรับผิดชอบ ตามข้อมูลจาก Wikipedia คำศัพท์นี้ถูกนำเสนอโดย Edsger Dijkstra ในปี 1974 และตั้งแต่นั้นมาก็กลายเป็นรากฐานของสถาปัตยกรรมซอฟต์แวร์ การแยก ความรับผิดชอบช่วยให้นักพัฒนาสามารถเปลี่ยนเลเยอร์หนึ่งของโค้ดได้โดยไม่ส่งผลกระทบต่อเลเยอร์อื่น ซึ่งเป็นสิ่งสำคัญอย่างยิ่งในโปรเจกต์โมบายล์ที่มีวงจรการสนับสนุนที่ยาวนาน
ประเด็นสำคัญ
Separation of Concerns เป็นหลักการของการแยกส่วนระบบซอฟต์แวร์ออกเป็นส่วนอิสระ โดยแต่ละส่วนแก้ไขงานหนึ่งงาน คำว่า concern (ขอบเขตความรับผิดชอบ) หมายถึงส่วนใดๆ ที่แยกออกได้ของฟังก์ชันการทำงาน: การแสดงหน้าจอ การจัดการการแตะ การตรวจสอบข้อมูล หรือการสื่อสารเครือข่าย หลักการนี้กำหนดให้จัดกลุ่มโค้ดในลักษณะที่การเปลี่ยนแปลงในพื้นที่หนึ่งไม่จำเป็นต้องมีการเปลี่ยนแปลงในพื้นที่อื่น
ในการพัฒนาโมบายล์ SoC แสดงออกในหลายระดับ: ตั้งแต่การแบ่งแอปพลิเคชันออกเป็นหน้าจอไปจนถึงการจัดระเบียบโค้ดภายในคลาสเดียว Activity หรือ ViewController ที่โหลดข้อมูลจากเครือข่าย แยกวิเคราะห์ JSON และเรนเดอร์ UI พร้อมกันนั้นละเมิด Separation of Concerns — โค้ดแบบนี้ยากต่อการบำรุงรักษา ทดสอบ และขยาย ทางเลือกคือแยกความรับผิดชอบแต่ละอย่างออกเป็นคอมโพเนนต์แยกต่างหาก
หลักการนี้เกี่ยวข้องอย่างใกล้ชิดกับแนวคิดของ นามธรรม: แต่ละเลเยอร์จัดเตรียมอินเทอร์เฟซที่กำหนดอย่างเคร่งครัดและซ่อนรายละเอียดการนำไปใช้ ด้วยเหตุนี้ นักพัฒนาสามารถแทนที่ไลบรารีเครือข่ายหรือฐานข้อมูลได้โดยไม่ต้องเขียนตรรกะ UI ใหม่ ซึ่งมีคุณค่าเป็นพิเศษในโปรเจกต์ระยะยาวที่ความต้องการและเทคโนโลยีเปลี่ยนแปลงไปตามกาลเวลา
Edsger Dijkstra กำหนดแนวคิดของ Separation of Concerns ครั้งแรกในบทความปี 1974 เรื่อง «On the Role of Scientific Thought» เขาโต้แย้งว่าความซับซ้อนของระบบซอฟต์แวร์สามารถควบคุมได้โดยการแบ่งออกเป็นส่วนที่ถูกวิเคราะห์แยกกัน แนวทางนี้ตรงกันข้ามกับโปรแกรมแบบเสาหินในยุคนั้น ที่โค้ดผสมผสานการคำนวณ อินพุต-เอาต์พุต และอินเทอร์เฟซผู้ใช้
ในทศวรรษ 1980 แนวคิดนี้ถูกพัฒนาต่อโดยผู้สนับสนุน การเขียนโปรแกรมแบบมีโครงสร้าง และต่อมาโดยแนวทางเชิงวัตถุ ภาษาเช่น Smalltalk และ C++ จัดเตรียมกลไกการห่อหุ้มและการทำให้เป็นโมดูล ซึ่งทำให้ SoC เป็นเครื่องมือที่ใช้งานได้จริง รูปแบบสถาปัตยกรรมสมัยใหม่ — MVC, MVP, MVVM และ Clean Architecture — เป็นการแสดงออกโดยตรงของหลักการ Separation of Concerns
ในโลกของการพัฒนาโมบายล์ Apple ส่งเสริม MVC เป็นมาตรฐานสำหรับ iOS โดยที่ Model-View-Controller แยกข้อมูล การแสดงผล และตรรกะควบคุม Google สำหรับ Android เสนอคำแนะนำทางสถาปัตยกรรมที่อิงตาม ViewModel และ Repository — แต่ละคอมโพเนนต์แก้ไขงานจำกัดของตนเอง หากไม่มี SoC แอปพลิเคชันโมบายล์จะกลายเป็น Massive View Controller — คลาสที่มีโค้ดหลายพันบรรทัดซึ่งการเปลี่ยนแปลงใดๆ เสี่ยงต่อการทำลายฟังก์ชันการทำงานทั้งหมด
สี่เลเยอร์หลัก ก่อให้เกิดสถาปัตยกรรมแอปพลิเคชันโมบายล์ทั่วไปที่นำ Separation of Concerns ไปใช้ แต่ละเลเยอร์รับผิดชอบเฉพาะโดเมนของตนและโต้ตอบกับเลเยอร์ข้างเคียงผ่านอินเทอร์เฟซ
View รับผิดชอบเฉพาะการแสดงข้อมูลและการจัดการเหตุการณ์ผู้ใช้ ใน iOS คือ UIViewController และ UIView ใน Android — Fragment หรือ Activity ViewModel ประกอบด้วยสถานะหน้าจอและตรรกะในการแปลงข้อมูลเป็นรูปแบบที่พร้อมสำหรับการแสดงผล การแยกทำให้มั่นใจว่าการแทนที่ UIKit ด้วย SwiftUI หรือการเขียนหน้าจอใหม่ด้วย Jetpack Compose จะไม่ส่งผลกระทบต่อตรรกะธุรกิจ
การทดสอบ ViewModel ไม่จำเป็นต้องเรียกใช้อีมูเลเตอร์หรือซิมิวเลเตอร์ — การทดสอบหน่วยที่ตรวจสอบการแปลงข้อมูลและการตอบสนองต่อการกระทำของผู้ใช้ก็เพียงพอแล้ว นี่เป็นผลโดยตรงของ Separation of Concerns: UI ไม่ผสมกับกฎทางธุรกิจ และแต่ละคอมโพเนนต์ถูกทดสอบแยกกัน
Use Case (หรือ Interactor) ประกอบด้วยกฎทางธุรกิจของแอปพลิเคชัน — การคำนวณ การตรวจสอบ การเรียบเรียงการเรียกข้อมูล เลเยอร์นี้ไม่รู้จักการมีอยู่ของ UI หรือเฟรมเวิร์กของแพลตฟอร์ม Use Case รับข้อมูลจาก Repository ใช้ตรรกะกับข้อมูลนั้น และส่งคืนผลลัพธ์ที่เสร็จสมบูรณ์ให้ ViewModel การแยกช่วยให้สามารถใช้ Use Case เดียวกันซ้ำบนหน้าจอต่างๆ ได้
ตัวอย่างเช่น LoginUseCase ตรวจสอบความถูกต้องของอีเมล เรียก AuthRepository เพื่อยืนยันตัวตน และส่งคืนผลลัพธ์ มันไม่ได้ขึ้นอยู่กับว่าหน้าจอเข้าสู่ระบบมีลักษณะอย่างไร — SwiftUI, UIKit หรือ Compose หากกฎทางธุรกิจเปลี่ยนแปลง เพียงแค่แก้ไข Use Case เดียวก็เพียงพอ โดยไม่ต้องแตะต้อง UI หรือฐานข้อมูล
Repository ทำให้แหล่งข้อมูลเป็นนามธรรม: API ระยะไกล ฐานข้อมูลท้องถิ่น หรือแคชในหน่วยความจำ ViewModel และ Use Case ไม่ทราบว่าข้อมูลมาจากไหน — Repository ตัดสินใจว่าจะโหลดจากเครือข่ายหรือจากแคช การแยกนี้ช่วยให้เปลี่ยนการนำพื้นที่จัดเก็บไปใช้ได้โดยไม่ส่งผลกระทบต่อตรรกะธุรกิจหรือ UI
DataSource เป็นการแยกที่ ระดับต่ำกว่า อีกขั้น: NetworkDataSource รับผิดชอบเฉพาะคำขอ HTTP, LocalDataSource — สำหรับการทำงานกับ Room หรือ CoreData Repository รวมการเรียกไปยัง DataSources ต่างๆ เป็นอินเทอร์เฟซที่สอดคล้องกันเดียว แต่ละ DataSource ถูกทดสอบอย่างอิสระโดยใช้ม็อกหรือเซิร์ฟเวอร์จำลอง
การนำเลเยอร์ DataSource ไปใช้อย่างถูกต้องรับประกันว่าการเปลี่ยนสคีมาฐานข้อมูลหรือการแทนที่ REST API ด้วย GraphQL จะส่งผลกระทบต่อ DataSource เพียงตัวเดียว แต่ไม่ส่งผลต่อ Repository หรือผู้บริโภคของมัน นี่เป็นผลโดยตรงของ Separation of Concerns ในระดับโครงสร้างพื้นฐาน: แต่ละข้อกังวลทางเทคนิคถูกแยกออกและสามารถแทนที่ได้โดยไม่มีการเปลี่ยนแปลงแบบลูกโซ่
MVVM (Model-View-ViewModel) เป็นรูปแบบที่ได้รับความนิยมมากที่สุดสำหรับการพัฒนาโมบายล์ ซึ่งนำ Separation of Concerns ไปใช้โดยตรง Model ประกอบด้วยข้อมูลและตรรกะธุรกิจ View รับผิดชอบการแสดงผล และ ViewModel เชื่อมต่อพวกมันผ่านกลไกปฏิกิริยา ใน Flutter BLoC มีบทบาทคล้ายกันกับการแยกเป็นเหตุการณ์ สถานะ และตรรกะธุรกิจ
Clean Architecture ของ Robert Martin (Uncle Bob) นำ SoC ไปสู่ระดับสูงสุด: ระบบถูกแบ่งออกเป็นวงแหวนอิสระ — เอนทิตี, use cases, อแดปเตอร์ และเฟรมเวิร์ก วงแหวนภายใน (เอนทิตี) ไม่ขึ้นอยู่กับวงแหวนภายนอก (เฟรมเวิร์ก) สิ่งนี้ช่วยให้เปลี่ยนฐานข้อมูล เฟรมเวิร์ก UI และแม้แต่แพลตฟอร์มได้โดยไม่ต้องเขียนตรรกะหลักของแอปพลิเคชันใหม่
ในทางปฏิบัติ โปรเจกต์โมบายล์ไม่ค่อยนำ Clean Architecture ที่สมบูรณ์ไปใช้ — สำหรับแอปพลิเคชันส่วนใหญ่ สถาปัตยกรรม สามเลเยอร์ ก็เพียงพอ: UI, โดเมน และข้อมูล เลเยอร์โดเมนประกอบด้วย Use Cases และโมเดลธุรกิจ และถูกแยกออกจาก Android SDK หรือ iOS SDK โดยสิ้นเชิง การแยกเช่นนี้ให้ผลประโยชน์ 80% ด้วยความพยายาม 20%
// Data layer — รับผิดชอบเฉพาะการดึงข้อมูล
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): User = api.fetchUser(id)
}
// Domain layer — ตรรกะธุรกิจ ไม่รู้จัก API หรือฐานข้อมูล
class GetUserNameUseCase(
private val repo: UserRepository
) {
suspend fun invoke(id: String): String {
val user = repo.getUser(id)
return "${user.firstName} ${user.lastName}"
}
}
// UI layer — การแสดงผลเท่านั้น
class UserViewModel(
private val getUserName: GetUserNameUseCase
) {
fun onUserLoaded(id: String) {
viewModelScope.launch {
_name.value = getUserName.invoke(id)
}
}
}
โค้ดด้านบนสาธิต การแยกที่บริสุทธิ์: UserRepository ทำงานเฉพาะกับ API, GetUserNameUseCase ประกอบด้วยตรรกะธุรกิจของการจัดรูปแบบชื่อ และ UserViewModel จัดการสถานะ UI แต่ละคลาสมีเหตุผลหนึ่งประการในการเปลี่ยนแปลง ซึ่งเป็นแก่นแท้ของ Separation of Concerns
ข้อได้เปรียบหลักของ SoC คือความสามารถในการบำรุงรักษา โค้ดที่แบ่งออกเป็นเลเยอร์อิสระนั้นวิเคราะห์ได้ง่ายกว่า: นักพัฒนามองเฉพาะเลเยอร์ที่เกิดข้อผิดพลาดและไม่ถูกรบกวนโดยเลเยอร์อื่น ในโปรเจกต์ระยะยาว สิ่งนี้ช่วยลดเวลาในการค้นหาและแก้ไขบักลง 30–50% เมื่อเทียบกับโค้ดแบบเสาหิน
ข้อได้เปรียบสำคัญประการที่สองคือ ความสามารถในการทดสอบ เมื่อตรรกะธุรกิจถูกแยกออกจาก UI และเฟรมเวิร์ก มันถูกครอบคลุมโดยการทดสอบหน่วยโดยไม่ต้องเรียกใช้อีมูเลเตอร์ โปรเจกต์ Android และ iOS ที่มีการครอบคลุมการทดสอบหน่วยสูง มีการถดถอยน้อยลงอย่างมากเมื่อเพิ่มฟีเจอร์ใหม่
ข้อจำกัดหลักคือ ความซับซ้อนที่เพิ่มขึ้น การแยกย่อยมากเกินไปเป็นไมโครเลเยอร์และนามธรรมนำไปสู่การที่การเพิ่มปุ่มธรรมดา นักพัฒนาต้องแก้ไขห้าไฟล์ หลักการ Separation of Concerns ต้องการความสมดุลที่สมเหตุสมผล: แยกเฉพาะพื้นที่ที่เปลี่ยนแปลงอย่างอิสระจริงๆ สำหรับโปรเจกต์ขนาดเล็ก การแยกพื้นฐานเป็น UI ตรรกะ และข้อมูลโดยไม่มีนามธรรมเพิ่มเติมก็เพียงพอ
คำถามที่พบบ่อย
SoC เป็นหลักการของการแยกตามขอบเขตความรับผิดชอบ ในขณะที่โมดูลาริตีเป็นวิธีการจัดระเบียบโค้ดเป็นโมดูลทางกายภาพ SoC สามารถนำไปใช้ภายในโมดูลเดียวผ่านเลเยอร์หรือคลาส ในขณะที่โมดูลาริตีต้องการการแบ่งเป็นบิลด์อิสระ
SoC เป็น โครงสร้างเหนือ หลักการ SOLID หลักการความรับผิดชอบเดียว (S) คือ SoC ในระดับคลาสเดียว หลักการการกลับด้านการพึ่งพา (D) ช่วยนำ SoC ไปใช้ระหว่างเลเยอร์ผ่านอินเทอร์เฟซและการฉีดพึ่งพา
ใช่ แต่ใน ระดับที่พอเหมาะ สำหรับแอปพลิเคชันง่ายๆ การแยก UI และตรรกะธุรกิจก็เพียงพอ จำนวนเลเยอร์ที่มากเกินไปจะทำให้โค้ดซับซ้อนโดยไม่มีประโยชน์ในทางปฏิบัติ เมื่อโปรเจกต์เติบโตขึ้น จำนวนเลเยอร์จะเพิ่มขึ้นทีละน้อย
ไม่มีผลกระทบโดยตรงต่อ ประสิทธิภาพ — SoC เกี่ยวข้องกับสถาปัตยกรรมโค้ด ไม่ใช่การทำงาน อย่างไรก็ตาม การแยกเป็นเลเยอร์อาจเพิ่มโอเวอร์เฮดทางอ้อมเนื่องจากการเรียกเพิ่มเติมระหว่างเลเยอร์ ในทางปฏิบัติ ผลกระทบนี้เล็กน้อยเมื่อเทียบกับประโยชน์ของความสามารถในการบำรุงรักษา
การฉีดพึ่งพา (Hilt, Koin, Swinject) จัดการขอบเขตระหว่างเลเยอร์อย่างชัดเจน กฎ lint ทางสถาปัตยกรรมใน Detekt (Android) และ SwiftLint (iOS) ห้ามนำเข้าจากเลเยอร์ที่ไม่ได้รับอนุญาต Git hooks สามารถตรวจสอบว่าเลเยอร์ธุรกิจไม่นำเข้าไลบรารี UI
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม