Giới thiệu về Singleton Pattern

Bạn đã từng nghĩ chương trình của bạn sẽ thế nào khi có n object cùng kết nối tới database? Lúc đó, chương trình của chúng ta có thể sẽ không còn chạy một cách chính xác nữa. Điều đó khiến chúng ta đặt ra câu hỏi rằng : “Làm thế nào để có thể chỉ có một kết nối đến database và các tác vụ được thực hiện tuần tự và truy cập được đồng bộ?”. Singleton lúc này là một giải pháp hữu hiệu mà chúng ta có thể nghĩ đến. Nhưng sau khi Singleton ra đời và trải qua một thời gian được áp dụng trong thực tế, nó đã gây ra nhiều tranh cãi giữa các nhà phát triển rằng Singleton có khá nhiều điểm hạn chế, nó nên được xếp vào nhóm anti pattern. 

Vậy Singleton là gì? Được dùng ra sao? Lợi ích và hạn chế của nó là gì? Hôm nay mình sẽ cùng các bạn tìm hiểu về vấn đề này.

Nội dung bài viết của mình được chia làm 3 phần chính:

  • Singleton Pattern là gì?
  • Cách implement Singleton Pattern 
  • Hạn chế của Singleton

1. Singleton Pattern là gì? 

Singleton là một trong những design pattern thuộc nhóm khởi tạo – Creational Patterns. Singleton thường được sử dụng khi chúng ta mong muốn chỉ có một đối tượng tồn tại duy nhất và có thể truy cập ở mọi lúc.

Singleton đảm bảo rằng 1 class chỉ có 1 instance duy nhất tồn tại ở bất kỳ thời điểm nào class đó được request. 

Trong suốt chương trình, có những object mà chúng ta chỉ cần 1 instance của chúng. Ví dụ điển hình như là những shared resource database, file hoặc những kiểu object không cần thiết phải tạo instance mới mỗi lần sử dụng như logging object, dialog boxes,… Hãy thử tưởng tượng, với những object như vậy, nếu chúng ta khởi tạo nhiều hơn một đối tượng thì rất dễ dẫn đến việc chương trình của chúng ta sẽ có vấn đề như hành vi hoặc kết quả không chính xác.

Cung cấp một điểm access global 

Cũng giống như biến toàn cục (global variables), Singleton cung cấp một điểm global access cho instance mà nó tạo ra, bất cứ đâu cần dùng đều có thể truy cập, như một hình thức tái sử dụng.

Nếu đọc đến đây, chắc hẳn bạn sẽ đặt ra một câu hỏi: “Thay vì sử dụng Singleton, tại sao chúng ta không dùng một global variables?”. Nếu bạn đặt một global variable, nó sẽ được khởi tạo ngay khi chương trình chạy. Hãy thử tưởng tượng, sẽ ra sao khi đối tượng này khởi tạo tốn rất nhiều tài nguyên và ứng dụng của bạn không bao giờ kết thúc việc sử dụng nó. Ngoài ra, khi đặt một global variable, giá trị của nó hoàn toàn có thể bị reassign. Singleton khắc phục được nhược điểm này, nó chỉ được khởi tạo khi được gọi đến và việc khởi tạo như thế nào hoàn toàn do nó quyết định, không thể reassign.

2. Cách implement Singleton Pattern 

Để implement Singleton Pattern, chúng ta sẽ cùng nhau đi trả lời câu hỏi ứng với nhiệm vụ của Singleton:

  • Làm sao để đảm bảo rằng một class chỉ có một instance?
    Tạo private constructor của class đó để đảm bảo rằng các lớp khác không thể truy cập constructor và khởi tạo instance.
    Tạo một biến private static cho instance của class đó để đảm bảo rằng nó là duy nhất và chỉ được tạo ra trong class đó.
  • Làm sao để tạo global access?
    Tạo một public static method trả về instance vừa khởi tạo bên trên

Sau đây, chúng ta cùng tham khảo một vài cách implement Singleton: 

Eager Initialization:

Instance được khởi tạo ngay khi chương trình chạy nhưng chưa chắc đã dùng đến. 

 

Lazy Initialization:

Khắc phục nhược điểm của Eager Initialization, chỉ khi nào được gọi, instance mới được khởi tạo. Tuy nhiên, Lazy Initialization hoạt động tốt với trường hợp chương trình của chúng ta là đơn luồng, nếu tại một thời điểm mà có hai luồng cùng gọi đến phương thức getInstance() thì sẽ xảy ra trường hợp cùng một lúc 2 instance sẽ được khởi tạo.

Thread Safe Initialization

Khắc phục nhược điểm của Lazy Initialization bằng cách gọi phương thức synchronized của hàm getInstance(), việc này sẽ đảm bảo cho việc tại một thời điểm chỉ có một luồng được phép truy cập vào hàm getInstance() nhưng sẽ làm chậm chương trình.

Thread Safe Upgrade Initialization:

Khắc phục nhược điểm của Thread Safe Initialization bằng cách đặt synchronized bên trong method getInstance() trước khi khởi tạo instance.

3. Hạn chế của Singleton Pattern

– Singleton Pattern mâu thuẫn với Single Responsibility Principle. Single Responsibility Principle là nguyên tắc đầu tiên trong nguyên lý SOLID(nguyên lý phát triển phần mềm), nguyên tắc này nói về việc :”Mỗi lớp chỉ nên chịu trách nhiệm về một nhiệm vụ cụ thể nào đó mà thôi”. Trong khi đó, Singleton đang đảm nhiệm cả 2 nhiệm vụ cùng một lúc:

  • Đảm bảo 1 class chỉ có 1 instance
  • Cung cấp một global access

– Singleton có global access, làm phá vỡ đi tính chất Encapsulation(tính đóng gói) trong OOP.

– Singleton ẩn đi các dependencies, các dependencies bị ẩn đi trong code, bên ngoài không thể nhìn thấy. Qua thời gian, bạn sẽ gặp khó khăn trong vấn đề quan sát sự phụ thuộc và sự liên quan giữa các object với nhau.

Hiện nay, có rất nhiều ý kiến xoay quanh Singleton pattern, vì Singleton dễ để hiểu ý tưởng và dễ đem vào sử dụng, nên các nhà thiết kế hệ thống quá lạm dụng nó. Theo như sự tìm hiểu từ bản thân mình, đầu tiên họ ép những class có nhiều thể hiện thành một class có một thể hiện làm cho code không còn dễ dàng để thay đổi, mất đi tính flexible và làm cho code khó test hơn. Điều này đã gây khó khăn cho những lập trình viên đến sau, phải loay hoay đi tìm chỗ refactor và tốn nhiều thời gian cho những việc như thế. Nói tóm lại, chúng ta cần áp dụng Singleton một cách “healthy và balance”, tránh việc lạm dụng gây ra những hậu quả không đáng có. Bạn có thể cân nhắc tìm hiểu và sử dụng Dependency Injection như một cách thay thế Singleton.

Bài viết của mình đến đây là kết thúc. Mình mong rằng chút kiến thức ít ỏi của mình có thể giúp ích cho các bạn khi tìm hiểu về Singleton. Hẹn gặp lại các bạn trong series tiếp theo về design pattern nhé.

Add a Comment

Scroll Up