1. MVVM và MVC Pattern là gì?

1.1 MVC (Model-View-Controller)

MVC là một mẫu thiết kế kiến trúc chia ứng dụng thành ba thành phần chính:

  • Model: Quản lý dữ liệu và logic nghiệp vụ
  • View: Hiển thị dữ liệu cho người dùng
  • Controller: Xử lý tương tác người dùng và cập nhật Model/View

  classDiagram
    class Model {
        +data
        +businessLogic()
        +updateData()
        +notifyObservers()
    }

    class View {
        +render()
        +bindEvents()
        +updateUI()
    }

    class Controller {
        +handleUserInput()
        +updateModel()
        +updateView()
    }

    Controller --> Model : Cập nhật
    Controller --> View : Cập nhật
    Model --> View : Thông báo thay đổi
    View --> Controller : Gửi tương tác người dùng

    note for Model "Chứa dữ liệu và logic nghiệp vụ"
    note for View "Hiển thị UI và nhận tương tác"
    note for Controller "Điều phối luồng dữ liệu"

1.2 MVVM (Model-View-ViewModel)

MVVM là một biến thể của MVC, phù hợp hơn với ứng dụng web hiện đại:

  • Model: Tương tự MVC, quản lý dữ liệu và logic nghiệp vụ
  • View: Giao diện người dùng
  • ViewModel: Trung gian giữa Model và View, xử lý logic hiển thị

  classDiagram
    class Model {
        +data
        +businessLogic()
        +updateData()
    }

    class ViewModel {
        +observableData
        +commands
        +transformData()
        +handleUserActions()
        +notifyPropertyChanged()
    }

    class View {
        +render()
        +bindToViewModel()
    }

    View --> ViewModel : Data Binding
    ViewModel --> Model : Cập nhật
    Model --> ViewModel : Đọc dữ liệu
    ViewModel --> View : Thông báo thay đổi

    note for Model "Chứa dữ liệu và logic nghiệp vụ"
    note for ViewModel "Chuyển đổi dữ liệu Model thành dạng phù hợp với View"
    note for View "Hiển thị UI và liên kết với ViewModel"

2. Triển khai trong JavaScript

2.1 Ví dụ về MVC với Todo App

// Model
class TodoModel {
  constructor() {
    this.todos = [];
    this.observers = [];
  }

  addTodo(title) {
    const todo = {
      id: Date.now(),
      title,
      completed: false,
    };
    this.todos.push(todo);
    this.notify();
  }

  toggleTodo(id) {
    const todo = this.todos.find((t) => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.notify();
    }
  }

  deleteTodo(id) {
    this.todos = this.todos.filter((t) => t.id !== id);
    this.notify();
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  notify() {
    this.observers.forEach((observer) => observer(this.todos));
  }
}

// View
class TodoView {
  constructor() {
    this.app = this.getElement("#root");
    this.title = this.createElement("h1");
    this.title.textContent = "Todos";

    this.form = this.createElement("form");
    this.input = this.createElement("input");
    this.input.type = "text";
    this.input.placeholder = "Add todo";
    this.submitButton = this.createElement("button");
    this.submitButton.textContent = "Add";

    this.todoList = this.createElement("ul");

    this.form.append(this.input, this.submitButton);
    this.app.append(this.title, this.form, this.todoList);
  }

  createElement(tag) {
    return document.createElement(tag);
  }

  getElement(selector) {
    return document.querySelector(selector);
  }

  displayTodos(todos) {
    while (this.todoList.firstChild) {
      this.todoList.removeChild(this.todoList.firstChild);
    }

    if (todos.length === 0) {
      const p = this.createElement("p");
      p.textContent = "Nothing to do! Add a task?";
      this.todoList.append(p);
    } else {
      todos.forEach((todo) => {
        const li = this.createElement("li");
        li.id = todo.id;

        const checkbox = this.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = todo.completed;

        const span = this.createElement("span");
        span.contentEditable = true;
        span.textContent = todo.title;

        const deleteButton = this.createElement("button");
        deleteButton.textContent = "Delete";
        deleteButton.className = "delete";

        li.append(checkbox, span, deleteButton);
        this.todoList.append(li);
      });
    }
  }

