Single Responsibility Principle
class หนึ่งควรมีเหตุผลให้เปลี่ยนแปลงเพียงเหตุผลเดียว
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Single Responsibility Principle (SRP) คือตัว “S” ตัวแรกใน SOLID กล่าวไว้ในถ้อยคำดั้งเดิมของ Robert C. Martin ว่า “A class should have only one reason to change” — class หนึ่งควรมีเหตุผลให้ต้องเปลี่ยนแปลงเพียงเหตุผลเดียวเท่านั้น หลักการนี้ถูกกล่าวถึงในรูปแบบนี้ครั้งแรกในบทความ “The Principles of OOD” ของ Martin ก่อนจะกลายเป็นบทหนึ่งในหนังสือ Agile Software Development, Principles, Patterns, and Practices (2003) โดยมีรากฐานมาจากแนวคิดเรื่อง cohesion ของ Tom DeMarco และ Meilir Page-Jones
คำว่า “เหตุผล” (reason) กลายเป็นจุดที่ทำให้เกิดความเข้าใจผิดกันมาก เพราะฟังดูเป็นนามธรรมเกินไป ในปี 2014 Martin จึงเขียน block post “The Single Responsibility Principle” เพื่อนิยามใหม่ให้ชัดว่า “This principle is about people” — หลักการนี้พูดถึงคน ไม่ใช่ function เขาเปลี่ยนคำอธิบายเป็น actor: “A module should be responsible to one, and only one, actor” โดย actor หมายถึงกลุ่มผู้มีส่วนได้ส่วนเสีย (stakeholder) หนึ่งกลุ่มที่เป็นต้นตอของการร้องขอให้เปลี่ยน code ส่วนนั้น
ตัวอย่างคลาสสิกของ Martin คือ class Employee ที่มี3 method:
calculatePay()— คำนวณค่าจ้าง เปลี่ยนแปลงตามนโยบายของฝ่ายบัญชี (CFO)reportHours()— จัดรูปแบบรายงานชั่วโมงทำงานเพื่อการตรวจสอบ เปลี่ยนแปลงตามฝ่ายปฏิบัติการ (COO)save()— บันทึกข้อมูลลงฐานข้อมูล เปลี่ยนแปลงตามฝ่าย IT/สถาปัตยกรรมข้อมูล (CTO)
ทั้ง3 method อยู่ใน class เดียวกัน แต่มีต้นตอการเปลี่ยนแปลงคนละกลุ่ม (actor) หากฝ่ายบัญชีขอแก้สูตรคำนวณเงินเดือน นักพัฒนาต้องแตะ class Employee ทั้ง class และเสี่ยงที่จะทำให้ reportHours() หรือ save() พังไปด้วยโดยไม่ตั้งใจ ทั้งที่ทั้งสองฝ่ายนั้นไม่ได้ขอให้เปลี่ยนแปลงอะไรเลย นี่คือแก่นของปัญหาที่ SRP พยายามป้องกัน
SRP เกี่ยวข้องอย่างใกล้ชิดกับแนวคิดเรื่อง coupling และ cohesion: coupling คือระดับความเชื่อมโยงที่แยกไม่ออกระหว่างส่วนต่าง ๆ ของ application ส่วน cohesion คือความเกี่ยวข้องกันของเนื้อหาภายใน class หรือ package หนึ่ง ๆ เนื้อหาทั้งหมดภายใน class เดียวย่อมมี coupling สูงต่อกัน เพราะตัว class เองเป็นหน่วยเดียวที่ถูก compile ทดสอบ และ deploy ไปด้วยกันทั้งหมด เมื่อ class หนึ่งมี cohesion ต่ำ (รวมความรับผิดชอบที่ไม่เกี่ยวข้องกันไว้ในที่เดียว) การเปลี่ยนแปลงส่วนใดส่วนหนึ่งก็บังคับให้ทุก class ที่พึ่งพาต้องถูกทดสอบซ้ำทั้งหมด พื้นที่ที่ได้รับผลกระทบจากการเปลี่ยนแปลงจึงกว้างเกินจำเป็น ในทางกลับกัน หากแตก class ออกเป็นหลาย class ที่มี cohesion สูง แต่ละ class จะถูกใช้โดยองค์ประกอบอื่นน้อยลง การเปลี่ยนแปลง class ใด class หนึ่งจึงกระทบระบบโดยรวมน้อยลง
อย่างไรก็ตาม เช่นเดียวกับหลักการอื่น ๆ การพยายามยัด SRP ให้ทุก class ตั้งแต่วันแรกไม่ใช่เรื่องฉลาด หาก class หนึ่งเสถียรดีอยู่แล้วและไม่ได้สร้างความเจ็บปวดจากการเปลี่ยนแปลงบ่อย ๆ ก็ไม่จำเป็นต้องแตะต้องมันก่อนเวลาอันควร จงฝึกใช้ Pain Driven Development — แยกความรับผิดชอบเมื่อมันเริ่มเจ็บจริง ๆ ไม่ใช่แยกล่วงหน้าตามทฤษฎี
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”- ลด blast radius ของการเปลี่ยนแปลง — เมื่อ actor หนึ่งขอแก้ไข การเปลี่ยนแปลงจะจำกัดอยู่ใน class เดียวที่เกี่ยวข้อง ไม่ลามไปกระทบ behavior ที่ actor อื่นพึ่งพาอยู่ ลดความเสี่ยงของ regression ที่ไม่คาดคิด
- ทดสอบง่ายขึ้น — class ที่มีความรับผิดชอบเดียวมักมี dependency น้อย ทำให้เขียน unit test ได้ตรงจุดและ mock ได้ง่าย ต่างจาก class อ้วนที่ต้อง setup สภาพแวดล้อมซับซ้อนก่อนทดสอบแม้แต่ function เดียว
- อ่านและทำความเข้าใจง่าย — ชื่อ class ที่สื่อความรับผิดชอบเดียวชัดเจน ช่วยให้ทีมค้นหา code ที่ต้องแก้ได้เร็ว ลดเวลาที่ใช้ไล่อ่าน “iceberg class” ที่ทำหลายอย่างเกินกว่าชื่อจะบอกได้
- ป้องกัน merge conflict ระหว่างทีม — เมื่อสองทีม (เช่นทีมบัญชีกับทีมรายงาน) ไม่ต้องแก้ file เดียวกันเพื่อ requirement คนละเรื่อง ความขัดแย้งในการรวม code จึงลดลงตามธรรมชาติ
- เป็นรากฐานของ SOLID ตัวอื่น — class ที่มีความรับผิดชอบเดียวมักเปิด/ปิดต่อการขยาย (OCP) ได้ง่ายกว่า และมัก inject dependency ได้สะอาดกว่า เพราะ interface ที่พึ่งพามีขอบเขตแคบและชัดเจน
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างที่ละเมิด SRP
หัวข้อที่มีชื่อว่า “ตัวอย่างที่ละเมิด SRP”class InvoiceProcessor ด้านล่างมีถึงสี่เหตุผลให้เปลี่ยนแปลง: กฎการคำนวณภาษี รูปแบบการพิมพ์ใบแจ้งหนี้ วิธีการบันทึกลงฐานข้อมูล และช่องทางการแจ้งเตือนลูกค้า
// ละเมิด SRP: รวมสี่ความรับผิดชอบไว้ใน class เดียวpublic class InvoiceProcessor{ public decimal CalculateTotal(Invoice invoice) { // เหตุผลที่ 1: เปลี่ยนตามนโยบายภาษีของฝ่ายบัญชี decimal subtotal = invoice.Items.Sum(i => i.Price * i.Quantity); return subtotal + subtotal * 0.07m; // VAT 7% }
public string FormatAsHtml(Invoice invoice) { // เหตุผลที่ 2: เปลี่ยนตามทีมออกแบบเอกสาร return $"<html><body><h1>Invoice {invoice.Id}</h1></body></html>"; }
public void SaveToDatabase(Invoice invoice) { // เหตุผลที่ 3: เปลี่ยนตามทีมโครงสร้างพื้นฐานข้อมูล using var connection = new SqlConnection("..."); connection.Execute("INSERT INTO Invoices ...", invoice); }
public void SendEmailNotification(Invoice invoice) { // เหตุผลที่ 4: เปลี่ยนตามทีม CRM/การแจ้งเตือน var client = new SmtpClient(); client.Send("billing@shop.com", invoice.CustomerEmail, "Invoice Ready", "..."); }}หากฝ่ายบัญชีขอเปลี่ยนอัตราภาษี นักพัฒนาต้องเปิด file นี้ ทดสอบทั้ง class และเสี่ยงที่จะทำให้ logic การส่งอีเมลพังไปด้วย ทั้งที่ทีม CRM ไม่ได้ร้องขออะไรเลย
หลังปรับให้เป็นไปตาม SRP
หัวข้อที่มีชื่อว่า “หลังปรับให้เป็นไปตาม SRP”// แต่ละ class รับผิดชอบต่อ actor เดียว เปลี่ยนแปลงอิสระจากกันpublic class InvoiceCalculator{ // เปลี่ยนเมื่อ actor: ฝ่ายบัญชี ขอแก้กฎภาษี public decimal CalculateTotal(Invoice invoice) { decimal subtotal = invoice.Items.Sum(i => i.Price * i.Quantity); return subtotal + subtotal * 0.07m; }}
public class InvoiceHtmlFormatter{ // เปลี่ยนเมื่อ actor: ทีมออกแบบเอกสาร ขอปรับรูปแบบ public string Format(Invoice invoice) => $"<html><body><h1>Invoice {invoice.Id}</h1></body></html>";}
public class InvoiceRepository{ // เปลี่ยนเมื่อ actor: ทีมโครงสร้างพื้นฐานข้อมูล ขอเปลี่ยนวิธีจัดเก็บ public void Save(Invoice invoice) { using var connection = new SqlConnection("..."); connection.Execute("INSERT INTO Invoices ...", invoice); }}
public class InvoiceNotifier{ // เปลี่ยนเมื่อ actor: ทีม CRM ขอเปลี่ยนช่องทางแจ้งเตือน public void SendEmail(Invoice invoice) { var client = new SmtpClient(); client.Send("billing@shop.com", invoice.CustomerEmail, "Invoice Ready", "..."); }}
// class ที่ประสานงาน (orchestrator) เชื่อมทุกอย่างเข้าด้วยกัน// โดยตัวมันเองมีความรับผิดชอบเดียวคือ "ควบคุมลำดับขั้นตอน"public class InvoiceWorkflow{ private readonly InvoiceCalculator _calculator; private readonly InvoiceRepository _repository; private readonly InvoiceNotifier _notifier;
public InvoiceWorkflow( InvoiceCalculator calculator, InvoiceRepository repository, InvoiceNotifier notifier) { _calculator = calculator; _repository = repository; _notifier = notifier; }
public void Process(Invoice invoice) { invoice.Total = _calculator.CalculateTotal(invoice); _repository.Save(invoice); _notifier.SendEmail(invoice); }}ตอนนี้แต่ละ class มี actor เดียวเป็นเจ้าของเหตุผลในการเปลี่ยนแปลง ทดสอบแยกกันได้ และเปลี่ยนอัตราภาษีได้โดยไม่ต้องแตะ code การส่งอีเมลเลย ส่วน InvoiceWorkflow ทำหน้าที่เป็นตัวประสานงาน (คล้ายรูปแบบ Facade) ซึ่งเป็นความรับผิดชอบของมันเองที่ชัดเจนคือ “ควบคุมลำดับการทำงาน” ไม่ใช่ “ทำทุกอย่างเอง”
แผนภาพ dependency
หัวข้อที่มีชื่อว่า “แผนภาพ dependency”graph LR Workflow[InvoiceWorkflow] Calc[InvoiceCalculator] Repo[InvoiceRepository] Notify[InvoiceNotifier] Format[InvoiceHtmlFormatter] Workflow --> Calc Workflow --> Repo Workflow --> Notify Workflow --> Format
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- ชื่อ method ใน class เดียวกันพูดถึงคนละ domain กัน เช่น
CalculateTax,RenderPdf,SendSmsใน class เดียว — สัญญาณของ Blob หรือ God Class - แก้ bug หนึ่งจุดแล้วของอีกจุดหนึ่งพัง ทั้งที่ดูไม่เกี่ยวข้องกัน — อาการคลาสสิกของ cohesion ต่ำ
- คุณอธิบายว่า class ทำอะไรด้วยคำว่า “และ” (and) เช่น “class นี้ validate ข้อมูล และ บันทึกลงฐานข้อมูล และ ส่งอีเมล” — ถ้าประโยคอธิบายต้องใช้ “และ” หลายครั้ง นั่นคือหลายความรับผิดชอบ
- การเปลี่ยนแปลงเล็ก ๆ จากทีมหนึ่งบังคับให้ทีมอื่นต้อง re-test/re-deploy ด้วย — สัญญาณของ Shotgun Surgery หรือ Divergent Change
- file class ยาวหลายร้อยหรือหลายพันบรรทัด และมี field/dependency จำนวนมากที่ใช้เฉพาะบาง method เท่านั้น
- ยากที่จะตั้งชื่อ class ให้สื่อความหมายเดียวได้ — ถ้าต้องใช้ชื่อกำกวมอย่าง
Manager,Processor,Helper,Utilityแบบไม่จำกัดขอบเขต มักเป็นสัญญาณว่ากำลังยัดหลายหน้าที่ไว้ด้วยกัน
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- SOLID — SRP เป็นหลักการตัวแรกในชุดหลักการนี้
- Separation of Concerns — แนวคิดกว้างกว่าที่ SRP นำไปประยุกต์ใช้ในระดับ class
- Open/Closed Principle — class ที่มีความรับผิดชอบเดียวมักเปิดขยายได้ง่ายกว่าโดยธรรมชาติ
- Blob (God Class) — แอนตี้ pattern ที่เกิดจากการละเลย SRP อย่างต่อเนื่อง
- Shotgun Surgery — กลิ่น code ที่มักเป็นผลข้างเคียงของการรวมความรับผิดชอบผิดที่
- Pain Driven Development — แนวทางตัดสินใจว่าเมื่อไรควรแยกความรับผิดชอบจริง ๆ