Bạn đã bao giờ phải xử lý một đối tượng cần được chia sẻ trên nhiều phần khác nhau của ứng dụng của bạn—có thể là kết nối cơ sở dữ liệu, WebSocket client hoặc trình quản lý cấu hình chưa? Làm thế nào để bạn quản lý một đối tượng như vậy để nó vẫn nhất quán và có thể truy cập được trong suốt vòng đời của ứng dụng hoặc quy trình? Đây là lúc Singleton Design Pattern phát huy tác dụng.
Tổng quan
Singleton là một mẫu thiết kế khởi tạo, một loại mẫu thiết kế xử lý các vấn đề khác nhau phát sinh với cách khởi tạo đối tượng thông thường bằng từ khóa hoặc toán tử new.
Singleton Design Pattern tập trung vào việc giải quyết hai vấn đề chính:
- Làm thế nào chúng ta có thể cung cấp một điểm truy cập toàn cục cho instance của chúng ta?
- Và làm thế nào chúng ta có thể đảm bảo rằng một lớp hoặc một loại đối tượng cụ thể chỉ có một instance?
Nó có thể đơn giản hóa và tiêu chuẩn hóa cách chúng ta quản lý một loại trạng thái toàn cục cụ thể như kết nối cơ sở dữ liệu, WebSocket client, dịch vụ caching hoặc bất cứ thứ gì chúng ta cần duy trì và thay đổi trong bộ nhớ trong suốt vòng đời của ứng dụng.
Cách triển khai Singleton Design Pattern
Làm thế nào chúng ta có thể triển khai Singleton Design Pattern? Sơ đồ trên được chuyển thành lớp TypeScript sau:
class Singleton { private static instance: Singleton // other properties... public authorName: string private constructor({ authorName }: { authorName: string }) { this.authorName = authorName } public static getInstance(params) { if (!this.instance) { this.instance = new Singleton(params) } return this.instance } // other methods...
}
Lớp này nên định nghĩa một thuộc tính tĩnh để lưu trữ instance chia sẻ duy nhất.
Từ khóa static có nghĩa là đối tượng instance không được liên kết với các instance của lớp mà với chính định nghĩa lớp.
Constructor của lớp nên được đánh dấu là private. Cách duy nhất để lấy một instance của lớp chúng ta là bằng cách gọi phương thức tĩnh getInstance.
const instance = Singleton.getInstance({ authorName: "Sidali Assoul" }) // let's imagine
const instance1 = Singleton.getInstance({ authorName: "Sidali Assoul" }) // "Sidali Assoul"
const instance2 = Singleton.getInstance({ authorName: "John Doe" }) // "Sidali Assoul"
Chúng ta có thể sử dụng lớp trên bằng cách gọi phương thức tĩnh getInstance được liên kết với lớp Singleton.
Phương thức getInstance đảm bảo rằng chúng ta luôn nhận được cùng một instance ngay cả khi chúng ta khởi tạo lớp nhiều lần ở các vị trí khác nhau trong codebase của chúng ta.
Ứng dụng thực tế của Singleton
Vì vậy, cả hai biến (instance1 và instance2) đều chia sẻ cùng một singleton instance. Prisma là một ORM nổi tiếng trong hệ sinh thái JavaScript. Để sử dụng Prisma trong ứng dụng của bạn, bạn phải import PrismaClient sau đó khởi tạo một đối tượng từ nó.
import { PrismaClient } from "@prisma/client" export const prismaClient = new PrismaClient()
Prisma client kết nối với cơ sở dữ liệu một cách lười biếng, hay nói cách khác, chỉ khi bạn lần đầu tiên cố gắng truy vấn hoặc thay đổi một số thực thể.
import { prismaClient } from "@/db" const users = await prismaClient.user.findMany() // query on the users table
Mỗi khi prismaClient được import trong một file, một instance mới sẽ được tạo ra từ PrismaClient. Do đó, nhiều kết nối cơ sở dữ liệu sẽ được thiết lập mỗi khi chúng ta sử dụng các instance đó.
export const prismaClient = new PrismaClient() // a new instance is created every time it gets imported then used.
Nhiều kết nối cơ sở dữ liệu đang mở sẽ làm giảm hiệu suất của ứng dụng của bạn và thậm chí có thể dẫn đến việc tắt cơ sở dữ liệu vì cơ sở dữ liệu thường chỉ có thể xử lý một số lượng kết nối hạn chế.
Singleton Design Pattern có thể giúp chúng ta ngăn chặn vấn đề như vậy bằng cách tránh việc có nhiều hơn một instance của lớp PrismaClient và bằng cách cung cấp một điểm duy nhất để truy cập nó thông qua phương thức tĩnh PrismaClientSingleton.getInstance().
import { PrismaClient } from "@prisma/client" class PrismaClientSingleton { private static instance: PrismaClient private constructor() {} public static getInstance(): PrismaClient { if (!PrismaClientSingleton.instance) { PrismaClientSingleton.instance = new PrismaClient() } return PrismaClientSingleton.instance }
} export default PrismaClientSingleton
Kịch bản thực hành thứ hai: Rate Limiter
Một kịch bản thực tế khác mà chúng ta sẽ xem xét là một dịch vụ hạn chế tốc độ trong bộ nhớ (in-memory rate limiter service). Người dùng hoặc hacker có thể spam một endpoint cụ thể bằng cách gửi hàng loạt request đến nó.
Điều này có thể dẫn đến các lỗ hổng bảo mật, chi phí không mong muốn hoặc lỗi máy chủ. Để ngăn chặn điều đó, chúng ta có thể triển khai một dịch vụ hạn chế tốc độ trong bộ nhớ cơ bản. Dịch vụ này sẽ hạn chế số lượng request trên mỗi địa chỉ IP trong một khoảng thời gian cụ thể (ví dụ: 60 giây).
class RateLimiterService { private static instance: RateLimiterService private requests: Map<string, { count: number; lastRequestTime: number }> private readonly limit: number // Maximum number of requests private readonly window: number // Time window in milliseconds private constructor(limit: number = 5, window: number = 60000) { this.requests = new Map() this.limit = limit this.window = window } // Method to get a unique singleton instance public static getInstance(): RateLimiterService { if (!RateLimiterService.instance) { RateLimiterService.instance = new RateLimiterService() } return RateLimiterService.instance } public isRateLimited(ip: string): boolean { const currentTime = Date.now() const userRequestData = this.requests.get(ip) if (userRequestData) { const isExpired = currentTime - userRequestData.lastRequestTime > this.window if (isExpired) { userRequestData.count = 1 userRequestData.lastRequestTime = currentTime return false } else { userRequestData.count++ if (userRequestData.count > this.limit) { return true } return false } } else { this.requests.set(ip, { count: 1, lastRequestTime: currentTime }) return false } }
} export default RateLimiterService
Lớp RateLimiterService lưu trữ một map theo dõi số lượng request (requests[ip].count) được thực hiện bởi một người dùng cụ thể được xác định bằng địa chỉ IP (khóa map) trong một khoảng thời gian nhất định (requests[ip].lastRequestTime).
RateLimiterService của chúng ta được thiết kế để sử dụng trên toàn cục, hoặc nói cách khác, chúng ta không muốn đặt lại các giá trị trạng thái bên trong bao gồm map requests, biến limit và window mỗi khi RateLimiterService được import.
Kết luận
Singleton Design Pattern là một công cụ mạnh mẽ để quản lý hiệu quả các tài nguyên được chia sẻ trong ứng dụng của chúng ta.
Những điểm chính mà chúng ta cần nhớ:
- Singleton đảm bảo một lớp chỉ có một instance và cung cấp một điểm truy cập toàn cục cho nó.
- Nó hữu ích cho việc quản lý các tài nguyên được chia sẻ như kết nối cơ sở dữ liệu, cài đặt cấu hình hoặc bộ nhớ cache.
- Các ứng dụng thực tế bao gồm tối ưu hóa kết nối cơ sở dữ liệu với các ORM như Prisma và triển khai các dịch vụ hạn chế tốc độ.