  bindAddTodo(handler) {
    this.form.addEventListener("submit", (event) => {
      event.preventDefault();
      const title = this.input.value.trim();
      if (title) {
        handler(title);
        this.input.value = "";
      }
    });
  }

  bindDeleteTodo(handler) {
    this.todoList.addEventListener("click", (event) => {
      if (event.target.className === "delete") {
        const id = parseInt(event.target.parentElement.id);
        handler(id);
      }
    });
  }

  bindToggleTodo(handler) {
    this.todoList.addEventListener("change", (event) => {
      if (event.target.type === "checkbox") {
        const id = parseInt(event.target.parentElement.id);
        handler(id);
      }
    });
  }
}

// Controller
class TodoController {
  constructor(model, view) {
    this.model = model;
    this.view = view;

    this.model.subscribe((todos) => this.view.displayTodos(todos));
    this.view.bindAddTodo(this.handleAddTodo.bind(this));
    this.view.bindDeleteTodo(this.handleDeleteTodo.bind(this));
    this.view.bindToggleTodo(this.handleToggleTodo.bind(this));

    this.model.notify();
  }

  handleAddTodo(title) {
    this.model.addTodo(title);
  }

  handleDeleteTodo(id) {
    this.model.deleteTodo(id);
  }

  handleToggleTodo(id) {
    this.model.toggleTodo(id);
  }
}

// Usage
const app = new TodoController(new TodoModel(), new TodoView());

2.2 Ví dụ về MVVM với Counter App

// Model
class CounterModel {
  constructor() {
    this.value = 0;
    this.step = 1;
  }

  increment() {
    this.value += this.step;
    return this.value;
  }

  decrement() {
    this.value -= this.step;
    return this.value;
  }

  setStep(step) {
    this.step = Number(step);
    return this.step;
  }

  getValue() {
    return this.value;
  }

  getStep() {
    return this.step;
  }
}

// ViewModel
class CounterViewModel {
  constructor(model) {
    this.model = model;
    this.subscribers = new Map();
  }

  subscribe(event, callback) {
    if (!this.subscribers.has(event)) {
      this.subscribers.set(event, []);
    }
    this.subscribers.get(event).push(callback);
  }

  notify(event, data) {
    if (this.subscribers.has(event)) {
      this.subscribers.get(event).forEach((callback) => callback(data));
    }
  }

  increment() {
    const value = this.model.increment();
    this.notify("valueChanged", value);
  }

  decrement() {
    const value = this.model.decrement();
    this.notify("valueChanged", value);
  }

  setStep(step) {
    const newStep = this.model.setStep(step);
    this.notify("stepChanged", newStep);
  }

  getFormattedValue() {
    return `Counter: ${this.model.getValue()}`;
  }

  getFormattedStep() {
    return `Step: ${this.model.getStep()}`;
  }
}

// View
class CounterView {
  constructor(viewModel) {
    this.viewModel = viewModel;
    this.initializeView();
    this.bindEvents();
  }

  initializeView() {
    this.container = document.createElement("div");
    this.container.className = "counter-container";

    this.valueDisplay = document.createElement("h2");
    this.valueDisplay.textContent = this.viewModel.getFormattedValue();

    this.stepDisplay = document.createElement("p");
    this.stepDisplay.textContent = this.viewModel.getFormattedStep();

    this.incrementButton = document.createElement("button");
    this.incrementButton.textContent = "+";

    this.decrementButton = document.createElement("button");
    this.decrementButton.textContent = "-";

    this.stepInput = document.createElement("input");
    this.stepInput.type = "number";
    this.stepInput.value = "1";
    this.stepInput.min = "1";

    this.container.append(
      this.valueDisplay,
      this.stepDisplay,
      this.decrementButton,
      this.incrementButton,
      this.stepInput
    );

    document.body.appendChild(this.container);
  }

