SoC (Separation of Concerns) เป็นตัวย่อของหลักการที่ระบบซอฟต์แวร์ถูกแบ่งออกเป็นพื้นที่ความรับผิดชอบที่แยกจากกัน ตามข้อมูลของ Martin Fowler การแยกความกังวลเป็นองค์ประกอบสำคัญของโค้ดที่บำรุงรักษาได้ หลักการ SoC ช่วยให้นักพัฒนาสามารถเปลี่ยนเลเยอร์หนึ่งของแอปพลิเคชันโดยไม่กระทบต่อเลเยอร์อื่น ซึ่งสำคัญอย่างยิ่งในการพัฒนาแอปมือถือเป็นทีม
ประเด็นสำคัญ
SoC ย่อมาจาก Separation of Concerns — «การแบ่งแยกความรับผิดชอบ» หรือ «การแบ่งแยกพื้นที่ความสนใจ» ในบริบทของการพัฒนา คำว่า concern หมายถึงฟังก์ชันการทำงานใดๆ ที่สามารถแยกออกได้: การแสดงผลอินเตอร์เฟซผู้ใช้ การประมวลผลคลิก การตรวจสอบข้อมูล การสื่อสารเครือข่าย หรือการทำงานกับฐานข้อมูล หลักการ SoC กำหนดให้จัดกลุ่มโค้ดรอบพื้นที่เหล่านี้เพื่อให้การเปลี่ยนแปลงในพื้นที่หนึ่งไม่กระทบต่อพื้นที่อื่น
ตัวย่อ SoC ถูกใช้อย่างแพร่หลายในวรรณกรรมทางเทคนิค การอภิปรายทางสถาปัตยกรรม และเอกสารประกอบของเฟรมเวิร์ก ตัวอย่างเช่น ในเอกสารประกอบของ Android Architecture Components มีการกล่าวถึง SoC ซ้ำๆ เป็นแรงจูงใจในการแยก ViewModel และ View ในชุมชน iOS คำนี้ถูกใช้เมื่ออภิปรายปัญหา Massive View Controller ซึ่งเป็นผลโดยตรงของการขาด SoC
สิ่งสำคัญคือต้องเข้าใจว่า SoC ไม่ใช่การกระทำเพียงครั้งเดียว แต่เป็นกระบวนการที่ต่อเนื่อง เมื่อแอปพลิเคชันเติบโตขึ้น พื้นที่ความรับผิดชอบใหม่ๆ ก็เกิดขึ้น และสถาปัตยกรรมต้องได้รับการทบทวน โค้ดเบสที่ดีต้องผ่านการแยกหลายครั้งก่อนที่จะถึงสถานะที่มั่นคงซึ่งแต่ละ concern ถูกแยกออกและจัดการได้
Separation of Concerns และตัวย่อ SoC หมายถึงหลักการเดียวกัน ความแตกต่างอยู่ที่บริบทการใช้งานเท่านั้น: ชื่อเต็มถูกใช้ในเอกสารทางการ สื่อการศึกษา และเมื่ออธิบายแนวคิดให้กับนักพัฒนาใหม่เป็นครั้งแรก SoC สะดวกในการอภิปรายทางเทคนิค การตรวจสอบโค้ด และเอกสารประกอบที่ต้องการความกระชับ
ในสภาพแวดล้อมทางวิชาชีพ ทั้งสองคำ สามารถใช้แทนกันได้ นักพัฒนาสามารถพูดว่า «ที่นี่ละเมิด SoC» หรือ «สิ่งนี้ละเมิด Separation of Concerns» — ความหมายไม่เปลี่ยนแปลง อย่างไรก็ตาม ในประกาศรับสมัครงานและข้อกำหนดทางสถาปัตยกรรม มักใช้ชื่อเต็มมากกว่า ในขณะที่ในการแชทและการตรวจสอบโค้ด ใช้ตัวย่อ การรู้ทั้งสองรูปแบบจำเป็นสำหรับการเข้าสู่อุตสาหกรรมอย่างสบายใจ
มีความ สับสนทางคำศัพท์: ตัวย่อ SoC ยังถูกใช้ในบริบทฮาร์ดแวร์สำหรับ System-on-a-Chip (ระบบบนชิป) ในการพัฒนาแอปมือถือ บริบทจะชัดเจนจากสภาพแวดล้อม — หากการอภิปรายเกี่ยวกับสถาปัตยกรรมโค้ด จะหมายถึง Separation of Concerns ในบทความนี้ SoC หมายถึงหลักการของการแบ่งแยกความรับผิดชอบเสมอ
สถาปัตยกรรมสามชั้น เป็นวิธีที่พบมากที่สุดในการนำ SoC ไปใช้ในแอปพลิเคชันมือถือ มันแบ่งโค้ดออกเป็น Presentation (UI), Domain (ตรรกะทางธุรกิจ) และ Data (แหล่งข้อมูล) แต่ละชั้นมีประเภทของคลาสที่กำหนดไว้อย่างเคร่งครัดและถูกแยกออกจากชั้นข้างเคียงผ่านอินเทอร์เฟซ วิธีการนี้มีประสิทธิภาพเท่าเทียมกันสำหรับโปรเจกต์ iOS, Android และ Flutter
View และ ViewModel รวมกันเป็นชั้นการนำเสนอ View มีหน้าที่เรนเดอร์อินเตอร์เฟซและส่งต่อเหตุการณ์ผู้ใช้ ViewModel เก็บสถานะของหน้าจอและแปลงข้อมูลจากชั้น Domain เป็นรูปแบบที่พร้อมสำหรับการแสดงผล ViewModel ไม่มีการอ้างอิงถึง Activity, Fragment หรือ UIViewController — ซึ่งรับประกัน SoC ระหว่าง UI และตรรกะ
ตัวอย่างเช่น ใน Android Jetpack ViewModel อยู่รอดจากการหมุนหน้าจอในขณะที่ UI ถูกสร้างใหม่ หากไม่มี SoC จะต้องบันทึกสถานะใน Activity ซึ่งผสมการจัดการวงจรชีวิตเข้ากับข้อมูล ViewModel แก้ปัญหานี้อย่างแยกอิสระ แสดงให้เห็นถึงการนำหลักการแบ่งแยกความรับผิดชอบไปใช้อย่างสะอาด
Use Cases มีกฎทางธุรกิจที่ไม่ขึ้นกับแพลตฟอร์ม ชั้นนี้ไม่นำเข้า Android SDK, iOS UIKit หรือ Flutter framework Use Case รับข้อมูลจาก Repository ใช้ตรรกะทางธุรกิจกับข้อมูลนั้น และส่งคืนผลลัพธ์ ด้วย SoC ทำให้ Use Case เดียวสามารถนำกลับมาใช้ใหม่บนหน้าจอและแพลตฟอร์มต่างๆ ได้
ตัวอย่างคลาสสิกคือ ValidateAndSaveUseCase สำหรับแบบฟอร์มลงทะเบียน มันตรวจสอบความถูกต้องของอีเมลและรหัสผ่าน เรียก UserRepository เพื่อบันทึก และส่งคืน ValidationResult ทั้ง UI และฐานข้อมูลไม่รู้กฎการตรวจสอบ — กฎเหล่านั้นรวมศูนย์อยู่ในที่เดียว ทำให้ง่ายต่อการเปลี่ยนแปลง
Repository ทำให้แหล่งข้อมูลเป็นนามธรรมจากส่วนที่เหลือของแอปพลิเคชัน ViewModel ไม่รู้ว่าข้อมูลมาจากไหน — จาก REST API, GraphQL, ฐานข้อมูลท้องถิ่น หรือแคช Repository ตัดสินใจว่าจะใช้แหล่งใดและซ่อนตรรกะนี้ไว้เบื้องหลังอินเทอร์เฟซ นี่คือ SoC ระหว่างการดึงข้อมูลและการใช้ข้อมูล
DataSource ให้การแบ่งแยกที่ ลึกยิ่งขึ้น: RemoteDataSource รับผิดชอบเฉพาะคำขอ HTTP, LocalDataSource รับผิดชอบการทำงานกับ Room, CoreData หรือ SharedPreferences Repository รวมพวกมันโดยใช้กลยุทธ์การแคช แต่ละ DataSource สามารถถูกแทนที่ได้อย่างอิสระ ซึ่งสำคัญอย่างยิ่งเมื่อย้ายระหว่างเซิร์ฟเวอร์หรือฐานข้อมูล
ระบบ DataSource หลายระดับ ดังกล่าวนำ SoC ไปใช้ในระดับโครงสร้างพื้นฐาน: การสื่อสารเครือข่าย การจัดเก็บในท้องถิ่น และการแคชเป็น concerns ที่แยกจากกัน แต่ละอันมีตรรกะและวงจรชีวิตของตัวเอง เมื่อเปลี่ยน HTTP client เฉพาะ RemoteDataSource เท่านั้นที่เปลี่ยนแปลง ในขณะที่ Repository และชั้นที่สูงกว่ายังคงไม่ถูกแตะต้อง ยืนยันคุณค่าในทางปฏิบัติของการแบ่งแยกความรับผิดชอบ
MVP (Model-View-Presenter) เป็นหนึ่งในรูปแบบแรกๆ ที่นำ SoC ไปใช้อย่างชัดเจนในการพัฒนาแอปมือถือ Presenter มีตรรกะและควบคุม View ผ่านอินเทอร์เฟซ View เป็นแบบพาสซีฟ — มันแสดงเฉพาะสิ่งที่ Presenter บอกเท่านั้น การแยกช่วยให้การทดสอบง่ายขึ้น: Presenter ถูกทดสอบโดยไม่ต้องใช้โปรแกรมจำลอง และ View ยังคงเรียบง่ายจนไม่มีอะไรเสียหาย
MVVM เพิ่มการเชื่อมโยงแบบรีแอคทีฟ: View สมัครสมาชิกการเปลี่ยนแปลงของ ViewModel ผ่าน Observable หรือ StateFlow ViewModel ไม่เก็บการอ้างอิงถึง View ซึ่งกำจัดความเสี่ยงของการรั่วไหลของหน่วยความจำและแยก concerns ออกไปอีก ใน Android MVVM กลายเป็นมาตรฐานต้องขอบคุณ Jetpack ViewModel และ LiveData; ใน iOS — ต้องขอบคุณ Combine และ RxSwift
Clean Architecture โดย Robert Martin ผลักดัน SoC ไปสู่การแบ่งแยกอย่างรุนแรงเป็นวงแหวน วงแหวนรอบนอก (เฟรมเวิร์กและไดรเวอร์) ขึ้นอยู่กับวงแหวนภายใน (เอนทิตี) แต่ไม่ใช่ในทางกลับกัน ในทางปฏิบัติ โปรเจกต์มือถือไม่ค่อยนำวงแหวนทั้งสี่มาใช้ — ชั้น Domain และ Data รอบ Presentation ก็เพียงพอแล้ว แต่หลักการ «การพึ่งพาเข้าด้านใน» ให้ข้อได้เปรียบที่สำคัญเมื่อเปลี่ยนเฟรมเวิร์ก
// View — แสดงผลเท่านั้น ไม่มีตรรกะ
final class LoginViewController: UIViewController {
let viewModel: LoginViewModel
func loginTapped() {
viewModel.login(emailField.text, passwordField.text)
}
}
// ViewModel — มีตรรกะหน้าจอ ไม่รู้จัก UIKit
final class LoginViewModel {
private let loginUseCase: LoginUseCase
func login(email: String?, password: String?) {
loginUseCase.execute(email, password)
}
}
// Use Case — ตรรกะทางธุรกิจ ไม่ขึ้นกับแพลตฟอร์ม
final class LoginUseCase {
private let repo: AuthRepository
func execute(email: String?, password: String?) {
guard let e = email, let p = password else { return }
repo.authenticate(e, p)
}
}
ตัวอย่างแสดง สามระดับของ SoC: LoginViewController ส่งต่อเหตุการณ์เท่านั้น, LoginViewModel จัดการสถานะ, LoginUseCase มีกฎทางธุรกิจ แต่ละคลาสถูกทดสอบอย่างอิสระ และการเปลี่ยนเฟรมเวิร์ก UI ไม่กระทบต่อ Use Case
Massive View Controller คือการละเมิด SoC ที่พบบ่อยที่สุดใน iOS คลาสที่จัดการ UI, จัดการคำขอเครือข่าย, แยกวิเคราะห์ JSON และบันทึกข้อมูลละเมิดหลักการในทุกระดับ วิธีแก้ไขคือแยกแต่ละความรับผิดชอบออกเป็นคอมโพเนนต์แยกต่างหาก: NetworkingService, JSONParser, CoreDataStack โดยให้ ViewController จัดการเฉพาะ View
ใน Android ปัญหาที่คล้ายกันคือ God Activity หรือ God Fragment หนึ่งแอคทิวิตีที่โหลดข้อมูล, ตรวจสอบฟอร์ม, แสดงไดอะล็อก และอัปเดต UI ได้รับการแก้ไขโดยการนำ ViewModel และ Repository เข้ามา ซึ่งรับผิดชอบการจัดการสถานะและข้อมูล ViewModel ยังป้องกันการสูญหายของข้อมูลเมื่อหมุนหน้าจอ
การละเมิดที่สามคือ การผสมโค้ดแพลตฟอร์มและโค้ดทางธุรกิจ ตัวอย่างเช่น การวางคำขอ HTTP โดยตรงใน SwiftUI View หรือ Android Composable ซึ่งทำให้โค้ดไม่สามารถพกพาได้และทดสอบได้ยาก วิธีที่ถูกต้องคือย้ายคำขอไปยัง Repository ซึ่งถูกเรียกผ่าน Use Case ในขณะที่ View สมัครสมาชิกผลลัพธ์เท่านั้น แต่ละองค์ประกอบของระบบแก้ปัญหาของตัวเองและไม่เกินขอบเขตของมัน
คำถามที่พบบ่อย
ไม่ SoC เป็นหลักการทั่วไปในการแบ่งระบบออกเป็นพื้นที่ความรับผิดชอบ SOLID คือชุดของกฎเฉพาะห้าข้อสำหรับการออกแบบเชิงวัตถุ หลักการแรกของ SOLID (Single Responsibility) เป็นกรณีพิเศษของ SoC ในระดับคลาสเดียว
ใช้ กฎสาเหตุเดียวสำหรับการเปลี่ยนแปลง (Single Responsibility) หากคลาสเปลี่ยนแปลงเนื่องจากการเปลี่ยนแปลงของ UI, รูปแบบข้อมูล และกฎทางธุรกิจ — SoC ถูกละเมิด เครื่องมืออย่าง ArchTest (Android) และ StrictConcurrency (iOS) ช่วยตรวจจับการละเมิดดังกล่าวโดยอัตโนมัติ
ในทางทฤษฎี ชั้นเพิ่มเติมเพิ่มการเรียกทางอ้อม แต่ในทางปฏิบัติ ผลกระทบต่อประสิทธิภาพของแอปพลิเคชันมือถือนั้นเล็กน้อยมาก คอมไพเลอร์ทำ inline หลายการเรียก และการปรับแต่ง JIT และ AOT กำจัดค่าใช้จ่ายส่วนเกิน ความสามารถในการบำรุงรักษาโค้ดได้ประโยชน์มากกว่าที่สูญเสียไปในสิ่งที่เป็นนามธรรม
เริ่มต้นด้วยการ แยก คำขอเครือข่ายจาก UI ไปยัง Repository จากนั้นย้ายตรรกะทางธุรกิจไปยัง Use Cases ใช้ dependency injection เพื่อเชื่อมต่อชั้นต่างๆ ทำการเปลี่ยนแปลงแบบวนซ้ำ โดยครอบคลุมโค้ดใหม่ด้วยการทดสอบ — สิ่งนี้รับประกันว่าการปรับโครงสร้างจะไม่ทำลายฟังก์ชันการทำงานที่มีอยู่
ใน ต้นแบบ สามารถละเมิด SoC เพื่อความเร็วได้ แต่ถ้าต้นแบบเปลี่ยนไปสู่การพัฒนาผลิตภัณฑ์ ค่าใช้จ่ายในการปรับโครงสร้างอาจมากกว่าประโยชน์ของการเริ่มต้นที่รวดเร็ว วิธีที่ดีที่สุดคือรักษาการแยกขั้นต่ำ (UI และข้อมูล) แม้ในต้นแบบ เพื่อหลีกเลี่ยงการเขียนทุกอย่างใหม่ตั้งแต่ต้นเมื่อเปิดตัว
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม