← C4 Model: vẽ kiến trúc phần mềm

Bài 8 · Vận dụng · 22 phút

Diagrams as code

Biên soạn bởi Nguyễn Anh Tuấn

Viết mô hình bằng code thay vì kéo-thả: Structurizr DSL (một model sinh nhiều view), C4-PlantUML và Mermaid C4 - để sơ đồ luôn khớp hệ thống, version cùng Git.

Công cụ kéo-thả như draw.io hay Visio dễ dùng và cho ra hình đẹp. Nhưng khi sơ đồ cần sống lâu cùng dự án, chúng lộ điểm yếu - đúng nỗi lo "lạc hậu" ta gặp ở mức Component.

  • Tách rời code: sơ đồ là một file ảnh nằm đâu đó, dễ quên cập nhật khi hệ thống đổi.
  • Khó review: một ảnh PNG không "diff" được; người review không thấy đổi gì.
  • Lặp công: cùng một phần tử xuất hiện ở nhiều bức; đổi một chỗ phải sửa tay nhiều bức.

Diagrams as code đảo ngược cách làm: bạn MÔ TẢ mô hình bằng văn bản, rồi để công cụ tự vẽ ra sơ đồ. Vì giờ sơ đồ là text, nó hưởng mọi thứ tốt đẹp của code.

  • Version cùng Git: sơ đồ đổi theo từng commit, có lịch sử rõ ràng.
  • Review qua Pull Request: người khác xem diff text và góp ý ngay.
  • Một model, nhiều view: mô tả phần tử một lần, sinh ra nhiều sơ đồ luôn khớp nhau.
  • Tự canh bố cục: khỏi mất công kéo từng hộp cho thẳng hàng.

Thật ra, chính các sơ đồ trong khoá này cũng là "diagrams as code" - chúng được sinh ra từ dữ liệu (không vẽ tay), nên luôn nhất quán. Ví dụ một sơ đồ Container do code dựng:

Container - bên trong Ngân Hàng Số

[Người dùng]

Khách hàng

Giao dịch trực tuyến.

[Container: React]

Web App

Ngân hàng trên trình duyệt.

[Container: Swift / Kotlin]

Mobile App

App ngân hàng trên điện thoại.

[Container: Java / Spring]

API Application

Cung cấp API cho web & mobile.

[Container: Oracle]

Cơ sở dữ liệu

Lưu đăng nhập, nhật ký giao dịch.

[Container: RabbitMQ]

Hàng đợi tin

Xử lý giao dịch bất đồng bộ.

[Hệ thống ngoài]

Hệ thống lõi (Core Banking)

Tài khoản & giao dịch.

Dùng [HTTPS]Dùng [HTTPS]Gọi [JSON/HTTPS]Gọi [JSON/HTTPS]Đọc/ghi [SQL/TCP]Đẩy giao dịch [AMQP]Lấy số dư [XML/HTTPS]

Structurizr do chính Simon Brown (cha đẻ C4) làm ra, sinh đúng cho C4. Điểm mạnh: bạn dựng một model, rồi định nghĩa nhiều view từ model đó.

ngan-hang.dsl - Structurizr DSL (render ra sơ đồ, không in ra terminal)

workspace {
  model {
    khachHang = person "Khách hàng"
    nganHang = softwareSystem "Ngân Hàng Số" {
      web = container "Web App" "React"
      api = container "API Application" "Java/Spring"
      db = container "Cơ sở dữ liệu" "Oracle"
    }
    khachHang -> web "Dùng [HTTPS]"
    web -> api "Goi [JSON/HTTPS]"
    api -> db "Doc/ghi [SQL]"
  }
  views {
    systemContext nganHang {
      include *
      autolayout lr
    }
    container nganHang {
      include *
      autolayout lr
    }
  }
}
  • Khối model khai báo phần tử & quan hệ MỘT lần.
  • Khối views sinh ra hai sơ đồ (systemContext và container) từ cùng model đó.
  • Đổi tên một container? Sửa một dòng, cả hai sơ đồ cập nhật theo.

Hai lựa chọn phổ biến khác mô tả TỪNG sơ đồ một (không có khái niệm model dùng chung), nhưng rất tiện và nhiều nơi hỗ trợ sẵn.

C4-PlantUML - dùng macro Person/Container/Rel (render bằng PlantUML)

@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

Person(khachHang, "Khách hàng")
System_Boundary(nganHang, "Ngân Hàng Số") {
  Container(web, "Web App", "React")
  Container(api, "API Application", "Java/Spring")
  ContainerDb(db, "Co so du lieu", "Oracle")
}

Rel(khachHang, web, "Dung", "HTTPS")
Rel(web, api, "Goi", "JSON/HTTPS")
Rel(api, db, "Doc/ghi", "SQL")
@enduml

Mermaid C4 - render thẳng trong Markdown trên GitHub

C4Context
  title System Context - Ngan Hang So
  Person(khachHang, "Khách hàng", "Giao dịch trực tuyến")
  System(nganHang, "Ngân Hàng Số", "Ngân hàng online")
  System_Ext(core, "Core Banking", "Tài khoản & giao dịch")
  Rel(khachHang, nganHang, "Dùng", "HTTPS")
  Rel(nganHang, core, "Lấy số dư", "XML/HTTPS")

