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 ModelView
+------------------+
|      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