7 phút

Giữ mã sinh ra dễ bảo trì: nguyên tắc kiến trúc nhàm chán

Tìm cách giữ mã sinh dễ bảo trì bằng nguyên tắc kiến trúc nhàm chán: ranh giới thư mục rõ, quy ước đặt tên nhất quán và mặc định đơn giản để giảm công việc sửa sau này.

Giữ mã sinh ra dễ bảo trì: nguyên tắc kiến trúc nhàm chán

Tại sao bảo trì khó hơn với mã sinh ra

Mã được sinh thay đổi công việc hàng ngày. Bạn không chỉ xây tính năng, bạn còn hướng hệ thống có thể tạo nhiều file nhanh chóng. Tốc độ là thật, nhưng những không nhất quán nhỏ nhân lên rất nhanh.

Kết quả sinh thường trông ổn nếu nhìn riêng lẻ. Chi phí lộ ra ở lần thay đổi thứ hai, thứ ba: bạn không biết phần nào thuộc nơi nào, bạn sửa cùng hành vi ở hai chỗ, hoặc bạn né không động vào file vì không biết ảnh hưởng ra sao.

“Thông minh” quá mức tốn kém vì khó dự đoán. Mẫu tuỳ chỉnh, magic ẩn và abstraction nặng có thể hợp lý ngày đầu. Đến tuần thứ sáu, thay đổi tiếp theo chậm lại vì bạn phải học lại mánh trước khi cập nhật an toàn. Với sinh mã hỗ trợ AI, sự tinh vi đó còn có thể làm rối các lần sinh sau, dẫn tới logic lặp lại hoặc từng lớp mới chồng lên nhau.

Kiến trúc nhàm chán là ngược lại: ranh giới rõ ràng, tên đơn giản và mặc định hiển nhiên. Không phải theo đuổi hoàn hảo. Mà là chọn cách bố trí mà đồng đội mệt mỏi (hoặc chính bạn tương lai) có thể hiểu trong 30 giây.

Mục tiêu đơn giản: làm cho thay đổi tiếp theo dễ, chứ không phải ấn tượng. Thường nghĩa là một chỗ rõ ràng cho mỗi loại mã (UI, API, dữ liệu, tiện ích dùng chung), tên dễ đoán thể hiện chức năng file, và ít “magic” như auto-wiring, global ẩn, hay metaprogramming.

Ví dụ: nếu bạn yêu cầu Koder.ai thêm “mời nhóm”, bạn muốn nó đặt UI trong khu UI, thêm một route API trong khu API và lưu dữ liệu invite ở tầng data, chứ không phát minh một thư mục hay pattern mới chỉ cho tính năng đó. Sự nhất quán nhàm chán đó giữ sửa đổi sau này rẻ.

Nguyên tắc kiến trúc nhàm chán

Mã sinh tốn kém khi nó cho nhiều cách làm cùng một việc. Nguyên tắc kiến trúc nhàm chán đơn giản: làm cho thay đổi tiếp theo dễ dự đoán, dù lần xây đầu tiên ít “thông minh” hơn.

Bạn nên trả lời những câu này nhanh:

  • Tính năng này nằm ở đâu?
  • Tôi đặt file mới ở đâu?
  • Tôi nên đặt tên nó thế nào?
  • Đường ngắn nhất từ UI đến dữ liệu là gì?

Quy tắc

Chọn một cấu trúc đơn giản và dính chặt nó ở mọi nơi. Khi công cụ (hoặc đồng đội) gợi ý pattern hoa mỹ, câu trả lời mặc định là “không” trừ khi nó thực sự loại bỏ đau đầu.

Các mặc định thực tế giữ được theo thời gian:

  • Một trách nhiệm cho mỗi thư mục và mỗi file. Nếu một file có hai lý do để thay đổi, tách nó ra.
  • Dự đoán được tốt hơn linh hoạt. Cùng loại việc luôn vào cùng một chỗ.
  • Ưu tiên cách làm chuẩn của stack hơn mini-frameworks tự chế.
  • Làm đường dẫn thuận tiện rõ ràng. Người mới nên đoán đúng vị trí mà không hỏi.
  • Từ chối “magic.” Tránh hành vi ẩn, mẹo nặng reflection, và abstraction quá tinh vi.

Bài kiểm tra tinh thần nhanh

Tưởng tượng một dev mới mở repo và cần thêm nút “Hủy đăng ký”. Họ không nên phải học kiến trúc tuỳ chỉnh trước. Họ nên tìm thấy khu tính năng rõ ràng, component UI rõ ràng, một client API duy nhất và một đường truy cập dữ liệu duy nhất.

Quy tắc này đặc biệt phù hợp với công cụ tạo nhanh như Koder.ai: bạn có thể sinh nhanh, nhưng vẫn hướng kết quả vào cùng ranh giới nhàm chán mỗi lần.

Ranh giới thư mục đơn giản có thể mở rộng

Mã sinh có xu hướng tăng nhanh. Cách an toàn nhất để giữ nó dễ bảo trì là một bản đồ thư mục nhàm chán, nơi ai cũng đoán được sự thay đổi thuộc chỗ nào.

Một layout top-level nhỏ phù hợp nhiều web app:

  • app/ screens, routing, and page-level state
  • components/ reusable UI pieces
  • features/ one folder per feature (billing, projects, settings)
  • api/ API client code and request helpers
  • server/ backend handlers, services, and business rules

Điều này làm ranh giới rõ ràng: UI nằm trong app/components/, lời gọi API nằm trong api/, và logic backend nằm trong server/.

Truy cập dữ liệu cũng nên nhàm chán. Giữ SQL và repository gần backend, không rải rác trong file UI. Trong setup Go + PostgreSQL, quy tắc đơn giản: HTTP handlers gọi services, services gọi repositories, repositories nói chuyện với database.

Types và utils chia sẻ xứng đáng có chỗ rõ ràng, nhưng giữ nó nhỏ. Đặt types chéo vào types/ (DTOs, enums, shared interfaces) và helper nhỏ vào utils/ (format ngày, validator đơn giản). Nếu utils/ bắt đầu như một app thứ hai, có lẽ mã nên về thư mục feature.

Sinh so với viết tay

Đối xử với thư mục sinh như có thể thay thế.

  • Đặt đầu ra sinh vào generated/ (hoặc gen/) và tránh sửa trực tiếp.
  • Giữ logic tùy chỉnh trong features/ hoặc server/ để tái sinh không ghi đè.
  • Nếu phải vá hành vi sinh, bọc nó (adapter file) thay vì sửa nguồn.

Ví dụ: nếu Koder.ai sinh một client API, lưu nó dưới generated/api/, rồi viết các wrapper mỏng trong api/ để thêm retry, logging, hoặc thông báo lỗi rõ ràng mà không chạm vào file sinh.

Quy ước đặt tên tránh nhầm lẫn

Mã sinh dễ tạo và dễ chất đống. Đặt tên giữ nó đọc được sau một tháng.

Chọn một style và đừng trộn:

  • Thư mục và file: kebab-case (user-profile-card.tsx, billing-settings)
  • React components: PascalCase (UserProfileCard)
  • Hàm và biến: camelCase (getUserProfile)
  • Hằng số: SCREAMING_SNAKE_CASE (MAX_RETRY_COUNT)

Đặt tên theo vai trò, không theo cách nó hoạt động hiện tại. user-repository.ts là một vai trò. postgres-user-repository.ts là chi tiết triển khai có thể thay đổi. Chỉ thêm hậu tố triển khai khi thực sự có nhiều triển khai.

Tránh các "ngăn kéo rác" như misc, helpers, hay một utils khổng lồ. Nếu hàm chỉ dùng bởi một feature, giữ nó gần feature đó. Nếu nó chia sẻ, làm tên mô tả khả năng (date-format.ts, money-format.ts, id-generator.ts) và giữ module nhỏ.

Quy ước giúp điều hướng nhanh

Khi routes, handlers và components theo pattern, bạn tìm được thứ mà không cần search:

  • Routes: routes/users.ts với đường dẫn như /users/:userId
  • Handlers (HTTP): handlers/users.get.ts, handlers/users.update.ts
  • Services (business rules): services/user-profile-service.ts
  • Data access: repositories/user-repository.ts
  • UI components: components/user/UserProfileCard.tsx

