🎎Prototype Pattern

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

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

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

🧐 โจทย์

สมมุติว่าเรามี 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 ยังไงเสียก่อน ถึงจะสามารถแก้ปัญหาตัวนี้และตัวถัดไปได้ขอรับ

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

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

🔥 แก้ไขปัญหา

จากปัญหาต่างๆที่ว่ามาถ้าวิเคราะห์ดีๆจะพบว่ามันเกิดจาก เราพยายามจะไปสร้าง 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) จะทำได้ยาก และอาจเกิดปัญหาตามมาภายหลัง

🤙 ทางเลือก

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

Last updated

Was this helpful?