Software Design

☝️ Singleton Pattern

แนวคิดในการสร้าง object ที่มีได้เพียงตัวเดียว

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

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

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

🧐 โจทย์

สมมุติว่าในเกมของเรามีอีเว้นท์พิเศษตัวนึง โดยมันจะปล่อย Boss ที่ชื่อว่า Detardeurus มาให้ผู้เล่นทุกคนช่วยกันปราบ ซึ่งถ้ามีผู้เล่นคนไหนปราบมันลงได้ มันก็จะกลับมาเกิดใหม่ในทุกๆ 2 ชั่วโมง ตามรูปด้านล่าง

และด้วยความพิเศษที่มันเป็นอีเว้นท์ เลยทำให้เจ้าบอสตัวนี้จะต้อง มีได้เพียงตัวเดียวเท่านั้น และ ผู้เล่นทุกคนจะต้องเข้าถึงข้อมูลบอสตัวนี้ได้ ไม่ว่าจากที่ไหนก็ตามอีกด้วย

ผู้เล่นทุกคนเข้ามาดูได้ว่าสุขภาพของบอสเป็นยังไง ใกล้ถึงเวลาเกิดหรือยัง

แล้วเราจะเขียนโค้ดออกมายังไงดี เพื่อให้มันสามารถตอบโจทย์ความต้องการแบบนี้ ได้อย่างไม่มีปัญหากันนะ ?

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

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

public class EventBoss
{
private Detardeurus boss = new Detardeurus();
public Detardeurus GetDetardeurus()
{
return boss;
}
}

ส่วนถ้าเราอยากให้ทุกคนสามารถเข้าถึง object ตัวนี้ได้จากที่ไหนก็ได้ เราก็จะให้มันเป็น static member ยังไงล่ะ แก้โค้ดแพร๊บ

public class EventBoss
{
private static Detardeurus boss = new Detardeurus();
public static Detardeurus GetDetardeurus()
{
return boss;
}
}

ไหนลองตรวจดูดิ๊ว่าตอนที่ไปเอา object นี้ออกมา มันจะเป็น object เดียวกันหรือเปล่านะ

static void Main(string[] args)
{
var boss1 = EventBoss.GetDetardeurus();
var boss2 = EventBoss.GetDetardeurus();
Console.WriteLine(boss1 == boss2);
}

ผลลัพท์ True

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

เสียใจด้วยนะมันไม่ได้ง่ายแบบนั้นหรอก แม้ว่าเราจะได้ object เดียวกันกลับมาเสมอก็จริง แต่เราจะต้องเรียกใช้งานผ่าน EventBoss.GetDetadeurus() เท่านั้น ... แล้วมันจะเกิดอะไรขึ้นถ้ามีคนอื่นดันไปสร้าง object นั้นขึ้นมาตรงๆด้วยคำสั่ง new กันล่ะ ?

var myboss1 = new Detardeurus();
var myboss2 = new Detardeurus();
Console.WriteLine(myboss1 == myboss2);

ผลลัพท์ False

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

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

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

ถ้าเราวิเคราะห์ปัญหาดีๆ สาเหตุที่แท้จริงของปัญหาในตอนนี้คือ ใครอยากสร้าง object นี้ก็สร้างได้เลย เพียงแค่ใช้คำสั่ง new นั่นเอง (เพราะมันคือพื้นฐานของ class)

แต่ถ้าเราลองไล่ลำดับการทำงานของ class จริงๆเราจะพบว่า เมื่อใช้คำสั่ง new ปุ๊ป สิ่งแรกที่มันจะทำก็คือ มันจะเรียกใช้ Constructor เป็นลำดับแรก ซึ่งโดยปรกติ default constructor จะเป็น public นั่นเอง เลยทำให้ใครอยากสร้าง object ก็สามารถสร้างได้เลย ตามรูป

แนะนำให้อ่าน สำหรับใครที่ลืมหรืออยากทบทวนการทำงานของ Class & Constructor ก็สามารถเข้าไปดูได้จากลิงค์ตัวนี้เลยครัช มารู้จักกับ Constructor กันบ้าง

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

public class Detardeurus
{
private Detardeurus()
{
}
}

ไม่เชื่อลองไปเขียนโค้ดดูดิ มันจะสร้าง object จากคลาสนั้นไม่ได้เลย

new Detardeurus(); // ERROR

