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.
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:
MVC is one of the oldest and most popular patterns. The Controller handles user actions, updates the Model, and tells the View to refresh.
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;
}
}
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);
}
}
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 | 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 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.
View is passive: The View never directly touches the Model. All communication goes through the Presenter.
class TodoModel {
todos: { id: number; text: string; done: boolean }[] = [];
addTodo(text: string): void {
this.todos.push({ id: Date.now(), text, done: false });
}
}
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;
}
}
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 | 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 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).
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.
class TodoModel {
todos: { id: number; text: string; done: boolean }[] = [];
addTodo(text: string): void {
this.todos.push({ id: Date.now(), text, done: false });
}
}
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
}
}
// 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 | 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 |
| 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) |
Use MVC if:
Use MVP if:
Use MVVM if: