Slide 1

Slide 1 text

COUNTERINTUITIVE RAILS @inemation PART I

Slide 2

Slide 2 text

THOSE DANGEROUS RUBIES!! @inemation PART 1

Slide 3

Slide 3 text

THE AUTHOR IVAN NEMYTCHENKO Started using Rails in 2006 Did lots of different stuff (freelancing, agencies, conferences, lean poker, teaching) Former GitLab developer advocate (stickers!)

Slide 4

Slide 4 text

THE AUTHOR IVAN NEMYTCHENKO Was giving a talk here 3 years ago Created RailsHurts.com Started writing a book

Slide 5

Slide 5 text

LET’S DREAM!

Slide 6

Slide 6 text

AN IDEAL SYSTEM 1) Must oppose to a bad code 2) Doesn’t fight against the framework 3) There should be one way of doing a specific thing

Slide 7

Slide 7 text

PEOPLE ARE LAZY

Slide 8

Slide 8 text

DON’T RELY ON DISCIPLINE

Slide 9

Slide 9 text

DISCIPLINE

Slide 10

Slide 10 text

THEORY, PRACTICE AND A DREAM

Slide 11

Slide 11 text

THERE WILL BE DIRT. ALWAYS.

Slide 12

Slide 12 text

THE PROBLEM IS WHEN YOUR SYSTEM ENCOURAGES YOU TO REPLICATE DIRT

Slide 13

Slide 13 text

FLAT MODELS STRUCTURE

Slide 14

Slide 14 text

SMALL PROJECT MODELS TYPICAL WAY OF DOING IT:

Slide 15

Slide 15 text

REDMINE MODELS 70 models 1 folder TYPICAL WAY OF DOING IT:

Slide 16

Slide 16 text

GITLAB MODELS 128 models at the top level 16 folders TYPICAL WAY OF DOING IT:

Slide 17

Slide 17 text

DISCOURSE MODELS 171 models 0 folders TYPICAL WAY OF DOING IT:

Slide 18

Slide 18 text

HIERARCHY BASED ON ENTITIES’ DEPENDENCIES class Article < ApplicationRecord has_many :comments, dependent: :destroy has_many :links, dependent: :destroy, inverse_of: :article has_many :likes, class_name: "Article::Comment::Like" end class Article::Link < ApplicationRecord belongs_to :article validates :url, presence: true end class Article::Comment < ApplicationRecord belongs_to :article has_many :likes end class Article::Category < ApplicationRecord has_many :articles end class Article::Comment::Like < ApplicationRecord belongs_to :comment belongs_to :article end BETTER WAY OF DOING IT:

Slide 19

Slide 19 text

BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES ⊕ Makes it easier to understand what is your app about ⊕ Zero long term cost ⊕ Rails fully supports this ⊖ sometimes: class_name: “Article::Comment::Like" ± have to think about "dependencies"

Slide 20

Slide 20 text

FLAT CONTROLLERS STRUCTURE

Slide 21

Slide 21 text

SMALL SCALE TYPICAL WAY OF DOING IT:

Slide 22

Slide 22 text

LARGER SCALE TYPICAL WAY OF DOING IT:

Slide 23

Slide 23 text

SMALL SCALE TYPICAL WAY OF DOING IT: class InvitesController < ApplicationController requires_login only: [ :destroy, :create, :create_invite_link, :rescind_all_invites, :resend_invite, :resend_all_invites, :upload_csv ] skip_before_action :check_xhr, except: [:perform_accept_invitation] skip_before_action :preload_json, except: [:show] skip_before_action :redirect_to_login_if_required before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation before_action :ensure_not_logged_in, only: [:show, :perform_accept_invitation] #...

Slide 24

Slide 24 text

LARGER SCALE TYPICAL WAY OF DOING IT: class PostsController < ApplicationController requires_login except: [ :show, :replies, :by_number, :short_link, :reply_history, :replyIids, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest, :user_posts_feed ] skip_before_action :preload_json, :check_xhr, only: [ :markdown_id, :markdown_num, :short_link, :latest, :user_posts_feed ] #...

Slide 25

Slide 25 text

LARGER SCALE TYPICAL WAY OF DOING IT: class UsersController < ApplicationController skip_before_action :authorize_mini_profiler, only: [:avatar] requires_login only: [ :username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, :preferences, :create_second_factor, :update_second_factor ] skip_before_action :check_xhr, only: [ :show, :badges, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login ] before_action :respond_to_suspicious_request, only: [:create] skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :redirect_to_login_if_required, only: [:check_username, :create, :get_honeypot_value, :account_created, :activate_account, :perform_account_activation, :send_activation_email, :update_activation_email, :password_reset, :confirm_email_token, :email_login, :admin_login, :confirm_admin]

Slide 26

