Giải Thích Chi Tiết Về Các Design Patterns Front-End (Phần 1)
Trong lập trình, các nguyên tắc SOLID là năm nguyên tắc thiết kế hướng đối tượng quan trọng, giúp nhà phát triển viết mã dễ bảo trì, mở rộng và dễ hiểu. Năm nguyên tắc này lần lượt là:
- Single responsibility priciple (SRP): Một lớp chỉ nên chịu trách nhiệm về một nhiệm vụ cụ thể.
- Open/Closed principle (OCP): Không được sửa đổi một class có sẵn, nhưng có thể mở rộng bằng kế thừa
- Liskov substitution principe (LSP): Các đối tượng kiểu class con cần có khả năng thay thế lớp cha mà không gây ra lỗi.
- Interface segregation principle (ISP): Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể
- Dependency inversion principle (DIP): Module cấp cao không nên phụ thuộc vào module cấp thấp, cả hai nên phụ thuộc vào sự trừu tượng.
Mấy nguyên tắc này cơ bản là đang khuyến khích chúng ta ta: Chia nhỏ code ra đi! Mỗi module làm đúng một việc thôi, đừng bắt nó vừa nấu cơm, vừa rửa bát, lại còn phải lau nhà. Chia nhỏ ra thì sau này muốn sửa hay nâng cấp cũng nhẹ nhàng tình cảm, không phải đập đi xây lại cả toà nhà.
Còn Design Patterns? Nó chính là 'văn mẫu', là bí kíp võ công mà các bậc tiền bối đi trước đã đổ máu (và tóc) để đúc kết lại. Thay vì ngồi vò đầu bứt tai sáng tạo lại cái bánh xe (mà thường là méo), ta cứ áp dụng mấy chiêu này cho lẹ, vừa chuẩn bài vừa tuân thủ mấy cái nguyên tắc SOLID nghe có vẻ cao siêu kia.
Thú thật đi, làm Front-end bây giờ đâu chỉ có HTML/CSS 'sương sương'. App phình to nhanh như mặt tôi mỗi dịp tết đến, logic và giao diện mà không quy hoạch rõ ràng là y như rằng biến thành một bát mì Spaghetti rối nùi. Nắm vững Design Patterns không chỉ giúp code của bạn trông 'đẹp trai' hơn, mà còn đảm bảo sau này sếp có bắt sửa tính năng thì bạn cũng không cần vừa code vừa khóc.
Thôi dạo đầu thế đủ rồi, giờ thắt dây an toàn vào, chúng ta cùng đi 'bóc phốt' xem mặt mũi mấy ông thần Design Patterns phổ biến này tròn méo ra sao nhé!
Tổng Quan Về Design Patterns
Ngày xưa làm Front-end chill phết, viết vài dòng script cho vui cửa vui nhà là xong. Còn giờ? Cứ như đang xây cả cái vũ trụ Marvel vậy, phức tạp kinh hồn!
Logic nghiệp vụ thì nặng như tạ, deadline thì dí sát mông, mà sếp vẫn đòi code phải đẹp, phải xịn, phải dễ sửa để anh em vào sau không lôi người đi trước ra 'tế'. Nghe áp lực chưa? Đây chính là cái kiếp nạn thứ 82 mà dev nào cũng phải nếm trải nếu không muốn code của mình biến thành nồi lẩu thập cẩm.
Nhưng đừng lo, Design Patterns đã ở đây để giải cứu thế giới (hoặc ít nhất là giải cứu giấc ngủ của bạn). Đừng nhầm nhé, nó không phải là mấy dòng code mì ăn liền để copy-paste từ StackOverflow đâu. Nó là cả một 'bầu trời tư duy', là tuyển tập 'bí kíp võ công' mà các bậc cao nhân đã test nát chuột rồi mới truyền lại. Có nó trong tay, gặp mấy con bug khó nhằn hay mấy yêu cầu oái oăm, bạn cứ thế mà triển theo bài, đỡ phải vò đầu bứt tai đi sáng tạo lại cái bánh xe làm gì cho mệt
Thông thường, design patterns có thể được chia thành ba loại sau:
1. Creational Patterns (Nhóm Khởi tạo)
Nhóm này tập trung vào cơ chế tạo ra đối tượng. Thay vì để hệ thống tự khởi tạo trực tiếp (thường dẫn đến hard-code), nhóm này sẽ trừu tượng hóa quá trình đó.
- Mục đích: Tách biệt hệ thống khỏi cách thức cụ thể mà các đối tượng được tạo ra, cấu thành và biểu diễn.
- Hiệu quả: Giúp mã nguồn giảm bớt sự phụ thuộc lẫn nhau, từ đó dễ dàng bảo trì và kiểm thử hơn.
2. Structural Patterns (Nhóm Cấu trúc)
Nhóm này giải quyết vấn đề tổ chức và kết hợp các lớp (classes) hoặc đối tượng (objects) lại với nhau để tạo thành những cấu trúc lớn hơn.
- Mục đích: Tìm ra cách lắp ghép các thành phần riêng lẻ thành một hệ thống hợp nhất mà vẫn đảm bảo tính hiệu quả.
- Hiệu quả: Giúp định hình mối quan hệ giữa các đối tượng một cách rõ ràng, làm cho hệ thống trở nên linh hoạt và dễ dàng tái sử dụng mã nguồn hơn.
3. Behavioral Patterns (Nhóm Hành vi)
Nhóm này chú trọng vào sự giao tiếp và phân công trách nhiệm giữa các đối tượng.
- Mục đích: Không chỉ quy định cách các đối tượng tương tác (gửi/nhận thông tin) mà còn xác định rõ "ai làm việc gì".
- Hiệu quả: Giúp luồng điều khiển của hệ thống trở nên mạch lạc, tăng tính linh hoạt trong giao tiếp giữa các thành phần và dễ dàng mở rộng khi cần thiết.
I. Creational Patterns
Factory Pattern
Factory Pattern cung cấp một cách tạo đối tượng mà không cần chỉ định trực tiếp quá trình khởi tạo của một lớp cụ thể. Thông qua mẫu này, mã client không cần quan tâm đến cách triển khai cụ thể và quá trình khởi tạo của đối tượng, chỉ cần thông qua một phương thức nhà máy để lấy được thể hiện đối tượng mong muốn.
Tư tưởng cốt lõi:
- Đóng gói việc tạo đối tượng: Mẫu Nhà Máy đóng gói logic tạo đối tượng trong một lớp nhà máy, client thông qua nhà máy để lấy đối tượng, mà không cần tự mình chịu trách nhiệm khởi tạo.
- Định nghĩa interface hoặc lớp trừu tượng: Mẫu Nhà Máy thường định nghĩa một interface hoặc lớp trừu tượng cho sản phẩm, lớp triển khai cụ thể được cung cấp bởi phương thức nhà máy. Nhà máy dựa trên các điều kiện khác nhau (như tham số, cấu hình, v.v.) để trả về các đối tượng sản phẩm thuộc loại khác nhau.
Bạn có thể hình dung nó giống như một nhà máy thực sự. Ví dụ, bạn muốn mua một chiếc xe hơi, bạn không cần biết chiếc xe đó được lắp ráp như thế nào trên dây chuyền sản xuất (không cần gọi new Car(...) và truyền vào các tham số phức tạp như động cơ, lốp xe), bạn chỉ cần nói với nhà máy xe hơi (hàm nhà máy) mẫu mã bạn muốn (tham số), nhà máy sẽ trả về cho bạn một chiếc xe hoàn chỉnh (thể hiện đối tượng).
Mục đích chính của việc này là tách biệt việc tạo đối tượng với việc sử dụng nó, từ đó mang lại tính linh hoạt và khả năng bảo trì rất cao.
Tình Huống Thực Tế:
**React.createElement**là một trong các API cốt lõi nhất của framework React, được sử dụng để tạo các phần tử DOM ảo (Virtual DOM). Khi sử dụng JSX, chúng ta thường không gọi nó một cách tường minh, nhưng mỗi biểu thức JSX cuối cùng sẽ được biên dịch thành lệnh gọi React.createElement

