The Open-Closed Principle เปลี่ยนพฤติกรรม Code โดยไม่แก้ไข Code ทำยังไงหละนี้ (ตอน 1)
“ Dutch Door — A door divided in two horizontally so that either part can be left open or closed. ”
ใน Software Development Cycle นั้น ระบบมันจะมีการเปลี่ยนแปลงอยู่เสมอในแต่ละ Cycle (Requirements ใหม่ๆจากลูกค้า)
คำถามคือเราจะทำยังไงให้ Code ที่เราเขียนขึ้นเสถียร เสถียรในที่นี้คือ จะทำยังไงให้เราไม่ต้องกลับมาแก้ไขมันบ่อยๆ หรือถ้าจะให้ดีคือเขียนครั้งเดียวใช้ได้ไปตลอดเลย ไม่ต้องกลับมาแก้ไขมันอีกแล้ว
นี้ก็เป็นที่มาของ OCP: The Open-Closed Principle เป็น Principle ที่ 2 ของ SOLID ซึ่งมีการนำมา apply ใน OOP เพื่อแก้ไขปัญหานี้
Principle ของมันมีอยู่ว่า
“ Code ที่เราเขียนขึ้นมาควรเปิดให้เปลี่ยนแปลงพฤติกรรมของมัน แต่ปิดไม่ให้แก้ไข”
อ่านอีกที
จะทำยังไงให้ Code ที่เราเขียนขึ้นมาเปลี่ยนแปลงพฤติกรรมของมันได้ โดยที่ไม่เปลี่ยนแปลง Code ของมันเลย
อ่านมาถึงตรงนี้ หลายๆคนโดยเฉพาะมือใหม่แบบผมเนี่ย คงคิดว่าบ้าไปแล้ว คนคิด Principle นี้มันต้องบ้าๆแน่ มันดูไม่เป็นเหตุเป็นผลเลย เปลี่ยนแปลงโดยห้ามแก้ไข โอ้วว ใครจะทำได้วะครับ 555+
ดีครับใครที่คิดแบบนี้ เกิดมาเป็นคนอย่าเชื่ออะไรง่ายๆครับ 555+ จนกว่าเราจะได้เห็นด้วยตา หรือได้พิสูจน์ด้วยตัวเอง
อะครับงั้นผมจะพาไปพิสูจน์กันว่ามันทำได้จริงๆน้า
มาเริ่มกันเลย
ตัวอย่างนะครับ สมมุติลูกค้าต้องการเขียนโปรแกรมเพื่อเล่นเสียงดนตรีไทย
ตอนแรกลูกค้า require เข้ามาต้องการเครื่องดนตรีแค่ 2 ประเภทครับ คือ
- เครื่องดีด (Tap)
- และเครื่องสี (Scrape)
โดยเครื่องดนตรีที่มีในรุ่น Beta ก็จะเป็น พิณ และ ซอครับผม ผมก็สร้าง Class ขึ้นมา แล้วก็มี property ชื่อ itsType สำหรับบันทึกว่าเครื่องดนตรีชนิดนี้เป็นประเภทไหน
จะเห็นได้ว่าถ้าต้องการให้โปรแกรมเล่นพิณ เราต้องเรียก method tap(ดีด) ส่วนถ้าจะเล่นซอก็ต้องเรียก method scrape(สี) ครับ
ผมก็สร้าง function playAllInstruments ขึ้นมา โดยรับ Object ของเครื่องดนตรีหลายๆชิ้นเข้ามา ตรงนี้ผมใช้ Array<any> อยู่นะครับ คือรับอะไรเข้ามาก็ได้ จาก Code จะเห็นได้ว่าผมต้องเช็ค Propery ของ Object instrument ทุกครั้ง แต่เดี๋ยวปัญหาเหล่านี้จะถูกแก้ไขครับ : )
ตอนนี้ระบบทำงานได้ราบรื่น Ok ดีครับ
จนวันนึงลูกค้า require เข้ามา ว่าอยากให้ระบบเล่นได้ครบเลย ทั้งเครื่องดีด สี ตี เป่า เท่ากับว่าผมต้องเพิ่ม case ของเครื่อง ตี กับ เป่า เข้าไปครับ
จะแก้ปัญหายังไงดีหละ ???
อย่างแรกเลยคือต้องเพิ่ม Type ของ ตี กับ เป่า เข้าไป ใน enum InstrumentType ครับ
- มีหนึ่งที่แล้วนะครับที่เราต้องแก้ไข (Break กฎเรียบร้อบ)
ยังไม่หมดครับผมต้องเพิ่ม case ใน function playAllInstruments ด้วยครับ ดังรูป
- 2 ตำแหน่งแล้วครับที่ต้องแก้ (Break กฎอีกแล้ว)
คราวนี้ลองนึกถึงอนาคตนะครับ ถ้ามีเครื่องดนตรีประเภทใหม่ๆเข้ามาอีก (อาจจะแบบใช้ลิ้นเล่นอะไรแบบนี้ 555+) เท่ากับว่าคุณต้องมาเพิ่ม ชนิดของมันใน enum InstrumentType แล้วก็มาแก้ไข function playAllInstruments ทุกครั้ง ซึ่งมันผิดกับกฎข้อนี้เต็มๆเลยครับ
แล้วเราจะแก้ไขให้มันถูกกฎยังไงดีหละ อย่าลืมนะครับกฎมีอยู่ว่า
“ Class, module, function, etc ที่เราเขียนขึ้นมาควรเปิดให้เปลี่ยนแปลงพฤติกรรมของมันได้ แต่ปิดไม่ให้แก้ไข Code ในตัวของมัน ”
จะอ่านอีกกี่ทีก็รู้สึกว่ามันไม่น่าจะเป็นไปได้เลย 555+
แล้วจะแก้ไขยังไงดีหละ คำตอบคือ “Abstract” ครับผม สำหรับใน OOPL Abstract ก็คือการใช้ Interface หรือ Abstract Class นั่นเอง
ในที่นี้เราจะใช้ประโยชน์ของ polymorphism ครับ รายละเอียดของมันคร่าวๆก็คือ method ของแต่ละ class สามารถมีชื่อเหมือนกันได้ แต่พฤติกรรมใน method นั้นๆไม่จำเป็นต้องเหมือนกัน
สมมติง่ายๆครับ มี laptop กับ ประตู ทั้ง 2 objects นี้มี method ที่ชื่อว่าเปิด ครับ แต่เปิดประตู กับ เปิดlaptop เนี่ยพฤติกรรมมันต่างกันใช่ไหมหละครับ นี้แหละครับ polymorphism ง่ายๆ สั้นๆ เลย 555+ (อยากอ่านเพิ่มเติม Search ใน google ดูครับ)
คราวนี้เราก็มา Abstract เครื่องดนตรีของเรากันครับ ซึ่งในที่นี้เราก็รู้อยู่แล้วหละครับว่า พิณ กับ ซอ เนี่ย เป็น “เครื่องดนตรี (Instrument)” และเครื่องดนตรีทุกชนิดก็สามารถ “เล่น (play)” ได้ (นี้แหละครับคือการ Abstract)
ใน OOPL เราอาจจะใช้ Abstract Class หรือ Interface ก็ได้ครับ ในตัวอย่างนี้ผมใช้ Interface ก็แล้วกันครับ ดังนี้
จากตรงนี้จะเห็นได้ว่า instruments ที่รับเข้ามา จะต้องเป็น Array ของ Object ที่ implement Instrument ครับ ทำให้ผมไม่ต้องมานั่งเช็ค Type อีกแล้ว และก็แน่นอนทุก Object จะต้องมี method play นั่นเอง
ตอนนี้ Code อ่านง่ายแล้วก็ Clean ขึ้นเยอะเลยครับ แต่ที่สำคัญที่สุดคือต่อไป ถ้าลูกค้า require มาให้เพิ่มเครื่องดนตรีชิ้นอื่นๆ ประเภทอื่นๆอีก ผมก็ไม่ต้องกังวลใจอีกแล้ว ไม่ต้องมาแก้ไขใน function playAllInstruments อีกต่อไป ที่ต้องทำคือสร้าง Class ของดนตรีชิ้นนั้นๆขึ้นมาครับ (เขียน Code ใหม่ ไม่มีกลับไปแก้ของเก่า)
เวลาผ่านไปลูกค้าที่น่ารักอยากให้โปรแกรม เล่นเสียงขิม กับ ขลุ่ยได้ สิ่งที่ผมต้องทำก็คือ สร้าง 2 Class นี้ขึ้นมาครับ
ไม่มีต้องกลับไปแก้ของเก่าเลย : ) สรุปอีกที คือเป็นไปตามกฎ OCP ดังนี้
function playAllInstruments ของผมสามารถเล่นเครื่องดนตรีได้หลายชนิดขึ้นได้ โดยที่ไม่ต้องไปแก้ไข Code ในตัวของมันเลย (Closed for modification) ที่ต้องทำคือเขียน Code ใหม่ขึ้นมาเพื่อทำให้มันเล่นเครื่องดนตรีได้หลายชิ้น หลายชนิดขึ้น (Open for extension)
อันนี้มาดูเปรียบเทียบกันอีกทีก่อนและหลังครับ
ปล. วิธีการแก้ไขปัญหานี้ อาจจะยังไม่ใช่ Best Practice นะครับ แต่ที่อยากจะแชร์คืออยากให้ท่านผู้อ่านได้รู้จัก Concept ของ OCP และก็เข้าใจมันครับผม
ในบทความนี้ก็ขอจบไว้เท่านี้ก่อนนะครับ ถือว่าเป็น Part 1 ก็แล้วกัน ในบทความหน้าก็ยังคงเป็นเรื่องนี้อยู่ครับ แต่เดี๋ยวจะมี requirement ใหม่มาจากลูกค้า ซึ่งทำให้ผมต้อง break กฎข้อนี้อีกรอบ (ลูกค้านี้ชอบเปลี่ยน requirement จริงๆเลย)
ก็ขอขอบคุณทุกท่านที่เข้ามาอ่านนะครับ มีอะไรก็แนะนำติชมได้เลยครับ เต็มที่
สำหรับใครอยากรู้ว่าทำไมเราควร Apply SOLID Principlesใน Software ของเรา อ่านที่บทความนี้ได้เลยครับ
สำหรับ Principle แรกของ SOLID: The Single-Responsibility Principle อ่านได้ที่นี้จ้า
Reference:
Robert cecil martin. (2002). Agile Software Development, Principles, Patterns, and Practices. (1st ed.). England: Pearson.
“ Happiness Only Real When Shared ”
ที่เริ่มเขียน Blog เพราะไปดูหนังเรื่องนึงแล้วเจอ Quote นี้ ผมก็เลยจะลองดูครับว่ามันจริงหรือเปล่า
Fanpage ครับ : https://www.facebook.com/imkrish.developer/