  bindEvents() {
    this.incrementButton.addEventListener("click", () => {
      this.viewModel.increment();
    });

    this.decrementButton.addEventListener("click", () => {
      this.viewModel.decrement();
    });

    this.stepInput.addEventListener("change", (e) => {
      this.viewModel.setStep(e.target.value);
    });

    this.viewModel.subscribe("valueChanged", () => {
      this.valueDisplay.textContent = this.viewModel.getFormattedValue();
    });

    this.viewModel.subscribe("stepChanged", () => {
      this.stepDisplay.textContent = this.viewModel.getFormattedStep();
    });
  }
}

// Usage
const counterModel = new CounterModel();
const counterViewModel = new CounterViewModel(counterModel);
const counterView = new CounterView(counterViewModel);

3. Triển khai trong TypeScript

3.1 Ví dụ về MVC với User Management

// Types
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

// Model
class UserModel {
  private users: User[] = [];
  private observers: ((users: User[]) => void)[] = [];

  async fetchUsers(): Promise<void> {
    // Giả lập API call
    this.users = [
      { id: 1, name: "John Doe", email: "[email protected]", role: "admin" },
      { id: 2, name: "Jane Smith", email: "[email protected]", role: "user" },
    ];
    this.notify();
  }

  addUser(user: Omit<User, "id">): void {
    const newUser: User = {
      ...user,
      id: Date.now(),
    };
    this.users.push(newUser);
    this.notify();
  }

  updateUser(id: number, updates: Partial<User>): void {
    const user = this.users.find((u) => u.id === id);
    if (user) {
      Object.assign(user, updates);
      this.notify();
    }
  }

  deleteUser(id: number): void {
    this.users = this.users.filter((u) => u.id !== id);
    this.notify();
  }

  getUsers(): User[] {
    return [...this.users];
  }

  subscribe(observer: (users: User[]) => void): void {
    this.observers.push(observer);
  }

  private notify(): void {
    this.observers.forEach((observer) => observer(this.getUsers()));
  }
}

// View
class UserView {
  private container: HTMLElement;
  private userList: HTMLElement;
  private form: HTMLFormElement;

  constructor() {
    this.container = document.createElement("div");
    this.setupUserList();
    this.setupForm();
    document.body.appendChild(this.container);
  }

  private setupUserList(): void {
    this.userList = document.createElement("div");
    this.userList.className = "user-list";
    this.container.appendChild(this.userList);
  }

  private setupForm(): void {
    this.form = document.createElement("form");
    this.form.innerHTML = `
      <input type="text" name="name" placeholder="Name" required>
      <input type="email" name="email" placeholder="Email" required>
      <select name="role" required>
        <option value="user">User</option>
        <option value="admin">Admin</option>
      </select>
      <button type="submit">Add User</button>
    `;
    this.container.appendChild(this.form);
  }

  displayUsers(users: User[]): void {
    this.userList.innerHTML = "";
    users.forEach((user) => {
      const userElement = document.createElement("div");
      userElement.className = "user-item";
      userElement.innerHTML = `
        <span>${user.name} (${user.email}) - ${user.role}</span>
        <button class="edit" data-id="${user.id}">Edit</button>
        <button class="delete" data-id="${user.id}">Delete</button>
      `;
      this.userList.appendChild(userElement);
    });
  }

  bindAddUser(handler: (user: Omit<User, "id">) => void): void {
    this.form.addEventListener("submit", (e) => {
      e.preventDefault();
      const formData = new FormData(this.form);
      handler({
        name: formData.get("name") as string,
        email: formData.get("email") as string,
        role: formData.get("role") as "admin" | "user",
      });
      this.form.reset();
    });
  }

  bindDeleteUser(handler: (id: number) => void): void {
    this.userList.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.className === "delete") {
        const id = Number(target.dataset.id);
        handler(id);
      }
    });
  }

  bindUpdateUser(handler: (id: number, updates: Partial<User>) => void): void {
    this.userList.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.className === "edit") {
        const id = Number(target.dataset.id);
        const name = prompt("Enter new name:");
        if (name) {
          handler(id, { name });
        }
      }
    });
  }
}

// Controller
class UserController {
  constructor(
    private model: UserModel,
    private view: UserView
  ) {
    this.model.subscribe((users) => this.view.displayUsers(users));
    this.view.bindAddUser(this.handleAddUser.bind(this));
    this.view.bindDeleteUser(this.handleDeleteUser.bind(this));
    this.view.bindUpdateUser(this.handleUpdateUser.bind(this));
    this.initialize();
  }