Sẽ được Babel biên dịch thành:

Có thể thấy, React.createElement là một hàm nhà máy, được dùng để sản xuất ra các đối tượng nút ảo chuẩn hóa dựa trên type, props và phần tử con được truyền vào. Lợi ích của việc này là:
Tách biệt việc Khởi tạo và việc Sử dụng
React.createElement đóng gói quá trình tạo phần tử DOM ảo, phía client chỉ quan tâm cách sử dụng nó mà không cần hiểu cách triển khai cụ thể của từng phần tử. Sự tách biệt này giảm sự phụ thuộc giữa các đoạn mã, tách rời việc tạo component khỏi cách triển khai bên dưới, tăng cường tính linh hoạt của mã.
Single responsibility priciple
React.createElement với vai trò là một hàm nhà máy, tập trung vào nhiệm vụ tạo phần tử DOM ảo. Việc tập trung logic này vào một nơi đã đơn giản hóa quá trình tạo component, đảm bảo khả năng bảo trì và mở rộng của mã.
Open/Closed principle
Thông qua React.createElement, chúng ta có thể dễ dàng mở rộng các loại component mới mà không cần sửa mã hiện có. Chỉ cần thêm logic mới vào hàm nhà máy là có thể hỗ trợ loại component mới, tránh được việc sửa đổi mã đã có, phù hợp với nguyên tắc Đóng-Mở.
Tạo Form động
Cấu hình form từ backend, các loại type khác nhau cần render component khác nhau, chúng ta có thể sử dụng Factory Pattern để tạo động:

