Behavioral pattern xử lý cách các object giao tiếp với nhau. Nếu bạn đã từng viết đống callback loạn xạ, danh sách observer, hay state machine dính băng keo — thì đây là lúc cho mớ hỗn loạn đó một cái tên và cấu trúc rõ ràng. Đây là 9 pattern mình hay dùng nhất.
1. Chain of Responsibility
Cho phép chúng ta xử lý sự kiện bằng một hoặc nhiều Handler
Mình hay hình dung Chain of Responsibility giống như trò chuyền bóng cho request vậy. Mình chuyển đối tượng tiếp nhận event thành các handler độc lập, rồi nối chúng thành một chuỗi. Khi request đến, nó nhảy qua từng handler cho đến khi gặp thằng xử lý được. Mỗi handler tự quyết định: “mình xử lý” hoặc “chuyển tiếp đi.”
Cái hay ở đây là tách nhỏ một request cồng kềnh ra thành nhiều mảnh nhỏ gọn. Nếu bạn đã từng ngồi nhìn đống if..else lồng nhau mà muốn phát điên thì pattern này dành cho bạn. Mỗi handler lo đúng một logic, với một điều kiện cụ thể. Hết rối. Và bên gửi request chẳng cần biết handler nào cuối cùng sẽ xử lý.
1.1. Chain of Responsibility được sử dụng khi nào?
- Có nhiều hơn 1 đối tượng có thể xử lý request đó, nhưng đối tượng cụ thể nào thực hiện request đó lại phụ thuộc vào ngữ cảnh
- Khi có nhiều cách thức để xử lý cho cùng một yêu cầu được gửi tới
- Khi không muốn xác định rõ ràng cách thức xử lý một sự kiện được gửi tới
- Khi muốn đưa ra yêu cầu cho một trong nhiều đối tượng mà không chỉ định rõ ràng tượng nào sẽ nhận và xử lý yêu cầu
- Tập các đối tượng xử lí là tập các đối tượng độc lập và có khả năng biến đổi
1.2. Cấu trúc
Các thành phần tham gia vào Chain of Responsibility Pattern:
- Client: Nơi tạo ra các đối tượng handler và sử dụng chúng
- Handler: Interface tạo xương sống cho các handler.
- BaseHandler: (Tùy chọn) Nơi nhận request đầu tiên (handler đầu tiên). Bạn có thể không cần class này và có thể chỉ định một handler khác nhận request đầu tiên
- ConcreteHandlers: Các next handler
1.3. Thực hành
- Khai báo xương sống Handler
interface Handler {
setNext(handler: Handler): void;
handle(request: Request): any;
}
- Tạo BaseHandler triển khai từ Handler (là nơi nhận request đầu tiên)
class BaseHandler implements Handler {
protected next?: Handler;
init(next?: Handler) {
this.next = next
}
setNext(handler?: Handler) : void {
this.next = handler
}
handle(request: Request) : any {
if(this.next!=null)
return this.next.handle(request);
}
}
- Tạo các cấu trúc cho các handler phía sau
class ConcreteHandler extends BaseHandler {
protected next?: Handler;
init(next?: Handler) {
this.next = next
}
setNext(handler?: Handler) : void {
this.next = handler
}
handle(request: Request) : any {
if(this.canHandle(request)) {
// Code here
} else {
if(this.next!=null)
return this.next.handle(request);
}
}
canHandle(request: any) : boolean {
// return True or False
}
}
- Client tạo và sử dụng các handler
class Client {
sendRequest() {
let thirdHandler = new ConcreteHandler(null);
let secondHandler = new ConcreteHandler(thirdHandler);
let firstHandler = new ConcreteHandler(secondHandler);
let request = new Request();
firstHandler.handle(request);
}
}
2. Command
Command Pattern (còn gọi là Action Pattern hoặc Transaction Pattern) tạo ra một đối tượng có thể triển khai một phương thức xác định nào đó từ một đối tượng đầu vào khác mà không cần biết đối tượng đầu vào khác có tính chất gì
2.1. Vấn đề
Giả sử bạn đang code một website bán hàng. Mỗi tuần site gửi tin nhắn về sản phẩm bán chạy nhất qua email hoặc SMS, tùy theo setting của người dùng. Chọn email thì không SMS, chọn SMS thì không email. Vậy làm sao xây dựng một đối tượng gửi được tin nhắn qua 2 kênh khác nhau mà logic hoạt động hoàn toàn khác nhau?
Hoặc hình dung thế này: bạn đang xây dựng SlickUI, một framework GUI. Button đẹp, dialog xịn, icon bắt mắt — xong hết rồi. Nhưng rồi bạn đặt câu hỏi: “Giờ người ta dùng mấy cái này để làm gì?” Bạn mong SlickUI sẽ phổ biến, hàng nghìn dev dùng, hàng triệu SlickButton instance được tạo ra. Giải pháp quen thuộc là inheritance — yêu cầu dev tạo subclass cho mỗi loại button. Nhưng một app GUI phức tạp có thể có hàng trăm button. Hàng trăm subclass? Rồi còn menu item, radio button nữa. Hướng này không ổn.
2.2. Giải quyết
Đóng gói luôn cái action — việc cần làm khi button được ấn hay menu item được chọn — vào một object riêng. Tách code xử lý ra khỏi UI component. Những action đó chính là các command trong Command Pattern.
2.3. Khi nào nên sử dụng Command Pattern?
- Khi cần tham số hóa các đối tượng theo một hành động
- Khi cần tạo và thực thi các yêu cầu vào các thời điểm khác nhau
- Khi cần hỗ trợ tính năng undo, log, callback hoặc transaction
2.4. Cấu trúc
Các thành phần tham gia vào Command Pattern:
- Command: Interface chứa một phương thức trừu tượng thực thi (execute) một hành động (operation). Hành động sẽ được đóng gói dưới dạng Command
- ConcreteCommand: Triển khai từ Command. Ta đưa hành động vào và đóng gói thành một command. Thực thi bằng việc gọi operation() hoặc execute(). Mỗi một ConcreteCommand sẽ phục vụ cho một hành động riêng
- Invoker: Nơi quản lí command, có nhiệm vụ thực thi ConcreteCommand được đưa vào. Nhằm giảm sự phụ thuộc vào một ConcreteCommand cụ thể nào đó
- Receiver: Đây là thành phần thực sự xử lý business logic cho request/yêu cầu. Trong phương thức execute() của ConcreteCommand chúng ta sẽ gọi method thích hợp trong Receiver
- Client: Tiếp nhận request/yêu cầu từ phía người dùng, đóng gói request/yêu cầu thành ConcreteCommand thích hợp và thiết lập receiver của nó
2.5. Thực hành
- Tạo request/yêu cầu thực hiện hành động cho bóng đèn từ người dùng
class Light {
public light: string = "light";
}
- Khai cấu cấu trúc interface Command
interface Command {
execute(): any;
}
- Tạo ra 2 ConcreteCommand triển khai từ Command, ứng với 2 chức năng bật/tắt bóng đèn
class CommandOn implements Command {
private object?: Light;
constructor(object?: Light) {
this.object = object;
}
execute() {
console.log(this.object?.light + ' on')
}
}
class CommandOff implements Command {
private object?: Light;
constructor(object?: Light) {
this.object = object;
}
execute() {
console.log(this.object?.light + ' off')
}
}
- Tạo RemoteControl chỉ để làm nhiệm vụ thực thi ConcreteCommand được đưa vào (nhằm giảm sự phụ thuộc vào đối tượng ConcreteCommand cụ thể)
class RemoteControl {
private command?: Command;
setCommand(command: Command) {
this.command = command
}
run() {
this.command?.execute()
}
}
- Client tạo RemoteControl, nhận request từ người dùng, set ConcreteCommand tương ứng rồi đưa vào RemoteControl chạy
let remote = new RemoteControl();
remote.setCommand(new CommandOn(new Light()));
remote.run(); // light on
3. Mediator
Cung cấp một lớp trung gian có nhiệm vụ xử lý thông tin liên lạc giữa các lớp
Pattern này dùng mối quan hệ many-to-many giữa các đối tượng tương đồng để đạt trạng thái “full object”. Mình thấy Mediator đặc biệt hữu ích khi các component liên tục “giẫm chân” lẫn nhau.
3.1. Vấn đề
Bạn muốn thiết kế component tái sử dụng được. Tốt. Nhưng khi các component phụ thuộc lẫn nhau, bạn sẽ có spaghetti code.
“Spaghetti code” — ai code đủ lâu cũng gặp rồi. Luồng điều khiển phức tạp, rối rắm, đầy GOTO, exception, thread, hay các nhánh không có cấu trúc. Gọi là spaghetti vì luồng chương trình nhìn y hệt tô mì — xoắn và rối.
Thường xảy ra khi nhiều người với phong cách code khác nhau cùng sửa một codebase trong thời gian dài. Lập trình có cấu trúc giúp giảm đáng kể, nhưng đôi khi bạn cần thêm pattern để giữ trật tự.
3.2. Giải quyết
Mediator Pattern là câu trả lời. Thay vì để các Component nói chuyện trực tiếp với nhau, mình đẩy tất cả qua một đối tượng Mediator. Mediator nhận event từ các Component khác nhau và xử lý chúng. Coi nó như một tổng đài — một chỗ duy nhất điều phối mọi giao tiếp chéo giữa các component.
3.3. Mediator được sử dụng khi nào?
- Trường hợp có nhiều các đối tượng tương tác trực tiếp với nhau. Nó giúp các sự kiện của các đối tượng được điều tiết một cách rõ ràng, loại bỏ đi sự cồng kềnh và chồng chéo nhau của source code
- Điều chỉnh hành vi giữa các lớp một cách dễ dàng, không cần chỉnh sửa ở nhiều lớp
3.4. Ví dụ
Tháp điều khiển ở sân bay là ví dụ kinh điển, và mình thấy nó giải thích Mediator cực kỳ tốt. Phi công không nói chuyện trực tiếp với nhau — họ nói với tháp. Tháp quyết định ai cất cánh, ai hạ cánh, theo thứ tự nào. Nhưng tháp không điều khiển toàn bộ chuyến bay. Nó chỉ tồn tại để đảm bảo an toàn khi cất và hạ cánh. Đó chính xác là việc Mediator làm: điều phối, không phải kiểm soát.
3.5. Cấu trúc
Các thành phần tham gia vào Mediator Pattern:
- Components: Là các đối tượng chứa đựng các business logic và cần phải tương tác với nhau trong quá trình hoạt động
- Mediator: Là lớp trừu tượng để khai báo các phương thức giúp cho các Component có thể tương tác với nhau
- ConcreteMediator: Là đối tượng chung gian được triển khia từ interface Mediator giúp cho các Component tương tác qua lại với nhau
3.6. Thực hành
- Khai báo interface BaseComponent có một tham chiếu đến một ConcreteMediator và từ đó tạo ra 2 ConcreteComponent mẫu có các phương thức hành động
abstract class BaseComponent {
protected mediator?: Mediator
constructor(mediator?: Mediator) {
this.mediator = mediator
}
update(mediator?: Mediator) {
this.mediator = mediator
}
}
class Component1 extends BaseComponent {
constructor(mediator?: Mediator) {
super(mediator)
}
update(mediator?: Mediator) {
super.update(mediator)
}
doA() {
console.log("Component 1 does A.")
this.mediator?.notify("A")
}
doB() {
console.log("Component 1 does B.\n")
this.mediator?.notify("B")
}
}
class Component2 extends BaseComponent {
constructor(mediator?: Mediator) {
super(mediator)
}
update(mediator?: Mediator) {
super.update(mediator)
}
doC() {
console.log("Component 1 does C")
this.mediator?.notify("C")
}
doD() {
console.log("Component 1 does D")
this.mediator?.notify("D")
}
}
- Tạo ra interface Mediator có phương thức in ra thông báo tùy theo event đầu vào được gửi từ các ConcreteComponent và triển khai một ConcreteMediator
interface Mediator {
notify(event: String): void
}
class ConcreteMediator implements Mediator {
constructor(...components: BaseComponent[]) {
for(let component of components) {
component.update(this)
}
}
updateMediator(component: BaseComponent) {
component.update(this)
}
notify(event: String) {
console.log(`Mediator reacts on ${event}`)
}
}
- Sử dụng Mediator Pattern
let component1 = new Component1();
let component2 = new Component2();
let mediator = new ConcreteMediator(component1, component2);
component1.doA();
console.log("\n");
component2.doC();
- Kết quả
Component 1 does A.
Mediator reacts on A
Component 1 does C
Mediator reacts on C
4. Memento
Cho phép chúng ta lưu trữ và khôi phục trạng thái của một đối tượng mà không tiết lộ chi tiết bên trong của nó
4.1. Memento Pattern được sử dụng khi nào?
Mình dùng Memento khi cần chức năng save/restore. Nghĩ đến game đi — bạn save progress, tắt game, hôm sau mở lại và chơi tiếp từ chỗ cũ. Hoặc bất kỳ app nào cần Undo/Redo: chụp lại trạng thái đối tượng, cất đi, rồi rollback khi cần. Cũng rất hợp với các ứng dụng cần quản lý transaction.
4.2. Cách thức hoạt động
Memento cấu trúc dữ liệu cần lưu của Object thành một State, rồi lưu State đó lại. Các State sau khi lưu gọi là Memento. CareTaker chịu trách nhiệm lưu trữ State dạng Memento và trả Memento về dạng State khi cần dùng. Vì trạng thái Object nằm trong State, truyền State qua các Object khác nhau sẽ không lộ implementation chi tiết bên trong. Phân tách sạch sẽ.
4.3. Ưu điểm
- Bảo bảo nguyên tắc đóng gói: sử dụng trực tiếp trạng thái của đối tượng có thể làm lộ thông tin chi tiết bên trong đối tượng và vi phạm nguyên tắc đóng gói
4.4. Nhược điểm
- Khi có một số lượng lớn Memento được tạo ra có thể gặp vấn đề về bộ nhớ, performance của ứng dụng
- Khó đảm bảo trạng thái bên trong của Memento không bị thay đổi
4.5. Cấu trúc
Các thành phần tham gia vào Memento Pattern:
- Originator: Là Object có trạng thái được lưu trữ hoặc khôi phục
- Mementor: Là trạng thái (State) của Object khi đang được lưu trữ
- CareTaker: Đóng vai trò lưu trữ và cấp phát các Memento. Nó có trách nghiệm lưu trữ các State ở dạng Memento và cấp phát các State cho các Object khi cần
4.6. Ví dụ
- Khai báo cấu trúc Memento
class Memento {
constructor(private readonly state: string) { }
getSavedState(): string {
return this.state
}
}
- Tạo class Originator hỗ trợ lưu trữ và restore state từ Memento
class Originator {
private state!: string
set(state: string): void {
console.log("Originator: Setting state to " + state)
this.state = state
}
saveToMemento(): Memento {
console.log("Originator: Saving to Memento.")
return new Memento(this.state)
}
restoreFromMemento(memento: Memento): void {
this.state = memento.getSavedState()
console.log("Originator: State after restoring from Memento: " + this.state)
}
}
- Tạo CareTaker có nhiệm vụ lưu trữ state và lấy ra khi cần
// CareTaker
let savedStates : Array<Memento> = new Array<Memento>()
- Client sử dụng
originator.set("State #1");
originator.set("State #2");
savedStates.push(originator.saveToMemento());
originator.set("State #3");
savedStates.push(originator.saveToMemento());
originator.set("State #4");
// Restore lại state cũ nhất đã lưu
originator.restoreFromMemento(savedStates[0]);
- Kết quả
Originator: Setting state to State #1
Setting state to State #2
Saving to Memento.
Setting state to State #3
Saving to Memento.
Setting state to State #4
State after restoring from Memento: State #2
5. Observer
Một đối tượng, gọi là subject, duy trì một danh sách các thành phần phụ thuộc nó, gọi là observer, và thông báo tới chúng một cách tự động về bất cứ thay đổi nào, thường thì bằng cách gọi một hàm của chúng
5.1. Vấn đề
Hình dung bảng tính Excel với nhiều trang tính cung cấp dữ liệu cho biểu đồ. Bạn có thể tạo bao nhiêu biểu đồ tùy thích từ các trang tính đó. Giờ thay đổi một giá trị trong trang tính — mọi biểu đồ dùng dữ liệu đó đều phải cập nhật. Và số lượng biểu đồ phụ thuộc vào một trang tính là không giới hạn.
5.2. Giải quyết
Observer Pattern xử lý gọn gàng. Trang tính là subject, các biểu đồ là observer. Khi dữ liệu trang tính thay đổi, nó tự động thông báo đến tất cả biểu đồ phụ thuộc. Xong.
5.3. Khi nào nên dùng Observer Pattern?
- Sử dụng mối quan hệ 1 - many khi mà một đối tượng có sự thay đổi trạng thái, tất các thành phần phụ thuộc của nó sẽ được thông báo và cập nhật một cách tự động
- Một đối tượng có thể thông báo đến một số lượng không giới hạn các đối tượng khác
5.4. Cấu trúc
Các thành phần tham gia vào Observer Pattern:
- Subject:
- Biết danh sách không giới hạn các observers của nó
- Cung cấp một giao diện để có thể thêm và loại bỏ observer
- Observer:
- Định nghĩa một giao diện cập nhật cho các đối tượng sẽ được subject thống báo đến khi có sự thay đổi trạng thái
- ConcreteSubject:
- Lưu trữ trạng thái danh sách các ConcreateObserver.
- Gửi thông báo đến các observer của nó khi có sự thay đổi trạng thái
- ConcreteObserver:
- Có thể duy trì một liên kết đến đối tượng ConcreteSubject
- Lưu trữ trạng thái của subject
- Thực thi việc cập nhật để giữ cho trạng thái đồng nhất với subject gửi thông báo đến
5.5. Thực hành
- Khai báo interface Observer và triển khai ConcreteObserver
interface Observer {
update(mesage: string): void
}
class ConcreteObserver implements Observer {
constructor(private beforeMessage: string) {}
update(message: string) {
console.log(this.beforeMessage + " " + message)
}
}
- Tạo interface Subject và triển khai ConcreteSubject có chức năng gửi thông báo đến các ConcreteObserver khi có sự thay đổi
interface Subject {
observers : Array<Observer>
attach(observer: Observer) : void
detach(observer: Observer): void
notifyChange(message: string): void
}
class ConcreteSubject implements Subject {
observers: Array<Observer> = new Array<Observer>()
attach(observer: Observer): void {
this.observers.push(observer)
}
detach(observer: Observer): void {
this.observers.splice(this.observers.indexOf(observer), 1)
}
notifyChange(message: string): void {
for (let observer of this.observers) {
observer.update(message)
}
}
}
- Sử dụng
let subject : Subject = new ConcreteSubject()
let observer1 : Observer = new ConcreteObserver("Message 1 updated:")
let observer2 : Observer = new ConcreteObserver("Message 2 updated:")
subject.attach(observer1)
subject.attach(observer2)
subject.notifyChange('Subject notify!')
subject.detach(observer1)
console.log("Removed Observer 1\n")
subject.notifyChange('Subject notify!')
- Kết quả
Message 1 updated: Subject notify!
Message 2 updated: Subject notify!
Removed Observer 1
Message 2 updated: Subject notify!
6. Strategy
Cho phép chúng ta định nghĩa các business logic thành các đối tượng khác nhau và các đối tượng này có thể thay thế cho nhau trong quá trình runtime
6.1. Strategy Pattern được sử dụng khi nào?
Đây là một trong những pattern mình dùng nhiều nhất. Dùng khi bạn có hai hoặc nhiều hành vi cần hoán đổi cho nhau lúc runtime. Thay vì hard-code thuật toán nào chạy, bạn để caller quyết định bằng cách inject strategy phù hợp.
6.2. Cấu trúc
Các thành phần tham gia vào Strategy Pattern:
- Object using Strategy: Là đối tượng sử dụng các Concrete Strategy. Bên trong đối tượng sẽ chứa một tham chiếu có kiểu dữ liệu là Strategy Protocol
- Strategy Protocol: Định nghĩa các thuộc tính và phương thức mà tất cả các Concrete Strategy bắt buộc phải có và implement chúng
- Concrete Strategy: Là các class triển khai từ Strategy Protocol. Nó sẽ chứa đựng các business logic đặc thù của từng class
6.3. Thực hành
- Khai báo interface Strategy và triển khai 3 ConcreteStrategy thực hiện 3 phép toán khác nhau
interface Strategy {
doOperation(num1: number, num2: number): number
}
class OperationAdd implements Strategy {
doOperation(num1: number, num2: number): number {
return num1 + num2
}
}
class OperationSubstract implements Strategy {
doOperation(num1: number, num2: number) : number {
return num1 - num2
}
}
class OperationMultiply implements Strategy {
doOperation(num1: number, num2: number) : number {
return num1 * num2
}
}
- Tạo class Context có tham chiếu đến ConcreteStrategy tương ứng tùy theo ngữ cảnh và sử dụng Strategy đó
class Context {
constructor(private strategy: Strategy) { }
executeStrategy(num1: number, num2: number) : number {
return this.strategy.doOperation(num1, num2)
}
}
- Sử dụng
let context : Context = new Context(new OperationAdd())
console.log("10 + 5 = " + context.executeStrategy(10, 5))
context = new Context(new OperationSubstract())
console.log("10 - 5 = " + context.executeStrategy(10, 5))
context = new Context(new OperationMultiply())
console.log("10 * 5 = " + context.executeStrategy(10, 5))
- Kết quả
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
7. Visitor
Cho phép thay đổi, mở rộng các thao tác cho đối tượng mà không thay đổi cấu trúc, nội dung bên trong đối tượng
Ý tưởng là thế này: các đối tượng (Element) tách các thao tác ra phương thức riêng, định nghĩa trên các lớp Visitor tách biệt. Nhờ vậy các thao tác độc lập với cấu trúc đối tượng, thay đổi linh hoạt hơn nhiều.
Mỗi thao tác mới cho đối tượng, tạo thêm một visitor class. Vậy thôi — đối tượng gốc không đụng vào.
Ngoài ra đây cũng là kỹ thuật giúp phục hồi lại kiểu dữ liệu bị mất của đối số truyền vào. Nó gọi phương thức tương ứng dựa trên kiểu dữ liệu của cả đối tượng gọi và đối số truyền vào. Gọi là Double Dispatch.
7.1. Double Dispatch và Single Dispatch là gì?
- Single Dispatch: Tên phương thức được gọi chỉ dựa vào kiểu dữ liệu của đối tượng gọi nó
class TestClass {
testMethod(param: string) {
console.log(param)
}
}
new TestClass().testMethod("Hello World")
- Double Dispatch: Tên phương thức được gọi dựa vào kiểu dữ liệu của đối tượng gọi nó và kiểu dữ liệu của đối tượng đầu vào. Cũng là công nghệ mà Visitor Pattern sử dụng, do đó nó còn có tên là Double Dispatch
class Visitor {
visit(element: Element) {
console.log(element.hello())
}
}
class Element {
hello() {
return "Xin chào"
}
accept(Visitor: visitor) {
visitor.visit(this)
}
}
new Element().accept(new Visitor())
7.2. Ưu điểm
- Cho phép một hoặc nhiều hành vi được áp dụng cho một tập hợp các đối tượng tại thời điểm run-time, tách rời các hành vi khỏi cấu trúc đối tượng
- Đảm bảo nguyên tắc Open/Close: Đối tượng gốc không bị thay đổi, dễ dàng thêm hành vi mới cho đối tượng thông qua visitor
7.3. Khi nào nên dùng Visitor Pattern?
- Khi có một cấu trúc đối tượng phức tạp với nhiều class và interface. Người dùng cần thực hiện một số hành vi cụ thể của riêng đối tượng, tùy thuộc vào concrete class của chúng
- Chúng ta muốn di chuyển logic hành vi từ các đối tượng sang một lớp khác để xử lí để giảm phức tạp
- Khi cấu trúc dữ liệu của đối tượng ít khi thay đổi nhưng hành vi của chúng được thay đổi thường xuyên
- Khi muốn tránh sử dụng toán tử
instanceof
7.4. Cấu trúc