Slide 26 text

FLAT CONTROLLERS STRUCTURE - 1) REST misunderstanding - 2) respond_to - 3) lack of DDD thinking WHY DO WE DO THAT?

Slide 27

Slide 27 text

REST MISUNDERSATNDING

Slide 28

Slide 28 text

WHAT DO WE GET HERE? GET /POSTS/5

Slide 29

Slide 29 text

WHAT DO WE GET HERE? A RESOURCE?

Slide 30

Slide 30 text

WHAT DO WE GET HERE? GET /ADMIN/POSTS/5 GET /MODERATION/POSTS/5

Slide 31

Slide 31 text

WHAT DO WE GET HERE? A REPRESENTATION!

Slide 32

Slide 32 text

REST IS ALL ABOUT REPRESENTATIONS! - You never get the resource itself - There could be many representations, and it is fine! - They can be very different!

Slide 33

Slide 33 text

DEVELOPERS WHO ARE LIMITING THEMSELVES WITH ONE CONTROLLER PER MODEL DO NOT UNDERSTAND REST

Slide 34

Slide 34 text

SOMEONE COULD SAY: BUT WE SERVE DIFFERENT REPRESENTATIONS WITH RESPOND_TO!

Slide 35

Slide 35 text

RESPOND_TO USAGE

Slide 36

Slide 36 text

SMALL SCALE TYPICAL WAY OF DOING IT: respond_to do |format| format.html { render locals: {query: params[:q]} } format.json { @devices = @devices.first(5) @employees = @employees.first(3) } end

Slide 37

Slide 37 text

LARGER SCALE (REDMINE) TYPICAL WAY OF DOING IT: respond_to do |format| format.html { @issue_count = @query.issue_count @issue_pages = Paginator.new @issue_count, per_page_option, params['page'] @issues = @query.issues(:offset => @issue_pages.offset, :limit => @issue_pages.per_page) render :layout => !request.xhr? } format.api { @offset, @limit = api_offset_and_limit @query.column_names = %w(author) @issue_count = @query.issue_count @issues = @query.issues(:offset => @offset, :limit => @limit) Issue.load_visible_relations(@issues) if include_in_api_response?('relations') } format.atom { @issues = @query.issues(:limit => Setting.feeds_limit.to_i) render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { @issues = @query.issues(:limit => Setting.issues_export_limit.to_i) send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filen => 'issues.csv') } format.pdf { @issues = @query.issues(:limit => Setting.issues_export_limit.to_i) send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' } end

Slide 38

Slide 38 text

TEXT HTML, JSON, PDF, RSS - they all used in different ways - different subsets of data - different ways to generate results So it makes sense to create separate REST representations

Slide 39

Slide 39 text

BETTER WAY OF DOING IT: TOP-LEVEL NAMESPACES class Web::ArticlesController < Web::ApplicationController #... end class Api::ArticlesController < Api::ApplicationController #... end

Slide 40

Slide 40 text

LET’S APPLY SOME DDD

Slide 41

Slide 41 text

DOMAIN DRIVEN DESIGN

Slide 42

Slide 42 text

DOMAIN DRIVEN DESIGN BOUNDED CONTEXT

Slide 43

Slide 43 text

DOMAIN DRIVEN DESIGN BOUNDED CONTEXT EXAMPLE Creation/editing: Article - title - body - author Moderation: Article - title - body - author + category + publication_date

Slide 44

Slide 44 text

BETTER WAY OF DOING IT: BOUNDED CONTEXT EXAMPLE - No such thing as universal article here - Representation depends on context both in terms of behaviour and data sets

Slide 45

Slide 45 text

BETTER WAY OF DOING IT: HIERARCHY BASED ON “BOUNDED CONTEXTS” scope module: :web do namespace :moderation do resources :articles, only: [:index, :edit, :update, :show] do member do patch :publish end end end resources :articles do member do patch :moderate end scope module: :articles do resources :comments do scope module: :comments do resources :likes, only: [:create] end end end end root 'welcome#index' end

Slide 46

Slide 46 text

BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES complex universal controllers vs many simple controllers for specific representations

Slide 47

Slide 47 text

BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES ⊕ Makes it easier to understand what is your app about ⊕ Zero long term cost ⊕ Rails fully supports this ⊕ URLs structure matches folders structure ⊕ Reduces number of if-statements in controllers & views ⊖ Longer controller names

Slide 48

Slide 48 text

FLAT CONTROLLERS STRUCTURE - 1) No respond_to - 2) Separate namespaces per way of usage - 3) More namespaces per “bounded context” inside HOW TO FIX:

Slide 49

Slide 49 text

VIOLATING MVC PATTERN

Slide 50

Slide 50 text

MVC IS MEANT TO BE A MODULAR THING

Slide 51

Slide 51 text

WHAT IS MODULARITY?

Slide 52

Slide 52 text

WHAT MAKES A SYSTEM MODULAR?

Slide 53

Slide 53 text

MODULES?

Slide 54

Slide 54 text

MODULARITY IF TWO MODULES ARE INTERCONNECTED IT IS NOT A MODULARITY

Slide 55

Slide 55 text

MODULARITY MODULARITY IS THE LACK OF CIRCULAR CONNECTIONS (DEPENDENCIES)

Slide 56

Slide 56 text

MODULARITY KNOWLEDGE SHOULD ONLY GO IN ONE DIRECTION

Slide 57

Slide 57 text

MODEL THAT SENDS NOTIFICATIONS 90% of Rails projects! TYPICAL WAY OF DOING IT:

Slide 58

Slide 58 text

GEM LIKE ACTIVE_ADMIN OR ACTS_AS_API TYPICAL WAY OF DOING IT:

Slide 59

Slide 59 text

RENDERING IN MODELS TYPICAL WAY OF DOING IT:

Slide 60

Slide 60 text

CURRENT_USER IN MODELS TYPICAL WAY OF DOING IT:

Slide 61

Slide 61 text

GLOBALS IN MODELS TYPICAL WAY OF DOING IT:

Slide 62

Slide 62 text

VIOLATING MVC - using Rails features, that violate it - ignoring differences between application layers - using gems that violate MVC - not distinguishing Application logic from Business logic HOW DO WE VIOLATE MVC?

Slide 63

Slide 63 text

APPLICATION LOGIC VS BUSINESS LOGIC

Slide 64

Slide 64 text

EXAMPLES OF APPLICATION LOGIC TYPICAL WAY OF DOING IT:

Slide 65

Slide 65 text

TYPICAL WAY OF DOING IT: APPLICATION LOGIC IN MODELS class Person < ActiveRecord::Base validates_confirmation_of :user_name, :password validates_confirmation_of :email_address validates_acceptance_of :terms_of_service validates_acceptance_of :eula end

Slide 66

Slide 66 text

- Rails gives us this stuff - It is definitely the application logic - Still it is in our models WHAT IS WRONG ABOUT IT? APPLICATION LOGIC IN MODELS

Slide 67

Slide 67 text

WHAT ARE MODELS ABOUT?

Slide 68

Slide 68 text

PERSISTENCE?

Slide 69

Slide 69 text

WHAT ARE MODELS ABOUT? PERSISTENCE? - We can have models without persistence - Persistence is a technical detail

Slide 70

Slide 70 text

WHAT ARE MODELS ABOUT? DOMAIN MODEL - It is a system that imitates structure or functioning of explored domain

Slide 71

Slide 71 text

WHAT ARE MODELS ABOUT? - doesn’t depend on programming language - doesn’t depend on type of app - doesn’t depend on framework DOMAIN MODEL

Slide 72

Slide 72 text

TYPICAL WAY OF DOING IT: LET’S JUST PUT EVERYTHING INTO MODELS FAT MODELS, SKINNY CONTROLLERS!

Slide 73

Slide 73 text

PUTTING CODE IN RANDOM PLACES

Slide 74

Slide 74 text

HOW DOES COMPLEXITY CREEP IN?

Slide 75

Slide 75 text

HOW COMPLEXITY CREEPS IN? IT ALL STARTS PRETTY INNOCENT def create @employee = Employee.new(employee_params) if @employee.save flash.notice = "Employee updated!" redirect_to employee_path(@employee) else flash.alert = "Couldn't update employee!" render :edit end end

Slide 76

Slide 76 text

HOW COMPLEXITY CREEPS IN? BUT THEN…

Slide 77

Slide 77 text

HOW COMPLEXITY CREEPS IN? THEN IT BECOMES A BIT MORE MESSY def create @employee = Employee.new(employee_params) if @employee.save if params[:set_as_admin] == "1" User.create( email: employee.email, name: employee.name, employee: employee ) end if params[:add_another] == "1" redirect_to new_employee_path else redirect_to employees_path end else render :new flash.alert = "Couldn't add employee" end end

Slide 78

Slide 78 text

HOW TO FIGHT COMPLEXITY?

Slide 79

Slide 79 text

HOW TO FIGHT COMPLEXITY? AVOID IT!

Slide 80

Slide 80 text

HOW TO FIGHT COMPLEXITY? PUT STUFF INTO PROPER PLACES

Slide 81

Slide 81 text

FAT MODELS! SKINNY CONTROLLERS!

Slide 82

Slide 82 text

HELL, NO! FAT MODELS! SKINNY CONTROLLERS!

Slide 83

Slide 83 text

