CrackedRuby CrackedRuby

Model-View-Presenter (MVP)

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