เพียงแค่นี้ก็ไม่มีคนสร้าง object จากคลาสพิเศษของเราได้ละ ... แต่ก็เกิดคำถามใหม่ว่า ถ้าใช้คำสั่ง new สร้าง object ไม่ได้ แล้วเราจะเอา object ของมันออกมาได้ยังไงกันล่ะ ?

🔥 แก้ไขปัญหา

เมื่อเราคิดต่อดูอีกที การที่เราเปลี่ยน constructor เป็น private มันจะทำให้ภายนอกไม่สามารถเข้าถึงได้ แต่ว่า ภายในยังสามารถเข้าถึงได้ตามปรกติ นั่นเอง

ดังนั้นเราก็จะสร้าง object มันจากภายในคลาสมันเองยังไงล่ะ!! ตามรูปเลย อะชึบอะชึบ

public class Detardeurus
{
private Detardeurus instance;
private Detardeurus()
{
instance = new Detardeurus();
}
}

เห็นไหมว่าภายในก็ยังใช้งาน constructor ตัวเองได้ตามปรกติ ดังนั้นก็จะเหลือแค่ จะให้ภายนอกเข้ามาใช้งาน object ยังไงนั่นเอง ... ซึ่งตรงนี้มันก็ไม่ยากอีกต่อไป เพราะในโค้ดแรกสุดที่เราออกแบบไว้นั้นทำไว้เรียบร้อยแล้ว โดยการเปิดให้ภายนอกเข้าถึงได้ผ่าน static member นั่นเอง ดังนั้นเราก็จะได้โค้ดออกมาเป็นแบบนี้

public class Detardeurus
{
private static Detardeurus instance;
private Detardeurus()
{
instance = new Detardeurus();
}
public static Detardeurus GetInstance()
{
return instance;
}
}

จบเรียบร้อยแล้ว เพียงเท่านี้เราก็จะได้คลาสพิเศษที่ทั้งโปรแกรมของเรามี object ได้เพียง 1 ตัวเท่านั้น (เพราะภายนอกมันสร้าง object ตัวนี้ไม่ได้) แถมยังมีช่องทางให้เข้าถึงได้จากทุกที่อีกด้วย (ผ่านทาง static นั่นเอง)

🤔 Singleton คือไย ?

🔥 จุดกำเหนิด

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

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

เราสามารถจำกัดการสร้าง object ได้ตามที่เราต้องการ และมีช่องทางเข้าถึงแบบเจ้า object พวกนั้นแบบ Global

🔥 วิธีการใช้

ถ้าเราอยากให้คลาสไหนถูกจำกัดจำนวนในการสร้าง object เราก็แค่ ห้ามให้คนอื่นสร้าง object ได้ตามใจด้วยการทำให้คลาสนั้นเป็น private constructor ซะ ส่วนเงื่อนไขและการสร้าง object ตัวนั้นก็จะถูกจัดการอยู่ภายในคลาสตัวนั้นเอง และเปิดช่องทางให้คนอื่นเข้าถึงผ่าน static member ... เพียงแค่นี้เราก็จะได้คลาสที่เป็น Singleton เรียบร้อยแบ้ว

ไหนลองเอาที่เราออกแบบมาเทียบกันดูดิ๊ ... เหมือนกันเปี๊ยบเบย

🤠 เทคนิค

วิธีการนำ Singleton ไปใช้นั้นมีหลายแบบเลย ซึ่งแต่ละแบบก็จะมีข้อดีข้อเสียที่ต่างกันด้วย ดังนั้นเราลองมาดูกันหน่อยว่ามันมีอะไรกันบ้าง

🔥 Lazy Initialization

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

public class Singleton
{
private static Singleton instance;
private Singleton() { }
public static Singleton GetInstance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}

ซึ่งจากโค้ดจะเห็นว่า ถ้าไม่เคยมีใครเรียกใช้เมธอด GetInstance() นั่นก็หมายความว่า object ตัวนี้ก็จะไม่เคยถูกสร้างเลยนั่นเอง และ ถ้ามันเคยถูกสร้างแล้ว มันก็จะไม่ต้องไปสร้างใหม่อีกเลย

👍 ข้อดี

  • ไม่เสียเวลาในการสร้าง

  • ไม่เปลือง memory