Các thành phần tham gia vào Visitor Pattern:
- Element: Interface khai báo khung xương cho đối tượng xử lí dữ liệu. Đặc biệt phải khai báo phương thức
accept()để nhận các thao tác đưa vào - ConcreteElement: Đối tượng xử lí dữ liệu triển khai từ Element
- Visitor: Interface khai báo khung xương cho các visitor hỗ trợ định nghĩa và đưa các thao tác thay thế vào ConcreteElement
- ConcreteVisitor: Lớp hỗ trợ gọi các thao tác thay thế trên ConcreteElement được triển khai từ Visitor
7.5. Ví dụ
Mình thích ví dụ này vì nó hơi ngớ ngẩn nhưng giải thích rất dễ hiểu. Bạn là một ladykiller, muốn tỏ tình với một cô gái nhưng không biết quốc tịch của cô ấy. Rõ ràng không thể nói “anh yêu em” với cô gái Nhật Bản được — cô ấy chẳng hiểu gì. Phải nói “Aishite imasu” mới đúng. Nên mình viết một hàm chung sayLove() và truyền vào lời yêu tùy theo quốc tịch của mỗi nàng.
interface Lady {
sayLove(): void;
}
class AmericanLady implements Lady {
sayLove(): void {
console.log("I love you");
}
}
class JapanLady implements Lady {
sayLove(): void {
console.log("Aishite imasu");
}
}
let lady : Lady = new JapanLady();
lady.sayLove(); // Kết quả: Aishite imasu
Vấn đề xuất hiện khi bạn muốn thêm sayGoodBye(). Phải thêm vào interface Lady, rồi implement cho tất cả class đã triển khai. Tốn thời gian và thêm rủi ro. Đây chính là lúc Visitor Pattern phát huy.
- Đầu tiên sửa lại interface
Lady, triển khai lạiJapanLadyvàAmericanLadychỉ với phương thứcaccept(). Đẩy logic xử lý sangConcreteVisitor
interface Lady {
accept(visitor: Visitor): void
}
class AmericanLady implements Lady {
accept(visitor: Visitor): void {
visitor.visit(this)
}
}
class JapanLady implements Lady {
accept(visitor: Visitor): void {
visitor.visit(this)
}
}
- Khai báo interface
Visitortạo khung xương và triển khaiSayLoveVisitorin ra lời yêu với các lady (do Javascript không hỗ trợ đa hình mà không kế thừa nên tạm dùnginstanceofthay thế)
interface Visitor {
visit(lady: Lady): void
}
class SayLoveVisitor implements Visitor {
visit(lady: Lady): void {
if (lady instanceof AmericanLady)
console.log('I love you')
if (lady instanceof JapanLady)
console.log('Aishite imasu')
}
}
- Chạy thử
let lady: Lady = new AmericaLady()
lady.accept(new SayLoveVisitor()) // Kết quả: I love you
- Sau này chán rồi muốn
SayGoodByelady này để tán lady khác. Tạo thêm ConcreteVisitor cho chức năng này
class SayGoodByeVisitor implements Visitor {
visit(lady: Lady): void {
if (lady instanceof AmericanLady)
console.log('Good bye!')
if (lady instanceof JapanLady)
console.log('Sayounara!')
}
}
- Chạy thử nào
let lady: Lady = new JapanLady()
lady.accept(new SayGoodByeVisitor()) // Kết quả: Sayounara!
7.6. Kết luận
Khi muốn mở rộng thao tác của ConcreteElement, chỉ cần cập nhật Visitor — không đụng vào ConcreteElement. Open/Closed Principle thỏa mãn.
Hạn chế lớn nhất? Visitor không hỗ trợ mở rộng Element. Thêm Element mới nghĩa là phải cập nhật toàn bộ interface và class của Visitor. Tuy nhiên có thể khắc phục bằng cách tinh chỉnh pattern kết hợp một chút khéo léo trong cấu trúc dữ liệu.
8. State
Cho phép một đối tượng có thể thay đổi hành vi của nó khi có sự thay đổi trạng thái nội bộ trong lúc run-time
8.1. Ưu điểm
- Đối tượng được thay đổi trạng thái một cách rõ ràng
- Trạng thái của những đối tượng có thể chia sẻ lẫn nhau
- Đảm bảo nguyên tắc Single Responsibility Principle (SRP): tách biệt mỗi State tương ứng với 1 class riêng biệt
- Đảm bảo nguyên tắc Open/Closed Principle (OCP): chúng ta có thể thêm một State mới mà không ảnh hưởng đến State khác hay Context hiện có
- Giữ hành vi cụ thể tương ứng với trạng thái
8.2. State Pattern được sử dụng khi nào?
Mình gặp State Pattern nhiều trong các hệ thống có nhiều trạng thái khác nhau và thay đổi liên tục trong quá trình hoạt động. Số lượng state có thể có giới hạn hoặc không. Ví dụ kinh điển là đèn giao thông: “đỏ”, “vàng”, “xanh” — luân phiên liên tục. Ngoài ra, State Pattern cũng là liều thuốc tốt cho đống if-else lồng nhau quản lý state transition thủ công.
8.3. Cấu trúc
Các thần phần tham gia vào State Pattern:
- Context: Là đối tượng có chứa trạng thái hoặc hành vi thay đổi
- State: Là interface được sử dụng để liệt kê và khai báo các property và function cần thiết của State. Bằng cách này, chúng ta có thể xác định được các thuộc tính và các hàm cần phải có trong các ConcreteState
- ConcreteState: Triển khai từ State, lưu trữ trạng thái của Context. Có thể có nhiều ConcreteState, mỗi ConcreteState sẽ đại diện cho một trạng thái của Context
8.4. Cách thức hoạt động
- Trong State Pattern, Context được khởi tạo cùng với trạng thái mặc định
- Mỗi khi trạng thái thay đổi, nó lưu trạng thái mới thay cho cũ; đồng thời xử lý tác vụ theo trạng thái mới. Nghĩa là hành vi của Context thay đổi tùy thuộc trạng thái hiện tại
8.5. Luồng hoạt động
- Context định nghĩa hành vi giao tiếp với Client, nên client yêu cầu hành vi thông qua Context
- Context chứa một thể hiện của State; State ban đầu có thể set từ Client, nhưng sau đó Client không được sửa đổi
- Context có thể gửi chính nó làm argument cho State, để State truy cập Context và thay đổi trạng thái nếu cần
- Khi Context thực hiện hành vi, nó gọi State hiện tại để thực hiện; xong rồi, State có thể đổi trạng thái của Context nếu cần
8.6. Ví dụ
Mình định nghĩa interface State với 2 trạng thái: LowerCaseState và MultipleUpperCaseState. Một cái in chữ thường, cái kia in chữ hoa. Chúng tự động hoán đổi qua lại.
interface State {
writeName(context: StateContext, name: string): void;
}
class LowerCaseState implements State {
writeName(context: StateContext, name: string) {
console.log(name.toLowerCase());
context.setState(new MultipleUpperCaseState());
}
}
class MultipleUpperCaseState implements State {
private count: number = 0;
writeName(context: StateContext, name: string): void {
console.log(name.toUpperCase());
/* Change state after StateMultipleUpperCase's writeName() gets invoked twice */
if (++this.count > 1) {
context.setState(new LowerCaseState());
}
}
}
Lớp Context chứa biến state — trạng thái hiện tại. Khi khởi tạo, nó được gán state mặc định. Có thêm setter để đổi state mỗi khi thực hiện hành vi. Hành vi ở đây là writeName().
class StateContext {
private state!: State;
constructor() {
this.state = new LowerCaseState();
}
setState(newState: State): void {
this.state = newState;
}
writeName(name: string): void {
this.state.writeName(this, name);
}
}
Sử dụng
monday
TUESDAY
WEDNESDAY
thursday
FRIDAY
SATURDAY
sunday
8.7. Design Pattern liên quan
- Flyweight Pattern: Một trong những kết quả của State Pattern là các trạng thái có thể chia sẻ cho nhau. Flyweight Pattern chỉ rõ khi nào và cách nào để chia sẻ những trạng thái đó
- Singleton Pattern: Thường thì các đối tượng State được thiết kế theo Singleton, vì mỗi state chỉ cần một thể hiện duy nhất
- Strategy Pattern: Trong State Pattern, các state cụ thể liên kết với nhau qua cài đặt bên trong phương thức, nhờ đó state tự động chuyển đổi lúc runtime. Strategy Pattern không quan tâm state khác, chỉ nhận state Client đưa vào
9. Repository
Repository Pattern là lớp trung gian giữa việc truy cập dữ liệu và xử lý logic, giúp cho việc truy cập dữ liệu chặt chẽ và bảo mật hơn
9.1. Cấu trúc
Repository Pattern nằm giữa tầng Data Access và Business Logic. Nó làm cho việc truy cập dữ liệu gọn gàng và dễ bảo trì hơn.
Không có nó, bạn viết Controller query thẳng database. Với Repository Pattern, bạn đặt Repository giữa Controller và Model. Luồng đơn giản: request đến Controller, Controller gọi Repository, Repository gọi Model lấy và xử lý data. Controller không bao giờ đụng trực tiếp vào database.
9.2. Ưu điểm
- Dễ bảo trì và mở rộng code
- Tăng tính bảo mật và rõ ràng cho code
- Lỗi ít hơn
- Tránh việc lặp code
9.3. Ví dụ
getPost() {
let posts = Post.orderBy('id', 'desc').get();
return posts;
}
Đơn giản. Nhưng khách hàng muốn sắp xếp post theo bảng chữ cái thì sao? Bạn phải mò vào hàm sửa lại. Cấu trúc bảng thay đổi? Phải sửa getPost() giữa hàng đống method khác. Trường hợp xấu nhất, quản lý muốn đổi database và bạn phải viết lại hàm get từ đầu. Repository Pattern giải quyết hết mấy cái đó.
Tạo thêm class PostRepository, có thể đặt trong thư mục Repositories riêng. Logic lấy dữ liệu nằm hết ở đây. Khi yêu cầu thay đổi, sửa một file duy nhất.
class PostRepository {
getPostById() {
return Post.orderBy("id", "desc").get();
}
}
Giờ PostController chỉ cần gọi repository:
import { PostRepository } from './../Repositories';
class PostController extends Controller {
constructor(private postRepository: PostRepository) { }
getPost() {
let posts = this.postRepository.getPostById();
return posts;
}
}