What is Chain of Responsibility Design Pattern?
1. Giới thiệu về Chain of Responsibility
- Là một mẫu thiết kế hành vi (behavioral design pattern).
- Xác định một loạt các đối tượng xử lý (handlers) và kết nối chúng thành một chuỗi. Mỗi đối tượng sẽ xem xét yêu cầu và quyết định liệu nó có thể xử lý yêu cầu đó hay không. Nếu được, sẽ xử lý yêu cầu và chuyển tiếp yêu cầu đến đối tượng kế tiếp trong chuỗi.
- Xử lý yêu cầu một cách linh hoạt và tránh sự ràng buộc của các đối tượng.
2. Mục đích
- Tách biệt trách nhiệm: Mẫu Chain of Responsibility cho phép tách biệt trách nhiệm xử lý yêu cầu giữa các đối tượng xử lý. Điều này giúp tạo ra một phân chia rõ ràng về trách nhiệm và logic xử lý trong hệ thống. Đảm bảo tính Single Responsibility Principle.
- Tăng tính linh hoạt: Với Chain of Responsibility, bạn có thể dễ dàng thêm, xóa hoặc thay đổi các đối tượng xử lý trong chuỗi mà không làm ảnh hưởng đến người gửi yêu cầu. Điều này tạo ra tính linh hoạt trong việc quản lý và mở rộng chức năng của hệ thống. Đảm bảo tính Open/Closed Principle.
- Giảm sự ràng buộc: Làm giảm thiểu sự ràng buộc giữa người gửi yêu cầu và người xử lý yêu cầu. Bất kỳ đối tượng xử lý nào trong chuỗi có khả năng xử lý yêu cầu đều có thể được sử dụng.
- Loại bỏ logic phức tạp: Cho phép loại bỏ logic phức tạp trong người gửi yêu cầu. Thay vì phải xác định ai sẽ xử lý yêu cầu, người gửi yêu cầu chỉ cần gửi yêu cầu và để các đối tượng xử lý quyết định.
3. Kiến trúc
- Handle(Xử lý): Giao diện này định nghĩa phương thức handle() để xử lý yêu cầu và phương thức set_next() để thiết lập liên kết với đối tượng xử lý tiếp theo (nếu có). Một vài trường hợp, bạn cũng có thể thêm các phương thức khác tùy thuộc vào yêu cầu cụ thể.
- Concrete Handler (Xử lý cụ thể): Đây là các lớp cụ thể triển khai giao diện Handler. Mỗi Concrete Handler quyết định xem nó có thể xử lý yêu cầu hay không. Nếu có thể, nó thực hiện xử lý và có thể chuyển tiếp yêu cầu đến đối tượng xử lý tiếp theo trong chuỗi.
- Client (Khách hàng): Lớp này tạo ra và quản lý chuỗi các đối tượng xử lý. Nó gửi yêu cầu đến đối tượng xử lý đầu tiên trong chuỗi.
4. Khi nào nên sử dụng
- Hệ thống của bạn có nhiều đối tượng có thể xử lý yêu cầu, và bạn muốn linh hoạt xác định ai sẽ xử lý yêu cầu dựa trên điều kiện cụ thể.
- Mẫu Chain of Responsibility giúp tách biệt người gửi yêu cầu (client) và người xử lý yêu cầu (handlers), làm cho hệ thống dễ quản lý và bảo trì.
- Cho phép dễ dàng thêm mới, thay đổi hoặc loại bỏ các đối tượng xử lý trong chuỗi mà không làm thay đổi người gửi yêu cầu.
- Khi một tập hợp các đối tượng xử lý có thể thay đổi động: tập hợp các đối tượng có khả năng xử lý yêu cầu có thể không biết trước, có thể thêm bớt hay thay đổi thứ tự sau này.
5. Source code với TypeScript, Node.JS
Sau đây mình sẽ ví dụ về crawl dữ liệu từ một trang abc nào đó, …
+ Xác định interface của handler
interface CrawlHandler
{
setNextHandler(handler: CrawlHandler): void
handle(): Promise<ResultType | null>
}
+ Xác định lớp abstract cho crawlHandler
export abstract class CrawlHandlerAbstract implements CrawlHandler {
protected dateKey: string
protected rootUrl: string
private nextHandler: CrawlHandler | null | undefined
constructor(rootUrl: string, dateKey: string) {
this.rootUrl = rootUrl
this.dateKey = dateKey
}
protected successLog(): void {
console.log(`Fetch data from ${this.rootUrl} for ${this.dateKey} success!`)
}
protected failLog(): void {
console.log(`Fetch data from ${this.rootUrl} for ${this.dateKey} fail!`)
}
protected startingLog(): void {
console.log(`Fetching data from ${this.rootUrl} for ${this.dateKey}...`)
}
public setNextHandler(nextHandler: CrawlHandler) {
this.nextHandler = nextHandler
}
abstract buildURL(): string
async crawlData(): Promise<string> {
try {
const response = await axios.get(this.buildURL(), { timeout: 3000 })
return response.data
} catch (error) {
throw error
}
}
abstract parseData(): Promise<ResultTableType | null>
public async handle(): Promise<ResultTableType | null> {
try {
const result = await this.parseData()
if (!result) {
if (this.nextHandler) {
console.log(`Fetch data from ${this.rootUrl}fail! Move next handle`)
return this.nextHandler.handle()
}
return null
}
return result
} catch (error) {
if (this.nextHandler) { return this.nextHandler.handle() }
throw error
}
}
}
+ Logic của từng handle
Ví dụ là websiteA, websiteB, …
export class websiteA exends CrawlHandlerAbstract implements CrawlHandler{
constructor(datekey: string)
super(‘websiteA.com’,datekey)
}
buildURL(): string { return `https://${this.rootUrl}/ngay=${this.dateKey}` }
async parseData(): Promise<ResultTableType | null> {
try {
this.startingLog()
// add Logic get data from your website here
// ----------------------------------------- //
result = ….
if (Object.keys(result).length === 0 && result.constructor === Object) {
this.failLog()
return null
}
this.successLog()
return {
…result,
date_time: this.dateKey,
is_crawled: true,
source_url: this.rootUrl,
}
} catch (error) {
this.failLog()
throw error
}
}
Các bạn sẽ làm tương tự với websiteB, websiteC, vvv
+ Set next cho các handler
export const crawlerResult = async (dateKey: string): Promise<ResultTableType | null> => {
const websitea= new websiteA(dateKey)
const websiteb= new websiteB(dateKey)
const websitec= new websiteC(dateKey)
websitea.setNextHandler(websiteb)
websiteb.setNextHandler(websitec)
return await websitea.handle()
}
6. Lời kết thúc
Hy vọng rằng bài viết này đã giúp bạn hiểu rõ hơn về Chain of Responsibility và cách áp dụng nó trong các dự án phức tạp của mình. Chúng ta hãy tiếp tục nghiên cứu và áp dụng các mẫu thiết kế để trở thành những lập trình viên thông thái hơn. Chúc bạn thành công trong sự nghiệp của mình!
Tài liệu tham khảo
[1] Refactoring.Guru. https://refactoring.guru/design-patterns
[2] Design Patterns for Dummies, Steve Holzner, PhD