Singleton: dùng hay không?

Nhân tiện mọi người có vẻ hào hứng về chủ đề nho nhỏ này và đang viết được, viết nốt kẻo ít hôm lại lười.

Trả lời ngắn gọn là dùng. Dùng chứ. Nhưng dùng thế nào cho đúng thì phải hiểu rõ một chút. Bài viết này cố gắng cung cấp nhiều góc nhìn để cân nhắc.

Vì sao ra đời?

Trong GoF, Singleton được đưa ra với mục đích sau: “Ensure a class only has one instance, and provide a global point of access to it.”“Đảm bảo một class chỉ có duy nhất một instance, và cung cấp một điểm truy cập duy nhất trên toàn cục tới instance.”

Cái gì hay?

Hay thì rõ rồi, nó cung cấp ý tưởng của việc một instance duy nhất, điều mà chúng ta gặp trong nhiều bài toán thực tế: một ứng dụng được khởi tạo, một cấu hình hệ thống, một logger… Tôi cá là nhiều người không biết tới Singleton thì không biết giải quyết bài toán này thế nào.

  • Runtime:
    • Không giống như static trong class, object và các giá trị trong đó chỉ được khởi tạo khi cần thiết. Memory và cả CPU đều được tiết kiệm.
    • Cho phép chủ động quản lý life cycle, giải phóng khi cần thiết.
  • Design:
    • Abstract hơn sử dụng static trong class;
    • Có khả năng thừa kế;
    • Có thể kếp hợp với những design pattern khác.

Cái gì dở?

Dở thì cũng có, bởi vậy mới có nhiều tranh luận.

  • Tư tưởng: Singleton trong GoF dở cơ bản về tư tưởng bởi nó giải quyết 2 bài toán khác nhau (dù có vẻ liên quan): “Ensure a class only has one instance, and provide a global point of access to it.”
    • Một class có duy nhất một instance;
    • Cung cấp một điểm truy cập duy nhất trên toàn cục tới instance.
    • Tác giả đã vô tình kèm cả lời giải trong bài toán với giả định: để có một điểm truy cập toàn cục duy nhất thì chỉ có duy nhất một instance được tạo ra từ một class. Bài toán “một điểm truy cập” có thể được giải quyết bởi Facade, Wrapper… không nhất thiết phải là Singleton.
  • Design:
    • Coupling: Vì là global state nên các thành phần bị gắn chặt với nhau;
    • Khó / không thể viết test;
    • Viết dễ sai sót, để lại lỗ hổng (xem bài trước Singleton có thực sự dễ?).
  • Runtime:
    • Không “thân thiện” với threading.

Nên dùng thế nào?

Như vậy, ta thấy rằng đa phần những thứ dở của design pattern này là ở tư tưởng global state. Vậy nên những ai yêu thích functional programming thì sẽ rất anti-pattern này. Cũng đúng thôi, GoF sinh ra cho OOP, không phải FP. Và thời đại của GoF (1994) cũng không quan tâm nhiều tới concurrency – bài toán trở thành rất cơ bản trong thời đại này. Bởi vậy, việc sử dụng Singleton có chút thay đổi. Có 3 điều cần chú ý:

  1. Concurrency: Global state là điều tệ hại cho concurrency. Hãy giảm thiểu tối đa nếu có thể. Global state bẻ cong cách suy nghĩ về luồng và gây mệt mỏi cho việc debug trong concurrency. Nếu bạn muốn thiết kế hệ thống tối ưu hiệu năng và concurrency thì không sử dụng Singleton cũng là một ý hay.
  2. Memory:
    1. Lưu cái gì? Global state cũng là một ý hay vì khiến việc thiết kế và lập trình dễ dàng hơn, nó chỉ không hay khi bạn không cân nhắc tới nên lưu gì. Rất nhiều thứ có thể nhìn dưới góc độ một instance nếu chúng ta không có khả năng khái quát hoá. Logger là Singleton không? Hay có errorlog, accesslog? Database là single thì lưu cả database? Hay chỉ connection? Hay chỉ connectionString? Lưu ít nhất có thể. 
    2. Lưu khi nào? Nhiều người hay gắn Singleton với life cycle của cả ứng dụng, kèm theo việc lưu trữ nhiều, hoặc giữ strong reference dẫn đến GC không thể hoạt động; sớm muộn gì cũng gây ra memory leak. Khởi tạo muộn nhất có thể, giải phóng sớm nhất có thể. 
  3. Language: Cần lưu ý cách sử dụng trong từng ngôn ngữ (xem bài trước Singleton có thực sự dễ?Singleton:threading() in Java), mỗi ngôn ngữ khác nhau sẽ có vấn đề khác nhau. Dù design pattern là mức thiết kế song đừng mang nguyên cách cài đặt từ ngôn ngữ này sang ngôn ngữ khác, hãy nhìn vào diagram và đặc trưng ngôn ngữ.

Trên đây là một số góc nhìn, gợi ý để bạn dùng Singleton đúng hơn. Không có đúng hay sai khi dùng Singleton, dùng đúng hay không mới là vấn đề.