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.
A scoreboard for quiz night
The quizmaster types commands such as Owls +3, and Scoreboard adds the points and draws the board.
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.
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);
}
}
}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 |
|
The quiz's rules | Which teams can score, and how points add up |
|
The pub | How the board looks on the wall |
|
The pub, again | A ticker on the projector |
|
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.
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);
}
}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
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
BoardViewandTickerView. 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.
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 |
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
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.