Software Design

🎎 Prototype Pattern

แนวคิดในการก๊อปปี้ object แบบง่ายๆ

เจ้าตัวนี้ผมขอตั้งชื่อเป็นภาษาไทยว่า ต้นแบบ ซึ่งมันอยู่ในกลุ่มของ 🤰 Creational Patterns โดยเจ้าตัวนี้มันจะมาช่วยให้เราสามารถ ก๊อปปี้ object ตัวหนึ่ง ออกไปเป็นอีกตัวหนึ่งได้แบบง่ายๆ ซึ่งอ่านแล้วก็อาจจะ งงๆ ว่าทำไมต้องทำ ดังนั้นไปดูตัวอย่างปัญหาของเรากันดีกั่ว

แนะนำให้อ่าน บทความนี้เป็นส่วนหนึ่งของมหากาพย์ Design Patterns ที่จะมาเป็น guideline ในการแก้ปัญหาในการออกแบบซอฟต์แวร์โปรเจค หากใครสนใจอยากเข้าใจตั้งแต่ต้นว่ามันคืออะไร และเจ้า patterns ทั้ง 23 ตัวมีอะไรบ้าง ก็สามารถจิ้มตรงนี้เพื่อไปอ่านบทความหลักได้เบยครัช 👦 Design Patterns

หมายเหตุ เนื้อหาของบทความนี้จะเน้นให้เข้าใจหลักการทำงานของ Design Patterns แต่ละตัว โดยใช้เกม Ragnarok เป็นการอธิบาย ซึ่งบางอย่างอาจจะไม่ตรงกับตัวเกมจริงๆนะขอรับ Gravity อย่ามาจับผมนะผมโดนแมวน้ำครอบงำ + รู้เท่าไม่ถึงการ + ผมเป็นคนดี + ผมมีลูกมีเมียมีสามีที่ต้องดูแล 😭

เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺

🧐 โจทย์

สมมุติว่าเรามี object ของตัวละครอยู่ 1 ตัวซึ่งมีข้อมูลคือ มีเลเวล 2, ใส่หมวกกวาง, อาชีพนักผจญภัยฝึกหัด, พลังชีวิต 20 และ พลังเวทมนต์ 12 ตามรูปด้านล่าง

แล้วด้วยเหตุผลอะไรก็ตามแต่ ทำให้เราต้องสร้าง object ที่มีข้อมูลแบบเดียวกันแป๊ะๆเลย เราจะทำยังงุยกันน๊า ?

อยากได้ object ใหม่ที่มีข้อมูลเหมือนตัวเดิมเลยอ่าาาา

🧒 แก้โจทย์ครั้งที่ 1

หลายคนพอเห็นโจทย์แล้วก็ยิ้มหวานหมูเลย เพราะเราก็แค่ไปสร้าง object ใหม่ขึ้นมา โดยเอาข้อมูลจาก object เก่าไปยัดใส่ object ใหม่เท่านั้นเองก็เสร็จแล้วนิน่า ซึ่งสมมุติว่าตัวละครมันถูกสร้างมาจากคลาสประมาณนี้ละกันนะ

ดังนั้นโค้ดเราก็จะออกมาราวๆนี้ชิมิ ?

Character object2 = new Character();
object2.Level = object1.Level;
object2.Hat = object1.Hat;
object2.Class = object1.Class;
object2.HP = object1.HP;
object2.SP = object1.SP;

สมมุติว่า object ต้นฉบับของเราอยู่ในตัวแปรที่ชื่อว่า object1 นะ

จากโค้ดด้านบนก็ดูเหมือนว่าเราจะได้ผลลัพท์เป็นแบบด้านล่างละชิมิ?

🐱‍🐉 ปัญหาที่ 1

อะเชคราวนี้เรา ลองให้ object2 เปลี่ยนไปใส่หมวกแซนต้า กับ แก้พลังชีวิตเป็น 55 ดูดิ๊ ตามโค้ดด้านล่าง

