Separation of Concerns
อย่าเอาเรื่องที่ต่างกันมาปนอยู่ในที่เดียวกัน
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Separation of Concerns (SoC) คือหลักการที่บอกว่า ระบบซอฟต์แวร์ที่ซับซ้อนควรถูกแบ่งออกเป็น “concern” (เรื่องที่ต้องใส่ใจ) ที่แยกจากกันได้ชัดเจน แต่ละ concern ควรวิเคราะห์ ออกแบบ แก้ไข และทดสอบได้อย่างอิสระ โดยไม่ต้องเข้าใจ concern อื่นทั้งหมดไปพร้อมกัน คำนี้ถูกบัญญัติขึ้นโดย Edsger W. Dijkstra ในบทความปี 1974 ชื่อ “On the Role of Scientific Thought” (EWD447) ซึ่งเขาอธิบายว่านี่คือเทคนิคเดียวที่มีประสิทธิภาพในการจัดระเบียบความคิดของมนุษย์เมื่อต้องรับมือกับความซับซ้อน เพราะสมองคนไม่สามารถใส่ใจกับหลายมิติของปัญหาพร้อมกันได้ดีนัก การแยกให้ศึกษาแต่ละแง่มุม เช่น ความถูกต้อง (correctness) แยกจากประสิทธิภาพ (efficiency) จะได้ผลลัพธ์ที่ดีกว่าการพยายามคิดทุกอย่างพร้อมกัน
SoC ปรากฏได้หลายระดับ ไม่ใช่แค่การแบ่ง module (modularity) ใน code เท่านั้น:
- ระดับ code/function — method หนึ่งควรทำ IO (อ่าน/เขียน file เครือข่าย) หรือคำนวณ (calculation) อย่างใดอย่างหนึ่ง ไม่ใช่ทั้งสองอย่างปนกัน เพราะจะทำให้ตรรกะการคำนวณนำกลับมาใช้ซ้ำไม่ได้หากไม่ยอมทำ IO ไปด้วย
- ระดับ class/module — ความรับผิดชอบแต่ละอย่าง (เช่น การตรวจสอบข้อมูล การจัดรูปแบบ การเข้าถึงข้อมูล) ควรอยู่ในหน่วยที่แยกกัน ซึ่งใกล้ชิดกับ Single Responsibility Principle มาก
- ระดับสถาปัตยกรรม — การแบ่งเป็นชั้น (layer) เช่น presentation, business logic, data access หรือแบ่งเป็น module/feature ที่เปิด-ปิดได้อิสระ
- cross-cutting concerns — บาง concern เช่น logging, security, caching ตัดผ่านหลาย module พร้อมกัน วิธีจัดการมาตรฐานคือ dependency injection, middleware/pipeline หรือ aspect-oriented programming (AOP) เพื่อไม่ให้ตรรกะเหล่านี้แทรกซึมเข้าไปปนกับ business logic ทุกจุด
ตัวอย่างที่คุ้นเคยของ SoC ที่ทำงานได้ดีคือ Internet Protocol Stack (OSI model) — แต่ละชั้นดูแลเรื่องของตัวเองโดยไม่ต้องรู้รายละเอียดของชั้นอื่น หรือการแบ่ง HTML (โครงสร้าง) / CSS (การนำเสนอ) / JavaScript (พฤติกรรม) ในหน้าเว็บ
Separation of Concerns ยังเป็นผลลัพธ์ตามธรรมชาติของการยึดหลัก Don’t Repeat Yourself เพราะการสร้าง abstraction เพื่อไม่ให้แนวคิดหนึ่งถูกเขียนซ้ำทั่ว application มักบังคับให้ concern นั้นถูกรวมไว้ที่เดียวและแยกออกจาก concern อื่นไปในตัว นอกจากนี้ยังเป็นหนึ่งในหลักการชี้นำของ Clean Architecture ที่เน้นแยก business logic ออกจากการตัดสินใจทางเทคนิค (framework, database, UI)
flowchart TB
UI[Presentation UI]
APP[Application Use Cases]
DOMAIN[Business Logic Domain]
DATA[Data Access Infrastructure]
UI --> APP
APP --> DOMAIN
APP --> DATA
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ลดภาระทางสมอง (cognitive load) — เมื่อเปิด file หนึ่งขึ้นมา ผู้พัฒนาต้องคิดถึงเรื่องเดียว ไม่ต้องแบกทั้งตรรกะธุรกิจ การจัดรูปแบบ และการเข้าถึงข้อมูลไว้ในหัวพร้อมกัน
- ลด coupling เพิ่ม cohesion — Martin Fowler ระบุใน “Layering Principles” ว่าหลักการที่ทีมสถาปนิกเห็นพ้องเป็นเอกฉันท์ (11 เห็นด้วย, 0 ไม่เห็นด้วย) คือ “user interface modules should contain no business logic” และ “business logic layers contain no user interface” — การแยกที่ชัดเจนแบบนี้ทำให้แต่ละชั้นเปลี่ยนแปลงได้โดยไม่กระทบชั้นอื่น
- ทดสอบได้ทีละส่วน — Fowler ยังชี้ว่าการแยก concern ให้ “layers should be testable individually” (12/0) ทำให้มี seam สำหรับ mock/stub ได้ง่ายขึ้น ไม่ต้องพึ่งฐานข้อมูลจริงเพื่อทดสอบตรรกะคำนวณ
- นำกลับมาใช้ซ้ำได้ (reusability) — function คำนวณล้วน ๆ ที่ไม่ปนกับ IO สามารถเรียกใช้ได้จากหลายบริบท (เว็บ, batch job, unit test) โดยไม่ต้องพึ่งสภาพแวดล้อมภายนอก
- รองรับการเปลี่ยนแปลงแบบแยกส่วน — เมื่อ requirement ของ UI เปลี่ยน ทีมไม่จำเป็นต้องแตะ business logic และในทางกลับกัน ลดความเสี่ยงที่การแก้จุดหนึ่งจะทำจุดอื่นพัง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ก่อน — method เดียวปนทั้ง IO (อ่าน file), การคำนวณ, และการจัดรูปแบบผลลัพธ์เข้าด้วยกัน ทำให้ทดสอบตรรกะคำนวณลำบาก (ต้องมี file จริงบน disk) และนำกลับมาใช้ซ้ำในบริบทอื่นไม่ได้:
public class OrderReportService{ public string GenerateReport(string filePath) { // อ่าน file (IO) ปนกับคำนวณและจัดรูปแบบใน method เดียว var lines = File.ReadAllLines(filePath); decimal total = 0; foreach (var line in lines) { var parts = line.Split(','); var amount = decimal.Parse(parts[1]); total += amount * 1.07m; // รวมภาษี ฝังไว้ตรงนี้ }
// จัดรูปแบบข้อความปนอยู่ใน method เดิม return $"ยอดรวมทั้งหมด: {total:C}"; }}หลัง — แยกการอ่านข้อมูล (IO) การคำนวณ (business logic ล้วน ๆ) และการนำเสนอ (formatting) ออกเป็นคนละหน่วย แต่ละส่วนทดสอบได้อิสระ และคำนวณภาษีนำไปใช้ที่อื่นได้โดยไม่ต้องพึ่ง file:
// concern: อ่านข้อมูล (IO)public interface IOrderLineReader{ IEnumerable<OrderLine> ReadFrom(string filePath);}
public class CsvOrderLineReader : IOrderLineReader{ public IEnumerable<OrderLine> ReadFrom(string filePath) { foreach (var line in File.ReadAllLines(filePath)) { var parts = line.Split(','); yield return new OrderLine(parts[0], decimal.Parse(parts[1])); } }}
// concern: ตรรกะทางธุรกิจ ไม่รู้จัก IO หรือการจัดรูปแบบเลยpublic class OrderTotalCalculator{ private const decimal TaxRate = 1.07m;
public decimal CalculateTotal(IEnumerable<OrderLine> lines) => lines.Sum(line => line.Amount * TaxRate);}
// concern: การนำเสนอผลลัพธ์public class OrderReportFormatter{ public string Format(decimal total) => $"ยอดรวมทั้งหมด: {total:C}";}
// ประกอบทั้ง3 concern เข้าด้วยกันที่ชั้นบนสุดเท่านั้นpublic class OrderReportService{ private readonly IOrderLineReader _reader; private readonly OrderTotalCalculator _calculator; private readonly OrderReportFormatter _formatter;
public OrderReportService( IOrderLineReader reader, OrderTotalCalculator calculator, OrderReportFormatter formatter) { _reader = reader; _calculator = calculator; _formatter = formatter; }
public string GenerateReport(string filePath) { var lines = _reader.ReadFrom(filePath); var total = _calculator.CalculateTotal(lines); return _formatter.Format(total); }}ตอนนี้ OrderTotalCalculator ทดสอบได้ด้วย list ในหน่วยความจำล้วน ๆ ไม่ต้องมี file จริง และหากวันหนึ่งต้องเปลี่ยนแหล่งข้อมูลจาก CSV เป็น database ก็แค่เขียน IOrderLineReader ตัวใหม่ โดยไม่แตะตรรกะคำนวณภาษีหรือการจัดรูปแบบเลย
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- method หรือ class เดียวทำทั้ง IO และคำนวณ (I/O ปนกับ business logic) ทำให้เขียน unit test ต้องพึ่ง file ฐานข้อมูล หรือเครือข่ายจริง
- code UI (เช่น code-behind, controller, component) มีตรรกะทางธุรกิจฝังอยู่ เช่น การคำนวณราคา ส่วนลด หรือกฎ validation ที่ซับซ้อน
- แก้ไข feature หนึ่งแล้วต้องไปแตะ file ที่ดูเหมือนไม่เกี่ยวข้องหลาย file พร้อมกัน (อาการคล้าย Shotgun Surgery)
- class เดียวมีเหตุผลให้เปลี่ยนแปลงได้มากกว่าหนึ่งเหตุผล ซึ่งเป็นสัญญาณเดียวกับการละเมิด Single Responsibility Principle
- cross-cutting concern อย่าง logging, caching หรือ authorization ถูก copy-paste ไว้ในทุก method แทนที่จะห่อรวมไว้ที่จุดเดียว (middleware, decorator, หรือ aspect)
- ชั้น (layer) 1 reference หรือรู้จักรายละเอียดภายในของอีกชั้นโดยตรง เช่น business logic layer import namespace ของ UI framework
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Single Responsibility
- Don’t Repeat Yourself
- Encapsulate
- Dependency Inversion Principle
- Shotgun Surgery
- Big Ball of Mud
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/principles/separation-of-concerns
- Separation of concerns — Wikipedia
- bliki: Layering Principles — Martin Fowler
- Separate IO from Calculation — c2 wiki
- On the role of scientific thought (EWD447) — E. W. Dijkstra, 1974