MVC and Its Variants
Software Architecture Fundamentals
Chapter 3 · MVC and Its Variants
Chapter 2 said presentation should only ever "own formatting and input." That's true for all three variants in this chapter — MVC, MVP, and MVVM all agree the presentation layer shouldn't contain business rules. What they genuinely disagree on is how data actually flows between the view and everything behind it. This chapter builds all three, verifies the difference is real, and connects one of them directly back to a pattern you already know.
MVC: the View Reads the Model Directly
view.model is controller.model confirms True — the exact same object, not a copy. After controller.handle_add('Buy milk'), calling view.render() correctly reports "Todo list: Buy milk", purely because render() reads self.model.items directly at render time. The Controller never told the View anything — the View simply looked.
MVP: the View Never Touches the Model at All
MVP takes coupling the View to the Model away entirely — the View becomes "dumb," exposing only display methods, and a Presenter mediates every single interaction.
hasattr(view, 'model') returns False — unlike TodoView, TodoPresenterView was never given a reference to any model at all. Calling presenter.handle_add('Buy milk') correctly updates view.displayed_text to "Todo list: Buy milk" — but only because the Presenter explicitly called view.show_items(...) itself.
model.add_item('Walk the dog') directly — skipping the Presenter entirely — correctly updates model.items to ['Buy milk', 'Walk the dog']. But view.displayed_text stays exactly as it was: "Todo list: Buy milk" — genuinely stale. In MVP, nothing updates the View except the Presenter explicitly telling it to.
MVVM: the View Binds to the ViewModel — and This Is Just Observer
MVVM solves MVP's own staleness problem, but not by adding more explicit push calls — by using exactly the mechanism Design Patterns Chapter 8 already built: Observer. The ViewModel is the subject; the bound view is an observer.
view_model.attach(bound_view), calling view_model.add_item('Buy milk') correctly updates bound_view.displayed_text to "Todo list: Buy milk" — and a second call, add_item('Walk the dog'), correctly produces "Todo list: Buy milk, Walk the dog". Inspecting TodoViewModel.add_item's own source confirms it never references bound_view or any specific view type by name — it only calls self._notify(), exactly like Stock.set_price() only ever called observer.update(...) on whatever was attached.
Comparing All Three
| MVC | MVP | MVVM | |
|---|---|---|---|
| Can the View read the Model directly? | Yes — verified: same object identity | No — verified: hasattr(view, 'model') is False | No — the View only ever sees what the ViewModel notifies it with |
| Who updates the View? | The View reads for itself, on demand | The Presenter, explicitly, every time | The binding mechanism, automatically |
| What happens if you bypass the mediator? | N/A — there's no separate mediator to bypass | The View goes stale — verified above | Impossible by construction — there's no separate "tell the view" step to skip |
| Common in | Classic Rails/Django-style server-rendered apps | Older desktop GUI frameworks, testable Android (pre-Compose) | WPF, and reactive/data-bound web frameworks (Vue, some React state libraries) |
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| MVVM's binding verified as literally Design Patterns' own Observer, reused unchanged | Confirms this course's own Chapter 1 claim directly — a design-level pattern (Observer) is one of the concrete mechanisms an architecture-level decision (MVVM) is built from |
| MVP's verified stale-view bug when the Presenter is bypassed | Chapter 2's own "skipping a layer produces a silently wrong result, not a crash" finding — the same shape of bug, one level up |
| All three variants agreeing presentation owns no business rules | Chapter 5's coupling/cohesion criteria — the real difference between MVC/MVP/MVVM is entirely about coupling direction, not about what belongs in which layer |
Hands-On Exercises
Add a remove_item(text) method to this chapter's own MVC TodoModel and TodoController, following the exact shape of add_item/handle_add. Verify view.render() correctly reflects the removal, with no changes to TodoView at all.
Add a remove_item(text) method to this chapter's own MVP TodoPresenter (and matching model support). Verify it correctly pushes the update to view.displayed_text, and then verify that calling model's own removal method directly, bypassing the Presenter, leaves the View stale again — exactly like this chapter's own add_item bypass.
Attach a second TodoBoundView to this chapter's own TodoViewModel (alongside the first). Verify calling view_model.add_item(...) once updates both bound views correctly, and explain — using this chapter's own comparison table — what the MVP equivalent of adding a second view would have required that MVVM didn't.
Chapter 3 Quick Reference
- MVC: the View reads the Model directly — verified: same object identity, no mediator involved
- MVP: a dumb View, mediated entirely by a Presenter — verified: the View has no Model reference at all, and goes genuinely stale if the Presenter is bypassed
- MVVM: the View binds to the ViewModel — verified as literally Design Patterns' own Observer pattern, reused directly; updates happen automatically, with no explicit push step to forget
- Next chapter: Monolith vs. Microservices — a much bigger-scale version of the same "who's allowed to talk to whom" question