Open-Closed Principle
เปิดรับการขยาย แต่ปิดต่อการแก้ไข
Open-Closed Principle (OCP) กล่าวว่า สิ่งต่าง ๆ ในซอฟต์แวร์ (class module method ฯลฯ) ควร เปิดรับการขยาย (open for extension) แต่ปิดต่อการแก้ไข (closed for modification) นี่คือตัว “O” ตัวที่สองในอักษรย่อ SOLID
แนวคิดหลัก
หัวข้อที่มีชื่อว่า “แนวคิดหลัก”Bertrand Meyer เป็นผู้บัญญัติคำว่า open-closed principle ไว้ในหนังสือ Object-Oriented Software Construction (1988) โดยนิยามว่า module หนึ่ง “เปิด” หากยังขยายเพิ่ม field หรือ function ได้ และ “ปิด” หากมันเสถียรพอให้ module อื่นมาเรียกใช้ได้อย่างมั่นใจ วิธีที่ Meyer เสนอในตอนนั้นคือใช้ implementation inheritance — class หนึ่งถูก compile และเก็บไว้ใช้งานได้ (ปิด) แต่ก็ยังเปิดให้ class ลูกสืบทอดแล้วเพิ่มพฤติกรรมใหม่ได้โดยไม่ต้องแก้ class แม่หรือรบกวน client ที่ใช้มันอยู่
ในช่วงทศวรรษ 1990 Robert C. Martin ตีความหลักการนี้ใหม่ในบทความ “Design Principles and Design Patterns” (2000) จนกลายเป็นที่เรียกกันว่า Polymorphic Open-Closed Principle — แทนที่จะสืบทอดจาก class จริง เราให้ client ขึ้นกับ interface หรือ abstract class เท่านั้น ส่วนพฤติกรรมที่เปลี่ยนแปลงได้ก็ผลักไปไว้ใน implementation ที่สลับเปลี่ยนได้แบบ polymorphic นี่คือมุมมองที่ใช้กันแพร่หลายที่สุดในปัจจุบัน และเป็นฐานให้กับ design pattern เชิงประกอบ (compositional) อย่าง Strategy pattern
วิธีที่ง่ายที่สุดในการอธิบายหลักการนี้ คือลองพิจารณา method ที่ทำงานอย่างหนึ่ง สมมติว่ามันเขียนข้อมูลลง file หนึ่ง โดยชื่อ file ถูก hard-code ไว้ใน method หากความต้องการเปลี่ยนไป และตอนนี้ชื่อ file ต้องต่างออกไปในบางสถานการณ์ เราก็ต้องเปิด method ขึ้นมาแก้ชื่อ file แต่ในทางกลับกัน หากชื่อ file ถูกส่งเข้ามาเป็น parameter เราจะสามารถเปลี่ยนพฤติกรรมของ method นี้ได้โดยไม่ต้องแตะต้อง source ของมัน — คงสภาพ “ปิดต่อการแก้ไข” ไว้ได้
Craig Larman เชื่อมโยง OCP เข้ากับ pattern Protected Variations ของ Alistair Cockburn และแนวคิด information hiding ของ David Parnas ในบทความ “Protected Variation: The Importance of Being Closed” (IEEE Software, 2001) ใจความสำคัญตรงกัน คือ: มองหาจุดที่คาดว่าจะแปรผันในอนาคต แล้วห่อหุ้มมันไว้หลัง interface ที่มั่นคง
แผนภาพด้านล่างแสดงทิศทางการพึ่งพา (dependency direction) ที่ OCP ต้องการ — ทั้ง client และ implementation เดิมต่างก็ขึ้นกับ abstraction ตัวเดียวกัน เมื่อจะเพิ่มพฤติกรรมใหม่ เราเพิ่ม implementation ใหม่เข้ามาชี้ที่ abstraction เดิม โดยไม่ต้องแตะทั้ง client และ implementation เก่าเลย
flowchart LR
Client[Client code]
Abstraction[IDiscountStrategy interface]
Existing[Existing implementation]
New[New implementation added later]
Client --> Abstraction
Existing --> Abstraction
New --> Abstraction
เกี่ยวเนื่องกับ DRY: Open-Closed Principle จะทำงานได้ก็ต่อเมื่อยึดหลัก Don’t Repeat Yourself เพราะการขยายพฤติกรรมโดยไม่แก้ code เดิมต้องอาศัยการสร้าง abstraction ที่ไม่ซ้ำซ้อน หากตรรกะเดิมกระจัดกระจายซ้ำซ้อนอยู่หลายที่ การเพิ่มพฤติกรรมใหม่ก็จะบังคับให้ต้องไล่แก้ทุกจุดอยู่ดี
ทำไมถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมถึงสำคัญ”code ที่ผ่านการทดสอบแล้วและใช้งานจริงอยู่ในโปรดักชันมีความเสี่ยงทุกครั้งที่ถูกแก้ไข การแก้ code เดิมอาจทำให้เกิด regression ที่ไม่คาดคิด ต้อง compile ใหม่ ทดสอบใหม่ และ deploy ใหม่ทั้งระบบ ในขณะที่การ “เพิ่ม” code ใหม่เข้าไปโดยไม่แตะของเดิมนั้นปลอดภัยกว่ามาก เพราะ code เดิมที่ผ่านการพิสูจน์แล้วยังคงเสถียรอยู่เหมือนเดิม
Uncle Bob ยกตัวอย่างสถาปัตยกรรม plugin อย่าง Eclipse, IntelliJ, Vim หรือ Minecraft เป็นบทพิสูจน์ว่า OCP ใช้งานได้จริงในระดับสถาปัตยกรรม ไม่ใช่แค่ทฤษฎี — ระบบเหล่านี้ทำให้ dependency ไหลเข้าหาแกนกลางเสมอ (plugin ขึ้นกับ core ไม่ใช่ core ขึ้นกับ plugin) ทำให้เพิ่มความสามารถใหม่ได้เรื่อย ๆ โดยไม่ต้องแก้ core เลย
ประโยชน์โดยสรุป:
- ลดความเสี่ยง regression เพราะ code ที่ทดสอบผ่านแล้วไม่ถูกแตะต้อง
- หลีกเลี่ยง shotgun surgery — การเปลี่ยนแปลงหนึ่งครั้งไม่ต้องกระจายไปแก้หลาย file
- รองรับการขยายทีมและ feature คู่ขนาน เพราะทีมต่าง ๆ เพิ่ม implementation ใหม่ได้โดยไม่ชนกัน
- สอดคล้องกับ deployment ที่ปลอดภัยกว่า เช่น การเพิ่ม assembly/DLL ใหม่โดยไม่ต้อง redeploy ของเดิม
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ก่อน (ละเมิด OCP): ทุกครั้งที่มีลูกค้าประเภทใหม่ ต้องเปิด method นี้ขึ้นมาแก้ไข และเสี่ยงทำเงื่อนไขเดิมพัง
public class DiscountCalculator{ public decimal CalculateDiscount(Order order) { if (order.CustomerType == CustomerType.Regular) { return order.Total * 0.0m; } else if (order.CustomerType == CustomerType.Silver) { return order.Total * 0.05m; } else if (order.CustomerType == CustomerType.Gold) { return order.Total * 0.10m; } // ทุกครั้งที่เพิ่มลูกค้าประเภทใหม่ ต้องเข้ามาแก้ if-else นี้ throw new ArgumentException("Unknown customer type"); }}หลัง (ยึด OCP ด้วย Polymorphic OCP): ดึงพฤติกรรมที่แปรผันออกไปอยู่หลัง interface แล้วให้ DiscountCalculator ขึ้นกับ abstraction เท่านั้น
public interface IDiscountStrategy{ decimal Apply(decimal total);}
public class RegularDiscount : IDiscountStrategy{ public decimal Apply(decimal total) => total * 0.0m;}
public class SilverDiscount : IDiscountStrategy{ public decimal Apply(decimal total) => total * 0.05m;}
public class GoldDiscount : IDiscountStrategy{ public decimal Apply(decimal total) => total * 0.10m;}
// เพิ่มลูกค้าประเภทใหม่ เช่น Platinum ได้โดยเพิ่ม class ใหม่เท่านั้น// ไม่ต้องแก้ DiscountCalculator หรือ class ส่วนลดเดิมแม้แต่บรรทัดเดียวpublic class PlatinumDiscount : IDiscountStrategy{ public decimal Apply(decimal total) => total * 0.15m;}
public class DiscountCalculator{ private readonly IDiscountStrategy _strategy;
public DiscountCalculator(IDiscountStrategy strategy) { _strategy = strategy; }
public decimal CalculateDiscount(Order order) => _strategy.Apply(order.Total);}โครงสร้างนี้ตรงกับ Strategy pattern พอดี — DiscountCalculator (ผู้เรียก) และ class ส่วนลดเดิม (ผู้ถูกเรียก) ต่างขึ้นกับ IDiscountStrategy ตัวเดียวกัน เมื่อต้องเพิ่มส่วนลดแบบใหม่ ก็แค่เพิ่ม class ใหม่ที่ implement interface นี้ ไม่ต้องเปิด class เดิมขึ้นมาแก้เลย
สัญญาณว่ากำลังละเมิดหลักการนี้
หัวข้อที่มีชื่อว่า “สัญญาณว่ากำลังละเมิดหลักการนี้”- if-else หรือ switch chain ที่ยาวขึ้นเรื่อย ๆ ทุกครั้งที่มี requirement ใหม่ โดยเฉพาะที่แตกกิ่งตาม type หรือ enum
- การตรวจสอบชนิดข้อมูลด้วย
typeof/is/GetTypeเพื่อตัดสินใจพฤติกรรม แทนที่จะใช้ polymorphism - ต้องแก้ไข class ที่ผ่านการทดสอบและใช้งานจริงแล้ว ทุกครั้งที่มี feature ใหม่เข้ามา ทั้งที่ตรรกะเดิมไม่ได้เปลี่ยน
- shotgun surgery — การเปลี่ยนแปลงเล็ก ๆ หนึ่งอย่างบังคับให้ต้องไล่แก้หลาย file หลาย class พร้อมกัน
- ค่าคงที่หรือชื่อ file/path ถูก hard-code ไว้ใน method แทนที่จะรับเข้ามาเป็น parameter หรือ configuration
- ไม่มี seam หรือ extension point ใด ๆ ให้ทีมอื่นเพิ่มพฤติกรรมได้โดยไม่ต้อง fork หรือแก้ source เดิม
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- SOLID
- Don’t Repeat Yourself
- Once and Only Once
- Liskov Substitution Principle
- Dependency Inversion Principle
- Strategy Pattern
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/principles/open-closed-principle
- Open–closed principle — Wikipedia
- The Open-Closed Principle — Clean Coder Blog, Robert C. Martin
- Design Principles and Design Patterns — Robert C. Martin
- Protected Variation: The Importance of Being Closed — Craig Larman, IEEE Software (hosted at martinfowler.com)