Nếu bạn dùng Koder.ai (hoặc bất kỳ generator nào), đặt những quy tắc này trong prompt và giữ chúng nhất quán khi chỉnh sửa. Mục tiêu là tính dự đoán: nếu bạn đoán được tên file, thay đổi sau này sẽ rẻ hơn.

Mặc định không tinh ranh (nguyên tắc thực hành)

Chuyển đổi không lo sợ
Thử nghiệm cấu trúc an toàn bằng snapshot và rollback khi refactor đi sai hướng.

Mã sinh có thể trông ấn tượng ngày đầu và đau ngày thứ ba mươi. Chọn mặc định khiến mã hiển nhiên, ngay cả khi hơi lặp lại.

Bắt đầu bằng cách giảm magic. Bỏ qua dynamic loading, mẹo reflection, và auto-wiring trừ khi có nhu cầu đo lường. Những tính năng này che nơi xuất phát của thứ gì, làm debug và refactor chậm hơn.

Ưu tiên import rõ ràng và phụ thuộc minh bạch. Nếu file cần thứ gì, import trực tiếp. Nếu modules cần wiring, làm ở một chỗ nhìn thấy được (ví dụ, một file composition duy nhất). Người đọc không nên phải đoán thứ gì chạy trước.

Giữ config nhàm chán và tập trung. Đặt biến môi trường, feature flags và cài đặt app-wide trong một module với một cách đặt tên. Đừng rải config khắp file vì cảm thấy tiện.

Nguyên tắc giữ đội nhất quán:

  • Chọn rõ ràng hơn ngầm định (imports, routing, DI, side effects).
  • Nếu tiết kiệm 10 dòng nhưng thêm khái niệm mới, bỏ qua.
  • Giữ một cách duy nhất để làm mọi thứ (một tool logging, một module config).
  • Thích luồng dữ liệu đơn giản hơn observers ẩn hoặc chuỗi sự kiện.
  • Khi debug, xóa code tinh ranh trước.

Xử lý lỗi là nơi tinh ranh gây hại nhất. Chọn một pattern và dùng nó khắp nơi: trả về lỗi có cấu trúc từ tầng dữ liệu, map chúng sang HTTP responses ở một chỗ, và chuyển thành thông báo người dùng ở ranh giới UI. Đừng ném ba kiểu lỗi khác nhau tuỳ file.

Nếu bạn sinh app với Koder.ai, yêu cầu mặc định này từ đầu: wiring module rõ ràng, config tập trung, và một pattern lỗi duy nhất.

Ranh giới giữa UI, API và dữ liệu

Đường ranh rõ giữa UI, API và dữ liệu giữ thay đổi trong phạm vi. Hầu hết bug bí ẩn xảy ra khi một tầng bắt đầu làm việc của tầng khác.

UI: hiển thị trạng thái, thu thập input

Xem UI (thường React) là nơi render màn hình và quản lý state chỉ dành cho UI: tab mở, lỗi form, spinner tải và xử lý input cơ bản.

Giữ server state riêng: danh sách fetch, profile cache và bất cứ thứ gì phải khớp backend. Khi component UI bắt đầu tính tổng, validate luật phức tạp, hoặc quyết định quyền, logic lan rộng khắp màn hình và trở nên đắt để thay đổi.

API: mỏng, shapes ổn định

Giữ tầng API dự đoán được. Nó nên chuyển HTTP request thành lời gọi tới business code, rồi chuyển kết quả về dạng request/response ổn định. Tránh gửi mô hình database trực tiếp lên wire. Response ổn định cho phép refactor nội bộ mà không phá UI.

Một đường dẫn đơn giản hoạt động tốt:

  • UI gọi API client với objects request/response có kiểu.
  • API handlers validate input và gọi method service.
  • Services giữ business rules và workflow.
  • Repositories ẩn query database sau các method nhỏ.

Dữ liệu: ẩn query sau repository

