Slide 1

Slide 1 text

Architecting a Rails app A S H O R T S T O R Y by Luke Randall

Slide 2

Slide 2 text

PHP The horrors of (my)

Slide 3

Slide 3 text

Rails thinks for me + has a place for everything

Slide 4

Slide 4 text

Yay problem solved!

Slide 5

Slide 5 text

Hmm... problem solved?

Slide 6

Slide 6 text

complex? What makes your app

Slide 7

Slide 7 text

Business logic

Slide 8

Slide 8 text

Business logic everywhere!

Slide 9

Slide 9 text

Views

Slide 10

Slide 10 text

<% Post.last(3).each do |post| %> ... <% end %> Exhibit 1a

Slide 11

Slide 11 text

<% if current_user and current_user.admin? %> This counts as logic <% end %> Exhibit 1B

Slide 12

Slide 12 text

Presenters You should be using

Slide 13

Slide 13 text

presenter’s Your structure should reflect the views structure

Slide 14

Slide 14 text

Controllers

Slide 15

Slide 15 text

Picture a controller with lots of logic Exhibit 2a

Slide 16

Slide 16 text

Fat models Skinny controllers

Slide 17

Slide 17 text

def index @posts = Post.published .with_tag(params[:tag]) .reorder('published_at DESC') .include(:comments) .limit(5) end Exhibit 2B

Slide 18

Slide 18 text

Controllers should be a thin mapping between a user request and your actual API

Slide 19

Slide 19 text

Models

Slide 20

Slide 20 text

class User < ActiveRecord::Base extend ActiveSupport::Memoizable has_and_belongs_to_many :questionable_titles has_and_belongs_to_many :group_collectives belongs_to :preferred_group_collective, :class_name => 'GroupCollective' acts_as_authentic acts_as_audited :except => [:perishable_token, :last_request_at, :login_count, :last_login_at] i_want_to_hear_from_your_via :email validates_presence_of :name validate :has_an_authorized_fascist_score validates_format_of :password, :with => /(([0-9]+[A-Za-z]+)|([A-Za-z]+[0-9]+)).*/, :if => :require_password?, :message => "must include at least one number and one letter" def first_name name.andand.split.andand.first end def default_group_collective @default_group_collective ||= preferred_group_collective || group_collectives.first end def recent_group_collectives return [] unless recent_group_collectives_ids GroupCollective.find(:all, :conditions => ["id IN (?)", recent_group_collectives_ids.split(',')]) end def add_to_recent_group_collectives(group_collective_id) recent = recent_group_collectives_ids recent ||= '' ids = recent.split(',').insert(0, group_collective_id.to_s).uniq.select {|a| a}.first(10).join(',') update_attribute(:recent_group_collectives_ids, ids) end def remove_from_recent_group_collectives(group_collective_id) list = recent_group_collectives_ids.split(',') list.delete("#{group_collective_id}") self.recent_group_collectives_ids = list.join(',') self.save end def deliver_password_reset_instructions! reset_perishable_token! Notifier.deliver_password_reset_instructions(self) end # There are tnhaoeuhsaou oesnhuasonthu soaetnhu anoth: # * ntaohue sntahou snoatuhsntah stnoehu # * anstoeuh satnou sntahsntuha osnteh usnth # * sanoteh usnahsntuh santhousnthaosnthao sntuh # For more info snahtousa usahtnoeu snathousaotnhsn def is_group_superintendant?(space_station) is_space_station_superintendant?(space_station) && space_station.group_space_station? end def is_consolidation_superintendant?(space_station) is_space_station_superintendant?(space_station) && space_station.consolidation_space_station? end def is_space_station_superintendant?(space_station) return true if has_questionable_title?('super_user') # The user is a member of one of the space_station's group_collectives group_collectives.exists?(:space_station_id => space_station.id) && # and the user has the 'space_station_superintendant' questionable_title has_questionable_title?('space_station_superintendant') end def is_superintendant? has_questionable_title?('superintendant') || has_questionable_title?('super_user') end #memoize :is_superintendant? def is_master_and_commander_for_the_free_world? has_questionable_title?('super_user') end #memoize :is_master_and_commander_for_the_free_world? def authorized_to_access_group_collective?(group_collective_id, space_station = nil) log_time("authorizing #{email} for group_collective #{group_collective_id}", lambda { return false unless GroupCollective.exists?(group_collective_id) # If he is a master_and_commander_for_the_free_world he can access any group_collective return true if is_master_and_commander_for_the_free_world? # if the group_collective is associated with him then he can access it return true if group_collectives.exists?(group_collective_id) # The space_station parameter is required for authorization beyond this point return false if space_station.nil? # if he is a space_station_superintendant on a 'Group' space_station then he can only access the group_collective if it # belongs to the current space_station if is_group_superintendant?(space_station) return true if GroupCollective.exists?(:id => group_collective_id, :space_station_id => space_station.id) # if he is a consolidation_superintendant he can only access the group_collective if it # is beneath him in the consolidation tree elsif is_consolidation_superintendant?(space_station) return false unless space_station.group_collectives.exists?(:id => group_collective_id) # return false if the group_collective is not on his space_station # He is authorized if the group_collective is a decendent of any of his associated group_collectives group_collective_in_question = space_station.group_collectives.find(group_collective_id) return group_collectives.any? {|group_collective| group_collective_in_question.descendent_of?(group_collective)} end # he has not been authorized at any level return false }) end def has_many_group_collectives? group_collectives.size > 1 end def primary_group_collective return nil if group_collectives.empty? return group_collectives.first(:conditions => {:master_group_collective => true}) if group_collectives.exists?(:master_group_collective => true) group_collectives.first end def has_no_group_collective? group_collectives.empty? end def add_group_collective(group_collective) self.group_collectives << group_collective self.save! end # questionable_title based access methods # has_questionable_title? is used by questionable_title_requirement def has_questionable_title?(questionable_title_in_question) @_questionable_titles_list = self.questionable_titles.collect(&:name) @_questionable_titles_list.include?(questionable_title_in_question.to_s) end def authorised_for_questionable_title?(questionable_title_in_question) # Check if the use qualifies for the questionable_title_in_question implicitly # due to a higher questionable_title that they posses if questionable_titleS_AND_SUPERIORS[questionable_title_in_question].any?{|r| has_questionable_title?(r)} return true # The user does not qualify implicitly (they own no higher questionable_title). Check for # the questionable_title_in_question explicitly. elsif has_questionable_title?(questionable_title_in_question) return true end return false end def update_questionable_titles(questionable_titles) return valid? if questionable_titles.blank? questionable_titles.each do |questionable_title_name, status| delete_questionable_title(questionable_title_name) if status.to_i == 0 add_questionable_title(questionable_title_name) if status.to_i == 1 end return valid? end def delete_questionable_title(questionable_title_name) return unless self.has_questionable_title?(questionable_title_name) self.questionable_titles.delete(self.questionable_titles.find(:first, :conditions => {:name => questionable_title_name})) end def add_questionable_title(questionable_title_name) return false if questionable_title_name == 'super_user' return true if self.has_questionable_title?(questionable_title_name) self.questionable_titles << questionable_title.find_by_name(questionable_title_name) self.save end def authorised_fascist_scores return FASCIST_SCORES_WITH_COMMUNISM if is_superintendant? || is_master_and_commander_for_the_free_world? fascist_scores = questionable_titles.collect {|questionable_title| questionable_title.view_fascist_score} ordered_fascist_scores = [] # let's try loop through this so that the allowed fascist_scores are in the propers order, yo FASCIST_SCORES_WITH_COMMUNISM.each do |view_fascist_score| ordered_fascist_scores << view_fascist_score if fascist_scores.include?(view_fascist_score) end return ordered_fascist_scores end def has_an_authorized_fascist_score return if authorised_for_questionable_title?('superintendant') errors.add_to_base 'User must have at least one fascist_score that they are authorized to access.' if authorised_fascist_scores.blank? end end Exhibit 3a

