Beyond Inheritance

Dew
Black Lens
Published in
3 min readJul 19, 2014
Beyond Inheritance

หลังจากให้เวลากับการอ่านหนังสือด้านการลงทุนไปพักใหญ่ ช่วงนี้ก็ได้หยิบหนังสือ Head First Design Patterns ออกจากชั้นหนังสือมาปัดฝุ่นอ่าน

อ่านวันละนิดวันละหน่อย อาทิตย์แรกนี้ก็เพิ่งจะอ่านจบบทแรก เสน่ห์ของหนังสือ Head First Design Patterns คือรูปประกอบและคำอธิบายที่กระชับเข้าใจง่าย นำเสนอเป็นเรื่องราวแทนที่จะเป็นแค่เนื้อหาทางวิชาการทำให้น่าติดตาม โดยมีตัวละครเอกชื่อ Joe ซึ่งผมจะขอเรียกว่าโจแล้วกัน นายโจเป็นโปรแกรมเมอร์ในบริษัทแห่งหนึ่งซึ่งเขาต้องคอยปรับเปลี่ยนซอฟท์แวร์ให้รับกับ requirement ที่เปลี่ยนแปลงไป ต้องรับมือกับปัญหาที่เกิดขึ้นระหว่างการปรับเปลี่ยนดีไซน์ ผมอ่านแล้วรู้สึกว่าสนุกน่าติดตาม จึงคิดจะเขียนสรุปทิ้งไว้ในบล็อก จะได้กลับมาอ่านฉบับที่ผมย่อยเองในภายหลัง

เรื่องราวในบทแรกเกริ่นนำถึงโปรแกรม SimUDuck ซึ่งเป็นโปรแกรม simulator ของบริษัทที่โจทำงานอยู่ โปรแกรมนี้จำลองสระน้ำที่มีเป็ดหลายชนิดว่ายน้ำรวมกันอยู่ โจดีไซน์คลาสต่างๆที่จะใช้ในโปรแกรมไว้แบบนี้

SimUDuck v1
SimUDuck v.1

จากไดอะแกรมข้างต้นจะเห็นว่าคลาสเป็ดทุกแบบจะแชร์เมธอด quack() และ swim() และถูกบังคับให้โอเวอร์ไรด์แอบสแตรกเมธอด display() ซึ่งเป็นเมธอดที่ใช้แสดงผลหน้าตาของเป็ดแต่ละประเภท (แน่นอนว่าเป็ดแต่ละประเภทย่อมมีรูปลักษณ์แตกต่างกัน)

ต่อมาทางทีมบริหารอยากให้โปรแกรม simulator นี้มีเป็ดประเภทที่บินได้ด้วย โจจึงปรับดีไซน์ใหม่ออกมาแบบนี้

SimUDuck v2
SimUDuck v2

ส่ิงที่โจทำเพิ่มเพื่อให้เป็ดบินได้คือเขาไปเพิ่มเมธอด fly() ในคลาส Duck ซึ่งเป็นคลาสแม่ของคลาสเป็ดทุกคลาส แน่นอนตอนนี้เป็ดบินได้แล้ว แต่กลายเป็นว่าเป็ดบางประเภทที่ไม่สมควรบินได้อย่างเช่นคลาส RubberDuck หรือเป็ดตุ๊กตายางก็พลอยบินได้ไปด้วย!

ณ จุดๆนี้ในหนังสือเล่าว่าผู้บริหารโมโหมาก เพราะตอนไปเดโมเป็ดดันบินได้กันหมดทุกตัว โจโดนแซวทางโทรศัพท์ว่าให้ไปเปิดเว็บ monster.com ซึ่งเป็นเว็บหางานด้วย เหอๆ

ด้วยดีไซน์นี้โจเองก็คิดว่ามันสามารถแก้ปัญหา RubberDuck ได้ด้วยการโอเวอร์ไรด์เมธอด fly() และเมธอด quack() ในคลาส RubberDuck ให้ไม่ต้องทำอะไรเลย (รวมถึงไม่เรียกคลาสแม่ผ่าน super ด้วย) เพราะตุ๊กตาเป็ดไม่ร้องและไม่บิน แต่ข้อเสียคือถ้ามีคลาสที่มีข้อจำกัดประมาณนี้อีกก็ต้องมาใช้วิธีเดียวกันนี้ ซึ่งจะทำให้เราทำความเข้าใจ behaviour ของคลาสลูกแต่ละคลาสยาก เพราะมันสะเปะสะปะไปหมด ไม่สามารถบอกได้ในภาพดีไซน์แต่ต้องลงไปดู implementation ของคลาสลูกแต่ละตัวเพื่อทำความเข้าใจว่าตัวไหนที่ไม่ร้องไม่บิน นี่มันฝันร้ายชัดๆ!

โจจึงแก้ปัญหาด้วยการแยกทั้งเมธอด fly() และเมธอด quack() ออกจากคลาส Duck มาเป็น interface แล้วให้คลาสลูกแต่ละตัวเลือกไปอิมพลีเมนท์เองตามแต่เหมาะสม

SimUDuck v3
SimUDuck v3

วิธีข้างต้นก็ดูเหมือนจะดี สามารถแก้ปัญหาตุ๊กตาเป็ดบินได้ เพราโจไม่ได้ให้คลาส RubberDuck ไปอิมพลีเมนท์ Flyable interface และ Quackable interface แต่ก็ได้ปัญหาใหม่ตามมาคือความซ้ำซ้อนหรือ duplicate code นั่นเอง เพราะถ้าคลาสลูกที่บินได้มีวิธีการบินที่เหมือนกัน ทุกครั้งที่เปลี่ยนวิธีการบินก็ต้องไล่แก้ในทุกคลาส

มาถึงจุดนี้ในหนังสือได้ระบุข้อความหนึ่งที่น่าสนใจไว้ว่า

The one constant in software development : CHANGE

No matter how well you design an application, over time an application must grow and change or it will die.

ถึงตอนนี้ เป็นอันเข้าใจได้ว่า behaviour ของคลาสลูกสามารถเปลี่ยนไปได้เสมอ และก็ไม่ใช่ทุกคลาสที่จะมีครบทุก behaviour ทางออกคือยก behaviour ออกมาจากคลาส Duck ตรงนี้หนังสือยก design principle แรกขึ้นมาว่า

Design Principle

Identify the aspects of your application that vary and separate them from what stays the same.

จาก design principle ข้างต้นหนังสือขยายความต่อว่า

Take the parts that vary and encapsulate them, so that later you can alter or extend the parts that vary without affecting those that don’t.

จากแนวคิดข้างต้น จึงนำไปสู่การดึงกลุ่มของ flying behaviours และ quacking behaviour ออกไปจากคลาส Duck

Take the parts that vary and encapsulate them.
Take the parts that vary and encapsulate them.

เมื่อรู้แล้วว่าควรจะดึง behaviour ออกมา คำถามต่อมาคือจะดึงออกมาให้อยู่ในรูปแบบใด ตรงนี้หนังสือก็ยกอีก principle หนึ่งขึ้นมาว่า

Design Principle

Program to a supertype, not an implementation.

หลักการข้างต้นจะตรงข้ามกับสิ่งที่โจเคยทำมา ที่ผ่านมา behaviour ต่างๆจะถูกอิมพลีเมนท์ไว้ในคลาสแม่หรือไม่ก็คลาสลูก(ในกรณีที่คลาสลูกมี behaviour ที่แตกต่างออกไป)

จาก principle ข้างต้น supertype ในที่นี่เป็นได้ทั้ง interface และ abstract class เวลาเรียกใช้ behaviour ก็จะทำผ่าน supertype ด้วยเทคนิค polymorphism

SimUDuck v4
SimUDuck v4 using strategy pattern

จะเห็นว่าคราวนี้โจสามารถเลือกใส่ behaviour ที่ต้องการให้กับคลาส Duck ได้ เพราะคลาส Duck มีสมาชิกเป็น FlyBehaviour และ QuackBehaviour

แต่ถึงกระนั้นสุดท้ายในคลาสลูกก็ยังคงต้องกำหนดว่าจะใช้ behaviour ไหนเป็นค่า default ดังตัวอย่างคลาส MallardDuck

public class MallardDuck extends Duck {  //...    public MallardDuck(){
quackBehaviour = new Quack();
flyBehaviour = new FlyWithWings();
}
//...}

ลักษณะโค้ดข้างต้นยังคงเป็นแบบ program to implementation เพราะยังต้องระบุทั้ง Quack และ FlyWithWings ซึ่งทั้งคู่เป็น concrete implementation class

การเขียนแบบนี้เท่ากับว่ายังไม่ทำตาม principle ซะทีเดียว ซึ่งในหนังสือยังคงลักษณะการ implementation ไว้แบบนี้ก่อนในบทแรก และจะนำเสนอวิธีแก้ต่อไปในบทอื่น

หลังจากที่โจดึงส่วนของ behaviour ของเป็ดแยกออกไป แทนที่จะอิมพลีเมนท์ในคลาส Duck ตรงๆ หนังสือก็สนับสนุนวิธีการข้างต้นโดยระบุ principle ต่อมาว่า

Design Principle

Favour composition over inheritance.

หนังสือขยายความ principle ข้างต้นกับตัวอย่างคลาส Duck ว่า

Instead of inheriting their behaviour, the ducks get their behaviour by being composed with the right behaviour object.

สุดท้ายหนังสือก็สรุปว่า วิธีการข้างต้นที่โจเอามาจัดระเบียบดีไซน์ใหม่นี่คือ Strategy Pattern !

The Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

จากทั้งหมด ที่ผมเข้าใจ Strategy Pattern เป็นแพทเทิร์นประเภท Behaviour Pattern หรือแพทเทิรน์ที่จัดระเบียบพฤติกรรมการทำงานระหว่างกันของคลาสต่างๆ
Strategy pattern เหมาะกับเหตุการณ์ที่ algorithm หรือ behaviour สามารถปรับเปลี่ยนได้อย่างเสรี ณ เวลา runtime ตัวอย่างหนึ่งที่น่าจะเข้าใจง่ายคือเกมส์ที่ตัวละครในเกมส์แต่ละตัวสามารถเลือกเปลี่ยนอาวุธที่ใช้ในเกมส์ได้ ซึ่งอาวุธก็คือส่วนของ behaviour ที่ตัวละครสามารถปรับเปลี่ยนได้นั่นเอง

NOTE

UML diagram ข้างบนวาดด้วยโปรแกรม Astah Community ส่วนรูปแสดงการแยก Duck behaviours ใช้โปรแกรม yED Graphic Editor

--

--