object2.Hat.Name = "Santa Hat";
object2.HP = 55;

ซึ่งผลลัพท์ที่เราจะได้ก็คือ

หมวกของ object 1 ถูกเปลี่ยนด้วยเฉยเลย

โค้ดของเราแก้ไขข้อมูลให้กับ object2 เท่านั้น ไม่ได้ยุ่งอะไรกับ object1 เลย แต่การแก้ไขครั้งนี้ดันมีผลกับ object1 ด้วยในบางอย่าง เช่น หมวกโดนเปลี่ยนเหมือนกันทั้งคู่ แต่พลังชีวิตกลับเปลี่ยนแค่ object2 เท่านั้น

🐱‍🐉 ปัญหาที่ 2

แล้วถ้าคลาสของเรามันไม่ได้ง่ายแบบนั้นล่ะ เช่นมันมีพวก Private members อยู่ในนั้นด้วยล่ะ เราจะเอาค่ามันมาใส่ใน object 2 ได้ยังไง? ตามรูปด้านล่าง

อุ๊ต๊ะมี private member ด้วย !

จากรูปด้านบน เราไม่มีทางเข้าถึงตัวแปร privateData เลย ดังนั้นเราก็จะไม่มีทางกำหนดค่านี้ให้กับ object 2 ได้เลยเช่นกัน

🐱‍🐉 ปัญหาที่ 3

ถ้ากรณีที่เลวร้ายกว่านั้นคือ เราไม่รู้ว่า object ที่เราได้มามันถูกสร้างมาจาก class อะไรล่ะ เช่น เรามี object 1 โดยมันอยู่ในรูปของ interface ตามโค้ดด้านล่าง

public interface ICharacter { ... }
ICharacter object1;
ICharacter object2 = new ???

จากโค้ดด้านบนมันจะทำให้เราไม่สามารถสร้าง object2 ได้เลย เพราะเราไม่รู้ว่าจะไป new class อะไรนั่นเอง

🐱‍🐉 ปัญหาที่ 4

ถ้า Object 1 ของเรามันไปใช้คลาสอื่นๆอีกเต็มไปหมดเลย ตามรูปด้านล่าง แล้วเราจะต้องไปค่อยๆสร้างข้อมูลแต่ละตัวให้กับ Object 2 แล้วล่ะก็ นั่นหมายความว่าเรา ทุกครั้งที่เราจะก๊อปปี้ object 1 มันจะเกิด Dependency วุ่นวายเต็มแถวๆนั้นไปหมดเลย อ่ะดิ

ใช้คลาสอื่นๆเต็มไปหมดเบย

จะสร้าง Object 2 ได้ต้องไป import เจ้าพวกคลาส Equipment, Inventory, Skill, Status มาก่อนไม่งั้นสร้าง object พวกนั้นไม่ได้

นี่มันเกิดอะไรกันขึ้นนะ? ปัญหามันจะเยอะไปหน๋ายยย แล้วเราจะแก้ปัญหาพวกนี้ยังไงดีอ๊าาาาา 😭

🧒 แก้โจทย์ครั้งที่ 2

🔥 วิเคราะห์ปัญหา

(จากปัญหาที่ 1) เรื่องนี้ไม่ได้เกี่ยวข้องโดยตรงกับ Design Pattern ตัวนี้เท่าไหร่ แต่มันเป็นเรื่องพื้นฐานในการเขียนโปรแกรม นั่นคือเรื่องของการใช้ Value type กับ Reference type นั่นเอง ซึ่งมันทำให้เกิดปัญหาหมวกเปลี่ยนทั้ง 2 objects โดยที่เราไม่ตั้งใจ ส่วนวิธีการแก้ไขเจ้าเรื่องนี้เราต้อง เข้าใจว่า Value type ต่างกับ Reference type ยังไงเสียก่อน ถึงจะสามารถแก้ปัญหาตัวนี้และตัวถัดไปได้ขอรับ