DEFINITION SERVICE Service encapsulates single process of our business. They take all collaborators (database, logging, external adapters like Facebook, user parameters) and performs a given process. Services belongs to our domain - they shouldn’t know they’re within Rails or webapp!

Slide 84

Slide 84 text

FIGHTING COMPLEXITY BACK PUT STUFF INTO PROPER PLACES class CreateEmployee def self.process(params, set_as_admin) employee = Employee.new(employee_params) ActiveRecord::Base.transaction do employee.save if set_as_admin User.create( email: employee.email, name: employee.name, employee: employee ) end end employee end end def create @employee = CreateEmployee.process( employee_params, params[:set_as_admin] ) if @employee.valid? redirect_to employees_path else render :new flash.alert = "Couldn't add employee" end end

Slide 85

Slide 85 text

STATEFUL SERVICES

Slide 86

Slide 86 text

DEFINITION SERVICE In DDD, Evans defines a service as an action, not a thing. Instead of forcing the operation into an existing object, we should encapsulate it in a separate, stateless service.

Slide 87

Slide 87 text

TYPICAL WAY OF DOING IT: SERVICE class TripReservationService attr_reader :payment_adapter, :logger def initialize(payment_adapter, logger) @payment_adapter = payment_adapter @logger = logger end def process(user, trip, agency, reservation) raise AgencyRejectionError.new unless user.can_book_from?(agency) raise NoTicketError.new unless trip.has_free_tickets? begin receipt = payment_adapter.pay(trip.price) reservation.receipt_id = receipt.uuid unless reservation.save logger.info "Failed to save reservation: #{reservation.errors.inspect}" raise ReservationError.new end rescue PaymentError logger.info "User #{user.name} failed to pay for a trip #{trip.name}: #{$!.message}" raise TripPaymentError.new $!.message end end end

Slide 88

Slide 88 text

TYPICAL WAY OF DOING IT: SERVICE class TripReservationService attr_reader :payment_adapter, :logger def initialize(payment_adapter, logger) @payment_adapter = payment_adapter @logger = logger end def process(user, trip, agency, reservation) raise AgencyRejectionError.new unless user.can_book_from?(agency) raise NoTicketError.new unless trip.has_free_tickets? begin receipt = payment_adapter.pay(trip.price) reservation.receipt_id = receipt.uuid unless reservation.save logger.info "Failed to save reservation: #{reservation.errors.inspect}" raise ReservationError.new end rescue PaymentError logger.info "User #{user.name} failed to pay for a trip #{trip.name}: #{$!.message}" raise TripPaymentError.new $!.message end end end

Slide 89

Slide 89 text

BETTER WAY OF DOING IT: USING DEPENDENCY INJECTION CONTAINER Rails.application.configure do config.container = Dry::Container.new if Rails.env.test? config.container.register(:stripe_klass, -> { StubPaymentProcessor }) # ... else config.container.register(:stripe_klass, -> { StripePaymentProcessor }) # … end end stripe_klass = Rails.application.config.container.resolve(:stripe_klass)

Slide 90

Slide 90 text

TYPICAL WAY OF DOING IT: WITH INTERACTOR GEM class CreateEmployee include Interactor def call employee = Employee.new(organisation: context.organisation) employee.assign_attributes(context.params) context.employee = employee unless employee.save context.fail!(message: "add_employee.failure") end end end class NotifyAdminAboutAdding include Interactor def call NewEmployeeNotification.send(context.employee) end end class Add include Interactor::Organizer organize CreateEmployee, NotifyAdminAboutAdding end

Slide 91

Slide 91 text

WHY CAN’T WE SIMPLY DO THIS? def create_employee(params, organisation) employee = organisation.employees.new(params) return false if !employee.save NewEmployeeNotification.send(employee) employee end BETTER WAY OF DOING IT:

Slide 92

Slide 92 text

WHY CAN’T WE SIMPLY DO THIS? class EmployeeService class << self def create(params, organisation) employee = organisation.employees.new(params) return false if !employee.save NewEmployeeNotification.send(employee) employee end def update # ... end end end BETTER WAY OF DOING IT:

Slide 93

Slide 93 text

ENOUGH FOR TODAY? @inemation [email protected] RailsHurts.com BIT.LY/WROCRB

Slide 94

Slide 94 text

COUNTERINTUITIVE RAILS PART II RAILSHURTS.COM/PRINCIPLES

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

Additional layers & building blocks Frameworks on top of Rails Another framework Another language Rails engines Move logic to libs Move logic to gems

Slide 98

Slide 98 text

Services Interactors Form Objects Repositories Policies Value Objects

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

CALLBACKS

Slide 101

Slide 101 text

No content

Slide 102

Slide 102 text

BEFORE_VALIDATION class Mother < Person before_validation :set_gender def set_gender self.gender = "F" end end mother = Mother.new mother.gender = "M" mother.valid? mother.gender # "F"