Thông qua Schema từ backend trả về để xác định các trường trong form cần render, dựa vào type để tạo component tương ứng.
Singleton Pattern
Singleton Pattern đảm bảo rằng một lớp (class) chỉ có duy nhất một đối tượng (instance), đồng thời cung cấp một điểm truy cập toàn cục để lấy đối tượng này.
Nói cách khác, Singleton Pattern đảm bảo rằng trong hệ thống, một lớp cụ thể nào đó chỉ có đúng một đối tượng duy nhất bằng cách hạn chế số lần khởi tạo. Đối tượng này sẽ được dùng chung, giúp tránh lãng phí tài nguyên.
Tư tưởng cốt lõi:
- Instance duy nhất: Đảm bảo một lớp (class) chỉ có duy nhất một đối tượng (instance).
- Điểm truy cập toàn cục: Cho phép truy cập đối tượng đó thông qua một phương thức chung (toàn cục).
- Tải chậm (Lazy Loading): Thường áp dụng chiến lược tải chậm/trì hoãn, nghĩa là chỉ khi nào cần dùng đến lần đầu tiên thì đối tượng mới được tạo ra.
Ứng dụng thực tế:
Ứng dụng của Singleton Pattern rất đa dạng. Vì trong JavaScript, chúng ta có thể tạo trực tiếp một đối tượng và đối tượng đó mặc định là duy nhất trên toàn cục, nên Singleton trong JS giống như một tính chất "bẩm sinh", khiến chúng ta thường sử dụng mà không hề hay biết.
Ví dụ: Các thư viện quản lý trạng thái toàn cục như Vuex, Redux, cũng như nhiều thư viện bên thứ ba khác đều áp dụng Singleton Pattern. Dù bạn tham chiếu (import) chúng bao nhiêu lần thì cũng chỉ đang sử dụng cùng một đối tượng duy nhất, điển hình như jquery, moment, v.v.
Trong quản lý trạng thái Frontend, thiết kế cốt lõi của Redux thể hiện rõ việc ứng dụng Singleton Pattern. Thông qua một instance store duy nhất trên toàn cục, Redux thực hiện việc quản lý tập trung và truy cập thống nhất các trạng thái trong ứng dụng.
Ở đây chúng ta sẽ tạo một store thông qua hàm createStore, sau đó export (xuất) instance đó ra từ module, ví dụ:

Store này được khởi tạo ngay khi module được tải và duy trì tính duy nhất trong suốt vòng đời của ứng dụng.
Bên trong, Redux store duy trì trạng thái (state), danh sách người đăng ký (listeners) và phương thức dispatch. Các component sẽ được inject store vào thông qua Provider, sau đó sử dụng connect hoặc useSelector để lấy trạng thái, giúp hiện thực hóa việc chia sẻ trạng thái trong toàn bộ component tree. Ví dụ:

Redux sử dụng Singleton Pattern để đảm bảo chỉ có duy nhất một instance của store trên toàn cục, mang lại các lợi ích sau:
- Tính nhất quán toàn cục: Thông qua việc duy trì một store instance duy nhất, nó đảm bảo tính nhất quán và độ tin cậy của trạng thái (
state) trong ứng dụng, tránh được vấn đề không đồng bộ trạng thái giữa nhiều instance khác nhau. - Quản lý tập trung: Việc quản lý trạng thái tập trung giúp cho trạng thái của ứng dụng dễ dàng được theo dõi, debug và bảo trì hơn, đồng thời đơn giản hóa quá trình phát triển và kiểm thử.
II. Structural Patterns
Proxy Pattern
Proxy Pattern cung cấp một đối tượng đại diện (proxy object) cho một đối tượng khác, nhằm kiểm soát việc truy cập vào đối tượng thực tế đó thông qua đối tượng đại diện này.
Ý tưởng cốt lõi của Proxy Pattern là: Cung cấp một đối tượng đại diện cho một đối tượng nào đó để kiểm soát quyền truy cập. Đối tượng Proxy đóng vai trò trung gian giữa client (phía khách) và đối tượng thực (Real Object). Nó có thể thực hiện các thao tác bổ sung trước hoặc sau khi truy cập vào đối tượng thực, ví dụ như: tải chậm (lazy loading), kiểm soát quyền truy cập, bộ nhớ đệm (caching), ghi nhật ký (logging), xác thực dữ liệu, v.v.
Cấu trúc điển hình của mẫu Proxy bao gồm ba vai trò:
- Chủ thể thực (Real Subject): Là đối tượng thực tế được đại diện, chứa các logic nghiệp vụ cốt lõi.
- Proxy: Giữ tham chiếu đến đối tượng thực, cung cấp giao diện (interface) tương đương ra bên ngoài, và thực hiện các logic bổ sung trước hoặc sau khi gọi đến đối tượng thực.
- Khách hàng (Client): Truy cập gián tiếp vào đối tượng thực thông qua Proxy mà không cần quan tâm đến việc hiện thực hóa bên trong.
Ứng dụng thực tế: Reactivity System của Vue 3
Trong các framework frontend hiện đại, Proxy Pattern được sử dụng rộng rãi để chặn dữ liệu (data interception), xây dựng Reactivity System và lazy loading. Đặc biệt trong Reactivity System của Vue 3, đối tượng Proxy đã thay thế cho Object.defineProperty() của Vue 2 để trở thành phương thức hiện thực hóa cốt lõi.
Thông qua mẫu Proxy, Vue có thể tự động kích hoạt thu thập sự phụ thuộc (dependency collection) và logic cập nhật khung nhìn (view update) khi một thuộc tính được truy cập (get) hoặc sửa đổi (set), từ đó đạt được khả năng ràng buộc dữ liệu phản ứng tự động.

Cốt lõi của reactivity system trong Vue 3 được hiện thực hóa bởi phương thức reactive(). Bên trong, nó sử dụng Proxy của ES6 để chặn (intercept) và bao bọc đối tượng mục tiêu:

Trong đoạn code này, Proxy đóng vai trò là đối tượng đại diện, chặn các thao tác đọc và ghi đối với đối tượng dữ liệu thực:
getchịu trách nhiệm thu thập sự phụ thuộc (theo dõi xem component nào đang phụ thuộc vào dữ liệu này).setchịu trách nhiệm ràng buộc hàm cập nhật (thông báo cho các component liên quan render lại khi dữ liệu thay đổi).
Trong cài đặt reactivecủa Vue 3, mẫu Proxy mang lại 3 lợi ích lớn sau đây:
- Kiểm soát truy cập:
Proxycó thể chặn và kiểm soát việc truy cập dữ liệu một cách minh bạch (transparently), khiến cho các thao tác như thu thập sự phụ thuộc và cập nhật khung nhìn (view) không cần phải được quản lý một cách thủ công (tường minh). - Tăng cường chức năng: Thông qua cơ chế đại diện,
Proxybổ sung thêm các tính năng cho đối tượng dữ liệu (như tự động cập nhật view, thu thập phụ thuộc...) mà không cần phải sửa đổi cấu trúc dữ liệu gốc. - Thao tác trong suốt (Transparency): Lập trình viên không cần quan tâm đến cách hiện thực bên trong của proxy; proxy sẽ tự động xử lý sự phức tạp của các thao tác dữ liệu, giúp đơn giản hóa quá trình phát triển.
Function Memoization
Trong quá trình phát triển thực tế, việc tính toán của một số hàm có thể tốn nhiều thời gian hoặc bị lặp lại thường xuyên, ví dụ như các phép tính phức tạp, xử lý dữ liệu lớn hoặc các yêu cầu mạng (remote requests).
Để tránh việc phải tính toán lặp lại và nâng cao hiệu suất chương trình, chúng ta có thể tận dụng mẫu Proxy để tạo ra một "hàm đại diện" (proxy function) được tích hợp sẵn khả năng lưu bộ nhớ đệm (cache) cho hàm gốc.