แนะนำให้อ่าน สำหรับใครที่อยากศึกษาต่อว่า Value type กับ Reference type คืออะไร สามารถไปทำความเข้าใจได้กับบทความตัวนี้ขอรับ Value type vs Reference type มันเป็นเรื่องพื้นฐานสุดๆของการเขียนโปรแกรม ดังนั้นขอไม่ลงรายละเอียดตัวนี้ต่อนะ

(จากปัญหาที่ 2) ตัวข้อมูลที่เราเข้าถึงไม่ได้ เช่น protected, private, internal เราสามารถแก้ให้เข้าถึงด้วยช่องทางอื่นๆก็ได้ แต่ไม่ใช่เรื่องที่ดี เพราะมันจะเสียคุณสมบัติ Encapsulation และความปลอดภัยไป (ดังนั้นไม่ขอพูดถึงวิธีนี้) ซึ่งตัวที่มีสิทธิ์เข้าถึงข้อมูลพวกนั้นได้จริงๆก็คือ ตัวคลาสมันเองนั่นแหละ ถึงจะเหมาะสมสุด

แนะนำให้อ่าน อะไรนะลืมการทำ encapsulation ไปแล้วรึ งั้นไปทบทวนได้จากลิงค์นี้เบย 💖 Encapsulation

(จากปัญหาที่ 3) เราไม่มีทางรู้เลยว่า class ที่แท้จริงของมันคืออะไร ถ้าเราไม่นั่ง debug หรือไล่ไปดู source code ซึ่งการไล่หา class ที่แท้จริงของมันไม่ใช่เรื่องที่ดีนัก เพราะหลักในการออกแบบจริงๆเรา ไม่ควรทำงานกับ Concrete class ยังไงล่ะ

แนะนำให้อ่าน หลักในการออกแบบขั้นพื้นฐานคือการ ทำงานกับ Abstraction นั่นเอง ซึ่งถ้าอยากศึกษาว่าทำไมต้องเป็นแบบนั้นก็ไปอ่านได้จากลิงค์นี้ Creational Pattern ส่วนถ้าอยากเข้าใจลึกๆถึงแก่นจริงๆลองศึกษาได้จากบทความนี้ Dependency-Inversion Principle

🔥 แก้ไขปัญหา

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

ดังนั้นเราก็จะเพิ่มเมธอดในการก๊อปปี้ให้กับคลาส Character เข้าไป 1 ตัวนั่นก็คือ Clone นั่นเองตามรูป

เพราะคลาส Character สามารถเข้าถึง private members ได้ทุกตัวอยู่แล้วเลยไม่มีปัญหาที่จะเข้าถึงตัวแปร privateData ยังไงล่ะ ตามโค้ดด้านล่างเบย

public class Character
{
public int HP;
public int SP;
public int Level;
public string Class;
private int privateData;
public Character Clone()
{
return new Character
{
HP = HP,
SP = SP,
Level = Level,
Class = Class,
privateData = privateData
};
}
}

เพียงแค่นี้ปัญหาข้อ 2 ก็ถูกแก้ไขไปเรียบร้อยแล้ว (ส่วนข้อ 1 ก็ทำโคลนออกมาเหมือนกัน หรือจะสร้าง object ใหม่ก็แล้วแต่ความยากง่ายของมัน)

หรือต่อให้มันเป็น interface ตามรูปด้านล่าง (ที่เป็นปัญหาในข้อ 3) เจ้าเมธอด Clone ก็ทำงานได้เช่นเคยนะจ๊ะ

จากที่ทำมาทั้งหมด ปัญหาข้อ 4 ก็จะหายไปเอง เพราะเวลาใครอยากจะก๊อปปี้ object ก็เรียกใช้งานเมธอด Clone เท่านั้นเอง ไม่ทำให้เกิด Dependency ไปเลอะที่อื่นแล้วตามโค้ดด้านล่างเบย

ICharacter object1;
ICharacter object2 = object1.Clone();