Slide 103

Slide 103 text

CALLBACKS HELL class Issue < ActiveRecord::Base #... before_validation :default_assign, on: :create before_validation :clear_disabled_fields before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on after_save {|issue| issue.send :after_project_change if !issue.saved_change_to_id? && issue.saved_change_to_project_id?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :delete_selected_attachments, :create_journal # Should be after_create but would be called before previous after_save callbacks after_save :after_create_from_copy after_destroy :update_parent_attributes after_create :send_notification #...

Slide 104

Slide 104 text

BEFORE_VALIDATION class Mother < Person before_validation :set_gender def set_gender self.gender = "F" end end mother = Mother.new mother.gender = "M" mother.valid? mother.gender # "F"

Slide 105

Slide 105 text

BEFORE_VALIDATION → DEFAULTS class Mother < Person def initialize(attrs = {}) defaults = { gender: "F" } super(defaults.merge(attrs)) end end Mother.new => #

Slide 106

Slide 106 text

BEFORE_VALIDATION class Room < ActiveRecord::Base belongs_to :user geocoded_by :address before_validation :geocode end room = Room.new room.valid? # request to external geocoding server

Slide 107

Slide 107 text

YOU CAN’T TRUST ANYONE ON INTERNETS BAD NEWS

Slide 108

Slide 108 text

IT IS POSSIBLE TO LEARN TO MAKE YOUR OWN DECISIONS GOOD NEWS

Slide 109

Slide 109 text

BEFORE_VALIDATION → SERVICE class RoomsController < Web::ApplicationController def create RoomService.create(current_user, permitted_params) end end class RoomService class << self def create(user, params) lat, long = Geocoder.coordinates(params.address) user.rooms.build(params.merge(lat: lat, lng: long)).save! end end end

Slide 110

Slide 110 text

MUTATORS

Slide 111

Slide 111 text

BEFORE_VALIDATION → SERVICE + MUTATOR class RoomMutator class << self def create(user, params) user.rooms.build(params).save! end end end class RoomService class << self def create(user, params) lat, long = Geocoder.coordinates(params.address) RoomMutator.create(user, params) end end end

Slide 112

Slide 112 text

TEXT OKAYISH CALLBACKS before_create do self.has_exercise = lesson.exercise_practice.present? self.questions_count = lesson.questions.web.count end

Slide 113

Slide 113 text

TEXT OKAYISH CALLBACKS → GOOD CALLBACKS #TODO: move to mutator before_create do self.has_exercise = lesson.exercise_practice.present? self.questions_count = lesson.questions.web.count end

Slide 114

Slide 114 text

SERVICE? class CompanyService class << self def create(user, params) company = Company.new(params) company.locale = user.locale company.creator = user TransactionHelper.in_transaction do company.save! member = company.members.build(user: user, role: :manager) member.accept! user.current_company_member = member user.save! company end end end end

Slide 115

Slide 115 text

MUTATOR! class CompanyMutator class << self def create!(user, params) company = Company.new(params) company.locale = user.locale company.creator = user TransactionHelper.in_transaction do company.save! member = company.members.build(user: user, role: :manager) member.accept! user.current_company_member = member user.save! company end end end end

Slide 116

Slide 116 text

INTRODUCE COMPLEXITY HANDLERS GRADUALLY!

Slide 117

Slide 117 text

NICE WAY TO DO IT: INTRODUCE COMPLEXITY HANDLERS GRADUALLY Simple case: - do everything in controller More complex case: - create a service/mutator Super complex scenario: - use multiple mutators inside service

Slide 118

Slide 118 text

NICE WAY TO DO IT: INTRODUCE COMPLEXITY HANDLERS GRADUALLY def create_submission!(company) review = CodeReviewMutator.find_or_create_review!(run) submission = CodeReviewMutator.create_submission!(review, run) if submission.code_review.submissions.count == 1 EventSender.serve! :code_review_created, submission.code_review end CreateCodeReviewJob.perform_later(submission.id) end

Slide 119

Slide 119 text

THE PURPOSE OF LAYERS Models treat them as domain models, they contain relations and business rules Mutators to handle creation/editing/deletion logic (so that we always operate with objects in correct state) Services to handle business operations logic and interaction with external services Controllers application logic (additional form fields, sessions, flash messages, redirects)

Slide 120

Slide 120 text

HUGE FORM-OBJECTS

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

WHY DO WE NEED FROM OBJECTS:

Slide 123

Slide 123 text

WHY DO WE NEED FROM OBJECTS:

Slide 124

Slide 124 text

FORM OBJECTS NO SINGLE STANDARD:

Slide 125

Slide 125 text

TYPICAL FORM OBJECT:

Slide 126

Slide 126 text