  private async initialize(): Promise<void> {
    await this.model.fetchUsers();
  }

  private handleAddUser(user: Omit<User, "id">): void {
    this.model.addUser(user);
  }

  private handleDeleteUser(id: number): void {
    this.model.deleteUser(id);
  }

  private handleUpdateUser(id: number, updates: Partial<User>): void {
    this.model.updateUser(id, updates);
  }
}

// Usage
const userApp = new UserController(new UserModel(), new UserView());

3.2 Ví dụ về MVVM với Shopping Cart

// Types
interface Product {
  id: number;
  name: string;
  price: number;
}

interface CartItem extends Product {
  quantity: number;
}

// Model
class ShoppingCartModel {
  private items: CartItem[] = [];

  addItem(product: Product, quantity: number = 1): void {
    const existingItem = this.items.find((item) => item.id === product.id);
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push({ ...product, quantity });
    }
  }

  removeItem(productId: number): void {
    this.items = this.items.filter((item) => item.id !== productId);
  }

  updateQuantity(productId: number, quantity: number): void {
    const item = this.items.find((item) => item.id === productId);
    if (item) {
      item.quantity = quantity;
    }
  }

  getItems(): CartItem[] {
    return [...this.items];
  }

  getTotalItems(): number {
    return this.items.reduce((total, item) => total + item.quantity, 0);
  }

  getTotalPrice(): number {
    return this.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    );
  }
}

// ViewModel
class ShoppingCartViewModel {
  private subscribers = new Map<string, Function[]>();

  constructor(private model: ShoppingCartModel) {}

  subscribe(event: string, callback: Function): void {
    if (!this.subscribers.has(event)) {
      this.subscribers.set(event, []);
    }
    this.subscribers.get(event)?.push(callback);
  }

  private notify(event: string, data?: any): void {
    this.subscribers.get(event)?.forEach((callback) => callback(data));
  }

  addToCart(product: Product, quantity: number = 1): void {
    this.model.addItem(product, quantity);
    this.notifyCartUpdated();
  }

  removeFromCart(productId: number): void {
    this.model.removeItem(productId);
    this.notifyCartUpdated();
  }

  updateQuantity(productId: number, quantity: number): void {
    this.model.updateQuantity(productId, quantity);
    this.notifyCartUpdated();
  }

  private notifyCartUpdated(): void {
    this.notify("cartUpdated", {
      items: this.getFormattedItems(),
      total: this.getFormattedTotal(),
      itemCount: this.getFormattedItemCount(),
    });
  }

  getFormattedItems(): string[] {
    return this.model
      .getItems()
      .map(
        (item) =>
          `${item.name} x ${item.quantity} = $${(item.price * item.quantity).toFixed(2)}`
      );
  }

  getFormattedTotal(): string {
    return `Total: $${this.model.getTotalPrice().toFixed(2)}`;
  }

  getFormattedItemCount(): string {
    return `Items in cart: ${this.model.getTotalItems()}`;
  }
}

// View
class ShoppingCartView {
  private container: HTMLElement;
  private itemList: HTMLElement;
  private totalDisplay: HTMLElement;
  private itemCountDisplay: HTMLElement;

  constructor(private viewModel: ShoppingCartViewModel) {
    this.setupUI();
    this.bindEvents();
  }

  private setupUI(): void {
    this.container = document.createElement("div");
    this.container.className = "shopping-cart";

    this.itemList = document.createElement("ul");
    this.totalDisplay = document.createElement("p");
    this.itemCountDisplay = document.createElement("p");

    const addItemForm = document.createElement("form");
    addItemForm.innerHTML = `
      <input type="text" id="productName" placeholder="Product name" required>
      <input type="number" id="productPrice" placeholder="Price" required>
      <input type="number" id="quantity" value="1" min="1" required>
      <button type="submit">Add to Cart</button>
    `;

    this.container.append(
      this.itemCountDisplay,
      this.itemList,
      this.totalDisplay,
      addItemForm
    );

    document.body.appendChild(this.container);
  }