Đặt SQL (hoặc logic ORM) sau ranh repository để phần còn lại của app không “biết” cách lưu trữ. Trong Go + PostgreSQL, thường là các repository như UserRepo hoặc InvoiceRepo với các method nhỏ, rõ ràng (GetByID, ListByAccount, Save).

Ví dụ cụ thể: thêm mã giảm giá. UI hiển thị ô nhập và cập nhật giá hiển thị. API nhận code và trả {total, discount}. Service quyết định code có hợp lệ và cách giảm giá chồng nhau. Repository lấy và lưu các hàng cần thiết.

Từng bước: thiết lập mã sinh dễ bảo trì

App sinh có thể trông “xong” nhanh, nhưng cấu trúc mới làm cho thay đổi rẻ sau này.

Luồng thiết lập thực tế

Bắt đầu với một lượt hoạch định ngắn. Nếu bạn dùng Koder.ai, Planning Mode là nơi tốt để viết bản đồ thư mục và vài quy tắc đặt tên trước khi sinh bất cứ thứ gì.

Rồi theo trình tự này:

  1. Định nghĩa bản đồ và quy tắc bằng văn bản. Chọn ranh giới (ví dụ: ui/, api/, data/, features/) và vài quy tắc đặt tên.
  2. Sinh một lát dọc mỏng. Chọn tính năng nhỏ chạm UI, API và lưu trữ, như “tạo contact”. Mục tiêu là đường end-to-end, không phải tính năng đầy đủ.
  3. Refactor ngay để khớp ranh giới. Chuyển mã vào thư mục đã định, đổi tên file không rõ, xóa trùng. Tách hàm “làm mọi thứ” thành UI, handler và data access.
  4. Thêm tính năng thứ hai để kiểm tra hình dạng. Chọn cái tương tự, như “liệt kê contacts”. Nếu bạn bị ép phá quy tắc, có lẽ ranh giới sai.
  5. Khóa quy ước sớm. Thêm CONVENTIONS.md ngắn và coi nó như hợp đồng. Khi codebase lớn, đổi tên và mô hình thư mục sẽ tốn kém.

Thực tế: nếu người mới không đoán được nơi để đặt “edit contact” mà không hỏi, kiến trúc vẫn chưa đủ nhàm chán.

Kịch bản ví dụ: thêm tính năng mà không tạo mớ hỗn độn

Giảm magic ẩn
Giảm magic ẩn bằng cách yêu cầu Koder.ai ưu tiên import rõ ràng và wiring đơn giản.

Hình dung CRM đơn giản: trang danh sách contacts và form sửa contact. Bạn làm phiên bản đầu nhanh, rồi một tuần sau cần thêm “tags” cho contacts.

Xử lý app như ba hộp nhàm chán: UI, API và dữ liệu. Mỗi hộp có ranh giới rõ và tên đích thực để thay đổi “tags” giữ nhỏ.

Layout sạch có thể trông như:

  • web/src/pages/ContactsPage.tsxweb/src/components/ContactForm.tsx
  • server/internal/http/contacts_handlers.go
  • server/internal/service/contacts_service.go
  • server/internal/repo/contacts_repo.go
  • server/migrations/

Bây giờ “tags” trở nên dự đoán được. Cập nhật schema (bảng contact_tags mới hoặc cột tags), rồi chạm từng tầng: repo đọc/ghi tags, service validate, handler expose field, UI render và edit. Đừng lén đưa SQL vào handlers hoặc business rule vào component React.

Nếu sau này product yêu cầu “filter theo tag”, bạn hầu hết làm ở ContactsPage.tsx (UI state và query params) và HTTP handler (parsing request), còn repo xử lý query.

Cho tests và fixtures, giữ nhỏ và gần mã:

  • server/internal/service/contacts_service_test.go cho rule như “tên tag phải duy nhất trên contact”
  • server/internal/repo/testdata/ cho fixtures tối thiểu
  • web/src/components/__tests__/ContactForm.test.tsx cho hành vi form

Nếu bạn sinh bằng Koder.ai, quy tắc vẫn áp dụng sau khi export: giữ thư mục nhàm chán, tên literal, và chỉnh sửa không còn như khảo cổ.

