Low-level designCore9 min

MVC and its relatives

Split a user interface into a model that holds the data and rules, views that show it, and a controller that turns input into changes. A new way to show the data is then one new view, and the rules never learn how they're displayed.

Watch it evolve1 / 10
The problem

A scoreboard for quiz night

The quizmaster types commands such as Owls +3, and Scoreboard adds the points and draws the board.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A scoreboard for quiz night

A pub runs a quiz night. The quizmaster types commands such as Owls +3 into a laptop, and the scores show on the wall. The first version is one class, Scoreboard:

  • handle() reads a command and splits it into a team and points;
  • award() adds the points, and refuses a team that isn't playing;
  • a private print() draws the board after every change.
The first versionJava
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

class Scoreboard {
    private final Map<String, Integer> points = new LinkedHashMap<>();

    Scoreboard(List<String> teams) {
        for (String team : teams) points.put(team, 0);
    }

    void handle(String command) {
        String[] parts = command.split(" ");
        if (!award(parts[0], Integer.parseInt(parts[1]))) {
            System.out.println("Unknown team: " + parts[0]);
            return;
        }
        print();
    }

    boolean award(String team, int n) {
        if (!points.containsKey(team)) return false;
        points.merge(team, n, Integer::sum);
        return true;
    }

    private void print() {
        List<String> cells = new ArrayList<>();
        points.forEach((team, p) -> cells.add(team + " " + p));
        System.out.println(String.join(" | ", cells));
    }
}

public class Main {
    public static void main(String[] args) {
        Scoreboard board = new Scoreboard(List.of("Owls", "Foxes"));
        for (String command : List.of("Owls +3", "Foxes +2", "Hawks +1", "Foxes +5")) {
            System.out.println("> " + command);
            board.handle(command);
        }
    }
}
Scoreboard− points: Map<String, Integer>~ handle(String)~ award(String, int): boolean− print()

The class diagram of this version, drawn from its code: one class reads input, keeps the scores and draws them.

Four commands, one of them for a team that isn't playing:

> Owls +3
Owls 3 | Foxes 0
> Foxes +2
Owls 3 | Foxes 2
> Hawks +1
Unknown team: Hawks
> Foxes +5
Owls 3 | Foxes 7

2 · The change

A second display, and one class to edit

The pub puts up a projector and wants a one-line ticker on it: who leads, and by how much. In this design the only place that knows when scores change is Scoreboard, so the ticker would be drawn there too, next to the scoring rules.

Who asks

What they change

Where it lands today

The quizmaster

How commands are typed, say Owls 3 without the plus

Scoreboard.handle()

The quiz's rules

Which teams can score, and how points add up

Scoreboard.award()

The pub

How the board looks on the wall

Scoreboard.print()

The pub, again

A ticker on the projector

Scoreboard, once more

Every display the pub adds makes the class longer, and every change to how a score looks happens in the same file as the rules that compute it. Nothing can be tested without capturing what's printed. The practice checks give a hint: Scoreboard also prints output.

3 · The fix

Split the data, its display, and the input

Start from the first version, and give each of the three jobs its own class. The scores must still tell the board when they change, so the model keeps a list of listeners and calls each one.

Scores holds the points and the teams that may score, and award() refuses any other team. When points change, it calls scoresChanged() on every ScoreListener it was given. It never prints.

Java
class Scores {
    private final Map<String, Integer> points = new LinkedHashMap<>();
    private final List<ScoreListener> listeners = new ArrayList<>();

    Scores(List<String> teams) {
        for (String team : teams) points.put(team, 0);
    }

    void addListener(ScoreListener listener) {
        listeners.add(listener);
    }

    boolean award(String team, int n) {
        if (!points.containsKey(team)) return false;
        points.merge(team, n, Integer::sum);
        for (ScoreListener listener : listeners) listener.scoresChanged(this);
        return true;
    }

    Map<String, Integer> points() {
        return new LinkedHashMap<>(points);
    }
}
*«interface»ScoreListener+ scoresChanged(Scores)Scores− points: Map<String, Integer>− listeners: List<ScoreListener>~ addListener(ScoreListener)~ award(String, int): boolean~ points(): Map<String, Integer>BoardView+ scoresChanged(Scores)~ showError(String)QuizController− scores: Scores− view: BoardView~ handle(String)

After three steps the program prints exactly what the first version printed. After the fourth, the ticker follows each change of the board:

> Owls +3
Owls 3 | Foxes 0
Ticker: Owls lead by 3
> Foxes +2
Owls 3 | Foxes 2
Ticker: Owls lead by 1
> Hawks +1
Unknown team: Hawks
> Foxes +5
Owls 3 | Foxes 7
Ticker: Foxes lead by 4
One class: existing classes edited
1
MVC: existing classes edited
0

Existing classes the ticker edits. Both designs also add a line to main; with MVC that line hands the new view to the model.

4 · The pattern

Model, view, controller