จากโค้ดด้านบน ไหนลองเอา object2 มาเปลี่ยนเป็นหมวกแซนต้าแบบเดิมดูดิ๊

object2.Hat.Name = "Santa Hat";

ซึ่งผลลัพท์ที่ได้ก็จะทำให้ ตัวละครเป็น object คนละตัวกันอย่างแท้จริง เพียงแต่ข้อมูลมันถูกก๊อปปี้มาจาก object1 เท่านั้นเอง ตามรูปด้านล่าง

🤔 แล้วถ้ามันเป็นคลาสลูกล่ะ ?

จากที่ทำมาทั้งหมดเราจะสามารถก๊อปปี้ object จากคลาส Character ได้แล้ว แต่ถ้าเกิดว่ามีคลาสอื่นมา inheritance จากคลาสนี้ไปตามรูปด้านล่างล่ะ ซึ่งคลาสเหล่านั้นอาจจะมี members หรือ private members ที่ต่างจากคลาสแม่ก็เป็นได้นะ

เพื่อการออกแบบให้ยืดหยุ่นรองรับกรณีคลาสลูกด้วย ดังนั้นเราก็จะทำให้เจ้าเมธอด Clone มันเป็นกลางๆนั่นเอง เพื่อให้คลาสอื่นๆสามารถเอาไปปรับแก้ในรูปแบบของตัวเองต่อได้ง่ายๆ โดยการเปลี่ยนให้มันส่งค่า object กลับไป และให้คลาสลูกสามารถทำ override จากคลาสแม่ต่อได้

ดังนั้นเวลาที่เราอยากจะให้คลาสมันมีความสามารถในการก๊อปปี้ได้ เราสามารถเลือกทำได้หลายวิธีเช่นทำเป็น abstract class ไว้ เพื่อให้ concrete class เป็นคนจัดการต่อ

ทำเป็น abstract class ซะ

หรือเราจะสร้าง interface สำหรับทำก๊อปปี้ที่เป็นกลางๆเอาไว้ แล้วให้ concrete class ไปจัดการก็ได้เช่นกัน

ทำเป็น interface กลางๆ ใช้ได้ทุกสถานะการณ์

🤔 Prototype คือไย ?

🔥 จุดกำเหนิด

ในบางทีการก๊อปปี้ object ก็ไม่ใช่เรื่องง่าย เพราะบางทีเราก็ไม่รู้เหมือนกันว่าเราจะต้องสร้างมันจากคลาสไหน หรือบางทีเราก็ไม่สามารถเข้าไปกำหนดค่า private members ของมันได้ แถมไหนจะเรื่อง dependency ที่จะตามมาอีก

🔥 ผลจากการใช้

เราสามารถก๊อปปี้ object ที่มีค่าด้านในเหมือนกับต้นฉบับได้แบบไม่ผิดเพี้ยน เข้าถึงไส้ในมันได้ทะลุถึงไส้ถึงพุง แถมยังไม่ต้องมากังวลในเรื่องของ concrete class ที่จะสร้าง และ dependency ต่างๆที่จะตามมาด้วย โดยการเรียกผ่าน method เดียวเท่านั้นเอง

🔥 วิธีการใช้

ตรงจุดนี้จะขออธิบายออกเป็นทีละขั้นตอนแบบนี้ละกัน คนที่พึ่งหัดออกแบบจะได้เข้าใจได้ง่ายๆนะ ซึ่งในรอบนี้จะอธิบายในการใช้ interface ดูบ้างนะ เพราะเห็นตัวอย่างการทำแบบ abstraction ไปแล้ว

ถ้าเราอยากจะให้คลาสต่างๆมีความสามารถในการก๊อปปี้ได้ล่ะก็ เราก็จะสร้าง interface กลางๆขึ้นมา 1 ตัว ที่เอาไว้ใช้ในการก๊อปปี้อะไรก็ได้ ตามรูปด้านล่าง

