Counterintuitive Rails

Counterintuitive Rails

F9c1a378a1e3926ea1a58cf724140000?s=128

Ivan Nemytchenko

March 16, 2018
Tweet

Transcript

  1. COUNTERINTUITIVE RAILS @inemation PART I

  2. THOSE DANGEROUS RUBIES!! @inemation PART 1

  3. 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!)
  4. THE AUTHOR IVAN NEMYTCHENKO Was giving a talk here 3

    years ago Created RailsHurts.com Started writing a book
  5. LET’S DREAM!

  6. 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
  7. PEOPLE ARE LAZY

  8. DON’T RELY ON DISCIPLINE

  9. DISCIPLINE

  10. THEORY, PRACTICE AND A DREAM

  11. THERE WILL BE DIRT. ALWAYS.

  12. THE PROBLEM IS WHEN YOUR SYSTEM ENCOURAGES YOU TO REPLICATE

    DIRT
  13. FLAT MODELS STRUCTURE

  14. SMALL PROJECT MODELS TYPICAL WAY OF DOING IT:

  15. REDMINE MODELS 70 models 1 folder TYPICAL WAY OF DOING

    IT:
  16. GITLAB MODELS 128 models at the top level 16 folders

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

    IT:
  18. 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:
  19. 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"
  20. FLAT CONTROLLERS STRUCTURE

  21. SMALL SCALE TYPICAL WAY OF DOING IT:

  22. LARGER SCALE TYPICAL WAY OF DOING IT:

  23. 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] #...
  24. 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 ] #...
  25. 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]
  26. FLAT CONTROLLERS STRUCTURE - 1) REST misunderstanding - 2) respond_to

    - 3) lack of DDD thinking WHY DO WE DO THAT?
  27. REST MISUNDERSATNDING

  28. WHAT DO WE GET HERE? GET /POSTS/5

  29. WHAT DO WE GET HERE? A RESOURCE?

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

  31. WHAT DO WE GET HERE? A REPRESENTATION!

  32. 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!
  33. DEVELOPERS WHO ARE LIMITING THEMSELVES WITH ONE CONTROLLER PER MODEL

    DO NOT UNDERSTAND REST
  34. SOMEONE COULD SAY: BUT WE SERVE DIFFERENT REPRESENTATIONS WITH RESPOND_TO!

  35. RESPOND_TO USAGE

  36. 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
  37. 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
  38. 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
  39. BETTER WAY OF DOING IT: TOP-LEVEL NAMESPACES class Web::ArticlesController <

    Web::ApplicationController #... end class Api::ArticlesController < Api::ApplicationController #... end
  40. LET’S APPLY SOME DDD

  41. DOMAIN DRIVEN DESIGN

  42. DOMAIN DRIVEN DESIGN BOUNDED CONTEXT

  43. DOMAIN DRIVEN DESIGN BOUNDED CONTEXT EXAMPLE Creation/editing: Article - title

    - body - author Moderation: Article - title - body - author + category + publication_date
  44. 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
  45. 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
  46. BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES

    complex universal controllers vs many simple controllers for specific representations
  47. 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
  48. FLAT CONTROLLERS STRUCTURE - 1) No respond_to - 2) Separate

    namespaces per way of usage - 3) More namespaces per “bounded context” inside HOW TO FIX:
  49. VIOLATING MVC PATTERN

  50. MVC IS MEANT TO BE A MODULAR THING

  51. WHAT IS MODULARITY?

  52. WHAT MAKES A SYSTEM MODULAR?

  53. MODULES?

  54. MODULARITY IF TWO MODULES ARE INTERCONNECTED IT IS NOT A

    MODULARITY
  55. MODULARITY MODULARITY IS THE LACK OF CIRCULAR CONNECTIONS (DEPENDENCIES)

  56. MODULARITY KNOWLEDGE SHOULD ONLY GO IN ONE DIRECTION

  57. MODEL THAT SENDS NOTIFICATIONS 90% of Rails projects! TYPICAL WAY

    OF DOING IT:
  58. GEM LIKE ACTIVE_ADMIN OR ACTS_AS_API TYPICAL WAY OF DOING IT:

  59. RENDERING IN MODELS TYPICAL WAY OF DOING IT:

  60. CURRENT_USER IN MODELS TYPICAL WAY OF DOING IT:

  61. GLOBALS IN MODELS TYPICAL WAY OF DOING IT:

  62. 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?
  63. APPLICATION LOGIC VS BUSINESS LOGIC

  64. EXAMPLES OF APPLICATION LOGIC TYPICAL WAY OF DOING IT:

  65. 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
  66. - 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
  67. WHAT ARE MODELS ABOUT?

  68. PERSISTENCE?

  69. WHAT ARE MODELS ABOUT? PERSISTENCE? - We can have models

    without persistence - Persistence is a technical detail
  70. WHAT ARE MODELS ABOUT? DOMAIN MODEL - It is a

    system that imitates structure or functioning of explored domain
  71. WHAT ARE MODELS ABOUT? - doesn’t depend on programming language

    - doesn’t depend on type of app - doesn’t depend on framework DOMAIN MODEL
  72. TYPICAL WAY OF DOING IT: LET’S JUST PUT EVERYTHING INTO

    MODELS FAT MODELS, SKINNY CONTROLLERS!
  73. PUTTING CODE IN RANDOM PLACES

  74. HOW DOES COMPLEXITY CREEP IN?

  75. 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
  76. HOW COMPLEXITY CREEPS IN? BUT THEN…

  77. 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
  78. HOW TO FIGHT COMPLEXITY?

  79. HOW TO FIGHT COMPLEXITY? AVOID IT!

  80. HOW TO FIGHT COMPLEXITY? PUT STUFF INTO PROPER PLACES

  81. FAT MODELS! SKINNY CONTROLLERS!

  82. HELL, NO! FAT MODELS! SKINNY CONTROLLERS!

  83. 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!
  84. 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
  85. STATEFUL SERVICES

  86. 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.
  87. 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
  88. 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
  89. 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)
  90. 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
  91. 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:
  92. 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:
  93. ENOUGH FOR TODAY? @inemation nemytchenko@gmail.com RailsHurts.com BIT.LY/WROCRB

  94. COUNTERINTUITIVE RAILS PART II RAILSHURTS.COM/PRINCIPLES

  95. None
  96. None
  97. Additional layers & building blocks Frameworks on top of Rails

    Another framework Another language Rails engines Move logic to libs Move logic to gems
  98. Services Interactors Form Objects Repositories Policies Value Objects

  99. None
  100. CALLBACKS

  101. None
  102. 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"
  103. 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 #...
  104. 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"
  105. BEFORE_VALIDATION → DEFAULTS class Mother < Person def initialize(attrs =

    {}) defaults = { gender: "F" } super(defaults.merge(attrs)) end end Mother.new => #<Mother id: nil, gender: "F", created_at: nil, updated_at: nil>
  106. 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
  107. YOU CAN’T TRUST ANYONE ON INTERNETS BAD NEWS

  108. IT IS POSSIBLE TO LEARN TO MAKE YOUR OWN DECISIONS

    GOOD NEWS
  109. 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
  110. MUTATORS

  111. 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
  112. TEXT OKAYISH CALLBACKS before_create do self.has_exercise = lesson.exercise_practice.present? self.questions_count =

    lesson.questions.web.count end
  113. 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
  114. 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
  115. 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
  116. INTRODUCE COMPLEXITY HANDLERS GRADUALLY!

  117. 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
  118. 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
  119. 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)
  120. HUGE FORM-OBJECTS

  121. None
  122. WHY DO WE NEED FROM OBJECTS:

  123. WHY DO WE NEED FROM OBJECTS:

  124. FORM OBJECTS NO SINGLE STANDARD:

  125. TYPICAL FORM OBJECT:

  126. 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
  127. EXAMPLE SITUATION: TWO CONTEXTS Creation/editing: Article - title - body

    - author Moderation: Article - title - body - author + publication_date
  128. EXAMPLE SITUATION: RAILS-WAY SOLUTION CONDITIONAL VALIDATIONS! ⊖ pollutes model with

    a lot of conditionals ⊖ some of them are form-related conditionals
  129. 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
  130. 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
  131. CHEAPER FORM OBJECTS: PHORMS ⊕ Zero maintenance cost ⊕ No

    new abstractions ⊕ Within Rails-way ⊕ Plays nicely together with controllers hierarchy ⊖ dirty hack :)
  132. FLAG AS A SIGN OF IMPLICIT STATE

  133. 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'
  134. 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 #... #...
  135. 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?
  136. 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
  137. 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
  138. IF YOU USE FLAGS TO DEFINE STATES, YOU’LL BE IN

    TROUBLE
  139. WRONG WAY OF DOING IT: COMBINATORIAL COMPLEXITY EXPLOSION .trial_ended? .expired?

    .cancelled? .subscription_expired? 16
  140. 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 #... #...
  141. STATE MACHINES

  142. EXAMPLES: STATE MACHINES

  143. EXAMPLES: STATE MACHINES

  144. WHEN DO YOU NEED STATE MACHINE?

  145. WHEN SYSTEM BEHAVIOR DEPENDS ON ITS STATE!

  146. EXAMPLES: DOCKER STATE MACHINE

  147. 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
  148. 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? #...
  149. BETTER WAY OF DOING IT: STATE MACHINE ⊕ Explicitly defines

    states & transitions ⊕ Prevents combinatorial explosion ⊕ You can even have multiple state machines per model
  150. BETTER WAY OF DOING IT: gem state_machines-activerecord

  151. TEXT

  152. IF-STATEMENTS THROUGH THE WHOLE APP

  153. TYPICAL WAY OF DOING IT: TOO MUCH LOGIC IN VIEWS

    <h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
  154. 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 <h3>Welcome, <%= current_user.name -%></h3>
  155. 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 <h3>Welcome, <%= current_user.name -%></h3> def current_user @current_user ||= User.find_by_id(id: session[:user_id]) || Guest.new end
  156. 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
  157. 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 # #<OutsideInc:0x007ff105193c20>
  158. TEXT

  159. MODELS ARE CRUCIAL

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

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

  162. YOU’RE WELCOME!

  163. TEXT WHAT STAYS IN THE MODELS? 1) associations 2) business

    rules 3) state machines 4) defaults 5) scopes?
  164. 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
  165. 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
  166. None
  167. “100%” TEST COVERAGE

  168. DO WE REALLY NEED 100% TEST COVERAGE?

  169. THE PURPOSE OF TESTING SLA & UPTIME * per month

  170. WHAT TESTS TO WRITE? - Unit model tests? - Functional

    controller tests? - Acceptance capybara tests?
  171. WHAT TESTS TO WRITE?

  172. TEXT WHAT DO YOU COVER WITH TEST? Controllers Acceptance tests

  173. 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?
  174. 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
  175. GET RID OF STRINGS!

  176. 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)
  177. RSPEC VS MINITEST

  178. None
  179. WHY WE LOVE RSPEC? Semantics! ⊕ subject, describe, it, exepected_to,

    let, context, … ⊖ hard to structure complex tests ⊖ it_behaves_like “name" ??
  180. WHY WE LOVE RSPEC? Matchers! it { is_expected.not_to be_a_multiple_of(3) }

  181. PRAGMATISM > AESTHETICS

  182. PRAGMATISM > AESTHETICS a % 3 != 0 it {

    is_expected.not_to be_a_multiple_of(3) } vs
  183. THE WORST THING ABOUT RSPEC CAN’T REUSE THE KNOWLEDGE IN

    OTHER ECOSYSTEMS
  184. MINITEST IS NOT IDEAL 1) Does its job 2) Much

    faster 3) Low-level semantics. But so familiar!
  185. REPLACEMENT FOR MATCHERS gem power_assert

  186. FACTORIES VS FIXTURES

  187. None
  188. FACTORIES ⊖ Slow ⊖ Recreate the whole universe every time

    ⊖ Can’t do cross-references
  189. FIXTURES ⊕ Fast ⊕ Support cross-references ⊕ In most cases

    fixtures are more than enough ⊕ You can load them into you dev DB
  190. MOCKS VS STUBS

  191. MOCKS 1) With stubs you replace external stuff 2) With

    mocks your tests become dependent on internal structure
  192. HOW WE MAKE OUR LIFE HARDER? 1) Easiness over simplicity

    2) Aesthetics over pragmatism 3) Popularity over thoughtful analysis 4) Conventions over criticality
  193. https://phylogame.org/decks/oreilly-animals-starter-deck/ http://etc.usf.edu/clipart/ Pictures from: @inemation nemytchenko@gmail.com RailsHurts.com