Slide 21

Slide 21 text

The only letter left in MVC

Slide 22

Slide 22 text

Models are a catch-all for business logic

Slide 23

Slide 23 text

Models are a catch-all for business logic complexity

Slide 24

Slide 24 text

All models eventually have one billion methods doing completely di erent things * * actual number may vary

Slide 25

Slide 25 text

Single responsibility Open closed Liskov substitution Interface segregation Dependency inversion

Slide 26

Slide 26 text

“The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.” Joe Armstrong

Slide 27

Slide 27 text

means you’re doing something Pain in tests wrong

Slide 28

Slide 28 text

class oriented vs object oriented

Slide 29

Slide 29 text

Meaning comes from casting data into a role

Slide 30

Slide 30 text

Meaning comes from casting data into a role what the system is what the system does

Slide 31

Slide 31 text

user.extend FriendApprover user.approve(friend_request) * * stolen shamelessly from Jim Gay’s blog

Slide 32

Slide 32 text

user.extend FriendApprover user.approve(friend_request) * * stolen shamelessly from Jim Gay’s blog data role

Slide 33

Slide 33 text

Single responsibility Open closed Liskov substitution Interface segregation Dependency inversion

Slide 34

Slide 34 text

Roles are reusable Clearly defined interface

Slide 35

Slide 35 text

Roles are testable Use plain old Ruby objects

Slide 36

Slide 36 text

context roles have a

Slide 37

Slide 37 text

class AddToCartContext attr_reader :user, :book def self.call(user_id, book_id) AddToCartContext.new(user_id, book_id).call end def initialize(user_id, book_id) @user = User.find(user_id) @book = Book.find(book_id) @user.extend Customer end def call @user.add_to_cart(@book) end end * * stolen shamelessly from Mike Pack’s blog context readable logic

Slide 38

Slide 38 text

Controllers should be a thin mapping between a user request and your actual API Replay

Slide 39

Slide 39 text

Contexts are your actual API

Slide 40

Slide 40 text

Contexts read like the logic they implement

Slide 41

Slide 41 text

Business logic expressed in a first class, easily understood, easily modified, easily tested, entity

Slide 42

Slide 42 text

Guide us O Great Wizard!

Slide 43

Slide 43 text

DCI data context interaction

Slide 44

Slide 44 text

I invented MVC and DCI for each other “ ” (not actually) Trygve Reenskaug

Slide 45

Slide 45 text

extra overhead? what about the

Slide 46

Slide 46 text

“Architectural decisions are tradeoffs” Me, right now + many others

Slide 47

Slide 47 text

“A good architecture allows major decisions to be deferred ” Robert “Uncle Bob” Martin

Slide 48

Slide 48 text

We’re deferring adding bloaty, messy logic to the moment it is needed

Slide 49

Slide 49 text

Use this

Slide 50

Slide 50 text

Luke Randall @luke_randall lukerandall.github.com