- Published on
MVC vs DDD: Phân Tích Sâu Kiến Trúc Go Language
Tổng Quan
MVC và DDD là hai khái niệm kiến trúc phân lớp phổ biến trong phát triển backend. MVC (Model-View-Controller) là một mẫu thiết kế chủ yếu được sử dụng để tách biệt giao diện người dùng, logic nghiệp vụ và mô hình dữ liệu nhằm dễ dàng tách rời và phân lớp, trong khi DDD (Domain-Driven Design) là một phương pháp kiến trúc nhằm giải quyết khó khăn trong thiết kế và bảo trì các hệ thống phức tạp bằng cách xây dựng mô hình domain nghiệp vụ.
Trong hệ sinh thái Java, nhiều hệ thống đã dần chuyển từ MVC sang DDD. Tuy nhiên, trong các ngôn ngữ như Go, Python và NodeJS—những ngôn ngữ ủng hộ sự đơn giản và hiệu quả—MVC vẫn là kiến trúc chủ đạo. Dưới đây, chúng ta sẽ thảo luận cụ thể về sự khác biệt trong cấu trúc thư mục giữa MVC và DDD dựa trên ngôn ngữ Go.
Cấu Trúc Sơ Đồ MVC
+------------------+
| View | Lớp Giao diện Người dùng: chịu trách nhiệm hiển thị dữ liệu và tương tác người dùng (như trang HTML, phản hồi API)
+------------------+
| Controller | Lớp Controller: xử lý yêu cầu người dùng, gọi logic Service, điều phối Model và View
+------------------+
| Model | Lớp Model: chứa các đối tượng dữ liệu (như cấu trúc bảng cơ sở dữ liệu) và một số logic nghiệp vụ (thường rải rác trong lớp Service)
+------------------+
Cấu Trúc Sơ Đồ DDD
+--------------------+
| User Interface | Chịu trách nhiệm tương tác và hiển thị người dùng (như REST API, giao diện Web)
+--------------------+
| Application Layer | Điều phối các quy trình nghiệp vụ (như gọi domain services, quản lý transaction), không chứa quy tắc nghiệp vụ cốt lõi
+--------------------+
| Domain Layer | Lớp logic nghiệp vụ cốt lõi: chứa aggregate roots, entities, value objects, domain services, v.v., đóng gói các quy tắc nghiệp vụ
+--------------------+
| Infrastructure | Cung cấp các triển khai kỹ thuật (như truy cập cơ sở dữ liệu, message queues, APIs bên ngoài)
+--------------------+
Sự Khác Biệt Chính Giữa MVC và DDD
1. Logic Tổ Chức Code
- MVC phân lớp theo chức năng kỹ thuật (Controller/Service/DAO), tập trung vào việc triển khai kỹ thuật.
- DDD chia modules theo domain nghiệp vụ (như domain đơn hàng, domain thanh toán), cô lập logic nghiệp vụ cốt lõi thông qua bounded contexts.
2. Nơi Chứa Logic Nghiệp Vụ
- MVC thường áp dụng mô hình thiếu máu (anemic model), tách biệt dữ liệu (Model) và hành vi (Service), dẫn đến chi phí bảo trì cao do logic phân tán.
- DDD đạt được mô hình phong phú thông qua aggregate roots và domain services, tập trung logic nghiệp vụ trong domain layer và tăng cường khả năng mở rộng.
3. Khả Năng Áp Dụng và Chi Phí
- MVC có chi phí phát triển thấp và phù hợp với các hệ thống nhỏ đến trung bình có yêu cầu ổn định.
- DDD yêu cầu mô hình hóa domain trước và ngôn ngữ thống nhất, phù hợp với các hệ thống lớn có nghiệp vụ phức tạp và nhu cầu phát triển dài hạn, nhưng team phải có khả năng trừu tượng hóa domain.
Cấu Trúc Thư Mục Go Language MVC
MVC chủ yếu chia thành ba lớp: view, controller và model.
gin-order/
├── cmd
│ └── main.go # Điểm khởi đầu ứng dụng, khởi động Gin engine
├── internal
│ ├── controllers # Lớp Controller (xử lý HTTP requests), còn gọi là handlers
│ │ └── order
│ │ └── order_controller.go # Controller cho module Order
│ ├── services # Lớp Service (xử lý logic nghiệp vụ)
│ │ └── order
│ │ └── order_service.go # Triển khai Service cho module Order
│ ├── repository # Lớp truy cập dữ liệu (tương tác với cơ sở dữ liệu)
│ │ └── order
│ │ └── order_repository.go # Interface và triển khai truy cập dữ liệu cho module Order
│ ├── models # Lớp Model (định nghĩa cấu trúc dữ liệu)
│ │ └── order
│ │ └── order.go # Mô hình dữ liệu cho module Order
│ ├── middleware # Middleware (như xác thực, logging, chặn request)
│ │ ├── logging.go # Middleware logging
│ │ └── auth.go # Middleware xác thực
│ └── config # Module cấu hình (cơ sở dữ liệu, cấu hình server, v.v.)
│ └── config.go # Cấu hình ứng dụng và môi trường
├── pkg # Các gói tiện ích chung (như response wrappers)
│ └── response.go # Các phương thức tiện ích xử lý response
├── web # Tài nguyên frontend (templates và static assets)
│ ├── static # Tài nguyên tĩnh (CSS, JS, images)
│ └── templates # File template (HTML templates)
│ └── order.tmpl # Template view cho module Order (nếu cần render HTML)
├── go.mod # File quản lý Go module
└── go.sum # File khóa dependency Go module
Cấu Trúc Thư Mục Go Language DDD
DDD chủ yếu chia thành bốn lớp: interface, application, domain và infrastructure.
go-web/
│── cmd/
│ └── main.go # Điểm khởi đầu ứng dụng
│── internal/
│ ├── application/ # Lớp Application (điều phối logic domain, xử lý use cases)
│ │ ├── services/ # Lớp Service, thư mục logic nghiệp vụ
│ │ │ └── order_service.go # Order application service, gọi logic nghiệp vụ domain layer
│ ├── domain/ # Lớp Domain (logic nghiệp vụ cốt lõi và định nghĩa interface)
│ │ ├── order/ # Order aggregate
│ │ │ ├── order.go # Order entity (aggregate root), chứa logic nghiệp vụ cốt lõi
│ │ ├── repository/ # Interfaces repository chung
│ │ │ ├── repository.go # Interface repository chung (các thao tác CRUD)
│ │ │ └── order_repository.go # Interface Order repository, định nghĩa các thao tác trên dữ liệu order
│ ├── infrastructure/ # Lớp Infrastructure (triển khai các interfaces được định nghĩa trong domain layer)
│ │ ├── repository/ # Triển khai Repository
│ │ │ └── order_repository_impl.go # Triển khai Order repository, lưu trữ dữ liệu order cụ thể
│ └── interfaces/ # Lớp Interface (xử lý requests bên ngoài, như HTTP interfaces)
│ │ ├── handlers/ # HTTP handlers
│ │ │ └── order_handler.go # HTTP handler cho orders
│ │ └── routes/
│ │ │ ├── router.go # Thiết lập tiện ích router cơ bản
│ │ │ └── order-routes.go # Cấu hình routes Order
│ │ │ └── order-routes-test.go # Test routes Order
│ │ │ └── order-routes-test.go # Test routes Order
│ └── middleware/ # Middleware (ví dụ: xác thực, chặn, ủy quyền, v.v.)
│ │ └── logging.go # Middleware logging
│ ├── config/ # Cấu hình liên quan đến service
│ │ └── server_config.go # Cấu hình server (ví dụ: port, timeout settings, v.v.)
│── pkg/ # Thư viện công cộng có thể tái sử dụng
│ └── utils/ # Các lớp tiện ích (ví dụ: logging, xử lý date, v.v.)
Triển Khai Code Go Language MVC
Luồng theo: Controller (Lớp Interface) → Service (Lớp Logic Nghiệp Vụ) → Repository (Lớp Truy Cập Dữ Liệu) → Model (Mô Hình Dữ Liệu)
Lớp Controller
// internal/controller/order/order.go
package order
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gin-order/internal/model"
"github.com/gin-order/internal/service/order"
"github.com/gin-order/internal/pkg/response"
)
type OrderController struct {
service *order.OrderService
}
func NewOrderController(service *order.OrderService) *OrderController {
return &OrderController{service: service}
}
func (c *OrderController) GetOrder(ctx *gin.Context) {
idStr := ctx.Param("id")
id, _ := strconv.ParseUint(idStr, 10, 64)
order, err := c.service.GetOrderByID(uint(id))
if err != nil {
response.Error(ctx, http.StatusNotFound, "Order not found")
return
}
response.Success(ctx, order)
}
func (c *OrderController) CreateOrder(ctx *gin.Context) {
var req model.CreateOrderRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
response.Error(ctx, http.StatusBadRequest, "Invalid request")
return
}
order, err := c.service.CreateOrder(&req)
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "Failed to create order")
return
}
response.Success(ctx, order)
}
Lớp Service
// internal/service/order/order_service.go
package order
import (
"github.com/gin-order/internal/model"
"github.com/gin-order/internal/repository/order"
)
type OrderService struct {
repo *order.OrderRepository
}
func NewOrderService(repo *order.OrderRepository) *OrderService {
return &OrderService{repo: repo}
}
func (s *OrderService) GetOrderByID(id uint) (*model.Order, error) {
return s.repo.GetByID(id)
}
func (s *OrderService) CreateOrder(req *model.CreateOrderRequest) (*model.Order, error) {
order := &model.Order{
UserID: req.UserID,
Amount: req.Amount,
Status: "pending",
}
return s.repo.Create(order)
}
Lớp Repository
// internal/repository/order/order_repository.go
package order
import (
"gorm.io/gorm"
"github.com/gin-order/internal/model"
)
type OrderRepository struct {
db *gorm.DB
}
func NewOrderRepository(db *gorm.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) GetByID(id uint) (*model.Order, error) {
var order model.Order
err := r.db.First(&order, id).Error
return &order, err
}
func (r *OrderRepository) Create(order *model.Order) (*model.Order, error) {
err := r.db.Create(order).Error
return order, err
}
Lớp Model
// internal/model/order.go
package model
import "time"
type Order struct {
ID uint `json:"id" gorm:"primarykey"`
UserID uint `json:"user_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateOrderRequest struct {
UserID uint `json:"user_id" binding:"required"`
Amount float64 `json:"amount" binding:"required,gt=0"`
}
Triển Khai Code Go Language DDD
Lớp Domain
// internal/domain/order/order.go
package order
import (
"errors"
"time"
)
// Order aggregate root
type Order struct {
id uint
userID uint
amount float64
status OrderStatus
items []OrderItem
createdAt time.Time
updatedAt time.Time
}
type OrderStatus string
const (
StatusPending OrderStatus = "pending"
StatusConfirmed OrderStatus = "confirmed"
StatusCancelled OrderStatus = "cancelled"
)
type OrderItem struct {
ProductID uint
Quantity int
Price float64
}
// Các phương thức logic nghiệp vụ domain
func NewOrder(userID uint, items []OrderItem) (*Order, error) {
if len(items) == 0 {
return nil, errors.New("order must have at least one item")
}
var total float64
for _, item := range items {
if item.Quantity <= 0 || item.Price <= 0 {
return nil, errors.New("invalid item quantity or price")
}
total += float64(item.Quantity) * item.Price
}
return &Order{
userID: userID,
amount: total,
status: StatusPending,
items: items,
createdAt: time.Now(),
updatedAt: time.Now(),
}, nil
}
func (o *Order) Confirm() error {
if o.status != StatusPending {
return errors.New("only pending orders can be confirmed")
}
o.status = StatusConfirmed
o.updatedAt = time.Now()
return nil
}
func (o *Order) Cancel() error {
if o.status == StatusCancelled {
return errors.New("order is already cancelled")
}
o.status = StatusCancelled
o.updatedAt = time.Now()
return nil
}
// Getters
func (o *Order) ID() uint { return o.id }
func (o *Order) UserID() uint { return o.userID }
func (o *Order) Amount() float64 { return o.amount }
func (o *Order) Status() OrderStatus { return o.status }
func (o *Order) Items() []OrderItem { return o.items }
Interface Repository (Lớp Domain)
// internal/domain/repository/order_repository.go
package repository
import "github.com/go-web/internal/domain/order"
type OrderRepository interface {
GetByID(id uint) (*order.Order, error)
Save(order *order.Order) error
Delete(id uint) error
}
Application Service
// internal/application/services/order_service.go
package services
import (
"github.com/go-web/internal/domain/order"
"github.com/go-web/internal/domain/repository"
)
type OrderService struct {
orderRepo repository.OrderRepository
}
func NewOrderService(orderRepo repository.OrderRepository) *OrderService {
return &OrderService{orderRepo: orderRepo}
}
type CreateOrderRequest struct {
UserID uint `json:"user_id"`
Items []order.OrderItem `json:"items"`
}
func (s *OrderService) CreateOrder(req *CreateOrderRequest) (*order.Order, error) {
// Xác thực logic Domain
order, err := order.NewOrder(req.UserID, req.Items)
if err != nil {
return nil, err
}
// Lưu thông qua repository
err = s.orderRepo.Save(order)
if err != nil {
return nil, err
}
return order, nil
}
func (s *OrderService) ConfirmOrder(orderID uint) error {
order, err := s.orderRepo.GetByID(orderID)
if err != nil {
return err
}
err = order.Confirm()
if err != nil {
return err
}
return s.orderRepo.Save(order)
}
Triển Khai Infrastructure
// internal/infrastructure/repository/order_repository_impl.go
package repository
import (
"gorm.io/gorm"
"github.com/go-web/internal/domain/order"
"github.com/go-web/internal/domain/repository"
)
type orderRepositoryImpl struct {
db *gorm.DB
}
func NewOrderRepository(db *gorm.DB) repository.OrderRepository {
return &orderRepositoryImpl{db: db}
}
// OrderPO - Persistent Object cho database mapping
type OrderPO struct {
ID uint `gorm:"primarykey"`
UserID uint `gorm:"column:user_id"`
Amount float64 `gorm:"column:amount"`
Status string `gorm:"column:status"`
Items []OrderItemPO `gorm:"foreignKey:OrderID"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
type OrderItemPO struct {
ID uint `gorm:"primarykey"`
OrderID uint `gorm:"column:order_id"`
ProductID uint `gorm:"column:product_id"`
Quantity int `gorm:"column:quantity"`
Price float64 `gorm:"column:price"`
}
func (r *orderRepositoryImpl) GetByID(id uint) (*order.Order, error) {
var orderPO OrderPO
err := r.db.Preload("Items").First(&orderPO, id).Error
if err != nil {
return nil, err
}
return r.toDomain(&orderPO), nil
}
func (r *orderRepositoryImpl) Save(order *order.Order) error {
orderPO := r.toPersistent(order)
return r.db.Save(orderPO).Error
}
func (r *orderRepositoryImpl) Delete(id uint) error {
return r.db.Delete(&OrderPO{}, id).Error
}
// Các phương thức chuyển đổi Domain <-> Persistent Object
func (r *orderRepositoryImpl) toDomain(orderPO *OrderPO) *order.Order {
// Chuyển đổi PO thành domain object
// Chi tiết triển khai...
}
func (r *orderRepositoryImpl) toPersistent(order *order.Order) *OrderPO {
// Chuyển đổi domain object thành PO
// Chi tiết triển khai...
}
Lớp Interface (HTTP Handlers)
// internal/interfaces/handlers/order_handler.go
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-web/internal/application/services"
)
type OrderHandler struct {
orderService *services.OrderService
}
func NewOrderHandler(orderService *services.OrderService) *OrderHandler {
return &OrderHandler{orderService: orderService}
}
func (h *OrderHandler) CreateOrder(c *gin.Context) {
var req services.CreateOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
order, err := h.orderService.CreateOrder(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, order)
}
func (h *OrderHandler) ConfirmOrder(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid order id"})
return
}
err = h.orderService.ConfirmOrder(uint(id))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "order confirmed"})
}
Best Practices DDD trong Go
Dependency Inversion
Lớp domain không nên phụ thuộc trực tiếp vào lớp infrastructure; thay vào đó, nó dựa vào interfaces để đảo ngược dependency.
Lưu ý: Cốt lõi của kiến trúc DDD là dependency inversion (DIP). Domain là lõi trong cùng, chỉ định nghĩa các quy tắc nghiệp vụ và trừu tượng interface. Các lớp khác phụ thuộc vào Domain để triển khai, nhưng Domain không phụ thuộc vào bất kỳ triển khai bên ngoài nào.
// Lớp Domain: định nghĩa interface
type UserRepository interface {
GetByID(id int) (*User, error)
}
// Lớp Infrastructure: triển khai database
type userRepositoryImpl struct {
db *sql.DB
}
func (r *userRepositoryImpl) GetByID(id int) (*User, error) {
// Logic truy vấn database
}
Quản Lý Aggregate
Aggregate root quản lý vòng đời của toàn bộ aggregate:
type Order struct {
ID int
Items []OrderItem
Status string
}
func (o *Order) AddItem(item OrderItem) {
o.Items = append(o.Items, item)
}
Application Service
Application services đóng gói logic domain, ngăn chặn các lớp bên ngoài trực tiếp thao tác domain objects:
type OrderService struct {
repo OrderRepository
}
func (s *OrderService) CreateOrder(userID int, items []OrderItem) (*Order, error) {
order := Order{UserID: userID, Items: items, Status: "Pending"}
return s.repo.Save(order)
}
Kiến Trúc Hướng Sự Kiện
Domain events được sử dụng để tách rời. Trong Go, bạn có thể triển khai điều này qua Channels hoặc Pub/Sub:
type OrderCreatedEvent struct {
OrderID int
}
func publishEvent(event OrderCreatedEvent) {
go func() {
eventChannel <- event
}()
}
Kết Hợp CQRS (Command Query Responsibility Segregation)
DDD có thể được kết hợp với CQRS. Trong Go, bạn có thể sử dụng Command cho các thao tác thay đổi và Query cho việc đọc dữ liệu:
type CreateOrderCommand struct {
UserID int
Items []OrderItem
}
func (h *OrderHandler) Handle(cmd CreateOrderCommand) (*Order, error) {
return h.service.CreateOrder(cmd.UserID, cmd.Items)
}
Tổng Kết: Kiến Trúc MVC vs. DDD
Sự Khác Biệt Cốt Lõi Trong Kiến Trúc
Kiến Trúc MVC
- Lớp: Ba lớp—Controller/Service/DAO
- Trách Nhiệm:
- Controller xử lý requests, Service chứa logic
- DAO trực tiếp thao tác database
- Điểm Yếu: Lớp Service trở nên cồng kềnh, và logic nghiệp vụ được kết hợp với các thao tác dữ liệu
Kiến Trúc DDD
- Lớp: Bốn lớp—Lớp Interface / Lớp Application / Lớp Domain / Lớp Infrastructure
- Trách Nhiệm:
- Lớp Application điều phối các quy trình (ví dụ: gọi domain services)
- Lớp Domain đóng gói các thao tác nghiệp vụ nguyên tử (ví dụ: quy tắc tạo đơn hàng)
- Lớp Infrastructure triển khai các chi tiết kỹ thuật (ví dụ: truy cập database)
- Lợi Ích: Lớp domain độc lập với các triển khai kỹ thuật, và logic tương ứng chặt chẽ với cấu trúc lớp
Tính Modular và Khả Năng Mở Rộng
MVC:
- Coupling Cao: Thiếu ranh giới nghiệp vụ rõ ràng; các lệnh gọi cross-module (ví dụ: order service trực tiếp dựa vào bảng account) làm code khó bảo trì.
- Khả Năng Mở Rộng Kém: Thêm tính năng mới yêu cầu thay đổi toàn cục (ví dụ: thêm quy tắc kiểm soát rủi ro phải xâm nhập vào order service), dễ gây ra các vấn đề dây chuyền.
DDD:
- Bounded Context: Modules được chia theo khả năng nghiệp vụ (ví dụ: payment domain, risk control domain); sự hợp tác hướng sự kiện (ví dụ: sự kiện hoàn thành thanh toán đơn hàng) được sử dụng để tách rời.
- Tiến Hóa Độc Lập: Mỗi domain module có thể được nâng cấp độc lập (ví dụ: tối ưu hóa logic thanh toán không ảnh hưởng đến order service), giảm rủi ro cấp hệ thống.
Các Tình Huống Áp Dụng
- Ưu tiên MVC cho các hệ thống nhỏ đến trung bình: Nghiệp vụ đơn giản (ví dụ: blogs, CMS, admin backends), yêu cầu phát triển nhanh với quy tắc nghiệp vụ rõ ràng và không thay đổi thường xuyên.
- Ưu tiên DDD cho nghiệp vụ phức tạp: Nhiều quy tắc (ví dụ: giao dịch tài chính, chuỗi cung ứng), hợp tác đa domain (ví dụ: liên kết đơn hàng và tồn kho e-commerce), thay đổi thường xuyên trong yêu cầu nghiệp vụ.
Kết Luận
Cả MVC và DDD đều có vị trí của chúng trong phát triển Go. MVC mang lại sự đơn giản và phát triển nhanh chóng cho các ứng dụng đơn giản, trong khi DDD cung cấp một framework mạnh mẽ cho các domain nghiệp vụ phức tạp yêu cầu khả năng bảo trì và mở rộng dài hạn. Việc lựa chọn giữa chúng nên dựa trên độ phức tạp của dự án, chuyên môn của team và mục tiêu dài hạn.
Tham khảo: Nội dung này dựa trên phân tích toàn diện từ bài viết MVC vs DDD của Leapcell