TYPICAL FORM OBJECT: VIRTUS-BASED FORM-OBJECTS ⊖ doesn’t play well with accepts_nested_attributes_for ⊖ You have to construct the whole thing from scratch

Slide 127

Slide 127 text

EXAMPLE SITUATION: TWO CONTEXTS Creation/editing: Article - title - body - author Moderation: Article - title - body - author + publication_date

Slide 128

Slide 128 text

EXAMPLE SITUATION: RAILS-WAY SOLUTION CONDITIONAL VALIDATIONS! ⊖ pollutes model with a lot of conditionals ⊖ some of them are form-related conditionals

Slide 129

Slide 129 text

CHEAPER FORM OBJECTS: PHORMS module ApplicationPhorm extend ActiveSupport::Concern module ClassMethods delegate :model_name, to: :superclass delegate :name, to: :superclass end end class ModerationArticlePhorm < Article include ApplicationPhorm validates :publication_date, presence: true end def update @article = ModerationArticlePhorm.find(params[:id]) if @article.update(article_params) redirect_to moderation_articles_path else render 'edit' end end

Slide 130

Slide 130 text

CHEAPER FORM OBJECTS: PHORMS module ApplicationPhorm extend ActiveSupport::Concern module ClassMethods delegate :model_name, to: :superclass delegate :name, to: :superclass def permit(*args) @_args = args end def _args @_args end end def assign_attributes(attrs = {}, _options = {}) raise ArgumentError, 'expected hash' if attrs.nil? if attrs.respond_to? :permit permitted_attrs = attrs.send :permit, self.class._args super(permitted_attrs) else super(attrs) end end end bit.ly/phorm_object

Slide 131

Slide 131 text

CHEAPER FORM OBJECTS: PHORMS ⊕ Zero maintenance cost ⊕ No new abstractions ⊕ Within Rails-way ⊕ Plays nicely together with controllers hierarchy ⊖ dirty hack :)

Slide 132

Slide 132 text

FLAG AS A SIGN OF IMPLICIT STATE

Slide 133

Slide 133 text

TYPICAL WAY OF DOING IT: RANDOM METHODS AND FLAGS TO DETERMINE WHAT TO SHOW - if current_organisation_subscribed? %p You are currently subscribed, using the following payment method: %p== #{@org.card_brand}: **** **** **** #{@org.card_last4} %p== Expires #{@org.card_exp_month} / #{@org.card_exp_year} - if @org.last_charge %p== The last payment of $#{@org.last_charge.amount/100} was taken on #{@org.last_charge.created_at.strftime("%d %b %Y")} %p== The next payment of $100 is due to be taken on #{(@org.last_charge.created_at + 1.month).strftime("%d %b %Y")} = link_to "Update credit card", edit_subscription_path, class: "btn btn-primary" %p = link_to "Cancel my subscription", subscription_path, method: "DELETE", class: "btn btn-danger", data: {confirm: 'Are you sure?'} - else - if @org.trial_ended? %h2 Thank you for trialing our product %h4 To continue using our product please subscribe to the following plan: - else %h2== Thank you for trialing our product (#{distance_of_time_in_words(@org.created_at, Time.zone.now)} left) %h4 To continue using our product after trial please subscribe to the following plan: %p $100/month %p Up to 500 devices and 200 users %p 3 admin users %p CSV/XLS upload = link_to "Subscribe", new_subscription_path, class: 'btn btn-primary'

Slide 134

Slide 134 text

TYPICAL WAY OF DOING IT: RANDOM METHODS AND FLAGS TO DETERMINE WHAT TO SHOW - if current_organisation_subscribed? #... - if @org.last_charge #... - elsif @org.last_charge_status != 'success' #... #... - if @org.expires_at #... - else - if @org.trial_ended? #... - else #... #...

Slide 135

Slide 135 text

ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE FLAGS class Spaceship < ActiveRecord::Base include FlagShihTzu has_flags 1 => :warpdrive, 2 => :shields, 3 => :electrolytes end Spaceship#warpdrive Spaceship#warpdrive? Spaceship#warpdrive= Spaceship#not_warpdrive Spaceship#not_warpdrive? Spaceship#not_warpdrive= Spaceship#warpdrive_changed? Spaceship#all_warpdrives Spaceship#selected_warpdrives Spaceship#select_all_warpdrives Spaceship#unselect_all_warpdrives Spaceship#selected_warpdrives= Spaceship#has_warpdrive?

Slide 136

Slide 136 text

ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE FLAGS Boolean flags are an inseparable element of most Rails apps. They control various aspects of the model business logic – from giving particular user permissions to hiding certain unfinished features. has_flags 1 => :admin, 2 => :onboarded, 3 => :can_use_mailer

Slide 137

Slide 137 text

ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE FLAGS What is wrong with it? has_flags 1 => :admin, 2 => :onboarded, 3 => :can_use_mailer

Slide 138

Slide 138 text

IF YOU USE FLAGS TO DEFINE STATES, YOU’LL BE IN TROUBLE

Slide 139

Slide 139 text

WRONG WAY OF DOING IT: COMBINATORIAL COMPLEXITY EXPLOSION .trial_ended? .expired? .cancelled? .subscription_expired? 16

Slide 140

Slide 140 text

WRONG WAY OF DOING IT: FLAGS CAN BE HIDDEN - dates - periods, - associations, - status fields - if current_organisation_subscribed? #... - if @org.last_charge #... - elsif @org.last_charge_status != 'success' #... #... - if @org.expires_at #... - else - if @org.trial_ended? #... - else #... #...

Slide 141

Slide 141 text

STATE MACHINES

Slide 142

Slide 142 text

EXAMPLES: STATE MACHINES

Slide 143

Slide 143 text

EXAMPLES: STATE MACHINES

Slide 144

Slide 144 text

WHEN DO YOU NEED STATE MACHINE?

Slide 145

Slide 145 text

WHEN SYSTEM BEHAVIOR DEPENDS ON ITS STATE!

Slide 146

Slide 146 text

EXAMPLES: DOCKER STATE MACHINE

Slide 147

Slide 147 text

BETTER WAY OF DOING IT: STATE MACHINE state_machine :state, initial: :trial do state :trial state :trial_ended state :subscribed state :cancelled_subscription state :not_subscribed state :payment_error event :subscribe do transition [:trial, :not_subscribed, :trial_ended] => :subscribed end event :disable_trial do transition :trial => :trial_ended end event :usage_period_ended do transition :cancelled_subscription => :not_subscribed end #... end

Slide 148

Slide 148 text

BETTER WAY OF DOING IT: - if current_organisation.trial? #... - elsif current_organisation.trial_ended? #... - elsif current_organisation.subscribed? #... - elsif current_organisation.cancelled_subscription? #... - elsif current_organisation.payment_error? #... - elsif current_organisation.not_subscribed? #...

Slide 149

Slide 149 text

BETTER WAY OF DOING IT: STATE MACHINE ⊕ Explicitly defines states & transitions ⊕ Prevents combinatorial explosion ⊕ You can even have multiple state machines per model

Slide 150

Slide 150 text

BETTER WAY OF DOING IT: gem state_machines-activerecord

Slide 151

Slide 151 text

TEXT

Slide 152

Slide 152 text

IF-STATEMENTS THROUGH THE WHOLE APP

Slide 153

Slide 153 text

TYPICAL WAY OF DOING IT: TOO MUCH LOGIC IN VIEWS

Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %>

Slide 154

Slide 154 text

BETTER WAY OF DOING IT: MOVE LOGIC TO CONTROLLER require 'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end

Welcome, <%= current_user.name -%>

Slide 155

Slide 155 text

PERFECT WAY OF DOING IT: NULL OBJECTS class Guest def id nil end def admin? false end def moderator? false end def guest? true end def waiting_confirmation? false end def can_use_paid_features? false end def name 'Guest' end end

Welcome, <%= current_user.name -%>

def current_user @current_user ||= User.find_by_id(id: session[:user_id]) || Guest.new end

Slide 156

Slide 156 text

PERFECT WAY OF DOING IT: NULL OBJECTS ⊕ Explicitly define different types of entities in your system ⊕ You get polymorphic behavior for free ⊕ Logic-less controllers and views

Slide 157

Slide 157 text

MIND-BLOWING WAY OF DOING IT: NULL OBJECTS class Guest include Nigilist def name “Guest"s end def time_zone "UTC" end def guest? true end def company OutsideInc.new end end gem nihilist class OutsideInc include Nigilist def name "Outside inc." end def trial_ended? false end end current_user = Guest.new current_user.admin? # false current_user.guest? # true current_user.address # nil current_user.time_zone # 'UTC' current_user.posts # [] current_user.company # #

Slide 158

Slide 158 text

TEXT

Slide 159

Slide 159 text

MODELS ARE CRUCIAL

Slide 160

Slide 160 text

NICOLAUS COPERNICUS (1473 – 1543) https://en.wikipedia.org/wiki/Nicolaus_Copernicus

Slide 161

Slide 161 text

ARISTARCHUS OF SAMOS (310 – 230 BC) https://en.wikipedia.org/wiki/Aristarchus_of_Samos

Slide 162

Slide 162 text

YOU’RE WELCOME!

Slide 163

Slide 163 text

TEXT WHAT STAYS IN THE MODELS? 1) associations 2) business rules 3) state machines 4) defaults 5) scopes?

Slide 164

Slide 164 text

