# Model-View-Presenter (MVP) Pattern

## Problem

In MVC, the view can sometimes directly access the model, creating coupling that makes the view hard to test and replace. Complex view logic ends up in the controller or leaks into the view itself, especially in desktop or mobile UIs without data binding.

## Solution

**MVP** introduces a **Presenter** that acts as the middleman between a passive **View** and the **Model**:

- **Model** — Domain data and business rules.
- **View** — A passive interface. Only knows how to display data and forward user events to the Presenter. Has no logic.
- **Presenter** — Retrieves data from the Model, formats it, and pushes it to the View. Handles all user input by calling Model methods and updating the View.

The View never accesses the Model directly. The Presenter is fully testable because the View is abstracted behind an interface.

## When to Use

- UI platforms without data binding (Android Java, classic WinForms).
- When you need full testability of presentation logic with mock views.
- When the view should be completely passive and interchangeable.

## When to Avoid

- Frameworks with rich data binding (use MVVM instead).
- Trivial UIs where the indirection adds no value.
- Server-side rendering where MVC is more natural.

## Pseudocode

```
INTERFACE View:
    METHOD show_items(items)
    METHOD show_error(message)
    METHOD show_total(total)

CLASS Presenter:
    CONSTRUCTOR(view, model):
        STORE dependencies

    METHOD on_load():
        items = model.get_items()
        view.show_items(items)
        view.show_total(model.get_total())

    METHOD on_add(item):
        model.add_item(item)
        on_load()  // refresh view
```

## Python

```python
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class CartItem:
    name: str
    price: float


class CartModel:
    def __init__(self) -> None:
        self._items: list[CartItem] = []

    def add_item(self, item: CartItem) -> None:
        self._items.append(item)

    def remove_item(self, index: int) -> None:
        if 0 <= index < len(self._items):
            self._items.pop(index)

    def get_items(self) -> list[CartItem]:
        return list(self._items)

    def get_total(self) -> float:
        return sum(item.price for item in self._items)


class CartView(ABC):
    @abstractmethod
    def show_items(self, items: list[CartItem]) -> None: ...

    @abstractmethod
    def show_total(self, total: float) -> None: ...

    @abstractmethod
    def show_message(self, message: str) -> None: ...


class ConsoleCartView(CartView):
    def show_items(self, items: list[CartItem]) -> None:
        print("  [View] Cart contents:")
        if not items:
            print("    (empty)")
        for i, item in enumerate(items):
            print(f"    [{i}] {item.name} - ${item.price:.2f}")

    def show_total(self, total: float) -> None:
        print(f"  [View] Total: ${total:.2f}")

    def show_message(self, message: str) -> None:
        print(f"  [View] {message}")


class CartPresenter:
    def __init__(self, view: CartView, model: CartModel) -> None:
        self._view = view
        self._model = model

    def on_load(self) -> None:
        items = self._model.get_items()
        self._view.show_items(items)
        self._view.show_total(self._model.get_total())

    def on_add_item(self, name: str, price: float) -> None:
        self._model.add_item(CartItem(name, price))
        self._view.show_message(f"Added '{name}'")
        self.on_load()

    def on_remove_item(self, index: int) -> None:
        items = self._model.get_items()
        if 0 <= index < len(items):
            removed = items[index].name
            self._model.remove_item(index)
            self._view.show_message(f"Removed '{removed}'")
        else:
            self._view.show_message(f"Invalid index: {index}")
        self.on_load()


def main() -> None:
    model = CartModel()
    view = ConsoleCartView()
    presenter = CartPresenter(view, model)

    print("--- Initial load ---")
    presenter.on_load()

    print("\n--- Add items ---")
    presenter.on_add_item("Laptop", 999.99)
    print()
    presenter.on_add_item("Mouse", 29.99)
    print()
    presenter.on_add_item("Keyboard", 79.99)

    print("\n--- Remove item at index 1 ---")
    presenter.on_remove_item(1)


if __name__ == "__main__":
    main()
```

**Output:**
```
--- Initial load ---
  [View] Cart contents:
    (empty)
  [View] Total: $0.00

--- Add items ---
  [View] Added 'Laptop'
  [View] Cart contents:
    [0] Laptop - $999.99
  [View] Total: $999.99

  [View] Added 'Mouse'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Mouse - $29.99
  [View] Total: $1029.98

  [View] Added 'Keyboard'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Mouse - $29.99
    [2] Keyboard - $79.99
  [View] Total: $1109.97

--- Remove item at index 1 ---
  [View] Removed 'Mouse'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Keyboard - $79.99
  [View] Total: $1079.98
```

