6 design pattern giúp bạn bảo AI viết app đỡ rối
Bạn vibe code một app, nó chạy được, nhưng thêm feature là code rối thêm — và bạn không biết bảo AI sửa theo hướng nào. Biết tên 6 pattern thường gặp là đủ để chỉ đường cho AI: tách theo Strategy, bọc bằng Adapter, phát event theo Observer. Đây là bản cho người vibe code, không phải cho người build agent.
6 design pattern giúp bạn bảo AI viết app đỡ rối
Bạn vibe code một app, mọi thứ chạy được, rồi mỗi lần thêm payment, notification hay một provider mới thì code lại rối thêm một chút. Bạn biết cần bảo AI refactor, nhưng không biết nên sửa theo hướng nào. Thật ra, chỉ cần biết tên vài design pattern thường gặp là bạn đã có đủ vocabulary để chỉ đường cho AI rõ hơn.
Hồi đầu mình cũng hay prompt kiểu “làm đoạn này clean hơn” hoặc “tách code ra cho dễ maintain”. AI sẽ sửa, nhưng “clean” có quá nhiều cách hiểu, nên kết quả đôi khi chỉ là spaghetti code được chia sang nhiều file hơn.
Cái mình nhận ra là design pattern hữu ích với vibe coder không phải vì mình cần thuộc lý thuyết. Nó hữu ích vì mỗi pattern là tên gọi ngắn cho một kiểu cấu trúc. Thay vì mô tả vòng vo, mình có thể nói: “tách cái này theo Strategy” hoặc “bọc API này bằng Adapter”, và AI hiểu khá rõ hình dạng code mình muốn.
Mình có viết một bản cho người build agent system, còn bài này dành cho bạn đang vibe code một app thật bằng Cursor, Claude Code, v0 hay Lovable.
Strategy — cùng một việc, đổi cách làm mà không phải sửa cả app
Giả sử app của bạn gửi notification qua email. Một thời gian sau, bạn muốn thêm SMS và push notification. Nếu logic chọn provider, format nội dung và gọi API nằm chung trong một hàm, mỗi lần thêm cách gửi mới bạn lại phải mở hàm cũ ra sửa.
Strategy tách từng cách làm thành một phần riêng, nhưng tất cả cùng tuân theo một interface. Phần còn lại của app chỉ cần gọi send(), còn bên dưới là email, SMS hay push thì tùy config hoặc lựa chọn của user. Payment cũng là một ví dụ rất gần: Momo, VNPay và thẻ có cách xử lý khác nhau, nhưng app có thể gọi chúng qua cùng một flow.
Khi gặp hình dạng này, mình sẽ prompt:
“Tách phần gửi notification ra theo Strategy. Email, SMS và push dùng chung một interface để sau này thêm provider mới mà không phải sửa logic cũ.”
Strategy hợp khi câu mô tả bắt đầu bằng: “cùng một việc, nhưng có nhiều cách làm”. Nếu hiện tại app chỉ có một cách và chưa có dấu hiệu sẽ đổi, mình chưa cần ép pattern này vào.
Factory — gom việc tạo object về một chỗ
AI khá dễ rải việc khởi tạo object ở nhiều file. Chỗ này tự tạo API client, chỗ kia tự gắn config, một file khác lại tạo cùng client nhưng thiếu timeout hoặc dùng sai API key. App vẫn có thể chạy, nhưng lúc cần đổi cách khởi tạo thì mình phải đi tìm từng chỗ.
Factory là một nơi tập trung để tạo ra thứ app cần. Ví dụ, thay vì mỗi payment flow tự khởi tạo MomoClient hoặc VNPayClient, app gọi một factory và đưa vào tên provider. Factory chịu trách nhiệm chọn đúng client, gắn config và trả nó về ở trạng thái sẵn sàng dùng.
Mình thường nói với AI:
“Tạo một Simple Factory cho payment provider. Gom toàn bộ logic khởi tạo Momo, VNPay và card client vào một chỗ, đừng rải
newhoặc config khắp codebase.”
Với app nhỏ, Simple Factory thường đã đủ. Bạn chưa cần đi sâu vào Factory Method hay Abstract Factory chỉ vì tên nghe có vẻ bài bản hơn; thêm abstraction mà chưa có nhu cầu thật cũng là một cách làm code khó đọc.
Observer — một việc xảy ra, những phần liên quan tự nhận được tin
Khi user thanh toán thành công, app có thể cần gửi email, cộng điểm, cập nhật dashboard và ghi analytics. Cách nhanh nhất là nhét tất cả vào hàm completePayment(). Nhưng lúc đó payment bắt đầu biết quá nhiều chuyện không thuộc trách nhiệm của nó.
Observer tách các phản ứng đó thành những listener riêng. Payment chỉ phát một event như PaymentCompleted; phần email, loyalty và analytics tự subscribe rồi xử lý việc của mình. Webhook từ bên ngoài đi vào app cũng thường dẫn tới cấu trúc tương tự: nhận sự kiện, xác minh nó, rồi phát event nội bộ cho các phần liên quan.
Prompt mình dùng sẽ khá cụ thể:
“Refactor flow thanh toán theo Observer/event-driven: khi payment thành công thì phát
PaymentCompleted, còn email, cộng điểm và analytics là các listener riêng. Không gọi trực tiếp tất cả trong payment service.”
Pattern này giúp thêm một phản ứng mới mà ít đụng vào flow cũ. Nhưng mình vẫn yêu cầu AI làm rõ retry, lỗi và thứ tự xử lý, vì tách thành event không tự động làm những vấn đề đó biến mất.
Adapter — bọc API bên ngoài cho khớp với cách app của mình nói chuyện
API bên ngoài hiếm khi có cùng tên field, error format hay flow với code trong app. Stripe có cách tạo payment và kiểm tra trạng thái riêng; một cổng thanh toán Việt Nam lại trả dữ liệu theo kiểu khác. Nếu để format của từng provider lan vào business logic, đổi dịch vụ sẽ kéo theo một đợt sửa khắp codebase.
Adapter là lớp đứng giữa để dịch API lạ thành interface mà app của mình hiểu. App chỉ biết các thao tác như createPayment(), verifyWebhook() và refund(). Adapter của Stripe hay VNPay tự chuyển request, response và error về format chung đó.
Đây là câu mình sẽ đưa cho AI:
“Bọc API VNPay bằng một Adapter tuân theo
PaymentProviderhiện có. Business logic chỉ dùng interface chung và không được phụ thuộc trực tiếp vào response format của VNPay.”
Adapter đặc biệt đáng dùng khi bạn biết provider có thể đổi. Nó cũng giúp test dễ hơn, vì mình có thể thay API thật bằng một fake adapter mà không phải sửa flow chính.
Singleton — chỉ có một instance dùng chung, nhưng đừng biến mọi thứ thành global
Một database connection pool, config loader hoặc API client thường nên được tạo một lần rồi tái sử dụng. Nếu mỗi request lại tạo một connection hoặc client mới, app vừa khó kiểm soát resource vừa dễ có config không đồng nhất. Singleton là tên của ý tưởng đảm bảo chỉ có một instance như vậy trong app.
Tuy nhiên, đây cũng là pattern AI hay dùng quá tay. Nếu mọi service đều thành Singleton, dependency bị giấu trong global state và test bắt đầu khó cô lập. Trong nhiều framework, dependency injection hoặc lifecycle của framework đã lo chuyện “một instance dùng chung”, nên mình thường yêu cầu AI kiểm tra cơ chế sẵn có trước.
Prompt phù hợp sẽ là:
“Đảm bảo database client chỉ được khởi tạo một lần và tái sử dụng. Ưu tiên lifecycle hoặc dependency injection của framework; chỉ dùng Singleton nếu framework chưa quản lý việc này.”
Điểm cần nhớ không phải là “thấy database thì dùng Singleton”. Điểm cần nhớ là hỏi: instance này có thật sự cần duy nhất và dùng chung không, hay mình chỉ đang biến nó thành global cho tiện?
Facade — cho app một cửa gọn để đi qua phần phức tạp
Có lúc một thao tác nhìn từ bên ngoài rất đơn giản nhưng phía sau cần query database, kiểm tra quyền, đọc settings và gọi thêm service. Nếu component nào cũng tự làm đủ các bước đó, chi tiết bên dưới sẽ lan khắp app. Khi schema hoặc API đổi, rất nhiều chỗ cùng vỡ.
Facade tạo một cửa vào gọn cho cả cụm logic. Ví dụ, UI chỉ gọi getUserProfile(userId), còn phía sau facade tự lấy user, preferences, subscription và quyền truy cập. Nếu phần cần giấu chủ yếu là query database, bạn cũng có thể nghe tên Repository; ý chính vẫn là không để SQL hay chi tiết storage tràn vào UI và business logic.
Mình sẽ prompt như sau:
“Tạo một Facade cho user profile với
getUserProfile(). Giấu query, settings và subscription lookup phía sau; component không được gọi trực tiếp từng service.”
Facade không có nghĩa là gom cả app vào một file khổng lồ. Nó chỉ là một interface gọn cho một nhóm thao tác có liên quan, còn các phần phía sau vẫn nên giữ trách nhiệm riêng.
Câu hỏi thường gặp
Mình có cần học hết design pattern không?
Không. Với vibe coding, mình thấy nhận ra hình dạng vấn đề quan trọng hơn thuộc định nghĩa. Bạn có thể bắt đầu bằng sáu pattern này, dùng khi gặp đúng tình huống, rồi học thêm từ chính codebase của mình.
Không biết pattern thì app có chạy được không?
Có. Pattern không phải điều kiện để app chạy; nó giúp app dễ đổi hơn khi feature bắt đầu nhiều lên. Nếu một flow đang nhỏ, rõ và ổn định, giữ nó đơn giản thường tốt hơn việc ép thêm abstraction.
Nói tên pattern cho AI thì nó code tốt hơn thiệt không?
Tên pattern giúp prompt bớt mơ hồ, nhưng chỉ nói mỗi cái tên vẫn chưa đủ. Mình thường kèm theo mục tiêu, ví dụ cụ thể và constraint: provider nào cần tách, interface nào phải giữ, phần nào không được phụ thuộc trực tiếp. Pattern cho AI cái khung; context của app mới quyết định code có hợp hay không.
Làm sao biết AI đang dùng pattern quá tay?
Nếu sau khi refactor, một feature đơn giản phải đi qua quá nhiều file, interface chỉ có một implementation, hoặc bạn không giải thích được mỗi lớp đang bảo vệ thay đổi nào, có thể abstraction đang đi trước nhu cầu. Lúc đó mình sẽ hỏi AI: “Pattern này đang giải quyết thay đổi cụ thể nào? Nếu bỏ nó đi thì rủi ro thật là gì?”
Trong app bạn đang vibe code, chỗ nào bắt đầu rối nhất: payment, auth, notification, settings hay phần tích hợp API?