  private bindEvents(): void {
    this.viewModel.subscribe(
      "cartUpdated",
      (data: { items: string[]; total: string; itemCount: string }) => {
        this.updateDisplay(data);
      }
    );

    const form = this.container.querySelector("form");
    form?.addEventListener("submit", (e) => {
      e.preventDefault();
      const nameInput = document.getElementById(
        "productName"
      ) as HTMLInputElement;
      const priceInput = document.getElementById(
        "productPrice"
      ) as HTMLInputElement;
      const quantityInput = document.getElementById(
        "quantity"
      ) as HTMLInputElement;

      this.viewModel.addToCart(
        {
          id: Date.now(),
          name: nameInput.value,
          price: Number(priceInput.value),
        },
        Number(quantityInput.value)
      );

      form.reset();
    });

    this.itemList.addEventListener("click", (e) => {
      const target = e.target as HTMLElement;
      if (target.className === "remove-item") {
        const productId = Number(target.dataset.id);
        this.viewModel.removeFromCart(productId);
      }
    });
  }

  private updateDisplay(data: {
    items: string[];
    total: string;
    itemCount: string;
  }): void {
    this.itemList.innerHTML = data.items
      .map((item) => `<li>${item}</li>`)
      .join("");
    this.totalDisplay.textContent = data.total;
    this.itemCountDisplay.textContent = data.itemCount;
  }
}

// Usage
const cartModel = new ShoppingCartModel();
const cartViewModel = new ShoppingCartViewModel(cartModel);
const cartView = new ShoppingCartView(cartViewModel);

// Add some initial products
cartViewModel.addToCart({ id: 1, name: "Laptop", price: 999.99 });
cartViewModel.addToCart({ id: 2, name: "Mouse", price: 29.99 }, 2);

4. Ưu điểm và Nhược điểm

4.1 MVC Pattern


  graph TB
    subgraph "Ưu điểm"
    A1["Tách biệt trách nhiệm"]
    A2["Dễ bảo trì"]
    A3["Tái sử dụng Model"]
    A4["Dễ test từng thành phần"]
    end

    subgraph "Nhược điểm"
    B1["Phức tạp hóa ứng dụng đơn giản"]
    B2["Controller phụ thuộc vào View"]
    B3["Khó scale với ứng dụng lớn"]
    B4["Nhiều code boilerplate"]
    end

    style A1 fill:#d4f1d4,stroke:#5ca05c
    style A2 fill:#d4f1d4,stroke:#5ca05c
    style A3 fill:#d4f1d4,stroke:#5ca05c
    style A4 fill:#d4f1d4,stroke:#5ca05c
    style B1 fill:#f1d4d4,stroke:#a05c5c
    style B2 fill:#f1d4d4,stroke:#a05c5c
    style B3 fill:#f1d4d4,stroke:#a05c5c
    style B4 fill:#f1d4d4,stroke:#a05c5c

Ưu điểm

  1. Tách biệt trách nhiệm: Phân chia rõ ràng giữa dữ liệu, giao diện và xử lý
  2. Dễ bảo trì: Có thể thay đổi từng phần mà không ảnh hưởng các phần khác
  3. Tái sử dụng: Có thể tái sử dụng Model cho nhiều View khác nhau
  4. Dễ test: Dễ dàng test từng thành phần riêng biệt
  5. Phù hợp với server-side: Hoạt động tốt với ứng dụng web truyền thống

Nhược điểm

  1. Phức tạp: Có thể phức tạp hóa các ứng dụng đơn giản
  2. Tight coupling: Controller thường phụ thuộc chặt chẽ vào View
  3. Khó scale: Có thể khó scale khi ứng dụng phức tạp
  4. Nhiều code: Yêu cầu nhiều code boilerplate
  5. Cập nhật UI: Cần nhiều code để đồng bộ hóa UI với dữ liệu

4.2 MVVM Pattern


  graph TB
    subgraph "Ưu điểm"
    C1["Data binding tự động"]
    C2["Tách biệt UI logic"]
    C3["Dễ test ViewModel"]
    C4["Tái sử dụng ViewModel"]
    C5["Phù hợp với SPA"]
    end

    subgraph "Nhược điểm"
    D1["Đường cong học tập cao"]
    D2["Tiêu thụ nhiều bộ nhớ"]
    D3["Khó debug binding"]
    D4["Quá phức tạp cho ứng dụng nhỏ"]
    end

    style C1 fill:#d4f1d4,stroke:#5ca05c
    style C2 fill:#d4f1d4,stroke:#5ca05c
    style C3 fill:#d4f1d4,stroke:#5ca05c
    style C4 fill:#d4f1d4,stroke:#5ca05c
    style C5 fill:#d4f1d4,stroke:#5ca05c
    style D1 fill:#f1d4d4,stroke:#a05c5c
    style D2 fill:#f1d4d4,stroke:#a05c5c
    style D3 fill:#f1d4d4,stroke:#a05c5c
    style D4 fill:#f1d4d4,stroke:#a05c5c

Ưu điểm

  1. Data binding: Tự động đồng bộ dữ liệu giữa Model và View
  2. Tách biệt UI: View chỉ quan tâm đến hiển thị
  3. Dễ test: ViewModel dễ test vì không phụ thuộc vào View
  4. Reusable: ViewModel có thể được tái sử dụng
  5. Phù hợp với SPA: Hoạt động tốt với Single Page Applications
  6. Responsive: Dễ dàng xây dựng UI phản hồi nhanh

Nhược điểm

  1. Học tập: Đòi hỏi thời gian học và hiểu pattern
  2. Memory overhead: Data binding có thể tốn nhiều bộ nhớ
  3. Debugging: Khó debug khi có nhiều binding
  4. Overkill: Có thể quá phức tạp cho ứng dụng nhỏ
  5. Performance: Có thể gây vấn đề hiệu suất với binding phức tạp

5. Khi nào nên sử dụng?


  graph LR
    subgraph "Frameworks & Libraries"
    F1[Angular] --> MVVM
    F2[React] --> MVVM
    F3[Vue.js] --> MVVM
    F4[Backbone.js] --> MVC
    F5[Express.js] --> MVC
    F6[Ruby on Rails] --> MVC
    F7[Django] --> MVC
    F8[Laravel] --> MVC
    end

    subgraph "Use Cases"
    MVC --> UC1["Ứng dụng web truyền thống"]
    MVC --> UC2["CRUD đơn giản"]
    MVC --> UC3["Server-side rendering"]
    MVC --> UC4["Nhiều view cho cùng dữ liệu"]

    MVVM --> UC5["Single Page Applications"]
    MVVM --> UC6["Tương tác người dùng phức tạp"]
    MVVM --> UC7["Real-time updates"]
    MVVM --> UC8["Desktop-like applications"]
    end

    style MVC fill:#f9d5e5,stroke:#d64161
    style MVVM fill:#d5e8f9,stroke:#4186d6
    style F1 fill:#eeeeee,stroke:#999999
    style F2 fill:#eeeeee,stroke:#999999
    style F3 fill:#eeeeee,stroke:#999999
    style F4 fill:#eeeeee,stroke:#999999
    style F5 fill:#eeeeee,stroke:#999999
    style F6 fill:#eeeeee,stroke:#999999
    style F7 fill:#eeeeee,stroke:#999999
    style F8 fill:#eeeeee,stroke:#999999

5.1 MVC Pattern

  1. Ứng dụng web truyền thống: Với server-side rendering
  2. CRUD đơn giản: Các ứng dụng CRUD cơ bản
  3. Tương tác đơn giản: Khi tương tác người dùng không phức tạp
  4. Nhiều view: Khi cần nhiều cách hiển thị khác nhau cho cùng dữ liệu
  5. Server-side frameworks: Khi sử dụng Ruby on Rails, Django, Laravel, Express.js

5.2 MVVM Pattern

  1. SPA: Các ứng dụng Single Page Application
  2. Tương tác phức tạp: Khi có nhiều tương tác người dùng
  3. Real-time: Ứng dụng cần cập nhật real-time
  4. Desktop-like: Ứng dụng web giống desktop
  5. Modern frameworks: Khi sử dụng Angular, React, Vue.js
  6. Reactive programming: Khi cần reactive data flow