Trung thực

Các đoạn trên RENDER ra sơ đồ chứ không in ra terminal, nên không có "kết quả" dạng chữ để dán. Hãy thử dán Mermaid vào một file README trên GitHub, hoặc dùng trình xem Mermaid/PlantUML online để thấy hình.
draw.ioKéo-thả, tự do, đẹp nhanh - nhưng tách rời code, khó diff, dễ lạc hậu.
StructurizrMột model sinh nhiều view, đúng C4 nhất - học hơi cong nhưng bền cho đội nghiêm túc.
C4-PlantUMLPhổ biến, nhiều tích hợp CI/wiki - mô tả từng sơ đồ một.
MermaidRender ngay trong Markdown/GitHub, cú pháp gọn - hợp để bắt đầu & README.

Gợi ý chọn

Mới bắt đầu hoặc cần sơ đồ trong README: chọn Mermaid. Đội muốn quản lý kiến trúc nghiêm túc, nhiều sơ đồ luôn khớp nhau: chọn Structurizr. Không có lựa chọn "đúng tuyệt đối" - hợp nhu cầu là được.

Giờ bạn tạo được sơ đồ vừa đẹp vừa luôn khớp hệ thống. Còn một kỹ năng nữa quyết định sơ đồ có thực sự hữu ích: trình bày đúng mức cho đúng người.

Bước tiếp theo

Bài tới: Giao tiếp đúng khán giả - chọn mức zoom theo người nghe, kể câu chuyện kiến trúc cho từng phòng ban.

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

Không bắt buộc. Bản nháp nhanh trên bảng trắng hay draw.io vẫn tiện. Nhưng với sơ đồ cần LƯU LẠI, version và giữ khớp hệ thống lâu dài, viết bằng code thắng rõ: diff được, review qua PR, một model sinh nhiều view.

Structurizr DSL là mã nguồn mở, và Structurizr Lite chạy local hoàn toàn miễn phí (xem một workspace). Ngoài ra có dịch vụ cloud trả phí cho đội. Với người học, Structurizr Lite là đủ để thử.

Mermaid đánh dấu cú pháp C4 là experimental (thử nghiệm), nhưng nó render ngay trong Markdown trên GitHub/GitLab và nhiều trình xem, nên rất tiện cho README và giao tiếp nhanh. Cần sơ đồ nghiêm túc, lâu dài thì cân nhắc Structurizr.

Structurizr dựng một MÔ HÌNH (phần tử + quan hệ) rồi sinh NHIỀU sơ đồ (view) từ đó - đúng tinh thần "một mô hình, nhiều sơ đồ". PlantUML/Mermaid thì bạn mô tả TỪNG sơ đồ một; nếu một phần tử đổi, bạn phải sửa ở mọi sơ đồ có nó.

Lúc mới làm quen thì hơi chậm. Nhưng sau đó đổi nhanh (sửa một dòng), tự canh bố cục, version theo Git, và không lệch giữa các sơ đồ - những cái này bù lại nhiều lần.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Lợi ích nào KHÔNG phải của "diagrams as code" so với kéo-thả?

  1. 1

    Viết Context bằng Mermaid

    Viết một sơ đồ C4Context bằng Mermaid cho một hệ thống mèo con biết, rồi dán vào một trình xem Mermaid (hoặc README trên GitHub) để render thử.

    Hoàn thành khi: Sơ đồ render ra; có ≥1 Person, hệ thống trung tâm, ≥1 hệ thống ngoài, quan hệ có nhãn.

  2. 2

    Một model, hai view

    Bằng Structurizr DSL, viết một khối model rồi định nghĩa CẢ HAI view: systemContext và container cho cùng hệ thống.

    Hoàn thành khi: Một khối model duy nhất; hai view sinh ra từ nó; mỗi view có autolayout.

  3. 3

    Bảng so sánh công cụ

    Lập bảng ưu/nhược cho draw.io, Structurizr, Mermaid theo các tiêu chí: version trong Git, một-model-nhiều-view, độ dễ học, render trên GitHub.

    Hoàn thành khi: Bảng đủ 3 công cụ × 4 tiêu chí; mỗi ô một nhận xét ngắn.

  4. 4

    Vì sao diff dễ hơn

    Viết một đoạn ngắn giải thích vì sao review một thay đổi sơ đồ dạng code (text) dễ hơn so với một ảnh PNG.

    Hoàn thành khi: Nêu được: text diff dòng-theo-dòng, comment trong PR, lịch sử Git rõ ràng.

  5. 5

    Dịch một sơ đồ vẽ tay

    Lấy một sơ đồ mèo con từng vẽ tay (vd bài Context), viết lại bằng MỘT trong ba cú pháp (Structurizr/PlantUML/Mermaid).

    Hoàn thành khi: Bản code render đúng nội dung sơ đồ gốc; mọi quan hệ có nhãn.

  6. 6

    Chọn công cụ cho đội

    Tình huống: một đội nhỏ muốn sơ đồ hiển thị ngay trong README trên GitHub, ít công bảo trì. Chọn công cụ và biện hộ.

    Hoàn thành khi: Chọn rõ một công cụ (gợi ý: Mermaid) với ít nhất hai lý do bám tình huống.