Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Do You Need That Validation? Let Me Call You Back About It

Do You Need That Validation? Let Me Call You Back About It

Rails apps start nice and cute. Fast forward a year and business logic and view logic are entangled in our validations and callbacks - getting in our way at every turn. Wasn’t this supposed to be easy?

Let’s explore different approaches to improve the situation and untangle the web.

Tobias Pfeiffer

February 24, 2019
Tweet

More Decks by Tobias Pfeiffer

Other Decks in Programming

Transcript

  1. Thanks for making it!

    View Slide

  2. The magic hook

    View Slide

  3. Event Form

    View Slide

  4. Repetitive

    View Slide

  5. Much better!

    View Slide

  6. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  7. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  8. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  9. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end
    Datetime fields

    View Slide

  10. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  11. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  12. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  13. event =
    Event.new(
    name: "Ruby On Ice",
    location: "Tegernsee",
    date: "24.02.2019",
    crew_arrives_at: "6:45",
    performers_arrive_at: "9:30",
    open_at: "9:30",
    starts_at: "10:00",
    ends_at: "16:00"
    )
    event.valid?

    View Slide

  14. #name: "Ruby On Ice",
    location: "Tegernsee",
    date: Sun, 24 Feb 2019,
    crew_arrives_at: Sun, 24 Feb 2019 6:45:00,
    performers_arrive_at: Sun, 24 Feb 2019 9:00:00,
    open_at: Sun, 24 Feb 2019 9:00:00,
    starts_at: Sun, 24 Feb 2019 9:30:00,
    ends_at: Sun, 24 Feb 2019 20:00:00>
    It works!

    View Slide

  15. View Slide

  16. let(:event) do
    build :event,
    ends_at: Time.zone.local(2042, 1, 1, 15, 45)
    end

    View Slide

  17. let(:event) do
    build :event,
    ends_at: Time.zone.local(2042, 1, 1, 15, 45)
    end

    View Slide

  18. let(:event) do
    build :event,
    ends_at: Time.zone.local(2042, 1, 1, 15, 45)
    end
    it "works" do
    p event.ends_at # Wed, 01 Jan 2042 15:45:00
    event.save!
    p event.ends_at # Sun, 24 Feb 2019 15:45:00
    end
    Have fun debugging!

    View Slide

  19. View Slide

  20. let(:event) do
    create :event,
    ends_at: Time.zone.local(2042, 1, 1, 15, 45)
    end
    it "retrieves the right events" do
    query = FutureEvents.new
    expect(query.call(23.years)).to include(event)
    end
    Have fun debugging!

    View Slide

  21. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end

    View Slide

  22. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end
    Smell

    View Slide

  23. class Event < ApplicationRecord
    before_validation :set_datetimes_to_date
    def set_datetimes_to_date
    base_date = date.to_datetime
    DATE_TIME_FIELDS.each do |time_attribute|
    original = public_send(time_attribute)
    if original
    adjusted_time =
    base_date.change hour: original.hour,
    min: original.min
    self.public_send("#{time_attribute}=", adjusted_time)
    end
    end
    end
    end
    Why does the model clean up

    View Slide

  24. Validations

    View Slide

  25. validate :unique_email, if: :email_changed?
    validate :owns_notification_email,
    if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    gitlab/user

    View Slide

  26. validate :unique_email, if: :email_changed?
    validate :owns_notification_email,
    if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    gitlab/user

    View Slide

  27. Why the effort?

    View Slide

  28. Practice open?

    View Slide

  29. Practice open?
    No overlap?

    View Slide

  30. Practice open?
    No overlap?
    Right skills?

    View Slide

  31. Practice open?
    No overlap?
    Right skills?
    Patient can be contacted?

    View Slide

  32. Practice open?
    No overlap?
    Right skills?
    Patient can be contacted?
    Associated models

    View Slide

  33. Practice open?
    No overlap?
    Right skills?
    Patient can be contacted?
    Associated models
    ...

    View Slide

  34. Expensive Test Setup

    View Slide

  35. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    gitlab/user

    View Slide

  36. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    gitlab/user

    View Slide

  37. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    Ways to change?

    View Slide

  38. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    gitlab/user

    View Slide

  39. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    gitlab/user

    View Slide

  40. Why are we
    doing this?

    View Slide

  41. Affordance

    View Slide

  42. Wants to be cuddled

    View Slide

  43. Wants to be fed

    View Slide

  44. class MySolution
    def do_thing(argument)
    end
    end
    OOP Affordance

    View Slide

  45. Model
    View
    Controller
    Rails Affordance

    View Slide

  46. “Fat Models
    Skinny Controllers”

    View Slide

  47. 1 or 2 use cases
    stuck on every model

    View Slide

  48. Controllers
    Models

    View Slide

  49. Registrations
    User

    View Slide

  50. Registrations
    User
    Users

    View Slide

  51. Registrations
    User
    Users
    Users ##. ##. ##.

    View Slide

  52. Controllers
    Models
    Service Objects

    View Slide

  53. Registrations
    User
    Users
    Users ##. ##. ##.
    ##. ##. ##. ##. ##.

    View Slide

  54. Registrations
    User
    Users
    Users ##. ##. ##.
    ##. ##. ##. ##. ##.
    Business Logic Separated

    View Slide

  55. Registrations
    User
    Users
    Users ##. ##. ##.
    ##. ##. ##. ##. ##.
    Business Logic Separated
    Validations and Callbacks still mixed

    View Slide

  56. Registrations
    User
    Users
    Users ##. ##. ##.
    ##. ##. ##. ##. ##.
    Business Logic Separated
    Validations and Callbacks still mixed
    Run all the time by default

    View Slide

  57. Registrations
    User
    Users
    Users ##. ##. ##.
    ##. ##. ##. ##. ##.
    SignUp Show
    Index ##. ##. ##.

    View Slide

  58. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model

    View Slide

  59. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    A User Model

    View Slide

  60. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    Sign Up / Edit Only

    View Slide

  61. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    View Related

    View Slide

  62. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    WHWWHHYYY???

    View Slide

  63. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model
    Knowledge?

    View Slide

  64. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model
    Why Solve it here?

    View Slide

  65. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model
    Opt Out

    View Slide

  66. View Slide

  67. View Slide

  68. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model
    Could solve here

    View Slide

  69. Registrations
    User
    ##.
    SignUp
    View
    Controller
    Service
    Model
    Or here?

    View Slide

  70. View Slide

  71. Tobi complaining
    about validations
    and callbacks
    You’ve seen:

    View Slide

  72. Do You Need That Validation?
    Let Me Call You Back About It
    Tobias Pfeiffer
    @PragTob
    pragtob.info

    View Slide

  73. View Slide

  74. Specifics clutter Model

    View Slide

  75. Specifics clutter Model
    Hard to get overiew

    View Slide

  76. Specifics clutter Model
    Hard to get overiew
    Run all the time

    View Slide

  77. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    ActiveRecord Original

    View Slide

  78. What does Rails offer?

    View Slide

  79. module UserRegistration
    extend ActiveSupport#:Concern
    included do
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    end
    end
    Concerns

    View Slide

  80. module Copyable
    def copy_to(destination)
    Notification.suppress do
    # Copy logic that creates new
    # comments that we do not want
    # triggering notifications.
    end
    end
    end
    Suppress

    View Slide

  81. class Person < ApplicationRecord
    validates :email,
    uniqueness: true,
    on: :account_setup
    validates :age,
    numericality: true,
    on: :account_setup
    end
    Custom Contexts

    View Slide

  82. What’s out there?

    View Slide

  83. Form Objects

    View Slide

  84. Form Objects
    Form Objects
    Form Objects
    class Registration
    include ActiveModel#:Model
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :email, :password
    def save
    if valid?
    user = BaseUser.new(email: email, password_digest: hash_password)
    user.save!
    send_welcome_email
    true
    else
    false
    end
    end
    end

    View Slide

  85. Form Objects
    Form Objects
    Plain ActiveModel
    class Registration
    include ActiveModel#:Model
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :email, :password
    def save
    if valid?
    user = BaseUser.new(email: email, password_digest: hash_password)
    user.save!
    send_welcome_email
    true
    else
    false
    end
    end
    end

    View Slide

  86. Form Objects
    Form Objects
    Validations
    class Registration
    include ActiveModel#:Model
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :email, :password
    ###.
    end

    View Slide

  87. Form Objects
    Form Objects
    Attributes
    class Registration
    include ActiveModel#:Model
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :email, :password
    ###.
    end

    View Slide

  88. Form Objects
    Form Objects
    Map to ActiveRecord
    class Registration
    ###.
    def save
    if valid?
    user = BaseUser.new(
    email: email,
    password_digest: hash_password
    )
    user.save!
    send_welcome_email
    true
    else
    false
    end
    end

    View Slide

  89. Form Objects
    Form Objects
    Interface
    class Registration
    ###.
    def save
    if valid?
    user = BaseUser.new(
    email: email,
    password_digest: hash_password
    )
    user.save!
    send_welcome_email
    true
    else
    false
    end
    end

    View Slide

  90. Form Objects
    Form Objects
    Callbacks
    class Registration
    ###.
    def save
    if valid?
    user = BaseUser.new(
    email: email,
    password_digest: hash_password
    )
    user.save!
    send_welcome_email
    true
    else
    false
    end
    end

    View Slide

  91. def create
    @user = Registration.new(registration_params)
    if @user.save
    # ##.
    else
    # ##.
    end
    end
    Same Interface

    View Slide

  92. Inheritance!

    View Slide

  93. Inheritance!
    class User#:AsSignUp < ActiveType#:Record[User]
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end

    View Slide

  94. Inheritance!
    class User#:AsSignUp < ActiveType#:Record[User]
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end

    View Slide

  95. ActiveType
    class User#:AsSignUp < ActiveType#:Record[User]
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end

    View Slide

  96. class User < ApplicationRecord
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end
    Original

    View Slide

  97. Almost the same!
    class User#:AsSignUp < ActiveType#:Record[User]
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end

    View Slide

  98. Handle STI, Routes etc.
    class User#:AsSignUp < ActiveType#:Record[User]
    validates :email,
    presence: true,
    confirmation: true
    validates :password,
    confirmation: true,
    length: { minimum: 8 }
    validates :terms, acceptance: true
    attr_accessor :password
    before_save :hash_password
    after_commit :send_welcome_email, on: :create
    ###.
    end

    View Slide

  99. Changesets

    View Slide

  100. Changesets
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  101. Elixir
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  102. Pipe
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  103. Context
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  104. “strong parameters”
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  105. Validations
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  106. callback
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  107. Mixing concerns
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  108. validate :unique_email, if: :email_changed?
    validate :owns_notification_email, if: :notification_email_changed?
    validate :owns_public_email, if: :public_email_changed?
    validate :owns_commit_email, if: :commit_email_changed?
    before_validation :set_notification_email, if: :new_record?
    before_validation :set_public_email, if: :public_email_changed?
    before_validation :set_commit_email, if: :commit_email_changed?
    # in case validation is skipped
    before_save :set_public_email, if: :public_email_changed?
    # in case validation is skipped
    before_save :set_commit_email, if: :commit_email_changed?
    before_save :skip_reconfirmation!, if: #>(user) {
    user.email_changed? #& user.read_only_attribute?(:email)
    }
    before_save :check_for_verified_email, if: #>(user) {
    user.email_changed? #& !user.new_record?
    }
    Remember this?

    View Slide

  109. Combinable
    defmodule ValidationShowcase.Accounts.User do
    # ...
    def registration_changeset(user, attrs) do
    user
    |> base_changeset(attrs)
    |> cast(attrs, [:email, :password, :terms_of_service])
    |> validate_required([:email, :password])
    |> validate_confirmation(:email)
    |> validate_confirmation(:password)
    |> validate_length(:password, min: 8)
    |> validate_acceptance(:terms_of_service)
    |> hash_password()
    end
    end

    View Slide

  110. defmodule ValidationShowcase.Accounts do
    def create_user(attrs \\ %{}) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
    |> send_welcome_email()
    end
    end
    Context

    View Slide

  111. defmodule ValidationShowcase.Accounts do
    def create_user(attrs \\ %{}) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
    |> send_welcome_email()
    end
    end
    Changeset

    View Slide

  112. defmodule ValidationShowcase.Accounts do
    def create_user(attrs \\ %{}) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
    |> send_welcome_email()
    end
    end
    “after_commit”

    View Slide

  113. defmodule ValidationShowcaseWeb.UserController do
    def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
    {:ok, user} ->
    conn
    |> put_flash(:info, "User created successfully.")
    |> redirect(to: Routes.user_path(conn, :show, user))
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
    end
    end
    end
    Controller

    View Slide

  114. Form Objects
    Form Objects
    Separate Operations and Validators

    View Slide

  115. trailblazer

    View Slide

  116. class TbRegistrationsController < ApplicationController
    def create
    result = Registration#:Create.(params: params)
    if result.success?
    redirect_to "/users/", notice: 'User was created.'
    else
    @user = result["contract.default"]
    render "users/new"
    end
    end
    end

    View Slide

  117. class TbRegistrationsController < ApplicationController
    def create
    result = Registration#:Create.(params: params)
    if result.success?
    redirect_to "/users/", notice: 'User was created.'
    else
    @user = result["contract.default"]
    render "users/new"
    end
    end
    end
    Operation

    View Slide

  118. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end

    View Slide

  119. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    Setup Model

    View Slide

  120. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    Setup Form Object

    View Slide

  121. module Registration#:Contract
    class Create < Reform#:Form
    include Dry
    include Reform#:Form#:ActiveModel
    feature Coercion
    model :tb_registration
    property :email
    property :email_confirmation, virtual: true
    property :password, virtual: true
    property :password_confirmation, virtual: true
    property :terms, virtual: true, type: Types#:Params#:Bool
    validation do
    required(:email).filled.confirmation
    required(:password).value(min_size?: 8).confirmation
    required(:terms).value(:true?)
    end
    end
    end

    View Slide

  122. module Registration#:Contract
    class Create < Reform#:Form
    include Dry
    include Reform#:Form#:ActiveModel
    feature Coercion
    model :tb_registration
    property :email
    property :email_confirmation, virtual: true
    property :password, virtual: true
    property :password_confirmation, virtual: true
    property :terms, virtual: true, type: Types#:Params#:Bool
    validation do
    required(:email).filled.confirmation
    required(:password).value(min_size?: 8).confirmation
    required(:terms).value(:true?)
    end
    end
    end

    View Slide

  123. module Registration#:Contract
    class Create < Reform#:Form
    property :email
    property :email_confirmation, virtual: true
    property :password, virtual: true
    property :password_confirmation, virtual: true
    property :terms, virtual: true,
    type: Types#:Params#:Bool
    validation do
    required(:email).filled.confirmation
    required(:password).value(min_size?: 8).confirmation
    required(:terms).value(:true?)
    end
    end
    end

    View Slide

  124. Attributes
    module Registration#:Contract
    class Create < Reform#:Form
    property :email
    property :email_confirmation, virtual: true
    property :password, virtual: true
    property :password_confirmation, virtual: true
    property :terms, virtual: true,
    type: Types#:Params#:Bool
    validation do
    required(:email).filled.confirmation
    required(:password).value(min_size?: 8).confirmation
    required(:terms).value(:true?)
    end
    end
    end

    View Slide

  125. dry-validation
    module Registration#:Contract
    class Create < Reform#:Form
    property :email
    property :email_confirmation, virtual: true
    property :password, virtual: true
    property :password_confirmation, virtual: true
    property :terms, virtual: true,
    type: Types#:Params#:Bool
    validation do
    required(:email).filled.confirmation
    required(:password).value(min_size#: 8).confirmation
    required(:terms).value(:true?)
    end
    end
    end

    View Slide

  126. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    validate

    View Slide

  127. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    Callback

    View Slide

  128. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    Persist

    View Slide

  129. class Registration#:Create < Trailblazer#:Operation
    step Model(BaseUser, :new)
    step Contract#:Build(
    constant: Registration#:Contract#:Create
    )
    step Contract#:Validate(key: :tb_registration)
    step :hash_password
    step Contract#:Persist()
    step :send_welcome_email
    end
    Callback

    View Slide

  130. “Models are persistence-only and
    solely define associations and
    scopes. No business code is to be
    found here. No validations, no
    callbacks.”
    trailblazer

    View Slide

  131. The architecture eases keeping the
    business logic (entities) separated
    from details such as persistence or
    validations.
    hanami/model

    View Slide

  132. Takeaway

    View Slide

  133. I don’t hate Rails

    View Slide

  134. I don’t hate Rails
    Future in Rails?

    View Slide

  135. I don’t hate Rails
    Affordances
    Future in Rails?

    View Slide

  136. I don’t hate Rails
    Alternatives
    Affordances
    Future in Rails?

    View Slide

  137. Form Objects

    View Slide

  138. Inheritance!

    View Slide

  139. Changesets

    View Slide

  140. Form Objects
    Form Objects
    Separate Operations and Validators

    View Slide

  141. I don’t hate Rails
    Careful with validations and callbacks
    Alternatives
    Affordances
    Future in Rails?

    View Slide

  142. Thank you!

    View Slide