6. So sánh MVC và MVVM trong thực tế


  classDiagram
    class MVC {
        +Đơn giản hơn
        +Phù hợp với server-side
        +Dễ học
        +Ít abstraction
    }

    class MVVM {
        +Data binding mạnh mẽ
        +Phù hợp với client-side
        +Tách biệt UI tốt hơn
        +Dễ test hơn
    }

    MVC <|-- MVVM : Phát triển từ

    note for MVC "Phổ biến trong các framework server-side"
    note for MVVM "Phổ biến trong các framework JavaScript hiện đại"

6.1 Mối liên hệ với các Framework hiện tại

Các mẫu thiết kế kiến trúc MVC và MVVM đã được áp dụng rộng rãi trong các framework phát triển web hiện đại. Dưới đây là phân tích chi tiết về cách các framework phổ biến áp dụng các mẫu này:


  graph TD
    classDef mvc fill:#f9d5e5,stroke:#333,stroke-width:1px
    classDef mvvm fill:#eeeeee,stroke:#333,stroke-width:1px
    classDef hybrid fill:#d0f0c0,stroke:#333,stroke-width:1px
    classDef noteStyle fill:#ffffcc,stroke:#333,stroke-width:1px

    MVC[MVC Pattern]:::mvc
    MVVM[MVVM Pattern]:::mvvm

    Angular[Angular]:::mvvm
    React[React]:::hybrid
    Vue[Vue.js]:::mvvm
    Svelte[Svelte]:::mvvm

    Rails[Ruby on Rails]:::mvc
    Django[Django]:::mvc
    Laravel[Laravel]:::mvc
    Express[Express.js]:::mvc

    Ember[Ember.js]:::mvc
    Backbone[Backbone.js]:::mvc

    MVC --> Rails
    MVC --> Django
    MVC --> Laravel
    MVC --> Express
    MVC --> Ember
    MVC --> Backbone

    MVVM --> Angular
    MVVM --> Vue
    MVVM --> Svelte

    MVC & MVVM --> React

    subgraph Frontend Frameworks
        Angular
        React
        Vue
        Svelte
        Ember
        Backbone
    end

    subgraph Backend Frameworks
        Rails
        Django
        Laravel
        Express
    end

    note["Chú thích:<br> Màu hồng: MVC thuần túy<br> Màu xám: MVVM thuần túy<br> Màu xanh lá: Hybrid hoặc linh hoạt"]:::noteStyle

    note -.-> MVC
    note -.-> MVVM

6.1.1 Frontend Frameworks

  1. Angular

    • Áp dụng MVVM một cách chặt chẽ
    • Sử dụng two-way data binding
    • Components là ViewModel, Templates là View, Services là Model
    • Dependency Injection mạnh mẽ để kết nối các thành phần
  2. React

    • Kết hợp cả hai mẫu thiết kế
    • Components có thể được xem như Controller-View trong MVC
    • Với Redux/Context API, có thể triển khai theo mô hình gần với MVVM
    • Flux architecture (một biến thể của MVC) thường được sử dụng với React
  3. Vue.js

    • Thiên về MVVM
    • Two-way binding với v-model
    • Reactive data system
    • Single-file components kết hợp View và ViewModel
  4. Svelte

    • Áp dụng MVVM với compiler-first approach
    • Reactive statements và bindings
    • Không cần Virtual DOM như React
  5. Ember.js

    • Theo mô hình MVC truyền thống
    • Có Router, Controller, Template, và Model rõ ràng
    • Convention over configuration
  6. Backbone.js

    • MVC thuần túy
    • Models, Views, và Collections
    • Ít magic, nhiều boilerplate code

6.1.2 Backend Frameworks

  1. Ruby on Rails

    • MVC thuần túy
    • Convention over configuration
    • Active Record pattern cho Models
  2. Django (Python)

    • MVC (gọi là MTV: Model-Template-View)
    • ORM mạnh mẽ
    • Admin interface tự động
  3. Laravel (PHP)

    • MVC với Eloquent ORM
    • Blade templating
    • Service providers
  4. Express.js (Node.js)

    • Lightweight MVC
    • Router như Controller
    • Flexible middleware system

6.2 Xu hướng hiện tại

