Overview
Model-View-ViewModel (MVVM) is an architectural pattern that separates user interface code from business logic and data access code. The pattern emerged from Microsoft's development of Windows Presentation Foundation (WPF) and Silverlight, where data binding capabilities made the pattern practical. MVVM extends the Model-View-Controller pattern by introducing a ViewModel layer that acts as an intermediary between the View and Model.
The pattern divides application architecture into three components. The Model represents domain data and business logic. The View displays information to users and captures user input. The ViewModel exposes data from the Model in a format suitable for the View and handles presentation logic. Unlike traditional MVC where Controllers handle input, MVVM relies on data binding to synchronize the View and ViewModel automatically.
MVVM addresses the problem of tight coupling between user interface code and business logic. In applications without clear separation, UI code often contains business rules, making testing difficult and changes expensive. MVVM enforces separation by preventing direct references between Views and Models. The View knows only about the ViewModel, and the ViewModel knows only about the Model. This unidirectional dependency chain enables independent development and testing of each layer.
Data binding forms the core mechanism that makes MVVM practical. When a ViewModel property changes, the View updates automatically through the binding system. When a user interacts with the View, bound properties in the ViewModel update automatically. This bidirectional synchronization eliminates the manual glue code that couples Views to their data sources in other patterns.
# MVVM component interaction conceptual example
class UserViewModel
attr_reader :user
def initialize(user)
@user = user
@observers = []
end
def display_name
"#{user.first_name} #{user.last_name}"
end
def update_email(new_email)
user.email = new_email
notify_observers(:email_changed)
end
private
def notify_observers(event)
@observers.each { |observer| observer.call(event) }
end
end
MVVM particularly suits applications with complex user interfaces, frequent UI changes, or requirements for extensive automated testing. The pattern enables UI designers to work on Views while developers focus on ViewModels and Models. Teams can modify the View presentation without touching business logic, and vice versa.
Key Principles
Separation of Concerns divides the application into distinct layers with specific responsibilities. The Model layer handles data persistence, validation, and business rules. The View layer renders the user interface and captures user input. The ViewModel layer transforms Model data into formats suitable for display and translates user actions into Model operations. Each layer can change independently without affecting others when the contracts between layers remain stable.
Data Binding synchronizes data between Views and ViewModels without explicit code. Property binding connects UI elements to ViewModel properties. When the ViewModel property changes, the UI updates automatically. When the user modifies a bound UI element, the ViewModel property updates automatically. This eliminates the manual update code that clutters traditional UI implementations. Data binding requires an observable property mechanism where properties notify observers when values change.
Commands encapsulate user actions as objects rather than event handlers. Instead of Views calling ViewModel methods directly, Views execute commands exposed by ViewModels. Commands contain the action logic and determine when they can execute. A SaveCommand might disable itself when data is invalid, causing the bound UI button to disable automatically. This separates the decision logic from the UI event handling.
# Command pattern in MVVM
class Command
def initialize(&action)
@action = action
@can_execute = true
end
def execute
@action.call if can_execute?
end
def can_execute?
@can_execute
end
def set_can_execute(value)
@can_execute = value
end
end
class ArticleViewModel
attr_reader :save_command
def initialize(article)
@article = article
@save_command = Command.new { save_article }
update_command_state
end
def title=(value)
@article.title = value
update_command_state
end
private
def save_article
@article.save
end
def update_command_state
@save_command.set_can_execute(@article.valid?)
end
end
Testability improves because ViewModels contain no references to UI frameworks. Tests can instantiate ViewModels, set properties, execute commands, and verify results without rendering any UI components. This enables fast, reliable unit tests for presentation logic. The Model layer remains testable through traditional unit tests. Only the View requires UI testing, which minimizes the need for slow, brittle UI automation tests.
Presentation Logic resides in ViewModels rather than Views or Models. Logic for formatting data for display, aggregating data from multiple Models, handling user input validation, and managing UI state belongs in the ViewModel. Views contain only the minimal code required to render bound data and route user interactions to commands. Models contain only domain logic and data persistence code.
View Ignorance means ViewModels contain no references to Views or UI frameworks. ViewModels expose properties and commands but never call View methods or import View classes. This enables ViewModels to work with any View implementation, making it possible to swap UI technologies without changing ViewModels. The pattern maintains this separation through observer patterns and data binding rather than direct method calls.
# ViewModel with no View dependencies
class ProductListViewModel
attr_reader :products, :selected_product
def initialize(product_repository)
@product_repository = product_repository
@products = []
@selected_product = nil
@observers = {}
load_products
end
def select_product(product)
@selected_product = product
notify(:product_selected, product)
end
def filter_by_category(category)
@products = @product_repository.find_by_category(category)
notify(:products_changed, @products)
end
def add_observer(event, &block)
@observers[event] ||= []
@observers[event] << block
end
private
def load_products
@products = @product_repository.all
notify(:products_changed, @products)
end
def notify(event, data)
return unless @observers[event]
@observers[event].each { |observer| observer.call(data) }
end
end
Model Independence ensures ViewModels aggregate and transform Model data but never contain business rules. When a business rule changes, only the Model changes. ViewModels adapt to Model changes but don't duplicate domain logic. This prevents the logic duplication that makes applications fragile and difficult to maintain.
Ruby Implementation
Ruby lacks native data binding frameworks found in WPF or Silverlight, but Ruby's metaprogramming capabilities enable MVVM implementations. The Observer pattern combined with method_missing or define_method creates observable properties. Ruby's blocks and procs implement command objects cleanly. Libraries like Glimmer provide data binding for desktop Ruby applications.
Observable Properties notify observers when values change. Ruby's attr_accessor creates simple getters and setters, but observable properties require notification logic. A custom implementation overrides the setter to trigger notifications.
module Observable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def observable_attr(*attributes)
attributes.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
old_value = instance_variable_get("@#{attr}")
return if old_value == value
instance_variable_set("@#{attr}", value)
notify_observers(attr, old_value, value)
end
end
end
end
def add_observer(&block)
@observers ||= []
@observers << block
end
def notify_observers(property, old_value, new_value)
return unless @observers
@observers.each do |observer|
observer.call(property, old_value, new_value)
end
end
end
class UserViewModel
include Observable
observable_attr :first_name, :last_name, :email
def initialize(user)
@user = user
@first_name = user.first_name
@last_name = user.last_name
@email = user.email
end
def full_name
"#{first_name} #{last_name}"
end
def save
@user.first_name = first_name
@user.last_name = last_name
@user.email = email
@user.save
end
end
# Usage with observer
view_model = UserViewModel.new(user)
view_model.add_observer do |property, old_val, new_val|
puts "#{property} changed from #{old_val} to #{new_val}"
end
view_model.first_name = "Jane"
# Output: first_name changed from John to Jane
Rails and MVVM differs from traditional MVVM because Rails views use server-side rendering rather than client-side data binding. Rails controllers act partially as ViewModels by preparing data for views, but they also handle routing and HTTP concerns. A pure MVVM approach in Rails extracts presentation logic into explicit ViewModel objects.
# Rails ViewModel (Presenter pattern)
class ArticleViewModel
delegate :title, :author, :created_at, to: :@article
def initialize(article, current_user)
@article = article
@current_user = current_user
end
def formatted_date
created_at.strftime("%B %d, %Y")
end
def can_edit?
@current_user.admin? || @article.author == @current_user
end
def summary
return @article.body if @article.body.length <= 100
"#{@article.body[0..97]}..."
end
def tag_list
@article.tags.map(&:name).join(", ")
end
end
# In controller
class ArticlesController < ApplicationController
def show
article = Article.find(params[:id])
@view_model = ArticleViewModel.new(article, current_user)
end
end
# In view
<h1><%= @view_model.title %></h1>
<p>By <%= @view_model.author.name %> on <%= @view_model.formatted_date %></p>
<% if @view_model.can_edit? %>
<%= link_to "Edit", edit_article_path(@view_model) %>
<% end %>
<p><%= @view_model.summary %></p>
<p>Tags: <%= @view_model.tag_list %></p>
Desktop Ruby Applications benefit more directly from MVVM. Glimmer provides data binding for SWT, GTK, Tk, and other GUI toolkits. The framework handles property synchronization and command binding automatically.
# Glimmer DSL for SWT example
require 'glimmer-dsl-swt'
class ContactViewModel
attr_accessor :first_name, :last_name, :email
def initialize
@first_name = ""
@last_name = ""
@email = ""
end
def full_name
"#{first_name} #{last_name}".strip
end
def valid?
!first_name.empty? && !last_name.empty? && email.include?("@")
end
end
include Glimmer
view_model = ContactViewModel.new
shell {
text "Contact Form"
composite {
grid_layout 2, false
label { text "First Name:" }
text {
layout_data :fill, :center, true, false
text <=> [view_model, :first_name]
}
label { text "Last Name:" }
text {
layout_data :fill, :center, true, false
text <=> [view_model, :last_name]
}
label { text "Email:" }
text {
layout_data :fill, :center, true, false
text <=> [view_model, :email]
}
button {
text "Save"
enabled <= [view_model, :valid?]
on_widget_selected { puts "Saving: #{view_model.full_name}" }
}
}
}.open
Sinatra with Client-Side MVVM separates concerns by using Sinatra for API endpoints and a JavaScript MVVM framework for the UI. Ruby ViewModels serialize Model data to JSON for API responses. This approach works well for single-page applications.
require 'sinatra'
require 'json'
class TaskViewModel
def initialize(task)
@task = task
end
def to_json(*args)
{
id: @task.id,
title: @task.title,
completed: @task.completed?,
priority: priority_label,
due_date: formatted_due_date,
can_complete: can_complete?
}.to_json(*args)
end
private
def priority_label
case @task.priority
when 1 then "High"
when 2 then "Medium"
when 3 then "Low"
else "None"
end
end
def formatted_due_date
return nil unless @task.due_date
@task.due_date.strftime("%Y-%m-%d")
end
def can_complete?
!@task.completed? && @task.assigned_to.present?
end
end
get '/api/tasks' do
content_type :json
tasks = Task.all
tasks.map { |task| TaskViewModel.new(task) }.to_json
end
get '/api/tasks/:id' do
content_type :json
task = Task.find(params[:id])
TaskViewModel.new(task).to_json
end
Testing Ruby ViewModels requires no special frameworks since ViewModels contain no UI dependencies. Standard testing tools like RSpec or Minitest work directly with ViewModels.
require 'rspec'
RSpec.describe ArticleViewModel do
let(:user) { double('User', admin?: false) }
let(:article) do
double('Article',
title: "Test Article",
body: "This is a test article body that is quite long.",
created_at: Time.new(2025, 1, 15),
author: user,
tags: [double(name: "Ruby"), double(name: "Testing")]
)
end
subject { ArticleViewModel.new(article, user) }
describe '#formatted_date' do
it 'formats date correctly' do
expect(subject.formatted_date).to eq("January 15, 2025")
end
end
describe '#can_edit?' do
context 'when user is author' do
let(:article) { double('Article', author: user) }
it 'returns true' do
expect(subject.can_edit?).to be true
end
end
context 'when user is not author' do
let(:different_user) { double('User', admin?: false) }
let(:article) { double('Article', author: different_user) }
it 'returns false' do
expect(subject.can_edit?).to be false
end
end
end
describe '#tag_list' do
it 'joins tag names with commas' do
expect(subject.tag_list).to eq("Ruby, Testing")
end
end
end
Design Considerations
Pattern Selection depends on application requirements, team expertise, and platform capabilities. MVVM suits applications with complex UIs, frequent UI changes, or strong testing requirements. The pattern adds architectural overhead that small applications may not need. MVC often suffices for server-rendered web applications where data binding provides limited value. MVP works better than MVVM when the platform lacks data binding support and implementing binding manually would add unnecessary complexity.
Data Binding Complexity increases with MVVM adoption. Platforms without native binding require custom implementations that add code and potential bugs. Ruby's lack of built-in binding means teams must build or adopt binding frameworks. This infrastructure cost makes sense for large applications but burdens small projects. Teams must maintain binding implementations and debug binding-related issues alongside business logic.
Learning Curve affects team productivity during MVVM adoption. Developers familiar with MVC must learn ViewModel concepts, binding syntax, and command patterns. The mental shift from imperative UI updates to declarative binding takes time. Teams transitioning to MVVM should expect reduced velocity initially while members gain proficiency. Organizations should consider whether the long-term benefits justify the transition cost.
State Management becomes more explicit in MVVM. ViewModels centralize presentation state that might scatter across controllers and views in MVC. This centralization simplifies debugging and testing but requires discipline. Developers must resist placing business logic in ViewModels or UI logic in Models. Clear boundaries between layers need documentation and code review enforcement.
# State management in ViewModel
class OrderViewModel
attr_reader :order, :editing, :saving
def initialize(order)
@order = order
@editing = false
@saving = false
@observers = {}
end
def start_editing
return if editing
@editing = true
@original_values = {
quantity: order.quantity,
shipping_address: order.shipping_address
}
notify(:state_changed)
end
def cancel_editing
return unless editing
@editing = false
order.quantity = @original_values[:quantity]
order.shipping_address = @original_values[:shipping_address]
notify(:state_changed)
end
def save
return unless editing && order.valid?
@saving = true
notify(:state_changed)
order.save
@editing = false
@saving = false
notify(:state_changed)
end
def can_save?
editing && !saving && order.valid?
end
end
Testing Strategy changes with MVVM adoption. The pattern enables more unit tests and fewer integration tests. ViewModels test independently from Views and Models through mocking. This fast feedback cycle improves development speed. However, teams still need integration tests to verify View-ViewModel binding works correctly. The testing pyramid shifts toward more unit tests, but integration testing remains necessary.
Platform Constraints determine MVVM viability. Platforms with mature data binding (WPF, Xamarin, SwiftUI) make MVVM natural. Web applications using frameworks like Vue, React, or Angular implement MVVM-like patterns through their binding systems. Server-rendered web frameworks like Rails gain less from MVVM because page refreshes break binding continuity. Desktop Ruby applications need explicit binding libraries. Mobile Ruby applications using RubyMotion can implement MVVM but lack the ecosystem support found in Swift or Kotlin.
Team Structure influences pattern choice. Organizations with separate UI designers and developers benefit from MVVM's separation. Designers modify Views while developers change ViewModels and Models independently. Small teams where developers handle both UI and logic may find MVC simpler. The coordination overhead of maintaining three layers instead of two only pays off when different people work on different layers.
Performance Trade-offs exist in binding-heavy applications. Each property change triggers notification propagation and UI updates. Complex views with hundreds of bindings can cause performance issues. Developers must batch updates, throttle notifications, or use virtual scrolling for large data sets. MVVM without careful performance consideration leads to sluggish UIs. The pattern doesn't inherently cause performance problems, but binding convenience can hide inefficiencies.
# Batching updates in ViewModel
class ProductGridViewModel
def initialize(products)
@products = products
@observers = []
@update_queue = []
@batch_mode = false
end
def batch_update
@batch_mode = true
yield
@batch_mode = false
flush_updates
end
def update_product_price(product_id, new_price)
product = @products.find { |p| p.id == product_id }
product.price = new_price
queue_update(:product_changed, product)
end
def update_product_stock(product_id, new_stock)
product = @products.find { |p| p.id == product_id }
product.stock = new_stock
queue_update(:product_changed, product)
end
private
def queue_update(event, data)
if @batch_mode
@update_queue << [event, data]
else
notify_observers(event, data)
end
end
def flush_updates
@update_queue.each do |event, data|
notify_observers(event, data)
end
@update_queue.clear
end
end
# Usage
view_model.batch_update do
view_model.update_product_price(1, 29.99)
view_model.update_product_stock(1, 50)
view_model.update_product_price(2, 39.99)
end
# UI updates once instead of three times
Code Organization requires more files and classes in MVVM. Each Model may have corresponding ViewModels for different views. A User model might need UserListItemViewModel, UserDetailViewModel, and UserEditViewModel. This granularity improves testability but increases the codebase size. Directory structure should clearly separate the three layers to prevent accidental cross-layer dependencies.
Common Patterns
Property Notification implements the Observer pattern at the property level. ViewModels inherit from an observable base class or include an observable module. When properties change, the ViewModel notifies all registered observers. Views register as observers during initialization and update themselves when notifications arrive.
class ObservableViewModel
def initialize
@property_observers = Hash.new { |h, k| h[k] = [] }
end
def observe_property(property, &block)
@property_observers[property] << block
end
def notify_property_changed(property)
@property_observers[property].each(&:call)
end
protected
def set_property(name, value)
ivar = "@#{name}"
old_value = instance_variable_get(ivar)
return if old_value == value
instance_variable_set(ivar, value)
notify_property_changed(name)
end
end
class TaskViewModel < ObservableViewModel
def initialize(task)
super()
@task = task
@title = task.title
@completed = task.completed
end
def title
@title
end
def title=(value)
set_property(:title, value)
end
def completed
@completed
end
def toggle_completed
set_property(:completed, !@completed)
end
end
Computed Properties derive values from other properties and update automatically when dependencies change. A full_name property that combines first_name and last_name must update when either name changes. ViewModels track property dependencies and trigger computed property notifications when dependencies change.
class ContactViewModel < ObservableViewModel
attr_reader :first_name, :last_name
def initialize(contact)
super()
@contact = contact
@first_name = contact.first_name
@last_name = contact.last_name
end
def first_name=(value)
set_property(:first_name, value)
notify_property_changed(:full_name)
notify_property_changed(:initials)
end
def last_name=(value)
set_property(:last_name, value)
notify_property_changed(:full_name)
notify_property_changed(:initials)
end
def full_name
"#{first_name} #{last_name}".strip
end
def initials
"#{first_name&.first}#{last_name&.first}".upcase
end
end
Command Pattern encapsulates user actions with execution logic and availability logic. Commands expose a can_execute method that determines whether the command is currently valid. UI elements bind their enabled state to can_execute, causing buttons to disable when commands cannot execute.
class RelayCommand
def initialize(execute_proc, can_execute_proc = nil)
@execute_proc = execute_proc
@can_execute_proc = can_execute_proc
@can_execute_changed_observers = []
end
def execute
@execute_proc.call if can_execute?
end
def can_execute?
return true unless @can_execute_proc
@can_execute_proc.call
end
def notify_can_execute_changed
@can_execute_changed_observers.each(&:call)
end
def observe_can_execute_changed(&block)
@can_execute_changed_observers << block
end
end
class InvoiceViewModel
attr_reader :approve_command, :reject_command
def initialize(invoice)
@invoice = invoice
@approve_command = RelayCommand.new(
-> { approve_invoice },
-> { can_approve? }
)
@reject_command = RelayCommand.new(
-> { reject_invoice },
-> { can_reject? }
)
end
def status=(value)
@invoice.status = value
@approve_command.notify_can_execute_changed
@reject_command.notify_can_execute_changed
end
private
def approve_invoice
@invoice.approve!
status = @invoice.status
end
def reject_invoice
@invoice.reject!
status = @invoice.status
end
def can_approve?
@invoice.status == 'pending'
end
def can_reject?
@invoice.status == 'pending'
end
end
Collection ViewModels manage lists of items where each item has its own ViewModel. A list of products displays using ProductItemViewModel instances managed by a ProductListViewModel. The parent ViewModel handles collection operations like filtering, sorting, and selection while child ViewModels handle individual item presentation.
class ProductListViewModel
attr_reader :products, :selected_product
def initialize(product_repository)
@product_repository = product_repository
@products = []
@selected_product = nil
@filter = nil
@observers = {}
reload_products
end
def select_product(product_view_model)
@selected_product = product_view_model
notify(:selection_changed)
end
def filter_by_category(category)
@filter = category
reload_products
end
def clear_filter
@filter = nil
reload_products
end
def add_observer(event, &block)
@observers[event] ||= []
@observers[event] << block
end
private
def reload_products
models = @filter ?
@product_repository.find_by_category(@filter) :
@product_repository.all
@products = models.map { |model| ProductItemViewModel.new(model) }
notify(:products_changed)
end
def notify(event)
@observers[event]&.each(&:call)
end
end
class ProductItemViewModel
attr_reader :id, :name, :price, :in_stock
def initialize(product)
@product = product
@id = product.id
@name = product.name
@price = product.price
@in_stock = product.stock > 0
end
def display_price
"$#{'%.2f' % price}"
end
def stock_status
in_stock ? "Available" : "Out of Stock"
end
end
Dependency Injection supplies dependencies to ViewModels through constructor parameters rather than global state or singletons. This pattern enables testing with mock repositories and makes dependencies explicit. A UserViewModel receives a UserRepository rather than calling User.find directly.
class UserRepository
def find(id)
User.find(id)
end
def save(user)
user.save
end
end
class UserEditViewModel
def initialize(user_id, user_repository = UserRepository.new)
@user_repository = user_repository
@user = @user_repository.find(user_id)
@first_name = @user.first_name
@last_name = @user.last_name
@email = @user.email
end
def save
@user.first_name = @first_name
@user.last_name = @last_name
@user.email = @email
@user_repository.save(@user)
end
end
# Testing with mock repository
class MockUserRepository
def find(id)
OpenStruct.new(id: id, first_name: "Test", last_name: "User", email: "test@example.com")
end
def save(user)
true
end
end
# Test
repo = MockUserRepository.new
view_model = UserEditViewModel.new(1, repo)
view_model.save
Value Converters transform data between Model and View formats. A date stored as a DateTime object needs conversion to a formatted string for display. Converters handle this transformation bidirectionally, converting to display format when loading and parsing back to storage format when saving.
class Converter
def convert(value)
value
end
def convert_back(value)
value
end
end
class DateConverter < Converter
def convert(date)
return "" unless date
date.strftime("%m/%d/%Y")
end
def convert_back(string)
return nil if string.empty?
Date.strptime(string, "%m/%d/%Y")
rescue ArgumentError
nil
end
end
class CurrencyConverter < Converter
def convert(amount)
return "$0.00" unless amount
"$#{'%.2f' % amount}"
end
def convert_back(string)
string.gsub(/[$,]/, "").to_f
end
end
class EventViewModel
def initialize(event, date_converter = DateConverter.new)
@event = event
@date_converter = date_converter
end
def event_date
@date_converter.convert(@event.date)
end
def event_date=(value)
@event.date = @date_converter.convert_back(value)
end
end
Navigation Services decouple ViewModels from navigation logic. Instead of ViewModels creating or showing views directly, they call navigation services that handle view instantiation and display. This maintains View ignorance in ViewModels while still enabling navigation.
class NavigationService
def initialize
@navigation_stack = []
end
def navigate_to(view_model)
@navigation_stack.push(view_model)
show_view_for(view_model)
end
def navigate_back
return if @navigation_stack.size <= 1
@navigation_stack.pop
show_view_for(@navigation_stack.last)
end
def can_navigate_back?
@navigation_stack.size > 1
end
private
def show_view_for(view_model)
# Framework-specific view instantiation and display
end
end
class MainViewModel
def initialize(navigation_service)
@navigation_service = navigation_service
@open_settings_command = RelayCommand.new(
-> { open_settings }
)
end
private
def open_settings
@navigation_service.navigate_to(SettingsViewModel.new(@navigation_service))
end
end
Practical Examples
User Profile Management demonstrates a complete MVVM implementation for editing user profile data with validation and state management.
class User
attr_accessor :id, :username, :email, :bio, :avatar_url
def initialize(attributes = {})
@id = attributes[:id]
@username = attributes[:username]
@email = attributes[:email]
@bio = attributes[:bio]
@avatar_url = attributes[:avatar_url]
end
def save
# Database persistence logic
true
end
end
class UserRepository
def find(id)
# Simulated database fetch
User.new(
id: id,
username: "johndoe",
email: "john@example.com",
bio: "Software developer",
avatar_url: "https://example.com/avatar.jpg"
)
end
def save(user)
user.save
end
def username_exists?(username, exclude_id = nil)
# Simulated uniqueness check
username == "taken" && exclude_id != 1
end
end
module Observable
def add_observer(&block)
@observers ||= []
@observers << block
end
def notify_observers(property)
return unless @observers
@observers.each { |observer| observer.call(property) }
end
end
class UserProfileViewModel
include Observable
attr_reader :user, :save_command, :cancel_command
attr_accessor :username, :email, :bio
def initialize(user_id, user_repository = UserRepository.new)
@user_repository = user_repository
@user = @user_repository.find(user_id)
@original_state = {
username: @user.username,
email: @user.email,
bio: @user.bio
}
@username = @user.username
@email = @user.email
@bio = @user.bio
@validation_errors = {}
@save_command = RelayCommand.new(
-> { save },
-> { can_save? }
)
@cancel_command = RelayCommand.new(
-> { cancel }
)
end
def username=(value)
@username = value
validate_username
@save_command.notify_can_execute_changed
notify_observers(:username)
end
def email=(value)
@email = value
validate_email
@save_command.notify_can_execute_changed
notify_observers(:email)
end
def bio=(value)
@bio = value
notify_observers(:bio)
end
def has_changes?
username != @original_state[:username] ||
email != @original_state[:email] ||
bio != @original_state[:bio]
end
def username_error
@validation_errors[:username]
end
def email_error
@validation_errors[:email]
end
def avatar_url
@user.avatar_url
end
private
def can_save?
has_changes? && @validation_errors.empty?
end
def save
@user.username = username
@user.email = email
@user.bio = bio
if @user_repository.save(@user)
@original_state = {
username: username,
email: email,
bio: bio
}
@save_command.notify_can_execute_changed
notify_observers(:saved)
end
end
def cancel
self.username = @original_state[:username]
self.email = @original_state[:email]
self.bio = @original_state[:bio]
@validation_errors.clear
@save_command.notify_can_execute_changed
end
def validate_username
@validation_errors.delete(:username)
if username.nil? || username.strip.empty?
@validation_errors[:username] = "Username cannot be blank"
elsif username.length < 3
@validation_errors[:username] = "Username must be at least 3 characters"
elsif @user_repository.username_exists?(username, @user.id)
@validation_errors[:username] = "Username is already taken"
end
notify_observers(:username_error)
end
def validate_email
@validation_errors.delete(:email)
if email.nil? || email.strip.empty?
@validation_errors[:email] = "Email cannot be blank"
elsif !email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
@validation_errors[:email] = "Email format is invalid"
end
notify_observers(:email_error)
end
end
# Console-based view simulation
view_model = UserProfileViewModel.new(1)
view_model.add_observer do |property|
case property
when :username
puts "Username updated: #{view_model.username}"
puts "Error: #{view_model.username_error}" if view_model.username_error
when :email
puts "Email updated: #{view_model.email}"
puts "Error: #{view_model.email_error}" if view_model.email_error
when :saved
puts "Profile saved successfully!"
end
end
# User interaction simulation
view_model.username = "jd" # Triggers validation error
view_model.username = "johndoe_updated"
view_model.email = "invalid" # Triggers validation error
view_model.email = "john.doe@example.com"
view_model.save_command.execute if view_model.save_command.can_execute?
Shopping Cart with Real-time Updates shows MVVM handling dynamic collections and computed properties.
class Product
attr_reader :id, :name, :price
def initialize(id, name, price)
@id = id
@name = name
@price = price
end
end
class CartItem
attr_accessor :product, :quantity
def initialize(product, quantity = 1)
@product = product
@quantity = quantity
end
def subtotal
product.price * quantity
end
end
class ShoppingCart
attr_reader :items
def initialize
@items = []
end
def add_item(product, quantity = 1)
existing = @items.find { |item| item.product.id == product.id }
if existing
existing.quantity += quantity
else
@items << CartItem.new(product, quantity)
end
end
def remove_item(product_id)
@items.reject! { |item| item.product.id == product_id }
end
def update_quantity(product_id, quantity)
item = @items.find { |i| i.product.id == product_id }
return unless item
if quantity <= 0
remove_item(product_id)
else
item.quantity = quantity
end
end
def clear
@items.clear
end
end
class CartItemViewModel
include Observable
attr_reader :product_id, :name, :price, :remove_command
def initialize(cart_item, cart_view_model)
@cart_item = cart_item
@cart_view_model = cart_view_model
@product_id = cart_item.product.id
@name = cart_item.product.name
@price = cart_item.product.price
@remove_command = RelayCommand.new(-> { remove })
end
def quantity
@cart_item.quantity
end
def quantity=(value)
@cart_item.quantity = value
@cart_view_model.recalculate_totals
notify_observers(:quantity)
end
def subtotal
@cart_item.subtotal
end
def formatted_price
"$#{'%.2f' % price}"
end
def formatted_subtotal
"$#{'%.2f' % subtotal}"
end
private
def remove
@cart_view_model.remove_item(product_id)
end
end
class ShoppingCartViewModel
include Observable
attr_reader :items, :checkout_command, :clear_command
def initialize(cart)
@cart = cart
@items = []
@discount_code = nil
@discount_amount = 0
reload_items
@checkout_command = RelayCommand.new(
-> { checkout },
-> { can_checkout? }
)
@clear_command = RelayCommand.new(
-> { clear },
-> { !items.empty? }
)
end
def add_product(product, quantity = 1)
@cart.add_item(product, quantity)
reload_items
recalculate_totals
end
def remove_item(product_id)
@cart.remove_item(product_id)
reload_items
recalculate_totals
end
def apply_discount(code)
@discount_code = code
@discount_amount = calculate_discount(code)
recalculate_totals
notify_observers(:discount_applied)
end
def subtotal
@items.sum(&:subtotal)
end
def discount
@discount_amount
end
def tax
(subtotal - discount) * 0.08
end
def total
subtotal - discount + tax
end
def formatted_subtotal
"$#{'%.2f' % subtotal}"
end
def formatted_discount
@discount_amount > 0 ? "-$#{'%.2f' % discount}" : "$0.00"
end
def formatted_tax
"$#{'%.2f' % tax}"
end
def formatted_total
"$#{'%.2f' % total}"
end
def item_count
@items.sum(&:quantity)
end
def recalculate_totals
notify_observers(:totals_changed)
@checkout_command.notify_can_execute_changed
@clear_command.notify_can_execute_changed
end
private
def reload_items
@items = @cart.items.map { |item| CartItemViewModel.new(item, self) }
notify_observers(:items_changed)
end
def can_checkout?
!@items.empty?
end
def checkout
notify_observers(:checkout_requested)
end
def clear
@cart.clear
reload_items
@discount_code = nil
@discount_amount = 0
recalculate_totals
end
def calculate_discount(code)
case code
when "SAVE10"
subtotal * 0.10
when "SAVE20"
subtotal * 0.20
else
0
end
end
end
# Usage
cart = ShoppingCart.new
view_model = ShoppingCartViewModel.new(cart)
view_model.add_observer do |property|
case property
when :items_changed
puts "\nCart Items:"
view_model.items.each do |item|
puts " #{item.name} x#{item.quantity} = #{item.formatted_subtotal}"
end
when :totals_changed
puts "\nSubtotal: #{view_model.formatted_subtotal}"
puts "Discount: #{view_model.formatted_discount}"
puts "Tax: #{view_model.formatted_tax}"
puts "Total: #{view_model.formatted_total}"
when :discount_applied
puts "Discount code applied!"
when :checkout_requested
puts "Proceeding to checkout..."
end
end
# Simulate user interactions
product1 = Product.new(1, "Ruby Book", 49.99)
product2 = Product.new(2, "Rails Guide", 39.99)
view_model.add_product(product1, 2)
view_model.add_product(product2, 1)
view_model.apply_discount("SAVE10")
view_model.checkout_command.execute
Form Validation and Submission demonstrates comprehensive validation with multiple fields and async operations.
require 'net/http'
require 'json'
class RegistrationForm
attr_accessor :username, :email, :password, :password_confirmation, :agree_to_terms
def initialize
@username = ""
@email = ""
@password = ""
@password_confirmation = ""
@agree_to_terms = false
end
def submit
# Simulated API call
sleep(1)
{ success: true, user_id: 123 }
end
end
class AsyncCommand
def initialize(execute_proc, can_execute_proc = nil)
@execute_proc = execute_proc
@can_execute_proc = can_execute_proc
@executing = false
@observers = []
end
def execute
return if @executing || !can_execute?
@executing = true
notify_state_changed
Thread.new do
begin
@execute_proc.call
ensure
@executing = false
notify_state_changed
end
end
end
def can_execute?
return false if @executing
return true unless @can_execute_proc
@can_execute_proc.call
end
def executing?
@executing
end
def add_observer(&block)
@observers << block
end
private
def notify_state_changed
@observers.each(&:call)
end
end
class RegistrationViewModel
include Observable
attr_reader :submit_command, :form
def initialize(form = RegistrationForm.new)
@form = form
@validation_errors = {}
@submit_command = AsyncCommand.new(
-> { submit },
-> { can_submit? }
)
@submit_command.add_observer do
notify_observers(:submitting_changed)
end
end
def username
@form.username
end
def username=(value)
@form.username = value
validate_username
update_submit_state
end
def email
@form.email
end
def email=(value)
@form.email = value
validate_email
update_submit_state
end
def password
@form.password
end
def password=(value)
@form.password = value
validate_password
validate_password_confirmation if @form.password_confirmation.length > 0
update_submit_state
end
def password_confirmation
@form.password_confirmation
end
def password_confirmation=(value)
@form.password_confirmation = value
validate_password_confirmation
update_submit_state
end
def agree_to_terms
@form.agree_to_terms
end
def agree_to_terms=(value)
@form.agree_to_terms = value
update_submit_state
notify_observers(:agree_to_terms)
end
def username_error
@validation_errors[:username]
end
def email_error
@validation_errors[:email]
end
def password_error
@validation_errors[:password]
end
def password_confirmation_error
@validation_errors[:password_confirmation]
end
def has_errors?
!@validation_errors.empty?
end
def submitting?
@submit_command.executing?
end
private
def can_submit?
!has_errors? &&
@form.username.length > 0 &&
@form.email.length > 0 &&
@form.password.length > 0 &&
@form.password_confirmation.length > 0 &&
@form.agree_to_terms
end
def submit
result = @form.submit
if result[:success]
notify_observers(:submission_successful)
else
notify_observers(:submission_failed)
end
end
def update_submit_state
notify_observers(:validation_changed)
end
def validate_username
@validation_errors.delete(:username)
if @form.username.length < 3
@validation_errors[:username] = "Username must be at least 3 characters"
elsif @form.username.length > 20
@validation_errors[:username] = "Username must be less than 20 characters"
elsif !@form.username.match?(/\A[a-zA-Z0-9_]+\z/)
@validation_errors[:username] = "Username can only contain letters, numbers, and underscores"
end
notify_observers(:username_error)
end
def validate_email
@validation_errors.delete(:email)
unless @form.email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
@validation_errors[:email] = "Email format is invalid"
end
notify_observers(:email_error)
end
def validate_password
@validation_errors.delete(:password)
if @form.password.length < 8
@validation_errors[:password] = "Password must be at least 8 characters"
elsif !@form.password.match?(/[A-Z]/)
@validation_errors[:password] = "Password must contain at least one uppercase letter"
elsif !@form.password.match?(/[0-9]/)
@validation_errors[:password] = "Password must contain at least one number"
end
notify_observers(:password_error)
end
def validate_password_confirmation
@validation_errors.delete(:password_confirmation)
if @form.password != @form.password_confirmation
@validation_errors[:password_confirmation] = "Passwords do not match"
end
notify_observers(:password_confirmation_error)
end
end
# Console simulation
view_model = RegistrationViewModel.new
view_model.add_observer do |property|
case property
when :username_error
puts "Username error: #{view_model.username_error}" if view_model.username_error
when :email_error
puts "Email error: #{view_model.email_error}" if view_model.email_error
when :password_error
puts "Password error: #{view_model.password_error}" if view_model.password_error
when :password_confirmation_error
puts "Password confirmation error: #{view_model.password_confirmation_error}" if view_model.password_confirmation_error
when :submitting_changed
if view_model.submitting?
puts "Submitting registration..."
else
puts "Submission complete"
end
when :submission_successful
puts "Registration successful!"
end
end
# Simulate user input
view_model.username = "ab" # Too short
view_model.username = "newuser123"
view_model.email = "invalid" # Invalid format
view_model.email = "user@example.com"
view_model.password = "weak" # Too short
view_model.password = "StrongPass123"
view_model.password_confirmation = "different" # Doesn't match
view_model.password_confirmation = "StrongPass123"
view_model.agree_to_terms = true
view_model.submit_command.execute
Reference
Component Responsibilities
| Component | Responsibilities | Contains | Does Not Contain |
|---|---|---|---|
| Model | Domain data and business logic | Data structures, validation rules, business rules, persistence logic | UI logic, presentation formatting, view state |
| View | UI presentation and user input capture | UI markup, visual styling, input controls, basic UI event routing | Business logic, data formatting, navigation logic |
| ViewModel | Presentation logic and state management | View state, data formatting, input validation, command logic, property notifications | UI framework code, direct view references, business rules |
Pattern Comparison
| Aspect | MVVM | MVC | MVP |
|---|---|---|---|
| View-Controller coupling | Loose via data binding | Tight via direct calls | Medium via interface |
| Testability | High - no UI dependencies | Medium - controller tests need mocking | High - presenter fully testable |
| Data flow | Bidirectional binding | Controller to View | Presenter to View via interface |
| View intelligence | Minimal declarative binding | Varies by implementation | Passive - presenter driven |
| Platform requirements | Data binding support needed | Any platform | Any platform |
| Learning curve | Moderate - binding concepts | Low - familiar pattern | Low - straightforward |
| Best for | Complex UIs, frequent changes | Server-rendered apps | Apps without binding support |
Observable Property Implementation Checklist
| Step | Description | Implementation |
|---|---|---|
| 1 | Create observable module or base class | Include Observable module or inherit from base |
| 2 | Define observable properties | Use observable_attr or custom setters |
| 3 | Implement observer registration | add_observer method accepting blocks |
| 4 | Implement notification mechanism | notify_observers called on property changes |
| 5 | Track property dependencies | Document computed properties and their dependencies |
| 6 | Notify dependent properties | Trigger notifications for computed properties |
Command Pattern Checklist
| Element | Purpose | Required Methods |
|---|---|---|
| Execute action | Perform the command action | execute |
| Can execute predicate | Determine if command can run | can_execute? |
| State change notification | Notify when availability changes | notify_can_execute_changed |
| Observer registration | Register for state changes | observe_can_execute_changed |
ViewModel Design Checklist
| Consideration | Questions to Ask | Decision Criteria |
|---|---|---|
| Property exposure | What data does the View need? | Expose only data required for presentation |
| Command definition | What user actions are possible? | One command per logical user action |
| Validation location | Where should validation occur? | ViewModel for presentation rules, Model for business rules |
| State management | What UI state needs tracking? | Track editing state, loading state, selection state |
| Computed properties | What derived values are needed? | Create computed properties for formatted or aggregated data |
| Dependencies | What services does ViewModel need? | Inject all dependencies through constructor |
Data Binding Types
| Binding Type | Direction | Use Case | Example |
|---|---|---|---|
| One-way to View | ViewModel to View | Display-only data | Status messages, computed values |
| One-way to ViewModel | View to ViewModel | Action triggers | Button clicks, menu selections |
| Two-way | Bidirectional | Editable data | Text inputs, checkboxes, sliders |
| One-time | Initial only | Static data | Configuration values, constants |
Common ViewModel Properties
| Property Type | Example | Purpose |
|---|---|---|
| Formatted data | formatted_date, display_price | Present data in UI format |
| Computed values | full_name, total_price | Aggregate or calculate from other properties |
| Validation errors | email_error, password_error | Display field-specific errors |
| State flags | is_loading, is_editing, has_changes | Control UI behavior |
| Collections | items, selected_items | Manage lists and selections |
| Commands | save_command, delete_command | Encapsulate user actions |
Testing Strategy
| Test Type | Target | Tools | Coverage |
|---|---|---|---|
| ViewModel unit tests | Individual ViewModels | RSpec, Minitest | Property changes, command execution, validation logic |
| Model unit tests | Domain logic | RSpec, Minitest | Business rules, data validation, persistence |
| Integration tests | ViewModel-Model interaction | RSpec with test doubles | Data flow, repository calls, state transitions |
| UI tests | View-ViewModel binding | Framework-specific | Binding correctness, user interactions |
Anti-Patterns to Avoid
| Anti-Pattern | Description | Solution |
|---|---|---|
| Fat ViewModel | Business logic in ViewModel | Move business rules to Model layer |
| View-dependent ViewModel | ViewModel imports View classes | Use observer pattern and dependency injection |
| Anemic ViewModel | ViewModel as simple data holder | Add presentation logic and commands |
| Direct Model manipulation in View | View modifies Models directly | Route all changes through ViewModel |
| Global state in ViewModel | Static or singleton ViewModels | Create instance per use, inject dependencies |
| Missing validation | No input validation in ViewModel | Validate presentation rules in ViewModel |
| Circular dependencies | ViewModel references View, View references ViewModel | One-way dependency from View to ViewModel only |