ข้าม​ไป​ยัง​เนื้อหา

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