Model–view–controller, or MVC, splits a user interface into three roles:

  • The model holds the data and the rules that change it, here Scores. It doesn't know how it's shown.
  • A view shows the model, here BoardView and TickerView. There can be any number of views of one model.
  • The controller takes the user's input and turns it into calls on the model, or on the view, here QuizController.
*«interface»ScoreListener+ scoresChanged(Scores)Scores− points: Map<String, Integer>− listeners: List<ScoreListener>~ addListener(ScoreListener)~ award(String, int): boolean~ points(): Map<String, Integer>BoardView+ scoresChanged(Scores)~ showError(String)TickerView+ scoresChanged(Scores)QuizController− scores: Scores− view: BoardView~ handle(String)

The model in the middle: the controller changes it, and it tells every view through ScoreListener.

One command. The controller changes the model; the model tells each view, and each view reads what it needs.

The model telling its views is the Observer pattern, and the checks recognize it here. That link is what lets a view be added without touching the model: the model only knows it has listeners, never which ones.

5 · Its relatives

MVP and MVVM: who updates the view

Two widely used relatives keep the model and change how the view is fed:

  • In model–view–presenter (MVP), the view is passive. It forwards every input to a presenter, which reads the model, formats the data and tells the view what to display through an interface. The view never touches the model.
  • In model–view–viewmodel (MVVM), a view model exposes the data and commands a screen needs, already shaped for display. The view binds to its properties, and a binding framework keeps the two in step, so the view model holds no reference to the view.

Pattern

Who handles input

How the view gets the data

Does the view know the model?

MVC

The controller

The view reads the model when told it changed

Yes

MVP

The view forwards it to the presenter

The presenter pushes it through a view interface

No

MVVM

The view model's commands, bound from the view

Data binding to the view model's properties

No

Web frameworks use the name MVC for a request-and-response shape: a controller handles a request, works with model objects, and picks a template, the view, to render the response. There's no long-lived view observing the model, because the page is drawn once per request.

6 · When breaking it is fine

Three classes for one screen

The split has costs:

  • More pieces for small screens. A screen with one field and one button gets three classes and an interface.
  • Updates can cascade. Every change of the model redraws every view; a model changed fifty times in a row redraws fifty times unless the views batch their work.
  • Controllers grow. Logic with no obvious home tends to land in the controller, which then becomes the one class that does everything.

Keep one class when the interface is a short script or a throwaway tool that will never get a second display. Prefer the framework's shape when you use one: a web framework, a mobile toolkit or a data-binding library already decides which relative you write, and fighting it costs more than any purity gained.

7 · Getting it right

Keep the model free of screens

  • The model never imports a view. It talks to an interface such as ScoreListener, and tests can run it with none at all.
  • Views don't change the model. They read it; changes go through the controller, so there's one place that turns input into actions.
  • Keep the controller thin. It interprets input and calls the model. Rules such as "only playing teams can score" belong in the model, where every controller gets them.
  • Let each view read what it needs. Passing the model to scoresChanged() lets the board and the ticker each take a different view of the same data.

Language

How a view usually learns of a change

Java

A listener interface, as here, or property change support in desktop toolkits

C++

A listener class with virtual functions, or signals and slots in toolkits such as Qt

JavaScript

Events, or a framework that re-renders when state changes

Python

Callbacks or a Protocol, as here; toolkits have their own event systems

8 · Look-alikes

Patterns MVC is built from, and next to

MVC is made of smaller ideas and sits inside bigger ones.

Idea

How it relates to MVC

Observer

The mechanism: the model notifies its views without knowing them

Strategy

A controller can be swapped to change how a view responds to input

Layered architecture

MVC splits the presentation layer; the model is often the domain layer underneath

Single responsibility

The reason for the split: rules, display and input change for different people

MVC is usually called an architectural pattern rather than a design pattern, because it shapes a whole user interface rather than one collaboration between a few classes.

9 · In practice

Where you'll meet it

Server frameworks such as Rails and Django are organized around models, controllers and views or templates; Django names its version model, template, view, where its "view" plays the controller's part. Desktop and mobile toolkits lean toward MVP and MVVM, where data binding keeps a screen and its view model in step.

In the practice problems, the checks give a hint when a class that isn't named for display also prints output, as Scoreboard did. They also recognize the model-to-view link as Observer when a class keeps a list of listeners, adds to it, and calls each one.

Check yourself

4 questions

1. The pub wants the scores sent to a phone app as well. In the MVC version, what changes?
2. Where does the rule "teams that aren't playing can't score" belong?
3. In MVVM, how does a screen get new data?
4. Why does Scores talk to a ScoreListener interface instead of to BoardView?

Takeaways

Remember this

  • MVC splits a user interface into a model with the data and rules, views that show it, and a controller that turns input into changes.
  • The model notifies its views through an interface, so a new view is one new class and the model never learns about screens.
  • Rules belong in the model; the controller interprets input, and views only read.
  • MVP routes everything through a presenter and a passive view; MVVM binds the view to a view model's properties.
  • Web MVC is request and response: a controller handles the request and picks the template that renders it.