Trong những năm gần đây, MVVM đã trở nên phổ biến hơn trong phát triển web front-end, đặc biệt với sự phát triển của các framework JavaScript hiện đại. Tuy nhiên, MVC vẫn rất phổ biến trong phát triển back-end.

Một số xu hướng đáng chú ý:

  1. Component-based architecture: Cả React, Vue và Angular đều hướng tới kiến trúc dựa trên component, làm mờ ranh giới giữa MVC và MVVM truyền thống.

  2. State management libraries: Redux, MobX, Vuex đã thêm một lớp quản lý state vào các framework, tạo ra các biến thể mới của các mẫu thiết kế cổ điển.

  3. Server-side rendering (SSR): Kỹ thuật này đòi hỏi các mô hình lai giữa client và server, kết hợp cả MVC và MVVM.

  4. JAMstack: Kiến trúc này tách biệt frontend và backend hoàn toàn, với frontend thường sử dụng MVVM và backend cung cấp API.

6.3 Hybrid Approaches

Nhiều ứng dụng hiện đại kết hợp cả hai pattern:


  graph LR
    classDef server fill:#f9d5e5,stroke:#333,stroke-width:1px
    classDef client fill:#eeeeee,stroke:#333,stroke-width:1px

    Server[Backend Server]:::server
    Client[Frontend Client]:::client

    Server -->|API| Client

    subgraph "Server-side (MVC)"
        Controller[Controllers]
        Model[Models]
        API[API Endpoints]

        Controller --> Model
        Controller --> API
    end

    subgraph "Client-side (MVVM)"
        ViewModel[ViewModels]
        View[Views]
        Service[Services]

        ViewModel --> View
        ViewModel --> Service
        Service -->|HTTP Requests| API
    end

    note["Kiến trúc Hybrid phổ biến:<br> MVC ở phía server xử lý business logic<br> MVVM ở phía client xử lý UI và user interaction<br> Giao tiếp qua RESTful hoặc GraphQL API"]

Kiến trúc hybrid này tận dụng được ưu điểm của cả hai pattern:

  • MVC ở server: Xử lý business logic, database access, authentication
  • MVVM ở client: Tạo UI phản hồi nhanh, trải nghiệm người dùng mượt mà

7. Kết luận

MVVM và MVC là hai mẫu thiết kế kiến trúc quan trọng trong phát triển ứng dụng web. MVC phù hợp với các ứng dụng web truyền thống và CRUD đơn giản, trong khi MVVM thích hợp cho các ứng dụng web hiện đại với tương tác phức tạp.

Việc lựa chọn pattern nào phụ thuộc vào yêu cầu cụ thể của dự án, công nghệ sử dụng, và sở thích của team. Trong nhiều trường hợp, việc kết hợp cả hai pattern có thể mang lại lợi ích tối ưu cho ứng dụng của bạn.


  graph TD
    classDef decision fill:#f5f5f5,stroke:#333,stroke-width:1px

    Start[Chọn Pattern] -->|Phân tích yêu cầu| Decision{"Loại ứng dụng?"}:::decision

    Decision --> MVC[Chọn MVC]
    Decision --> MVVM[Chọn MVVM]
    Decision --> Hybrid[Hybrid Approach]

    MVC --> Frameworks1["Frameworks:<br/>Rails, Django, Laravel, Express"]
    MVVM --> Frameworks2["Frameworks:<br/>Angular, Vue, Svelte"]
    Hybrid --> Frameworks3["Frameworks:<br/>React + Redux, Next.js, Nuxt.js"]

    note["Lưu ý: Chọn pattern phù hợp với:<br/>1. Yêu cầu dự án<br/>2. Kinh nghiệm team<br/>3. Công nghệ đã chọn"]

Nhớ rằng, pattern chỉ là công cụ - không có pattern nào hoàn hảo cho mọi tình huống. Hiểu rõ ưu và nhược điểm của từng pattern sẽ giúp bạn đưa ra quyết định đúng đắn cho dự án của mình.

Khi làm việc với các framework hiện đại, việc hiểu rõ mẫu thiết kế cơ bản đằng sau chúng sẽ giúp bạn tận dụng tối đa sức mạnh của framework và tránh được những cạm bẫy phổ biến trong quá trình phát triển.