COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

Model-View-Controller (MVC)

MVC is an architectural pattern that separates an application into three interconnected components: the Model (data and business logic), the View (presentation layer), and the Controller (orchestration and user input handling).

Key Concepts

Interactive Demo

This task manager demonstrates MVC with clear separation. Try adding, completing, and deleting tasks while watching the event log.

    Event Log

    Watch how the Controller coordinates interactions between Model and View:

    [System] MVC demo initialized

    Source Code

    Model

    // MODEL - Data and Business Logic
    class TaskModel {
        constructor() {
            this.tasks = [];
            this.observers = [];
        }
    
        addObserver(observer) {
            this.observers.push(observer);
        }
    
        notifyObservers() {
            this.observers.forEach(obs => obs.update(this.tasks));
        }
    
        addTask(title) {
            const task = {
                id: Date.now(),
                title,
                completed: false,
                createdAt: new Date().toISOString()
            };
            this.tasks.push(task);
            this.notifyObservers();
            return task;
        }
    
        toggleTask(id) {
            const task = this.tasks.find(t => t.id === id);
            if (task) {
                task.completed = !task.completed;
                this.notifyObservers();
            }
        }
    
        deleteTask(id) {
            this.tasks = this.tasks.filter(t => t.id !== id);
            this.notifyObservers();
        }
    
        getTasks() {
            return this.tasks;
        }
    }

    View

    // VIEW - Presentation Layer
    class TaskView {
        constructor() {
            this.taskList = document.getElementById('mvc-task-list');
            this.taskInput = document.getElementById('mvc-task-input');
            this.addButton = document.getElementById('mvc-add-button');
            this.stats = document.getElementById('mvc-stats');
        }
    
        update(tasks) {
            this.renderTasks(tasks);
            this.renderStats(tasks);
        }
    
        renderTasks(tasks) {
            if (!this.taskList) return;
    
            this.taskList.innerHTML = tasks.map(task => `
                <li class="task-item ${task.completed ? 'completed' : ''}">
                    <input type="checkbox"
                           data-id="${task.id}"
                           ${task.completed ? 'checked' : ''}>
                    <span>${task.title}</span>
                    <button class="delete-btn" data-id="${task.id}">Delete</button>
                </li>
            `).join('');
        }
    
        renderStats(tasks) {
            if (!this.stats) return;
    
            const total = tasks.length;
            const completed = tasks.filter(t => t.completed).length;
            const active = total - completed;
    
            this.stats.innerHTML = `
                <div>Total: ${total}</div>
                <div>Active: ${active}</div>
                <div>Completed: ${completed}</div>
            `;
        }
    
        getTaskInput() {
            return this.taskInput.value;
        }
    
        clearInput() {
            this.taskInput.value = '';
        }
    
        bindAddTask(handler) {
            this.addButton.addEventListener('click', () => {
                const title = this.getTaskInput();
                if (title.trim()) {
                    handler(title);
                    this.clearInput();
                }
            });
        }
    
        bindToggleTask(handler) {
            this.taskList.addEventListener('change', (e) => {
                if (e.target.type === 'checkbox') {
                    handler(parseInt(e.target.dataset.id));
                }
            });
        }
    
        bindDeleteTask(handler) {
            this.taskList.addEventListener('click', (e) => {
                if (e.target.classList.contains('delete-btn')) {
                    handler(parseInt(e.target.dataset.id));
                }
            });
        }
    }

    Controller

    // CONTROLLER - Orchestration Layer
    class TaskController {
        constructor(model, view) {
            this.model = model;
            this.view = view;
    
            // Model observes itself and updates view
            this.model.addObserver(this.view);
    
            // View binds to controller handlers
            this.view.bindAddTask(this.handleAddTask.bind(this));
            this.view.bindToggleTask(this.handleToggleTask.bind(this));
            this.view.bindDeleteTask(this.handleDeleteTask.bind(this));
    
            // Initial render
            this.view.update(this.model.getTasks());
        }
    
        handleAddTask(title) {
            this.log(`Controller: Adding task "${title}"`);
            this.model.addTask(title);
        }
    
        handleToggleTask(id) {
            this.log(`Controller: Toggling task #${id}`);
            this.model.toggleTask(id);
        }
    
        handleDeleteTask(id) {
            this.log(`Controller: Deleting task #${id}`);
            this.model.deleteTask(id);
        }
    
        log(message) {
            const logEl = document.getElementById('mvc-log');
            if (logEl) {
                const entry = document.createElement('div');
                entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
                logEl.appendChild(entry);
                logEl.scrollTop = logEl.scrollHeight;
            }
        }
    }

    Initialization

    // INITIALIZATION
    const model = new TaskModel();
    const view = new TaskView();
    const controller = new TaskController(model, view);
    
    // Add some sample tasks
    model.addTask('Learn MVC Pattern');
    model.addTask('Build a web application');
    model.addTask('Master separation of concerns');

    Benefits of MVC

    Trade-offs