Những lỗi phổ biến khiến thay đổi sau này tốn kém

Mã sinh có thể trông sạch ngày đầu và vẫn đắt sau này. Thủ phạm thường không phải “mã xấu”, mà là không nhất quán.

Thói quen tốn kém là để generator phát minh cấu trúc mỗi lần. Một feature có thư mục, kiểu đặt tên và helper riêng, và bạn có ba cách làm cùng một việc. Chọn một pattern, viết nó ra, và coi pattern mới là thay đổi có chủ ý, không phải mặc định.

Bẫy khác là trộn các tầng. Khi component UI nói chuyện trực tiếp với database, hoặc handler API tự xây SQL, thay đổi nhỏ biến thành sửa rủi ro khắp app. Giữ ranh giới: UI gọi API, API gọi service, service gọi data access.

Dùng abstraction generic quá sớm cũng tăng chi phí. Một “BaseService” toàn năng trông gọn, nhưng abstraction sớm thường là phỏng đoán. Khi thực tế thay đổi, bạn chống lại framework tự tạo thay vì ra tính năng.

Đổi tên và tái tổ chức liên tục là dạng nợ im lặng. Nếu file di chuyển mỗi tuần, người ta mất lòng tin vào layout và các sửa nhanh rơi vào chỗ ngẫu nhiên. Ổn định bản đồ thư mục trước, rồi refactor theo đợt có kế hoạch.

Cuối cùng, cẩn thận với “platform code” không có người dùng thực. Thư viện chia sẻ và tooling tự làm chỉ có lợi khi bạn có nhu cầu lặp lại, nếu không, giữ mặc định trực tiếp.

Checklist nhanh trước khi phát hành

Từ repo tới app chạy được
Đưa bản build dễ bảo trì lên môi trường khi bạn sẵn sàng bằng deployment và hosting của Koder.ai.

Nếu người mới mở repo, họ nên trả lời một câu nhanh: “Tôi thêm cái này ở đâu?”

Bài kiểm tra điều hướng 2 phút

Giao project cho đồng đội (hoặc bạn tương lai) và bảo họ thêm tính năng nhỏ, như “thêm trường vào form đăng ký”. Nếu họ không tìm thấy chỗ đúng nhanh, cấu trúc chưa hiệu quả.

Kiểm tra ba nhà ở rõ ràng:

  • Thay đổi UI nằm ở một chỗ hiển nhiên.
  • Routes/handlers API dễ tìm từ UI.
  • Model dữ liệu và thay đổi DB có vị trí rõ.

Quy tắc đáng áp dụng khi review

  • UI, API và data mỗi thứ có nhà riêng, ngoại lệ hiếm.
  • Tên đọc như nhãn, không phải câu đố.
  • Những chỗ phá ranh (như UI truy cập DB) bị đánh dấu.
  • Các shortcut tinh ranh bị từ chối mặc định.

Nếu nền tảng của bạn hỗ trợ, giữ đường lui. Snapshot và rollback rất hữu ích khi bạn thử nghiệm cấu trúc và muốn quay về an toàn.

Bước tiếp theo: giữ nhàm chán, giữ rẻ

Bảo trì cải thiện nhanh nhất khi bạn ngừng tranh luận style và bắt đầu quyết vài điều dính. Viết ra một bộ quy ước nhỏ loại bỏ do dự hàng ngày: file để đâu, đặt tên thế nào, xử lý lỗi và config ra sao. Giữ nó đủ ngắn để đọc trong một phút.

Rồi làm một lượt dọn dẹp để khớp những quy tắc đó và ngừng xáo trộn hàng tuần. Tái tổ chức thường xuyên làm thay đổi tiếp theo chậm hơn, dù mã trông có vẻ đẹp hơn.

Nếu bạn xây với Koder.ai (koder.ai), lưu những quy ước này làm prompt khởi đầu để mỗi lần sinh mới nằm đúng cấu trúc. Công cụ có thể chạy nhanh, nhưng ranh giới nhàm chán là thứ giữ mã dễ sửa đổi.

Câu hỏi thường gặp

«Kiến trúc đơn giản» có nghĩa gì đối với mã được tạo tự động?

Dùng một sơ đồ thư mục đơn giản, tên gọi rõ nghĩa và một luồng duy nhất từ giao diện người dùng đến API rồi tới dữ liệu. Mục tiêu là để ai đó có thể đặt một thay đổi vào đúng chỗ mà không cần học trước một quy ước riêng.

Vì sao mã do AI tạo ra khó bảo trì hơn?

Mã được tạo tự động có thể thêm tệp rất nhanh, nên những thiếu nhất quán nhỏ cũng lan rộng nhanh. Một cấu trúc dễ đoán giúp lần sửa thứ hai và thứ ba không biến thành việc tìm kiếm trong logic trùng lặp và các thư mục lạ.

Ứng dụng web được tạo tự động nên dùng những thư mục nào?

Một điểm khởi đầu thực tế là dùng app/ cho màn hình và định tuyến, components/ cho giao diện tái sử dụng, features/ cho mã theo tính năng, api/ cho các yêu cầu từ máy khách và server/ cho logic phía máy chủ. Giữ cùng ranh giới này cho mọi tính năng.

Tôi nên đặt tên cho tệp và mã được tạo tự động như thế nào?

Hãy dùng nhất quán một kiểu: kebab-case cho tệp và thư mục, PascalCase cho các thành phần React, camelCase cho hàm và SCREAMING_SNAKE_CASE cho hằng số. Đặt tên tệp theo chức năng, như user-repository.ts hoặc money-format.ts.

Làm thế nào để tách mã UI, API và cơ sở dữ liệu?

Giữ mã giao diện tập trung vào việc hiển thị và nhận dữ liệu nhập, để trình xử lý API xác thực yêu cầu và trả về phản hồi ổn định, đặt quy tắc nghiệp vụ trong service, còn các lệnh gọi SQL hoặc ORM trong repository. Mỗi tầng nên gọi tầng kế tiếp thay vì làm công việc của tầng đó.

Tôi có nên chỉnh sửa trực tiếp các tệp được tạo tự động không?

Đặt đầu ra được tạo tự động trong thư mục riêng generated/ hoặc gen/ và tránh chỉnh sửa trực tiếp. Hãy viết wrapper hoặc adapter tùy chỉnh bên ngoài thư mục đó để bạn có thể tạo lại mã mà không mất các thay đổi của mình.

Tôi nên tránh những mẫu «thông minh» nào?

Tránh biến toàn cục ẩn, liên kết tự động, tải động và các lớp trừu tượng tạo thêm một khái niệm mới chỉ để tiết kiệm vài dòng mã. Ưu tiên import trực tiếp, cấu hình tập trung và thiết lập mô-đun rõ ràng.

Làm thế nào để thiết lập một dự án được tạo tự động nhưng vẫn dễ bảo trì?

Bắt đầu với một tính năng nhỏ xuyên suốt, chẳng hạn tạo liên hệ. Hãy tạo mã cho nó, chuyển mọi phần chưa rõ vào các thư mục đã định, loại bỏ phần trùng lặp, rồi xây dựng một tính năng thứ hai tương tự để xem cấu trúc có còn tự nhiên hay không.

Làm thế nào để giữ đầu ra của Koder.ai nhất quán?

Trước mỗi thay đổi, hãy đưa cho trình tạo các quy tắc cụ thể: sơ đồ thư mục, kiểu đặt tên, ranh giới giữa các tầng và mẫu xử lý lỗi. Với Koder.ai, bạn có thể lưu các quy tắc này trong lời nhắc lập kế hoạch để công việc mới tuân theo cùng cấu trúc.

Làm thế nào để kiểm tra liệu dự án của tôi có dễ thay đổi không?

Hãy nhờ ai đó thêm một thay đổi nhỏ, chẳng hạn một trường đăng ký, mà không hướng dẫn. Nếu họ nhanh chóng tìm được vị trí UI, trình xử lý API, service và dữ liệu, dự án của bạn dễ làm việc. Nếu không, hãy đơn giản hóa sơ đồ trước khi nó phát triển thêm.

Related posts