ส่วนคลาสไหนอย่าจะให้มันมีความสามารถในการก๊อปปี้ เราก็แค่ implement interface ตัวนั้นซะ ตามรูปด้านล่าง

ส่วนถ้ามันมีคลาสลูก ก็ให้คลาสลูกทำการ override ไปจัดการในส่วนที่มันต่างกับของคลาสแม่ซะ ตามรูปด้านล่าง

จากที่ร่ายยาวมาทั้งหมดเราก็จะได้แนวทางในการออกแบบที่ชื่อว่า Prototype Pattern ออกมา ซึ่งมีหน้าตาประมาณรูปด้านล่างนี้นั่นเองครัช

ไหนลองเอาที่เราออกแบบมาเทียบกันดูดิ๊ ... เหมือนกันเปี๊ยบเบย ต่างกันแค่ในตัวอย่างนี้ไม่ได้ทำเป็น interface เฉยๆ ซึ่งจะทำก็ได้ไม่ทำก็ได้แบ๊วแต่เรย

🤔 ทำไมต้องใช้ด้วย ?

จากปัญหาต่างๆที่ว่าไว้ในตอนแรก เราจะเห็นถึงความวุ่นวายในการสร้าง object แล้วชิมิ ดังนั้นถ้าเรานำ Prototype Pattern ไปใช้ มันจะ ช่วยลดความซับซ้อนในการสร้างและก๊อปปี้ข้อมูล จาก object A ไป object B ได้ด้วย เพราะความซับซ้อนทั้งหมดมันถูกซ่อนไว้ภายในเมธอด Clone แล้วยังไงล่ะ

🎯 บทสรุป

👍 ข้อดี

การนำ Prototype Pattern มาใช้งานนั้นจะช่วย ลดการผูกกันของโค้ดลง เพราะเราไม่ต้องไปวุ่นวายกับ Concrete class และ Dependency ต่างๆของมันอีกแล้ว แถมยังใช้งานได้ง่ายอีกด้วย

👎 ข้อเสีย

เพิ่มความซับซ้อนโดยไม่จำเป็น ถ้า object มันสามารถ copy ค่าได้ง่ายๆอยู่แล้วจะ new object ใหม่ธรรมดาอาจจะง่ายกว่า และถ้า object มีการอ้างกันแบบงูกินหาง (Circular references) จะทำได้ยาก และอาจเกิดปัญหาตามมาภายหลัง

เกร็ดความรู้ Circular references คือการที่ object A ไปอ้างถึง object B และเจ้า object B ก็ดันมีการอ้างกลับมาที่ object A เช่นกัน ตามโค้ดด้านล่างนี้

var a = new ObjectA();
var b = new ObjectB();
a.Ref = b;
b.Ref = a;

🤙 ทางเลือก

โดยปรกติเรื่องการก๊อปปี้ object นั้นเป็นปัญหามานานแล้ว ดังนั้นในแต่ละภาษาเขาจะมีวิธีการจัดการของเขาเองอยู่แล้ว เช่นของภาษา C# ก็จะมี interface ที่ชื่อว่า ICloneable อยู่แล้วเราไม่ต้องไปสร้างเอง หรือพวกเมธอด MemberwiseClone ดังนั้นจงไปศึกษามาตรฐานของภาษาที่ตัวเองใช้ให้ดีก่อนนะขอรับ

ข้อควรระวัง อย่านำ Prototype Pattern ไปใช้มั่วซั่ว เพราะมันทำให้โค้ดของเราซับซ้อนขึ้นเยอะเลยแทนที่เราจะใช้คำสั่ง new แบบปรกติ ดังนั้นให้ชั่งน้ำหนักให้ดีเสียก่อนว่าปัญหาที่เราเจออยู่นั้น มันวุ่นวาย เทสยาก โค้ดมันผูกกันอยู่เยอะหรือเปล่า ถ้าชั่งน้ำหนักแล้ว + มีเหตุผลที่เพียงพอที่จะใช้ก็จงใช้ให้สบายใจไปเถิด

เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