👎 ข้อเสีย

  • ตอนจะสร้างถ้ามันต้องไปทำงานนั่นนู่นนี่เยอะ มันจะทำให้โปรแกรมดูหน่วงๆหน่อยนึง จนกว่าจะสร้างเสร็จ

  • มีปัญหากับการทำงานแบบ Multi-Threading เพราะมันมีโอกาสเข้าไปสร้าง instance พร้อมกัน

🔥 Early Initialization

เป็นด้านตรงกันข้ามกับ Lazy Initialization เพราะมันจะสร้าง object ทิ้งไว้เลยตั้งแต่แรก ซึ่งเป็นโค้ดตามตัวอย่างแรกๆที่เราเขียนไว้ด้านบนเลย

public class Singleton
{
private static Singleton instance = new Singleton();
public static Singleton GetInstance()
{
return instance;
}
private Singleton() { }
}

👍 ข้อดี

  • หลังจากที่มันสร้างเสร็จมันจะพร้อมใช้งานทันที ดังนั้นตอนที่ถูกเรียกใช้ มันจะไม่รู้สึกหน่วงๆ

  • ไม่มีปัญหากับ Multi-Threading

👎 ข้อเสีย

  • เสียเวลาในการสร้าง (ไปหน่วงตอนเปิดแอพแทน)

  • เปลือง memory เพราะ object นั้นอาจไม่เคยถูกเรียกใช้เลยก็ได้ แต่มันถูกสร้างไว้แล้ว

🔥 Bindable Object

โดยปรกติเวลาที่เราทำงานร่วมกับ object ที่มีการเรียกเอาไปใช้งานนั้น เราจะไม่ค่อยสร้างเป็นเมธอดสักเท่าไหร่ เพราะมันเอาไปใช้ในการทำ Data Binding ไม่ได้ (เช่นพวก MVC, MVVM) ดังนั้นเพื่อเป็นการแก้ปัญหาเราจะนิยมไปสร้างเป็น Property มากกว่านั่นเอง

public class Singleton
{
private static Singleton instance;
public static Singleton Instance
=> instance;
protected Singleton()
{
instance = new Singleton();
}
}

👍 ข้อดี

  • นำไปใช้ในการ Binding ได้เลย (1-2 ways ได้หมด)

👎 ข้อเสีย

  • บางภาษาอาจจะไม่เหมาะสมทางเทคนิค

🥴 ข้อผิดพลาดที่เจอบ่อยๆ

⛔ ใช้ static class แทน

การใช้ static class แทนการทำ Singleton pattern นั้นจริงๆก็สามารถทำได้นะ ถ้าเราสามารถคุมการทำงานมันได้ แต่มันจะง่ายกว่าไหมเพียงแค่เปลี่ยนมันเป็น Singleton Pattern แล้วดูแลมันเป็น object ธรรมดาไปเลย ?

⛔ ไม่ใช้ private constructor

ถ้ามาคิดถึง Access Modifier จริงๆแล้วก็มีอีกหลายตัวนะที่ใช้แทน private ได้ เช่น protected (internal ยังไม่สมควรเพราะมันถูกสร้างได้จาก internal namespace นั่นเอง) แต่ถามว่ามันสมควรใช้ของพวกนั้นแทน private ไหม คำตอบคือไม่สมควร เพราะเรามีความตั้งใจที่อยากจะให้มันถูกควบคุมดูแลได้จากที่เดียวอยู่แล้ว ดังนั้นมันไม่ควรมีที่ไหนเข้ามาแก้ไขได้นอกจากตัวเองอีก + การทำ sub class จาก singleton จะมีปัญหาอื่นๆตามมาอีก

🎯 บทสรุป

👍 ข้อดี

  • ช่วยให้เราสามารถควบคุมการสร้าง object ได้

  • มีช่องทางให้เข้าถึงแบบ Global

  • ซ่อนความวุ่นวายในการสร้าง object

  • ถ้าการสร้าง object มีการเปลี่ยนแปลง ก็สามารถแก้ได้จากจุดเดียว

👎 ข้อเสีย

  • ยากต่อการจัดการกับ Life cycle ของมัน

  • มีปัญหากับการเขียนเทส

  • มีปัญหากับ Multi-Threading ถ้าไม่จัดการให้ดี

🤙 ทางเลือก

เราสามารถนำ Framework พวก Dependency Injection (DI) เข้ามาใช้แทนได้นะจ๊ะ โค้ดกระชับหลับสบายเต็มตื่นด้วย

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

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