Singleton ไม่ดีจริงเหรอ ?

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

บางคนบอกว่ามันเป็น Anti-Pattern ด้วยซ้ำ (มันคืออะไรเหรอครับคำนี้ :o)

Singleton คืออะไร

อ้างอิงจากหนังสือ Design Patterns, Elements of Reusable Object-Oriented Software (GoF) ซึ่งถือว่าเป็นหนังสือคลาสสิคของ Design Pattern กล่าวไว้ว่า

Intent Ensure a class only has one instance, and provide a global point of access to it.

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

ซึ่งสาระสำคัญอยู่ตรงที่ 1 instance ครับ

Implementation

ในหนังสือเขาจะเขียนว่า คลาสที่เป็น Singleton นั้นจะอารมณ์ประมาณนี้

class Singleton {
public:
  static Singleton* Instance();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

นี่ลอกมาเป๊ะ ๆ เลย

โดยในหนังสือเขาให้ตัวอย่างว่า ไอ้ฟังก์ชัน Singleton::Instance() จะมีหน้าตาประมาณนี้

Singleton* Singleton::Instance() {
  if(_instance == nullptr) {
    _instance = new Singleton;
  }
  return _instance;
}

ผมแก้นิดนึงให้เข้ากับ ศตวรรษที่ 21 หน่อย

ทีนี้ การทำ Lazy-Initialization ซึ่งเป็น Implicit Initialization กล่าวคือ ไอ้ตัวแปร _instance เนี่ยจะเป็น null มาตั้งแต่ต้น และไม่เคยถูก Init ขึ้นมาเลยจนกระทั่งฟังก์ชัน Instance() ถูกเรียกเป็นครั้งแรก ซึ่ง ผมมองว่าเป็นหลุมพรางที่หลาย ๆ คนตกลงไป

ถ้าอ่านดี ๆ ในบทนี้ หนังสือเขาเขียนแค่ว่า นี่เป็น Common-Implementation ไม่ใช่ The only implementation กล่าวคือ ตราบใดที่คลาสนี้มีแค่ instance เดียว จะเขียนแบบไหนก็เขียนได้ครับ

กรณีของ Singleton ผมจะชอบทำ Explicit Initialization มากกว่า คือแทนที่จะบอกว่า ใช้ได้เลย เดี๋ยวถ้า instance ยังไม่พร้อมเดี๋ยวสร้างใหม่เอง ก็เป็นว่าให้ user คุมไปเลยว่าจะสร้าง instance เมื่อไหร่ แล้วเราก็สามารถทำ Explicit Clean-up ได้ด้วย

ก็อารมณ์แบบนี้ครับ

class Singleton {
public:
  static Singleton* Instance() {return _instance;};
  static void Init();
  static void CleanUp();
protected:
  Singleton();
private:
  static Singleton* _instance;
}

มันก็แค่ฟังก์ชันอีกสองฟังก์ชันเท่านั้นเอง ว่าไหมครับ ถึงจุดนี้ผมก็ยังไม่ได้ละเมิดข้อตกลงที่ว่า ต้องมีแค่ instance เดียวนะ แค่บอกเฉย ๆ ว่ามันถูกสร้างเมื่อไหร่ และถูกทำลายเมื่อไหร่ เท่านั้นเอง

อ้อ ถ้า user ดันไปเรียกใช้ instance ก่อน init ก็ access-violation พังไปครับ อันนี้เรียกเสร่อ (ฮา) แต่ถ้าปราณีกันจะขว้าง exception กลับไปก็ดีเหมือนกัน

Instance ที่มันคืนมา ก็ไม่ได้จำเป็นว่าจะต้องมี type เดียวกับตัวคลาสด้วยซ้ำ

อันนี้เป็นอีกข้อนึงที่คนอ่านข้าม ทั้ง ๆ ที่มันก็อยู่หน้าข้าง ๆ ไอ้โค๊ดที่ผมก็อปมาเนี่ยแหละครับ

อย่างสมมติผมมีคลาส Singleton อันนึงชื่อ WindowManager (ลักษณะของคลาส Singleton อันนึงคือ มันชอบเป็น ผู้จัดการครับ ไม่รู้ทำไม) ผมสามารถสร้างโค๊ดในลักษณะนี้ได้ครับ

class WindowManager {
public:
  static WindowManager* Instance() {return _instance;};
  template<class T>
  static void Init();
  static void CleanUp();

protected:
  WindowManager();
private:
  static WindowManager* _instance;
};

class OSXWindowManager: public WindowManager {
//...
};

class LinuxWindowManager: public WindowManager {
//...
};

template<class T>
static void WindowManager::Init() {
  _instance = new T;
}

สังเกตตรงฟังก์ชัน WindowManager::Init() ที่เป็นเทมเพลตนะครับ คือตรงนี้เนี่ยเราระบุ type ตอน Init แบบ ให้ user เลือกเลยว่าอยากได้ class ไหน แต่เวลาเข้าถึงนี่เข้าได้จากที่เดียวนะ คือจาก WindowManager

ถ้าอ่านโค๊ดจากในหนังสือจะเห็นว่า ที่เขาวาง constructor ไว้เป็น protected ก็มีสาเหตุเพราะจะให้เราเลือกที่จะสร้าง subclass ก็ได้นะนี่แหละครับ แต่ก็มีข้อแม้ตรงที่ตัว subclass เองก็ควรเป็น protected ตามนะ ไม่งั้นเดี๋ยว user จะสร้าง instance เพิ่มได้ จะกลายเป็นละเมิดการออกแบบไป

หลายๆ คนน่าจะเห็นแล้วว่า โค๊ด WindowManager ข้างบนเนี่ย ดูแล้วเหมือนเป็นสองแพทเทิร์นรวมกัน ใช่ครับเราสามารถใช้ Singleton ร่วมกับ Abstract Factory ได้ด้วยครับ

Singleton เป็นตัวขวาง Dependency Injection??

จากการอ่านบนอินเตอร์เนทมา ผมพบว่ามีคนพูดถึงอันนี้เยอะอยู่ครับ คือเค้าว่ามันใช้คู่กับ DI ไม่ได้

แต่เอ ผมว่าผมเห็น Framework ที่ดังด้าน DI ตัวนึงอย่าง Spring เนี่ย ใช้ Singleton เพียบเลยนะ ?

ความเห็นส่วนตัวนะครับ คือเรามักจะเห็น DI ในท่าที่ว่า

result = state.Function(operatorObject, parameter);

แต่ถ้าเราใช้วิธีเปลี่ยน type ของ operatorObject ก่อนเรียก Function ล่ะ ?

OperatorClass.Init<TestOperator>();
result = state.Function(parameter);
OperatorClass.CleanUp();

เนี่ย ก็เป็นการ inject แบบอ้อม ๆ แล้วหรือเปล่า ?

แน่นอนว่าวิธีนี้ไม่เหมาะกับการใช้ในโค๊ดจริง แต่เป็นประโยชน์มากตอนทำ Unit Test ครับ

หรือจะเขียนแบบ

result = state.Function(OperatorClass.Instance(), parameter);

ก็ได้ ก็ดูตามสถานการณ์ อย่างกรณีหลังเนี่ยคือบางที OperatorClass อื่น ๆ อาจจะไม่ต้องเป็น Singleton แต่เผอิญว่าไอ้คลาสนี้จำเป็น อะไรแบบนี้

อีกอย่างคือ เราใช้ singleton ในการ implement ตัว state ก็ได้เหมือนกันครับ ก็จะเหมือนกับท่าปรกติทั่วไปนี่แหละ

แล้วปัญหาจริง ๆ มันคืออะไรกันแน่ ?

ส่วนตัวผมพบว่า Singleton เนี่ย คือ … มันมักจะถูกใช้ผิดที่น่ะครับ ตามนิยามของ Singleton เนี่ย คือ การออกแบบคลาสที่มีได้แค่ instance เดียว ซึ่งผมว่ามันเหมาะกับคลาสที่ต้องมี instance เดียว ไม่ใช่เราต้องการให้คลาสนี้มีแค่ instance เดียว(ต่างกันนะครับ) เช่นพวกคลาสที่เก็บ state ของ Hardware ในระบบ ส่วนตัวผมมักจะใช้สร้างตัว Renderer เพราะมันมักจะเป็นโค๊ดที่ ถ้าสร้าง instance ขึ้นมามากกว่าหนึ่งตัว โปรแกรมจะพัง (อันนี้ต้องเปิดเอกสาร API ประกอบครับ)

ถ้าเราต้องการให้มีแค่ instance เดียว แต่ถ้ามีมากกว่านั้นก็ไม่มีใครตาย ก็ไม่ต้องสร้างมันเป็น Singleton หรอกครับ สร้าง instance ไปแปะไว้สักคลาสนึงก็ได้ ไม่ต้องไปลิมิตว่ามันต้องมีแค่ 1 instance นะ อะไรแบบนี้

ที่เห็นคนใช้เยอะถึงเยอะมาก ๆ โดยไม่จำเป็น … คือ Logging System ครับ เอาจริง ๆ จะสร้างสักตัว Instance ตราบใดที่มันไม่เขียนลงไฟล์หรือ Device เดียวกันมันก็ไม่เจ๊ง ว่าไหมครับ ??

สรุป

Singleton เนี่ย เป็นคลาสที่ถูกออกแบบมาให้มีได้แค่ 1 instance นิยามมันมีแค่นี้ เราจะเล่นท่าไหนก็ได้ตราบใดที่ไม่ได้ละเมิดกฎข้อนี้ครับ และที่สำคัญคือเราไม่ควรใช้ถ้าไม่จำเป็น เมื่อมันจำเป็นมันจะเป็นแพทเทิร์นที่ทรงพลังมาก เพราะมันเป็นการป้องกันไม่ให้ user ไปสร้างของที่ไม่ควรสร้าง (instance ที่สองนั่นเอง)

ทั้งนี้ Dependency Injection นั้น จะใช้คู่กับ Singleton ก็ได้ ตรงนี้ขึ้นอยู่กับการออกแบบ Class Interface และ Function Interface ครับ และถึงแม้ว่าเรายังไปเรียก Instance()->DoSomething() ตรง ๆ (ซึ่งตามหลักแล้ว มันก็ไปละเมิด DI อยู่ดี) เรายังพอ Inject Instance ก่อนที่ฟังก์ชันนั้นจะถูกเรียกได้เหมือนกัน สิ่งที่สำคัญคือเราต้องรู้ว่ากำลังทำอะไรอยู่ครับ

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

This site uses Akismet to reduce spam. Learn how your comment data is processed.