Open/Closed Principle

👑 หัวใจหลักของ Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

"การออกแบบซอฟต์แวร์ จะต้องเพิ่มความสามารถใหม่ๆให้ซอฟต์แวร์เราได้ โดยที่ห้ามไปยุ่งกับโค้ดเดิมนะ!" นี่หัวคือใจหลักในเรื่อง OCP ในรอบนี้ แค่อ่านก็กาวแล้ว ถ้าเราจะเพิ่มความสามารถหรือพฤติกรรมใหม่ๆ ปรกติเราก็ต้องแก้โค้ดเดิมนิ แต่นี่เล่นไม่ให้แก้โค้ดเดิมเลย แล้วมันจะทำยังไงกันล่ะ?

❓ ทำไมต้องห้ามแก้โค้ดล่ะ ?

ในการแก้โค้ดเพียง 1 ครั้ง นั่นหมายถึงอาจะเกิด bug ตามมาในโปรแกรมได้ (บางทีแก้ 1 ครั้ง bug อาจะเกิดเป็นสิบๆตัวเลยก็ได้) ดังนั้นเขาเลยบอกว่าโค้ดที่มันเคยใช้งานได้อยู่แล้ว จงอย่าไปยุ่งกับมันปล่อยให้มันทำงานไป ส่วนถ้าอยากได้ความสามารถใหม่ๆจงเขียนโค้ดตัวใหม่เข้าไปเพิ่มซะ

😕 แล้วจะเพิ่มความสามารถใหม่ๆยังไง โดยไม่แก้โค้ดเดิม ?

โดยปรกติเวลาเราเขียนโค้ด เราจะชอบเขียนให้โค้ดมันดิ้นไม่ได้โดยไม่รู้ตัว เช่น กำหนดว่า method นี้จะต้องรับ parameter เป็น Concrete class นี้นะ ตามตัวอย่างด้านล่าง

Concrete class คือคลาสธรรมดาที่เราสร้างขึ้นมานี่แหละ และเป็นคลาสที่สามารถเอาไปใช้สร้าง object ได้เลย (ในคำนิยามจริงๆมันคือคลาสที่ตายตัวอ่ะนะ อ่านแล้วอาจจะ งงๆ)

// อ่านไฟล์ทั้งหมดแล้วแปลงเป็น string
public string ReadAllTextFromFile(PdfFile pdf)
{
// อ่านไฟล์ที่ส่งเข้ามาแล้วทำการแปลงเป็น string
}

ปัญหาจากโค้ดตัวอย่าง เราจะไม่สามารถส่ง object อื่นๆเข้าไปทำงานกับ method ReadAllTextFromFile() ได้เลยนอกเสียจาก object ที่มาจาก PdfFile และลูกๆของมันเท่านั้น ดังนั้นทุกครั้งที่เราอยากให้มันอ่านไฟล์แบบใหม่ได้ เราก็ต้องมาแก้โค้ดตรงนี้เสมอๆ แบบนี้ถือว่าละเมิดกฎ OCP

💡 การแก้ให้โค้ดของเราดิ้นได้

แทนที่เราจะไปผูกติด (coupling) โค้ดของเราเข้ากับ Concrete class เราก็แค่เปลี่ยนมาใช้ Abstraction งุยล่ะ

Abstraction คือคลาสที่ไม่ได้ระบุเฉพาะเจาะจงลงไปว่ามันต้องเป็นตัวไหน เช่น abstract class หรือ interface และรวมถึงการทำ Polymorphism ด้วย

ดังนั้นเราก็แค่เปลี่ยน method ด้านบนให้รับเป็น abstraction ตามโค้ดด้านล่างก็พอแล้ว

public interface IDocument
{
int TotalPages();
string ReadAllText(int pageIndex);
}
public string ReadAllTextFromFile(IDocument doc)
{
var sbd = new StringBuilder();
for (int i = 0; i < doc.TotalPages(); i++)
{
sbd.Append(doc.ReadAllText(i));
}
return sbd.ToString();
}

จากโค้ดด้านบนจะเห็นว่า เราสามารถส่งคลาสอะไรก็ได้ที่ implement IDocument เข้าไป มันก็จะสามารถทำงานด้วยได้หมดแล้วโดยที่เราไม่ต้องไปแก้ไขโค้ด ReadAllTextFromFile() แม้แต่บรรทัดเดียวเลย นี่แหละพลังแห่งการออกแบบและไม่ผิดกฎ OCP

😒 ควรออกแบบยังไงดี

สมมุติว่าเราต้องเขียนโปรแกรมจัดการไฟล์ PDF โดยที่มีคลาสจัดการไฟล์อยู่ 1 ตัว (DocumentManager) เราจะออกแบบยังไง ?

😟 การออกแบบที่ไม่ดี

ถ้าเราออกแบบให้ตัวจัดการไฟล์มันไปผูกติดอยู่กับ Concrete class เลยนั่นหมายถึง เวลาที่เราจะแก้ไขอะไร เราจะต้องไปแก้ไขเจ้า concrete class ตัวนั่นเท่านั้น นี่คือตัวอย่างการละเมิดกฎ OCP ทำให้ทุกครั้งที่จะเพิ่มไฟล์ประเภทใหม่ๆเข้าไป เช่น Word, Csv, Json ต่างๆ เราจะต้องแก้ไขโค้ดเดิมเสมอ ตามรูปด้านล่าง

😄 การออกแบบที่ควรเป็น

จากที่เราเคยผูกติดกับ Concrete class เราแค่เปลี่ยนมาเป็น Abstraction ซะ เพียงเท่านี้เราก็สามารถเปิดรับการทำงานร่วมกับไฟล์ใหม่ๆในอนาคตได้แล้ว เช่น Word, Csv, Json โดยที่เราไม่ต้องไปแก้ไขโค้ดเดิมเลย

หรือเราจะใช้ Design Pattern 2 ตัวนี้มาช่วยได้นะครับ

🎯 บทสรุป

Open/Closed Principle คือหนึ่งในหัวใจหลักในการออกแบบโค้ดแบบ OOP ซึ่งจะช่วยให้โค้ดเรายืดหยุ่นมากขึ้น reuse ได้และที่สำคัญคือบำรุงรักษาแก้ไขได้ง่ายขึ้นอีกจมเบย ดังนั้นอย่างนิ่งอยู่เฉย จงน้อมรับเอา OCP ไปฉีดเข้าเส้นไปซ๊าาา 🥴