## Go

```go
package main

import "fmt"

// --- Model ---

type CartItem struct {
	Name  string
	Price float64
}

type CartModel struct {
	items []CartItem
}

func (m *CartModel) AddItem(item CartItem) {
	m.items = append(m.items, item)
}

func (m *CartModel) RemoveItem(index int) {
	if index >= 0 && index < len(m.items) {
		m.items = append(m.items[:index], m.items[index+1:]...)
	}
}

func (m *CartModel) GetItems() []CartItem {
	result := make([]CartItem, len(m.items))
	copy(result, m.items)
	return result
}

func (m *CartModel) GetTotal() float64 {
	total := 0.0
	for _, item := range m.items {
		total += item.Price
	}
	return total
}

// --- View interface ---

type CartView interface {
	ShowItems(items []CartItem)
	ShowTotal(total float64)
	ShowMessage(message string)
}

// --- Console view ---

type ConsoleCartView struct{}

func (v *ConsoleCartView) ShowItems(items []CartItem) {
	fmt.Println("  [View] Cart contents:")
	if len(items) == 0 {
		fmt.Println("    (empty)")
	}
	for i, item := range items {
		fmt.Printf("    [%d] %s - $%.2f\n", i, item.Name, item.Price)
	}
}

func (v *ConsoleCartView) ShowTotal(total float64) {
	fmt.Printf("  [View] Total: $%.2f\n", total)
}

func (v *ConsoleCartView) ShowMessage(message string) {
	fmt.Printf("  [View] %s\n", message)
}

// --- Presenter ---

type CartPresenter struct {
	view  CartView
	model *CartModel
}

func NewCartPresenter(view CartView, model *CartModel) *CartPresenter {
	return &CartPresenter{view: view, model: model}
}

func (p *CartPresenter) OnLoad() {
	p.view.ShowItems(p.model.GetItems())
	p.view.ShowTotal(p.model.GetTotal())
}

func (p *CartPresenter) OnAddItem(name string, price float64) {
	p.model.AddItem(CartItem{name, price})
	p.view.ShowMessage(fmt.Sprintf("Added '%s'", name))
	p.OnLoad()
}

func (p *CartPresenter) OnRemoveItem(index int) {
	items := p.model.GetItems()
	if index >= 0 && index < len(items) {
		removed := items[index].Name
		p.model.RemoveItem(index)
		p.view.ShowMessage(fmt.Sprintf("Removed '%s'", removed))
	} else {
		p.view.ShowMessage(fmt.Sprintf("Invalid index: %d", index))
	}
	p.OnLoad()
}

func main() {
	model := &CartModel{}
	view := &ConsoleCartView{}
	presenter := NewCartPresenter(view, model)

	fmt.Println("--- Initial load ---")
	presenter.OnLoad()

	fmt.Println("\n--- Add items ---")
	presenter.OnAddItem("Laptop", 999.99)
	fmt.Println()
	presenter.OnAddItem("Mouse", 29.99)
	fmt.Println()
	presenter.OnAddItem("Keyboard", 79.99)

	fmt.Println("\n--- Remove item at index 1 ---")
	presenter.OnRemoveItem(1)
}
```

**Output:**
```
--- Initial load ---
  [View] Cart contents:
    (empty)
  [View] Total: $0.00

--- Add items ---
  [View] Added 'Laptop'
  [View] Cart contents:
    [0] Laptop - $999.99
  [View] Total: $999.99

  [View] Added 'Mouse'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Mouse - $29.99
  [View] Total: $1029.98

  [View] Added 'Keyboard'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Mouse - $29.99
    [2] Keyboard - $79.99
  [View] Total: $1109.97

--- Remove item at index 1 ---
  [View] Removed 'Mouse'
  [View] Cart contents:
    [0] Laptop - $999.99
    [1] Keyboard - $79.99
  [View] Total: $1079.98
```

## Related Patterns

- **MVC** — MVC allows the view to access the model directly; MVP makes the view fully passive and routes everything through the presenter.
- **MVVM** — MVVM uses data binding and a ViewModel; MVP uses explicit method calls between presenter and view interface.
- **Facade** — the presenter acts as a facade over the model, shielding the view from domain complexity.