TEXT REPOSITORIES module LikesRepository extend ActiveSupport::Concern included do scope :within_one_hour, -> { where('created_at > ?', 1.hour.ago) } scope :within_one_week, -> { where('created_at > ?', 1.week.ago) } scope :within_one_day, -> { where('created_at > ?', 1.day.ago) } scope :within_one_year, -> { where('created_at > ?', 1.year.ago) } end end class Like < ApplicationRecord include LikesRepository end

Slide 165

Slide 165 text

TEXT MODEL class Organisation < ApplicationRecord has_many :devices has_many :users has_many :employees has_many :charges state_machine :state, initial: :trial do state :trial state :trial_ended state :subscribed state :not_subscribed event :subscribe do transition [:trial, :not_subscribed, :trial_ended] => :subscribed end event :disable_trial do transition :trial => :trial_ended end event :usage_period_ended do transition :cancelled_subscription => :not_subscribed end end def active? !users.any? end end

Slide 166

Slide 166 text

No content

Slide 167

Slide 167 text

“100%” TEST COVERAGE

Slide 168

Slide 168 text

DO WE REALLY NEED 100% TEST COVERAGE?

Slide 169

Slide 169 text

THE PURPOSE OF TESTING SLA & UPTIME * per month

Slide 170

Slide 170 text

WHAT TESTS TO WRITE? - Unit model tests? - Functional controller tests? - Acceptance capybara tests?

Slide 171

Slide 171 text

WHAT TESTS TO WRITE?

Slide 172

Slide 172 text

TEXT WHAT DO YOU COVER WITH TEST? Controllers Acceptance tests

Slide 173

Slide 173 text

TESTING RIGHT QUESTIONS 1) Useful? 2) Easy to write? 3) Easy to support? 4) How fragile they are? 5) How often do you need to rewrite them?

Slide 174

Slide 174 text

TEXT CONTROLLER TESTS test "should post update" do @article = articles(:one) patch article_url(@article), params: {article: { title: "Supertitle", text: "Body", category_id: @secondary_category.id}} @article.reload assert { @article.title == "Supertitle" } assert { @article.state == 'draft' } assert { @article.category == @secondary_category } end test "should get edit" do get edit_article_url(articles(:one)) assert_response :success end

Slide 175

Slide 175 text

GET RID OF STRINGS!

Slide 176

Slide 176 text

NO STRINGS ALLOWED! 1. Always use named routes. 2. Always use i18n (even on single-language sites) 3. enumerize gem for enum fields 4. i18n for flash messages f(:success) f(:failure)

Slide 177

Slide 177 text

RSPEC VS MINITEST

Slide 178

Slide 178 text

No content

Slide 179

Slide 179 text

WHY WE LOVE RSPEC? Semantics! ⊕ subject, describe, it, exepected_to, let, context, … ⊖ hard to structure complex tests ⊖ it_behaves_like “name" ??

Slide 180

Slide 180 text

WHY WE LOVE RSPEC? Matchers! it { is_expected.not_to be_a_multiple_of(3) }

Slide 181

Slide 181 text

PRAGMATISM > AESTHETICS

Slide 182

Slide 182 text

PRAGMATISM > AESTHETICS a % 3 != 0 it { is_expected.not_to be_a_multiple_of(3) } vs

Slide 183

Slide 183 text

THE WORST THING ABOUT RSPEC CAN’T REUSE THE KNOWLEDGE IN OTHER ECOSYSTEMS

Slide 184

Slide 184 text

MINITEST IS NOT IDEAL 1) Does its job 2) Much faster 3) Low-level semantics. But so familiar!

Slide 185

Slide 185 text

REPLACEMENT FOR MATCHERS gem power_assert

Slide 186

Slide 186 text

FACTORIES VS FIXTURES

Slide 187

Slide 187 text

No content

Slide 188

Slide 188 text

FACTORIES ⊖ Slow ⊖ Recreate the whole universe every time ⊖ Can’t do cross-references

Slide 189

Slide 189 text

FIXTURES ⊕ Fast ⊕ Support cross-references ⊕ In most cases fixtures are more than enough ⊕ You can load them into you dev DB

Slide 190

Slide 190 text

MOCKS VS STUBS

Slide 191

Slide 191 text

MOCKS 1) With stubs you replace external stuff 2) With mocks your tests become dependent on internal structure

Slide 192

Slide 192 text

HOW WE MAKE OUR LIFE HARDER? 1) Easiness over simplicity 2) Aesthetics over pragmatism 3) Popularity over thoughtful analysis 4) Conventions over criticality

Slide 193

Slide 193 text

https://phylogame.org/decks/oreilly-animals-starter-deck/ http://etc.usf.edu/clipart/ Pictures from: @inemation [email protected] RailsHurts.com