Overview
Model-View-Presenter (MVP) is an architectural pattern derived from Model-View-Controller that enforces strict separation between presentation logic and business logic. The pattern emerged in the 1990s at Taligent (an Apple/IBM joint venture) as a refinement of MVC specifically designed to improve testability and maintainability of user interface code.
MVP divides an application into three distinct components. The Model contains business logic and data operations, maintaining no awareness of the presentation layer. The View handles display and user input capture, but contains minimal logic beyond basic display formatting. The Presenter acts as an intermediary that coordinates between Model and View, processing user actions, updating the Model, and instructing the View to refresh its display.
The defining characteristic of MVP is the passive nature of the View. Unlike MVC where the View may observe the Model directly, MVP requires all Model-View communication to flow through the Presenter. This creates a unidirectional dependency structure where the View depends on the Presenter, the Presenter depends on both View and Model, but the Model remains independent of both presentation components.
Consider a user registration form. In MVP, the View captures input fields and button clicks but performs no validation or processing. When the user submits the form, the View invokes a method on the Presenter. The Presenter retrieves values from the View, validates them, updates the Model to create a new user account, and instructs the View to display success or error messages. The View never directly accesses the Model, and the Model never directly modifies the View.
This strict separation creates several advantages. Presenters become easily testable since they interact with Views through interfaces that can be mocked. Business logic remains isolated in Models without UI dependencies. Views become thin, reducing the amount of logic tied to specific UI frameworks. The pattern particularly benefits applications with complex user interactions where presentation logic might otherwise become tangled with business rules.
Key Principles
MVP rests on several fundamental principles that distinguish it from other architectural patterns and define how components interact.
Separation of Concerns forms the foundation of MVP. Each component has a clearly defined responsibility that does not overlap with other components. The Model encapsulates business logic, data access, and domain rules without any knowledge of how data gets displayed. The View handles rendering and input capture without making decisions about what data means or how to process it. The Presenter orchestrates the interaction between Model and View, translating user actions into Model operations and Model state into View updates.
View Passivity represents the most distinctive principle of MVP. The View acts as a dumb rendering component that responds to direct instructions from the Presenter but makes no autonomous decisions. When a user interacts with the View—clicking a button, entering text, selecting an option—the View immediately delegates to the Presenter without processing the input. The View does not validate data, does not perform calculations, does not query the Model. This passivity extends to display updates: the View does not observe the Model or pull data on its own initiative, but waits for the Presenter to push display instructions.
# View passivity example
class PassiveView
attr_accessor :presenter
def initialize
@presenter = UserPresenter.new(self)
end
# View only captures input and delegates
def submit_button_clicked
presenter.handle_submit
end
# View provides accessors for Presenter to retrieve input
def username
@username_field.text
end
def password
@password_field.text
end
# View accepts display commands from Presenter
def display_error(message)
@error_label.text = message
@error_label.visible = true
end
def clear_form
@username_field.text = ""
@password_field.text = ""
end
end
Presenter Mediation requires all View-Model interaction to pass through the Presenter. The View never directly references Model objects. The Model never holds references to Views. This creates a dependency structure where changes to the Model or View do not cascade to each other. The Presenter translates between the two layers, converting View events into Model operations and Model data into View display instructions.
Testability Through Abstraction emerges from the pattern's structure. Because the Presenter interacts with the View through an interface rather than concrete UI components, tests can substitute mock Views that verify the Presenter's behavior without instantiating actual UI frameworks. The Presenter contains the majority of presentation logic, but this logic operates on plain data types and simple View interface methods rather than complex UI widgets.
# Presenter works against View interface
class UserPresenter
def initialize(view)
@view = view
@model = UserModel.new
end
def handle_submit
username = @view.username
password = @view.password
if username.empty?
@view.display_error("Username required")
return
end
if password.length < 8
@view.display_error("Password must be at least 8 characters")
return
end
user = @model.create_user(username, password)
@view.navigate_to_dashboard(user.id)
end
end
Single Presenter Per View establishes a one-to-one relationship between Presenters and Views. Each View has exactly one Presenter responsible for its behavior. Complex screens with multiple sub-views may have multiple Presenter-View pairs that coordinate through events or a parent Presenter, but each View maintains its own dedicated Presenter. This prevents presenters from becoming bloated orchestrators managing multiple disparate screens.
Model Independence ensures the Model layer remains unaware of presentation concerns. Models do not format data for display, do not manage UI state, do not fire UI events. A Model representing a User contains methods for authentication, profile updates, and business rules, but knows nothing about whether the user interface displays this data in a form, a table, or a mobile app. This independence allows Models to be reused across different presentation layers—web interfaces, mobile apps, API endpoints—without modification.
Design Considerations
Selecting MVP over alternative architectural patterns requires evaluating several design factors related to application complexity, testing requirements, and team capabilities.
MVP Versus MVC Trade-offs represent the most common architectural decision. MVC allows Views to observe Models directly, enabling automatic UI updates when Model state changes. This observer pattern reduces boilerplate code in simple applications where Views straightforwardly display Model data. MVP eliminates this direct connection, requiring explicit Presenter methods to update Views, which increases code volume but improves testability and reduces coupling.
MVC becomes problematic when presentation logic grows complex. Views that observe Models must decide how to interpret Model changes, embedding presentation rules in UI code. As these rules proliferate—conditional display, field validation, interaction coordination—Views accumulate untestable logic. MVP moves this logic into Presenters where it can be tested without UI frameworks.
Choose MVC when building simple applications with straightforward Model-to-View mappings, where each Model property maps to a single UI element, and where presentation logic remains minimal. The observer pattern's automatic updates reduce development time for these scenarios. Choose MVP when building applications with complex user interactions, conditional display rules, multi-step workflows, or when testability takes priority. The additional code required for explicit View updates pays dividends in maintainability and test coverage.
MVP Versus MVVM Considerations involve evaluating data binding capabilities. MVVM relies on two-way data binding between Views and ViewModels, where changes to View input fields automatically update ViewModel properties, and ViewModel property changes automatically refresh View displays. This automation reduces code volume but requires a data binding framework.
MVP requires manual synchronization through Presenter methods that pull data from Views and push updates to Views. This manual approach works well in environments without robust data binding support (like Ruby), provides explicit control over when updates occur, and makes the flow of data obvious in code. MVVM's automatic synchronization works well when data binding frameworks are available (like in Angular or WPF), but can create debugging challenges when understanding why Views update requires tracing through framework internals.
Complexity Overhead Assessment must account for MVP's additional abstractions. Every user interaction requires three classes: a Model for business logic, a Presenter for coordination, and a View for display. Simple CRUD applications with minimal logic may find this overhead burdensome compared to a direct approach where Views interact with Models.
The overhead proves worthwhile when applications grow beyond basic CRUD into domains like multi-step workflows, complex validation rules, conditional behavior based on user roles or application state, or integration with multiple systems. These scenarios benefit from MVP's clear separation because presentation logic remains isolated and testable rather than scattered across UI components.
Testing Strategy Impact often drives MVP adoption. Applications requiring high test coverage benefit from MVP's testability. Presenters can be tested with lightweight mock Views that verify display instructions without instantiating UI frameworks. This allows fast unit test execution and comprehensive coverage of presentation logic.
Applications with less stringent testing requirements or those using integration testing approaches may find MVP's testing benefits less compelling. If tests primarily verify end-to-end behavior through actual UI interactions, MVP's ability to test Presenters in isolation provides less value. The pattern still improves code organization, but the testing motivation diminishes.
Framework Integration Challenges arise when adopting MVP in frameworks designed for other patterns. Ruby on Rails assumes MVC architecture with Controllers handling web requests and Views rendering templates. Introducing MVP requires creating Presenter objects distinct from Controllers, deciding where Presenters live in the project structure, and ensuring proper instantiation and lifecycle management.
These integration challenges prove manageable but require thoughtful design. Presenters may serve as helper objects instantiated by Controllers, or Controllers may themselves act as Presenters with Views represented by template rendering logic. The key involves maintaining MVP's principle that presentation logic stays out of Views (templates) and Models, even if the framework's conventions don't naturally support this separation.
Ruby Implementation
Ruby supports MVP implementation through plain object-oriented design without requiring specialized frameworks. The language's dynamic nature and duck typing facilitate the pattern's interface-based approach to View abstraction.
Basic MVP Structure in Ruby organizes code into three distinct object types. Models inherit from frameworks like ActiveRecord or remain plain Ruby objects containing business logic. Views represent UI components, which in web applications correspond to template rendering, and in desktop or mobile applications correspond to actual UI widgets. Presenters act as plain Ruby objects that coordinate between Models and Views.
# Model - Business logic and data
class User
attr_reader :id, :username, :email, :created_at
def initialize(attributes = {})
@id = attributes[:id]
@username = attributes[:username]
@email = attributes[:email]
@created_at = attributes[:created_at] || Time.now
end
def self.create(username, email, password)
# Database creation logic
new(id: generate_id, username: username, email: email)
end
def self.find_by_username(username)
# Database lookup logic
end
def self.authenticate(username, password)
user = find_by_username(username)
return nil unless user
return nil unless verify_password(password, user.password_hash)
user
end
def update_profile(attributes)
@username = attributes[:username] if attributes[:username]
@email = attributes[:email] if attributes[:email]
save
end
private
def self.generate_id
# ID generation
end
def self.verify_password(password, hash)
# Password verification
end
def save
# Persistence logic
end
end
# Presenter - Coordinates View and Model
class LoginPresenter
def initialize(view)
@view = view
end
def handle_login_attempt
username = @view.username_input
password = @view.password_input
if username.empty?
@view.show_error("Username required")
return
end
if password.empty?
@view.show_error("Password required")
return
end
user = User.authenticate(username, password)
if user
@view.hide_error
@view.navigate_to_dashboard(user.id)
else
@view.show_error("Invalid username or password")
@view.clear_password_field
end
end
def handle_forgot_password
@view.navigate_to_password_reset
end
end
# View - Captures input and responds to display commands
class LoginView
attr_reader :presenter
def initialize
@presenter = LoginPresenter.new(self)
setup_ui
end
# Input accessors for Presenter
def username_input
@username_field.value
end
def password_input
@password_field.value
end
# Display command handlers
def show_error(message)
@error_label.text = message
@error_label.visible = true
end
def hide_error
@error_label.visible = false
end
def clear_password_field
@password_field.value = ""
end
def navigate_to_dashboard(user_id)
# Navigation logic
end
def navigate_to_password_reset
# Navigation logic
end
private
def setup_ui
@username_field = TextField.new
@password_field = TextField.new(secure: true)
@login_button = Button.new("Login")
@forgot_link = Link.new("Forgot Password?")
@error_label = Label.new(visible: false)
@login_button.on_click { presenter.handle_login_attempt }
@forgot_link.on_click { presenter.handle_forgot_password }
end
end
Rails Integration Approach adapts MVP to Rails' MVC architecture by introducing Presenter objects that handle presentation logic while Controllers remain thin request handlers. Controllers instantiate Presenters, pass them to Views, and the Views interact with Presenters for display logic.
# Controller acts as entry point
class UsersController < ApplicationController
def new
@presenter = UserRegistrationPresenter.new
end
def create
view_adapter = RailsViewAdapter.new(self)
presenter = UserRegistrationPresenter.new(view_adapter)
presenter.handle_registration(params[:user])
end
end
# Presenter handles presentation logic
class UserRegistrationPresenter
def initialize(view = nil)
@view = view
end
def form_fields
{
username: { label: "Username", type: :text, required: true },
email: { label: "Email", type: :email, required: true },
password: { label: "Password", type: :password, required: true },
password_confirmation: { label: "Confirm Password", type: :password, required: true }
}
end
def handle_registration(params)
username = params[:username].to_s.strip
email = params[:email].to_s.strip
password = params[:password]
password_confirmation = params[:password_confirmation]
errors = validate_registration(username, email, password, password_confirmation)
if errors.any?
@view.render_errors(errors)
return
end
user = User.create(username, email, password)
if user.persisted?
@view.redirect_to_dashboard(user.id)
else
@view.render_errors(["Registration failed. Please try again."])
end
end
private
def validate_registration(username, email, password, password_confirmation)
errors = []
errors << "Username required" if username.empty?
errors << "Email required" if email.empty?
errors << "Email format invalid" unless email.match?(/\A[^@\s]+@[^@\s]+\z/)
errors << "Password required" if password.to_s.empty?
errors << "Password must be at least 8 characters" if password.to_s.length < 8
errors << "Passwords do not match" if password != password_confirmation
errors << "Username already taken" if username_exists?(username)
errors
end
def username_exists?(username)
User.find_by_username(username).present?
end
end
# View adapter translates Rails rendering to View interface
class RailsViewAdapter
def initialize(controller)
@controller = controller
end
def render_errors(errors)
@controller.flash[:errors] = errors
@controller.redirect_to @controller.new_user_path
end
def redirect_to_dashboard(user_id)
@controller.redirect_to @controller.dashboard_path(user_id)
end
end
View Interface Design defines the contract between Presenter and View. Ruby's duck typing means this interface can be implicit—the Presenter simply calls methods it expects the View to implement. Explicit interface documentation or module inclusion helps clarify the contract.
# Explicit View interface definition
module UserFormView
def username_input
raise NotImplementedError
end
def email_input
raise NotImplementedError
end
def password_input
raise NotImplementedError
end
def show_validation_errors(errors)
raise NotImplementedError
end
def clear_form
raise NotImplementedError
end
def navigate_to_success_page(user_id)
raise NotImplementedError
end
end
class ConcreteUserFormView
include UserFormView
def initialize
@presenter = UserFormPresenter.new(self)
end
def username_input
@username_field.text
end
def email_input
@email_field.text
end
def password_input
@password_field.text
end
def show_validation_errors(errors)
@error_display.items = errors
@error_display.visible = true
end
def clear_form
@username_field.text = ""
@email_field.text = ""
@password_field.text = ""
end
def navigate_to_success_page(user_id)
Router.navigate("/users/#{user_id}/dashboard")
end
end
Presenter Instantiation Patterns determine when and how Presenters get created. Common approaches include View-creates-Presenter where Views instantiate their own Presenters during initialization, Controller-creates-Presenter where Controllers create Presenters and pass them to Views, and Factory-creates-Presenter where a factory object coordinates Presenter-View creation.
# View creates its own Presenter
class ProductListView
def initialize
@presenter = ProductListPresenter.new(self)
@presenter.load_products
end
def display_products(products)
@product_grid.items = products.map { |p| format_product(p) }
end
private
def format_product(product)
{ name: product.name, price: format_price(product.price) }
end
def format_price(price)
"$#{sprintf('%.2f', price)}"
end
end
# Controller creates Presenter and View
class ProductsController < ApplicationController
def index
presenter = ProductListPresenter.new(view_adapter)
presenter.load_products
render :index, locals: { presenter: presenter }
end
private
def view_adapter
RailsProductViewAdapter.new(self)
end
end
# Factory coordinates creation
class PresenterFactory
def self.create_product_list(view_context)
view = ProductListView.new(view_context)
presenter = ProductListPresenter.new(view)
view.presenter = presenter
[presenter, view]
end
end
Practical Examples
Real-world MVP implementations demonstrate how the pattern handles common application scenarios with varying complexity levels.
User Authentication Workflow shows MVP handling a multi-step process with validation, error handling, and state management across the authentication flow.
class AuthenticationPresenter
def initialize(view)
@view = view
@attempt_count = 0
@locked_until = nil
end
def handle_login_attempt
if account_locked?
seconds_remaining = (@locked_until - Time.now).to_i
@view.show_account_locked_message(seconds_remaining)
return
end
username = @view.username_input.strip
password = @view.password_input
validation_errors = validate_credentials(username, password)
if validation_errors.any?
@view.show_validation_errors(validation_errors)
return
end
user = User.authenticate(username, password)
if user
reset_attempt_count
session = create_session(user)
@view.store_session_token(session.token)
@view.navigate_to_dashboard(user.id)
else
handle_failed_attempt
end
end
def handle_logout
token = @view.retrieve_session_token
Session.destroy(token) if token
@view.clear_session_token
@view.navigate_to_login
end
def handle_password_reset_request
email = @view.email_input.strip
if email.empty?
@view.show_validation_errors(["Email required"])
return
end
user = User.find_by_email(email)
# Always show success to prevent email enumeration
@view.show_reset_email_sent_message
if user
token = PasswordResetToken.create(user)
PasswordResetMailer.send_reset_email(user, token)
end
end
private
def validate_credentials(username, password)
errors = []
errors << "Username required" if username.empty?
errors << "Password required" if password.empty?
errors
end
def account_locked?
@locked_until && @locked_until > Time.now
end
def handle_failed_attempt
@attempt_count += 1
if @attempt_count >= 3
@locked_until = Time.now + 300 # 5 minutes
@view.show_account_locked_message(300)
else
remaining = 3 - @attempt_count
@view.show_authentication_failed(remaining)
end
end
def reset_attempt_count
@attempt_count = 0
@locked_until = nil
end
def create_session(user)
Session.create(user_id: user.id, expires_at: Time.now + 3600)
end
end
Data Grid with Filtering and Sorting demonstrates MVP coordinating complex UI interactions involving multiple interdependent controls, data transformations, and display updates.
class ProductGridPresenter
def initialize(view)
@view = view
@products = []
@filters = { category: nil, min_price: nil, max_price: nil, in_stock: false }
@sort_column = :name
@sort_direction = :asc
end
def load_products
@products = Product.all
update_display
end
def handle_category_filter_change
@filters[:category] = @view.selected_category
update_display
end
def handle_price_range_change
min_price_text = @view.min_price_input
max_price_text = @view.max_price_input
@filters[:min_price] = parse_price(min_price_text)
@filters[:max_price] = parse_price(max_price_text)
update_display
end
def handle_stock_filter_change
@filters[:in_stock] = @view.in_stock_checkbox_checked
update_display
end
def handle_column_header_click(column)
if @sort_column == column
@sort_direction = @sort_direction == :asc ? :desc : :asc
else
@sort_column = column
@sort_direction = :asc
end
update_display
end
def handle_clear_filters
@filters = { category: nil, min_price: nil, max_price: nil, in_stock: false }
@view.reset_filter_controls
update_display
end
def handle_export_to_csv
filtered = apply_filters(@products)
sorted = apply_sorting(filtered)
csv_data = generate_csv(sorted)
@view.download_file("products.csv", csv_data)
end
private
def update_display
filtered = apply_filters(@products)
sorted = apply_sorting(filtered)
display_data = format_for_display(sorted)
@view.display_products(display_data)
@view.update_result_count(filtered.count)
@view.update_sort_indicators(@sort_column, @sort_direction)
end
def apply_filters(products)
filtered = products
if @filters[:category]
filtered = filtered.select { |p| p.category == @filters[:category] }
end
if @filters[:min_price]
filtered = filtered.select { |p| p.price >= @filters[:min_price] }
end
if @filters[:max_price]
filtered = filtered.select { |p| p.price <= @filters[:max_price] }
end
if @filters[:in_stock]
filtered = filtered.select { |p| p.stock_quantity > 0 }
end
filtered
end
def apply_sorting(products)
products.sort do |a, b|
comparison = compare_values(a.send(@sort_column), b.send(@sort_column))
@sort_direction == :asc ? comparison : -comparison
end
end
def compare_values(a, b)
return 0 if a == b
return -1 if a.nil?
return 1 if b.nil?
a <=> b
end
def format_for_display(products)
products.map do |product|
{
id: product.id,
name: product.name,
category: product.category,
price: format_currency(product.price),
stock: format_stock(product.stock_quantity),
stock_class: stock_css_class(product.stock_quantity)
}
end
end
def format_currency(price)
"$#{sprintf('%.2f', price)}"
end
def format_stock(quantity)
quantity > 0 ? "#{quantity} in stock" : "Out of stock"
end
def stock_css_class(quantity)
return "out-of-stock" if quantity == 0
return "low-stock" if quantity < 10
"in-stock"
end
def parse_price(text)
return nil if text.nil? || text.strip.empty?
text.gsub(/[^\d.]/, '').to_f
end
def generate_csv(products)
require 'csv'
CSV.generate do |csv|
csv << ["Name", "Category", "Price", "Stock"]
products.each do |product|
csv << [product.name, product.category, product.price, product.stock_quantity]
end
end
end
end
Form with Dynamic Field Dependencies illustrates MVP managing complex form behavior where field visibility and validation rules change based on user selections.
class AccountSettingsPresenter
def initialize(view)
@view = view
@user = nil
end
def load_user_settings(user_id)
@user = User.find(user_id)
populate_initial_values
end
def handle_account_type_change
account_type = @view.account_type_selection
case account_type
when :personal
@view.hide_business_fields
@view.show_personal_fields
when :business
@view.show_business_fields
@view.hide_personal_fields
when :enterprise
@view.show_business_fields
@view.show_enterprise_fields
end
update_feature_availability(account_type)
end
def handle_notification_preference_change
email_enabled = @view.email_notifications_enabled
sms_enabled = @view.sms_notifications_enabled
if email_enabled
@view.enable_email_frequency_control
else
@view.disable_email_frequency_control
end
if sms_enabled
@view.enable_phone_number_field
@view.require_phone_number
else
@view.disable_phone_number_field
@view.remove_phone_number_requirement
end
end
def handle_save_settings
settings = gather_settings_from_view
validation_errors = validate_settings(settings)
if validation_errors.any?
@view.display_validation_errors(validation_errors)
return
end
begin
update_user_settings(settings)
@view.show_success_message("Settings saved successfully")
@view.clear_validation_errors
rescue => error
@view.show_error_message("Failed to save settings: #{error.message}")
end
end
private
def populate_initial_values
@view.set_username(@user.username)
@view.set_email(@user.email)
@view.set_account_type(@user.account_type)
@view.set_email_notifications_enabled(@user.email_notifications)
@view.set_sms_notifications_enabled(@user.sms_notifications)
if @user.account_type == :business || @user.account_type == :enterprise
@view.set_company_name(@user.company_name)
@view.set_tax_id(@user.tax_id)
end
handle_account_type_change
handle_notification_preference_change
end
def gather_settings_from_view
{
username: @view.username_input,
email: @view.email_input,
account_type: @view.account_type_selection,
email_notifications: @view.email_notifications_enabled,
sms_notifications: @view.sms_notifications_enabled,
phone_number: @view.phone_number_input,
company_name: @view.company_name_input,
tax_id: @view.tax_id_input
}
end
def validate_settings(settings)
errors = []
errors << "Username required" if settings[:username].strip.empty?
errors << "Email required" if settings[:email].strip.empty?
errors << "Invalid email format" unless valid_email?(settings[:email])
if settings[:account_type] == :business || settings[:account_type] == :enterprise
errors << "Company name required" if settings[:company_name].to_s.strip.empty?
end
if settings[:sms_notifications]
errors << "Phone number required for SMS notifications" if settings[:phone_number].to_s.strip.empty?
errors << "Invalid phone number format" unless valid_phone?(settings[:phone_number])
end
errors
end
def update_user_settings(settings)
@user.update_attributes(
username: settings[:username],
email: settings[:email],
account_type: settings[:account_type],
email_notifications: settings[:email_notifications],
sms_notifications: settings[:sms_notifications],
phone_number: settings[:phone_number],
company_name: settings[:company_name],
tax_id: settings[:tax_id]
)
end
def update_feature_availability(account_type)
features = available_features_for(account_type)
@view.display_available_features(features)
end
def available_features_for(account_type)
case account_type
when :personal
["Basic reporting", "Email support"]
when :business
["Advanced reporting", "Priority support", "API access"]
when :enterprise
["Custom reporting", "Dedicated support", "API access", "SSO", "Audit logs"]
end
end
def valid_email?(email)
email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
end
def valid_phone?(phone)
phone.gsub(/\D/, '').length >= 10
end
end
Common Patterns
MVP implementations follow several established patterns that address recurring design challenges in different application contexts.
Passive View Pattern represents the most strict interpretation of MVP where Views contain absolutely minimal logic, deferring all decisions to Presenters. The View acts as a pure rendering surface that displays exactly what the Presenter instructs. This pattern maximizes testability because Presenters can be fully tested with mock Views, but requires more boilerplate code as Presenters must explicitly command every View update.
class PassiveViewPresenter
def initialize(view)
@view = view
@timer = 0
end
def start_countdown(seconds)
@timer = seconds
update_timer_display
schedule_next_tick
end
private
def update_timer_display
minutes = @timer / 60
seconds = @timer % 60
@view.set_timer_text(format("%02d:%02d", minutes, seconds))
if @timer == 0
@view.set_timer_color(:red)
@view.enable_reset_button
@view.disable_start_button
else
@view.set_timer_color(:black)
@view.disable_reset_button
end
end
def schedule_next_tick
return if @timer == 0
sleep(1)
@timer -= 1
update_timer_display
schedule_next_tick
end
end
Supervising Controller Pattern allows Views to handle simple data binding directly while Presenters manage complex interactions and coordination. The View can display Model properties without Presenter intervention, but user actions and complex display logic route through the Presenter. This pattern reduces boilerplate for straightforward display scenarios while maintaining Presenter control over business logic.
class SupervisingPresenter
attr_reader :product
def initialize(view)
@view = view
@product = nil
end
def load_product(id)
@product = Product.find(id)
# View can bind directly to @product for simple display
end
def handle_add_to_cart
quantity = @view.quantity_input.to_i
if quantity < 1
@view.show_error("Quantity must be at least 1")
return
end
if quantity > @product.available_quantity
@view.show_error("Only #{@product.available_quantity} available")
return
end
cart = ShoppingCart.current
cart.add_item(@product, quantity)
@view.show_success("Added to cart")
@view.update_cart_count(cart.item_count)
end
def handle_favorite_toggle
user = CurrentUser.get
if @product.favorited_by?(user)
@product.remove_favorite(user)
@view.set_favorite_button_state(:unfavorited)
else
@product.add_favorite(user)
@view.set_favorite_button_state(:favorited)
end
end
end
Presentation Model Pattern creates a dedicated object that wraps Model data specifically for display purposes. The Presentation Model exposes properties formatted for the View, handles display-specific calculations, and manages UI state. The Presenter coordinates between the domain Model and Presentation Model, while the View binds to the Presentation Model.
class OrderPresentationModel
attr_reader :order
def initialize(order)
@order = order
end
def display_id
"ORDER-#{order.id.to_s.rjust(8, '0')}"
end
def display_status
{
pending: "Pending",
processing: "Processing",
shipped: "Shipped",
delivered: "Delivered",
cancelled: "Cancelled"
}[order.status]
end
def status_color
{
pending: :yellow,
processing: :blue,
shipped: :purple,
delivered: :green,
cancelled: :red
}[order.status]
end
def display_date
order.created_at.strftime("%B %d, %Y")
end
def display_total
"$#{sprintf('%.2f', order.total)}"
end
def display_items
order.items.map do |item|
{
name: item.product.name,
quantity: item.quantity,
price: "$#{sprintf('%.2f', item.price)}",
subtotal: "$#{sprintf('%.2f', item.quantity * item.price)}"
}
end
end
def can_cancel?
[:pending, :processing].include?(order.status)
end
def can_track?
[:shipped].include?(order.status)
end
def tracking_url
return nil unless can_track?
"https://tracking.example.com/#{order.tracking_number}"
end
end
class OrderPresenter
def initialize(view)
@view = view
end
def load_order(id)
order = Order.find(id)
@presentation_model = OrderPresentationModel.new(order)
@view.bind_to_presentation_model(@presentation_model)
end
def handle_cancel_order
unless @presentation_model.can_cancel?
@view.show_error("Order cannot be cancelled in current status")
return
end
@presentation_model.order.cancel
load_order(@presentation_model.order.id) # Reload to update display
@view.show_success("Order cancelled")
end
end
Composition Pattern breaks complex Views into smaller View-Presenter pairs that compose together. Each sub-view manages its own display logic through a dedicated Presenter, while a parent Presenter coordinates communication between sub-presenters. This pattern manages complexity in applications with intricate UI structures.
class ProductImageGalleryPresenter
def initialize(view)
@view = view
@images = []
@current_index = 0
end
def load_images(product_id)
@images = ProductImage.where(product_id: product_id).order(:position)
update_display
end
def handle_next_image
@current_index = (@current_index + 1) % @images.length
update_display
end
def handle_previous_image
@current_index = (@current_index - 1) % @images.length
update_display
end
def handle_thumbnail_click(index)
@current_index = index
update_display
end
private
def update_display
return if @images.empty?
current_image = @images[@current_index]
@view.display_main_image(current_image.url)
@view.highlight_thumbnail(@current_index)
@view.show_image_counter(@current_index + 1, @images.length)
end
end
class ProductPricingPresenter
def initialize(view)
@view = view
end
def load_pricing(product)
base_price = product.price
discount = product.current_discount
if discount
final_price = base_price * (1 - discount.percentage)
@view.display_original_price(format_price(base_price))
@view.display_discount_percentage("#{(discount.percentage * 100).to_i}% OFF")
@view.display_final_price(format_price(final_price))
@view.show_discount_badge
else
@view.display_final_price(format_price(base_price))
@view.hide_discount_badge
end
end
private
def format_price(price)
"$#{sprintf('%.2f', price)}"
end
end
class ProductDetailPresenter
def initialize(view, image_presenter, pricing_presenter, reviews_presenter)
@view = view
@image_presenter = image_presenter
@pricing_presenter = pricing_presenter
@reviews_presenter = reviews_presenter
end
def load_product(id)
@product = Product.find(id)
@view.display_product_name(@product.name)
@view.display_product_description(@product.description)
@image_presenter.load_images(@product.id)
@pricing_presenter.load_pricing(@product)
@reviews_presenter.load_reviews(@product.id)
end
def handle_add_to_cart
# Coordinate cart addition
end
end
Testing Approaches
MVP's architecture makes presentation logic testable without UI frameworks through strategic use of test doubles and focused unit tests for each component.
Presenter Unit Testing with Mock Views forms the core testing strategy. Tests instantiate Presenters with mock View objects that verify the Presenter issues correct display commands in response to user actions.
require 'minitest/autorun'
class MockLoginView
attr_reader :error_message, :navigated_to, :cleared_password
def initialize
@username = ""
@password = ""
@error_message = nil
@navigated_to = nil
@cleared_password = false
end
def set_credentials(username, password)
@username = username
@password = password
end
def username_input
@username
end
def password_input
@password
end
def show_error(message)
@error_message = message
end
def navigate_to_dashboard(user_id)
@navigated_to = [:dashboard, user_id]
end
def clear_password_field
@cleared_password = true
end
def hide_error
@error_message = nil
end
end
class LoginPresenterTest < Minitest::Test
def setup
@view = MockLoginView.new
@presenter = LoginPresenter.new(@view)
end
def test_shows_error_when_username_empty
@view.set_credentials("", "password123")
@presenter.handle_login_attempt
assert_equal "Username required", @view.error_message
assert_nil @view.navigated_to
end
def test_shows_error_when_password_empty
@view.set_credentials("john", "")
@presenter.handle_login_attempt
assert_equal "Password required", @view.error_message
assert_nil @view.navigated_to
end
def test_navigates_to_dashboard_on_valid_credentials
@view.set_credentials("john", "password123")
User.stub :authenticate, User.new(id: 42, username: "john") do
@presenter.handle_login_attempt
assert_nil @view.error_message
assert_equal [:dashboard, 42], @view.navigated_to
end
end
def test_shows_error_and_clears_password_on_invalid_credentials
@view.set_credentials("john", "wrongpassword")
User.stub :authenticate, nil do
@presenter.handle_login_attempt
assert_equal "Invalid username or password", @view.error_message
assert @view.cleared_password
end
end
end
Model Testing Isolation verifies business logic independently from presentation concerns. Model tests focus on data operations, validation rules, and domain logic without involving Views or Presenters.
class UserModelTest < Minitest::Test
def test_authenticates_with_correct_password
user = User.create("testuser", "test@example.com", "password123")
authenticated = User.authenticate("testuser", "password123")
assert_equal user.id, authenticated.id
end
def test_returns_nil_for_incorrect_password
User.create("testuser", "test@example.com", "password123")
authenticated = User.authenticate("testuser", "wrongpassword")
assert_nil authenticated
end
def test_returns_nil_for_nonexistent_user
authenticated = User.authenticate("nonexistent", "password123")
assert_nil authenticated
end
def test_updates_profile_attributes
user = User.create("testuser", "test@example.com", "password123")
user.update_profile(username: "newusername", email: "new@example.com")
assert_equal "newusername", user.username
assert_equal "new@example.com", user.email
end
end
Integration Testing Strategy verifies that Presenters, Views, and Models work together correctly. Integration tests use real implementations but may stub external dependencies like databases or APIs.
class LoginIntegrationTest < Minitest::Test
def setup
@database = InMemoryDatabase.new
User.database = @database
end
def test_complete_login_flow
# Create user in database
user = User.create("integrationtest", "test@example.com", "password123")
# Create real view (or test double that simulates real behavior)
view = TestLoginView.new
view.enter_username("integrationtest")
view.enter_password("password123")
# Create presenter with real view
presenter = LoginPresenter.new(view)
# Execute login
presenter.handle_login_attempt
# Verify complete flow
assert view.error_hidden?
assert_equal user.id, view.current_dashboard_user_id
end
def test_multiple_failed_attempts_lock_account
User.create("locktest", "test@example.com", "password123")
view = TestLoginView.new
presenter = LoginPresenter.new(view)
# Attempt 1
view.enter_credentials("locktest", "wrong")
presenter.handle_login_attempt
refute view.shows_account_locked?
# Attempt 2
view.enter_credentials("locktest", "wrong")
presenter.handle_login_attempt
refute view.shows_account_locked?
# Attempt 3
view.enter_credentials("locktest", "wrong")
presenter.handle_login_attempt
assert view.shows_account_locked?
end
end
View Spy Pattern creates test doubles that record all method calls for later verification, allowing tests to assert the sequence and parameters of View interactions.
class SpyView
attr_reader :method_calls
def initialize
@method_calls = []
@return_values = {}
end
def set_return_value(method_name, value)
@return_values[method_name] = value
end
def method_missing(method_name, *args)
@method_calls << [method_name, args]
@return_values[method_name]
end
def received?(method_name, *expected_args)
@method_calls.any? do |call_method, call_args|
call_method == method_name &&
(expected_args.empty? || call_args == expected_args)
end
end
def call_count(method_name)
@method_calls.count { |m, _| m == method_name }
end
end
class ProductPresenterTest < Minitest::Test
def test_loading_product_updates_all_view_elements
view = SpyView.new
presenter = ProductPresenter.new(view)
product = Product.new(id: 1, name: "Test Product", price: 29.99)
Product.stub :find, product do
presenter.load_product(1)
end
assert view.received?(:display_product_name, "Test Product")
assert view.received?(:display_price, "$29.99")
assert view.received?(:display_availability, "In Stock")
assert_equal 1, view.call_count(:display_product_name)
end
end
Common Pitfalls
Developers implementing MVP frequently encounter specific problems that stem from misunderstanding the pattern's principles or making expedient design choices that compromise its benefits.
Presenter-View Coupling Through Concrete Types occurs when Presenters reference specific View implementation classes rather than interfaces. This prevents testing Presenters with mock Views and creates tight coupling between presentation and UI framework code.
# Problematic: Presenter coupled to concrete View
class BadPresenter
def initialize(view)
raise ArgumentError unless view.is_a?(ConcreteLoginView)
@view = view
end
def handle_submit
# Directly accessing View internals
username = @view.username_textfield.text
password = @view.password_textfield.text
end
end
# Correct: Presenter depends on View interface
class GoodPresenter
def initialize(view)
@view = view
end
def handle_submit
username = @view.username_input
password = @view.password_input
end
end
Business Logic Leaking into Presenters happens when developers place domain rules and calculations in Presenters instead of Models. Presenters should coordinate and format, not implement business logic. This mistake makes business rules difficult to test and reuse across different presentation layers.
# Problematic: Business logic in Presenter
class BadOrderPresenter
def handle_submit_order
items = @view.cart_items
# Business logic that belongs in Model
total = items.sum { |i| i.price * i.quantity }
tax = total * 0.08
shipping = total > 50 ? 0 : 5.99
final_total = total + tax + shipping
# Discount calculation in Presenter
if @view.coupon_code == "SAVE10"
final_total *= 0.9
end
@view.display_total(final_total)
end
end
# Correct: Business logic in Model
class GoodOrderPresenter
def handle_submit_order
items = @view.cart_items
coupon_code = @view.coupon_code
order = Order.new(items: items, coupon_code: coupon_code)
order.calculate_totals
@view.display_subtotal(order.subtotal)
@view.display_tax(order.tax)
@view.display_shipping(order.shipping)
@view.display_total(order.total)
end
end
God Presenters Managing Multiple Views violates the single-presenter-per-view principle. When one Presenter manages multiple Views or complex view hierarchies, it accumulates too many responsibilities and becomes difficult to test and maintain.
# Problematic: Single Presenter managing multiple views
class GodPresenter
def initialize(header_view, sidebar_view, content_view, footer_view)
@header_view = header_view
@sidebar_view = sidebar_view
@content_view = content_view
@footer_view = footer_view
end
def handle_navigation(page)
@header_view.highlight_menu_item(page)
@sidebar_view.load_sidebar_content(page)
@content_view.load_page_content(page)
@footer_view.update_breadcrumbs(page)
end
end
# Correct: Each View has dedicated Presenter
class HeaderPresenter
def initialize(view)
@view = view
end
def navigate_to(page)
@view.highlight_menu_item(page)
end
end
class ContentPresenter
def initialize(view)
@view = view
end
def navigate_to(page)
@view.load_page_content(page)
end
end
# Coordinator manages multiple Presenters
class NavigationCoordinator
def initialize(header_presenter, sidebar_presenter, content_presenter)
@header_presenter = header_presenter
@sidebar_presenter = sidebar_presenter
@content_presenter = content_presenter
end
def navigate_to(page)
@header_presenter.navigate_to(page)
@sidebar_presenter.navigate_to(page)
@content_presenter.navigate_to(page)
end
end
View Intelligence Through Helper Methods introduces logic into Views disguised as formatting helpers. Views should only perform trivial formatting; any conditional logic or calculations should occur in Presenters.
# Problematic: View contains logic in helper methods
class ProductView
def display_product(product)
@name.text = product.name
@price.text = format_price_with_discount(product)
@availability.text = availability_text(product)
end
private
# Logic that belongs in Presenter
def format_price_with_discount(product)
if product.discount_percentage > 0
original = product.price
discounted = product.price * (1 - product.discount_percentage)
"Was #{format_currency(original)}, now #{format_currency(discounted)}"
else
format_currency(product.price)
end
end
def availability_text(product)
if product.quantity > 10
"In Stock"
elsif product.quantity > 0
"Only #{product.quantity} left!"
else
"Out of Stock"
end
end
end
# Correct: Presenter provides formatted data
class ProductPresenter
def load_product(id)
product = Product.find(id)
@view.display_name(product.name)
@view.display_price(format_price(product))
@view.display_availability(availability_text(product))
end
private
def format_price(product)
if product.discount_percentage > 0
original = format_currency(product.price)
discounted = format_currency(product.discounted_price)
"Was #{original}, now #{discounted}"
else
format_currency(product.price)
end
end
def availability_text(product)
case product.quantity
when 0 then "Out of Stock"
when 1..10 then "Only #{product.quantity} left!"
else "In Stock"
end
end
def format_currency(amount)
"$#{sprintf('%.2f', amount)}"
end
end
Insufficient Abstraction in View Interface happens when View interfaces expose implementation details or provide overly specific methods. View interfaces should be abstract enough to accommodate different UI implementations while specific enough to be testable.
# Problematic: View interface exposes implementation details
class TightlyCoupledView
def get_textfield_value(textfield_id)
# Exposes that View uses textfields
end
def set_label_text_and_color(label_id, text, color)
# Too specific to implementation
end
end
# Correct: Abstract View interface
class WellAbstractedView
def username_input
# Abstract accessor for input value
end
def display_error(message)
# Abstract display command
end
def clear_form
# Abstract operation
end
end
Forgetting View State Management creates inconsistencies when Presenters fail to track View state or make assumptions about current display. Presenters should either explicitly manage View state or query it before making updates.
# Problematic: Assuming View state
class StatelessPresenter
def handle_next_page
# Assumes current page without tracking
@view.show_page(@current_page + 1)
end
end
# Correct: Explicit state management
class StatefulPresenter
def initialize(view)
@view = view
@current_page = 1
@total_pages = 0
end
def load_content
@total_pages = calculate_total_pages
show_page(1)
end
def handle_next_page
return if @current_page >= @total_pages
show_page(@current_page + 1)
end
private
def show_page(page_number)
@current_page = page_number
@view.display_page_content(fetch_page_content(page_number))
@view.update_page_indicator(@current_page, @total_pages)
@view.set_previous_enabled(@current_page > 1)
@view.set_next_enabled(@current_page < @total_pages)
end
end
Reference
MVP Component Responsibilities
| Component | Responsibilities | Should NOT |
|---|---|---|
| Model | Business logic, data operations, domain rules, validation, persistence | Reference View, contain display formatting, manage UI state |
| View | Capture user input, display data, trigger Presenter methods on user actions | Process input, validate data, query Model, make business decisions |
| Presenter | Coordinate View and Model, handle user actions, update View display, format data for display | Implement business logic, directly access UI framework, contain domain rules |
Presenter-View Interaction Patterns
| Pattern | View Responsibility | Presenter Responsibility | When to Use |
|---|---|---|---|
| Passive View | Provide input accessors, execute display commands | All presentation logic, all display decisions | Maximum testability, complex UI logic |
| Supervising Controller | Simple data binding to Model properties | Complex interactions, coordination | Reduce boilerplate, simpler displays |
| Presentation Model | Bind to Presentation Model properties | Create and update Presentation Model | Complex display formatting, computed properties |
Common View Interface Methods
| Method Category | Example Methods | Purpose |
|---|---|---|
| Input Accessors | username_input, selected_category, checkbox_checked | Retrieve user input values |
| Display Commands | display_error, show_success, update_list | Instruct View to update display |
| State Commands | enable_button, hide_panel, clear_form | Change View component states |
| Navigation Commands | navigate_to_dashboard, close_dialog | Trigger navigation or modal changes |
Testing Strategy Matrix
| Test Type | What to Test | Implementation Approach |
|---|---|---|
| Presenter Unit Tests | Presentation logic, View command sequencing, input validation | Mock View, real or stub Model |
| Model Unit Tests | Business logic, data operations, domain rules | No View or Presenter involvement |
| Integration Tests | Component interaction, data flow, complete workflows | Real components with test database |
| View Tests | Input capture, display rendering, event binding | Minimal logic verification only |
Design Decision Guide
| Consider MVP When | Consider Alternatives When |
|---|---|
| Complex presentation logic requiring thorough testing | Simple CRUD applications with minimal logic |
| Multiple presentation layers sharing same Models | Single UI implementation unlikely to change |
| Team prioritizes testability and maintainability | Rapid prototyping with disposable code |
| UI logic needs isolation from business rules | Framework strongly couples View and data |
| Application has complex user interaction workflows | Automatic data binding available and preferred |
Presenter Lifecycle Methods
| Method Type | Example | Purpose |
|---|---|---|
| Initialization | initialize(view), setup | Configure Presenter, store View reference |
| Data Loading | load_user(id), fetch_products | Retrieve Model data, update View |
| Event Handlers | handle_submit, handle_cancel | Process user actions |
| View Updates | update_display, refresh_list | Coordinate View refreshes |
| Cleanup | destroy, cleanup | Release resources, clear references |
Model-Presenter-View Data Flow
| Flow Direction | Example Scenario | Implementation |
|---|---|---|
| User to Model | Form submission | View captures input, Presenter validates, Presenter updates Model |
| Model to User | Display update | Model changes, Presenter queries Model, Presenter commands View display |
| User to User | UI state change | View captures action, Presenter updates View state |
Common Presenter Methods
| Method Pattern | Signature Example | Use Case |
|---|---|---|
| Event Handler | handle_button_click, on_selection_changed | Respond to user interactions |
| Data Formatter | format_currency(amount), format_date(date) | Transform data for display |
| Validator | validate_input(data), check_form_complete | Verify user input |
| View Updater | update_user_display, refresh_list | Synchronize View with state |
| Model Coordinator | save_changes, load_data | Orchestrate Model operations |