← Back to Course

🏗️ Architectural Patterns: MVC, MVP, and MVVM

As programs grow, organizing code into clear sections becomes essential. Architectural patterns like MVC, MVP, and MVVM provide proven blueprints for separating concerns and making code easier to understand, test, and maintain.

Why Architectural Patterns Matter

Without clear structure, code becomes tangled: logic mixed with user interface, hard to test, and difficult for teams to work on. These patterns separate code into layers:

  • Model: The data and business logic (core rules)
  • View: What users see on the screen (the interface)
  • Controller/Presenter/ViewModel: The bridge between model and view

🔄 MVC (Model-View-Controller)

MVC is one of the oldest and most popular patterns. The Controller handles user actions, updates the Model, and tells the View to refresh.

How MVC Works:

graph TD User["👤 User Interaction
(Click, Type, Submit)"] Controller["🎮 Controller
(Handles events)"] Model["📦 Model
(Data & Logic)"] View["📺 View
(Renders display)"] User -->|User acts| Controller Controller -->|Updates| Model Model -->|Notifies| View View -->|Shows| User User -->|Interacts with| View

Components:

  • Model: Stores data and business rules. Example: a shopping cart that knows prices and items.
  • View: Displays data to the user. Example: the HTML and CSS that shows the cart on screen.
  • Controller: Responds to user actions. Example: when a user clicks "Add to Cart", the controller tells the model to add the item.

Real Example: To-Do List

Model (Todo Logic)

class TodoModel {
  todos: { id: number; text: string; done: boolean }[] = [];
  
  addTodo(text: string): void {
    this.todos.push({ id: Date.now(), text, done: false });
  }
  
  completeTodo(id: number): void {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.done = true;
  }
}

Controller (Handles User Input)

class TodoController {
  constructor(private model: TodoModel, private view: TodoView) {}
  
  onAddTodoClicked(text: string): void {
    this.model.addTodo(text);
    this.view.render(this.model.todos);
  }
  
  onCompleteTodoClicked(id: number): void {
    this.model.completeTodo(id);
    this.view.render(this.model.todos);
  }
}

View (Displays Data)

class TodoView {
  render(todos: any[]): void {
    let html = "<ul>";
    todos.forEach(todo => {
      const checkmark = todo.done ? "✓" : "";
      html += `<li>${checkmark} ${todo.text}</li>`;
    });
    html += "</ul>";
    document.getElementById("app").innerHTML = html;
  }
}

Pros and Cons

Pros Cons
Simple to understand View and Model can become closely tied
Works well for small to medium apps Controller can grow large
Used in many web frameworks Harder to test when view updates the model

🎭 MVP (Model-View-Presenter)

MVP is similar to MVC, but with a key difference: the Presenter handles ALL interaction logic, and the View is completely passive. The View never directly updates the Model.

How MVP Works:

graph TD User["👤 User Interaction
(Click, Type, Submit)"] View["📺 View
(Display only)"] Presenter["🎭 Presenter
(Handles all logic)"] Model["📦 Model
(Data & Rules)"] User -->|User acts| View View -->|Tells Presenter| Presenter Presenter -->|Updates| Model Model -->|Provides data| Presenter Presenter -->|Tells View
what to show| View View -->|Shows to| User

Key Difference from MVC:

View is passive: The View never directly touches the Model. All communication goes through the Presenter.

Real Example: To-Do List with MVP

Model (Same as MVC)

class TodoModel {
  todos: { id: number; text: string; done: boolean }[] = [];
  
  addTodo(text: string): void {
    this.todos.push({ id: Date.now(), text, done: false });
  }
}

View (Completely Passive)

class TodoView {
  onAddButtonClicked: (text: string) => void = () => {};
  onCompleteButtonClicked: (id: number) => void = () => {};
  
  addTodoButton = document.getElementById("add-btn");
  
  render(todos: any[]): void {
    // View just displays, never updates model directly
    let html = "<ul>";
    todos.forEach(todo => {
      html += `<li>${todo.text}</li>`;
    });
    document.getElementById("app").innerHTML = html;
  }
}

Presenter (All Logic)

class TodoPresenter {
  constructor(private model: TodoModel, private view: TodoView) {
    // Presenter wires everything together
    this.view.onAddButtonClicked = (text: string) => this.handleAdd(text);
    this.view.onCompleteButtonClicked = (id: number) => this.handleComplete(id);
  }
  
  private handleAdd(text: string): void {
    this.model.addTodo(text);
    this.updateView();
  }
  
  private updateView(): void {
    this.view.render(this.model.todos);
  }
}

Pros and Cons

Pros Cons
View is completely testable without the Model More boilerplate code
Clear separation of concerns Presenter can become large
Easy to reuse the same View with different Presenters More complex to set up initially

⚙️ MVVM (Model-View-ViewModel)

MVVM is modern and used in frameworks like Vue.js, Angular, and WPF. The ViewModel is a transformed version of the Model that's ready for the View. Instead of the code pulling data, the ViewModel notifies the View when data changes (often through observables or reactive systems).

How MVVM Works:

graph TD User["👤 User Interaction"] View["📺 View
(Binds to ViewModel)"] ViewModel["⚙️ ViewModel
(Transforms Model)"] Model["📦 Model
(Data & Logic)"] User -->|User acts| View View -->|Two-way
binding| ViewModel ViewModel -->|Subscribes to
changes| Model Model -->|Notifies of
updates| ViewModel ViewModel -->|Pushes updates| View

Key Feature: Two-Way Binding

In MVVM, the View and ViewModel are connected through binding. When the user changes something in the View, the ViewModel updates automatically. When the Model changes, the View updates automatically without explicit code.

Real Example: To-Do List with MVVM

Model (Core Data)

class TodoModel {
  todos: { id: number; text: string; done: boolean }[] = [];
  
  addTodo(text: string): void {
    this.todos.push({ id: Date.now(), text, done: false });
  }
}

ViewModel (Prepared for View)

class TodoViewModel {
  todos$ = new Observable([]); // Observable = reactive stream
  
  constructor(private model: TodoModel) {
    // ViewModel subscribes to model changes and notifies view
    this.updateTodos();
  }
  
  addTodo(text: string): void {
    this.model.addTodo(text);
    this.updateTodos(); // Notify view of changes
  }
  
  private updateTodos(): void {
    this.todos$.next(this.model.todos); // Push to all subscribers
  }
}

View (Binds to ViewModel)

// In Vue.js, this would look like:
<template>
  <div>
    <input v-model="newTodoText" placeholder="Add a to-do" />
    <button @click="viewModel.addTodo(newTodoText)">Add</button>
    
    <ul>
      <li v-for="todo in viewModel.todos$">
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

Pros and Cons

Pros Cons
Automatic two-way synchronization Requires a reactive framework or library
Less boilerplate than MVP Can be harder to debug with automatic updates
Perfect for modern JavaScript frameworks Overkill for simple projects
ViewModel is highly testable Requires understanding of reactive programming

Side-by-Side Comparison

Aspect MVC MVP MVVM
View Updates Controller tells View to update Presenter tells View to update Automatic (binding)
View-Model Coupling Tight Loose Loose
View Independence Depends on Model Independent (testable alone) Independent
Complexity Low Medium Medium-High
Best For Web frameworks (Rails, Django) Desktop apps, complex UI logic Modern JS frameworks (Vue, Angular)

🎯 Choosing the Right Pattern

Use MVC if:

  • Building a traditional web app with a backend framework
  • You want simplicity and fast development
  • The project is small to medium-sized

Use MVP if:

  • Testing the UI logic is a priority (desktop apps, complex UIs)
  • You want maximum separation between View and Model
  • You need to reuse the same view with different logic

Use MVVM if:

  • You're using a modern framework (Vue.js, Angular, React + Redux)
  • You want automatic UI synchronization
  • You're comfortable with reactive programming concepts

🎯 Key Ideas

  • Architectural patterns help organize code into clear, separated layers
  • Model holds data and logic, View displays it
  • The middle layer (Controller/Presenter/ViewModel) differs by pattern
  • More separation = easier to test and maintain, but more code
  • Choose the pattern that fits your framework and team knowledge
← Back to Code Foundations