Khi đối tượng mục tiêu được đại diện là một hàm (hoặc đối tượng có thể gọi được - callable object), Proxy cung cấp bẫy (trap) apply chuyên dùng để chặn các lời gọi hàm.
Hàm _.memoize do thư viện Lodash cung cấp về bản chất cũng có logic tương tự như trên. Bên trong, nó cũng được hiện thực theo cơ chế: Cache tham số → Trả về kết quả.
Decorator Pattern
Decorator pattern là một mẫu thiết kế thuộc nhóm cấu trúc (structural design pattern). Nó cho phép bạn thêm động các chức năng bổ sung vào một đối tượng mà không làm thay đổi cấu trúc của nó. Mẫu Decorator thực hiện việc mở rộng và tăng cường chức năng cho đối tượng hiện có bằng cách tạo ra một đối tượng bao bọc (wrapper object).
Ý tưởng cốt lõi:
- Bao bọc để tăng cường: Decorator tăng cường hoặc sửa đổi hành vi của đối tượng gốc thông qua việc bao bọc (wrapping) nó, mà không cần thay đổi mã nguồn của đối tượng đó.
- Lớp vỏ bên ngoài: Decorator là một lớp vỏ bọc bên ngoài của đối tượng. Nó không thay đổi cấu trúc bên trong của đối tượng được trang trí (decorated object) mà có thể thêm các tính năng mới một cách linh hoạt.
- Linh hoạt hơn kế thừa: So với phương pháp kế thừa (inheritance), mẫu Decorator thường linh hoạt hơn và có thể tạo ra các tổ hợp hành vi phức tạp hơn.
Thực tế áp dụng:
Higher-Order Components (HOC) - Component bậc cao
Higher-Order Component (HOC) là một mẫu thiết kế trong React. Nó nhận vào một component làm tham số và trả về một component phiên bản đã được tăng cường.
- Mục đích: HOC chủ yếu được sử dụng để tái sử dụng code (code reuse) và tăng cường chức năng xuyên suốt các component (cross-component functionality).

Sơ đồ này minh họa cách hoạt động của HOC: Nó nhận vào một component gốc (Component A), bao bọc nó để thêm chức năng, và trả về một phiên bản mới mạnh mẽ hơn (Enhanced Component A)
Nguyên lý hoạt động của Higher-Order Component (HOC):
- Bao bọc (Wrapping): HOC nhận một component làm đầu vào và trả về một component mới. Component mới này thường sẽ tăng cường chức năng cho component gốc, ví dụ như thêm các
propsbổ sung, quản lý trạng thái (state management), hoặc các lifecycle hooks (móc vòng đời). - Tăng cường chức năng: HOC có thể thêm động các chức năng bổ sung vào component, ví dụ như render có điều kiện, kiểm tra quyền hạn, tải dữ liệu, v.v., mà không làm sửa đổi mã nguồn của component gốc.

Trong ví dụ này, MyComponent được kết hợp thông qua hai HOC là withLoading và withErrorHandling, lần lượt tăng cường chức năng trạng thái loading và xử lý lỗi. Cách kết hợp này giống như việc nhiều decorator (bộ trang trí) cùng tăng cường chức năng cho một đối tượng trong Mẫu Decorator.
Lý do Higher-Order Component sử dụng Mẫu Decorator là vì:
- Tăng cường chức năng động: Có thể thêm các chức năng mới cho component một cách linh động mà không cần sửa đổi component gốc.
- Nguyên lý Trách nhiệm Đơn lẻ (SRP): Mỗi HOC chỉ tập trung vào việc tăng cường một chức năng cụ thể, giúp mã nguồn trở nên module hóa và dễ bảo trì hơn.
- Khả năng kết hợp và tái sử dụng: Nhiều HOC có thể được kết hợp với nhau, làm cho việc tăng cường chức năng trở nên linh hoạt và có thể tái sử dụng cao.
Tự động thêm hiển thị Loading khi gửi yêu cầu
Trong quá trình phát triển thực tế, chúng ta thường cần hiển thị thông báo loading khi gửi Axios request, và ẩn nó đi sau khi request hoàn tất. Việc sử dụng Decorator Pattern giúp dễ dàng thêm việc này này vào phương thức request của Axios mà không cần sửa đổi logic của request gốc.

Thông qua decorator withLoading để tăng cường axiosRequest, ta tạo ra một phương thức mới là axiosWithLoading. Phương thức này tích hợp sẵn chức năng tự động hiển thị và ẩn loading. Kết thúc phần 1, chúng ta đã đi sâu vào 2 design pattern là Creational Patterns và Structural Patterns. Hãy cùng đón chờ phần 2 đẻ tìm hiểu thêm về các design pattern khác trong Front-end nhé