Slide 1

Slide 1 text

Building Extractable Libraries in Rails

Slide 2

Slide 2 text

Hello There! I’m @patricksroberts. I work at @iorahealth. I co-organize @bostonrb.

Slide 3

Slide 3 text

The Duality of a Rails Developer

Slide 4

Slide 4 text

Ruby is driven by <3

Slide 5

Slide 5 text

Our best practices change with our sense of beauty

Slide 6

Slide 6 text

Our ecosystem is driven by the many ways to solve problems

Slide 7

Slide 7 text

Rails is driven by Conventions

Slide 8

Slide 8 text

Rails frees us from making many tedious decisions

Slide 9

Slide 9 text

The freedom from decisions allows us unparalleled productivity

Slide 10

Slide 10 text

/lib is the Battlefield

Slide 11

Slide 11 text

I believe it can benefit from some conventions

Slide 12

Slide 12 text

In order to ship more code I believe it can benefit from some conventions

Slide 13

Slide 13 text

Namespace All The Things No Rails Application has a User class right??

Slide 14

Slide 14 text

Avoid the Autoload Trap Maintain explicit entry points into your library code using initializers

Slide 15

Slide 15 text

Use the Configuration Pattern Keep credentials as far away from your library code as possible

Slide 16

Slide 16 text

Isolate Interaction with your Domain Use DCI at the glue points of your application code

Slide 17

Slide 17 text

Namespace All The Things

Slide 18

Slide 18 text

# lib/twitter_user.rb class TwitterUser def initialize(user) @user = user end end # lib/twitter_profile.rb class TwitterProfile def initialize(twitter_user, image_url) @user = twitter_user @image_url = image_url end end

Slide 19

Slide 19 text

# lib/papa_smurf.rb class PapaSmurf end # lib/sleepy_smurf.rb class SleepySmurf end # lib/ninja_smurf.rb class NinjaSmurf end

Slide 20

Slide 20 text

# lib/smurf.rb module Smurf class Papa end class Sleepy end class Ninja end end

Slide 21

Slide 21 text

Avoiding the Autoload Trap

Slide 22

Slide 22 text

Rails 3 brought a wonderful convention for removing /lib from automatically being loaded on application boot...

Slide 23

Slide 23 text

... however our first instinct is to do this: # config/application.rb # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{config.root}/lib) Let’s try something different!

Slide 24

Slide 24 text

# config/initializers/twitter_wrangler.rb require_relative ‘../lib/twitter_wrangler’ # lib/twitter_wrangler.rb require ‘twitter_wrangler/authentication’ require ‘twitter_wrangler/authorization’ module TwitterWrangler end Combined with...

Slide 25

Slide 25 text

We have created a namespace and setup a proper layout when we need to extract our code outside the application into a gem. ... in ten seconds or less :)

Slide 26

Slide 26 text

Hide Your Credentials From Your Library

Slide 27

Slide 27 text

It’s so simple to do this: # lib/twitter_wrangler.rb module TwitterWrangler OAUTH_KEY = ‘ABCDEFG1234ZXYZB2324’ end Or even this: # lib/twitter_wrangler.rb module TwitterWrangler OAUTH_KEY = ENV[‘TWITTER_KEY’] end

Slide 28

Slide 28 text

But we already have an entry point for our library code. So let’s exploit the heck out of it!

Slide 29

Slide 29 text

# config/initializers/twitter_wrangler.rb require_relative ‘../lib/twitter_wrangler’ TwitterWrangler.configure do |config| config.oauth_key = ENV[:twitter_oauth_key] end

Slide 30

Slide 30 text

# config/initializers/twitter_wrangler.rb require_relative ‘../lib/twitter_wrangler’ TwitterWrangler.configure do |config| config.oauth_key = ENV[:twitter_oauth_key] end

Slide 31

Slide 31 text

# config/initializers/twitter_wrangler.rb require_relative ‘../lib/twitter_wrangler’ TwitterWrangler.configure do |config| config.oauth_key = ENV[:twitter_oauth_key] end

Slide 32

Slide 32 text

# config/initializers/twitter_wrangler.rb require_relative ‘../lib/twitter_wrangler’ TwitterWrangler.configure do |config| config.oauth_key = ENV[:twitter_oauth_key] end

Slide 33

Slide 33 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration < Struct.new(:oauth_key) end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end end

Slide 34

Slide 34 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration < Struct.new(:oauth_key) end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end end

Slide 35

Slide 35 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration < Struct.new(:oauth_key) end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end end

Slide 36

Slide 36 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration < Struct.new(:oauth_key) end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end end

Slide 37

Slide 37 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration < Struct.new(:oauth_key) end class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end end

Slide 38

Slide 38 text

With one additional, boilerplate class we’ve established a separation of credential concerns

Slide 39

Slide 39 text

Keeping Your Domain Models Focused

Slide 40

Slide 40 text

Prescriptions Queueing Authorization Authentication Timeouts Caching BMI Appointments

Slide 41

Slide 41 text

Web App body mass index tweet called Library Authorization Twitter message Authentication Queueing

Slide 42

Slide 42 text

ROLE

Slide 43

Slide 43 text

# lib/twitter_wrangler/tweeting_patient.rb module TwitterWrangler::TweetingPatient def bmi_twitter_message “Today my BMI is #{body_mass_index} and I’m #{percent_of_body_mass_index_goal} from my goal of #{body_mass_index_goal}!” end end

Slide 44

Slide 44 text

A Brief Interlude on Test Doubles

Slide 45

Slide 45 text

# lib/twitter_wrangler/support/fake_patient.rb class FakePatient def body_mass_index;27;end def percent_of_body_mass_index_goal;”95%”;end def body_mass_index_goal;25;end end Option #1 - The Fake Class

Slide 46

Slide 46 text

Option #2 - OpenStruct! patient = OpenStruct.new(body_mass_index: 27, percent_of_body_mass_index_goal: ’95%’, body_mass_index_goal: 25)

Slide 47

Slide 47 text

Option #3 - Mock patient = mock(body_mass_index: 27, percent_of_body_mass_index_goal: ’95%’, body_mass_index_goal: 25)

Slide 48

Slide 48 text

# spec/twitter_wrangler/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { FakePatient.new } before { patient.extend TweetingPatient } describe ‘#bmi_twitter_message’ do let(:tweet) { patient.bmi_twitter_message } it ‘crafts a lovely tweet’ do expect(tweet).to include ‘Today my BMI is...’ end end end

Slide 49

Slide 49 text

# spec/twitter_wrangler/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { FakePatient.new } before { patient.extend TweetingPatient } describe ‘#bmi_twitter_message’ do let(:tweet) { patient.bmi_twitter_message } it ‘crafts a lovely tweet’ do expect(tweet).to include ‘Today my BMI is...’ end end end

Slide 50

Slide 50 text

# spec/twitter_wrangler/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { FakePatient.new } before { patient.extend TweetingPatient } describe ‘#bmi_twitter_message’ do let(:tweet) { patient.bmi_twitter_message } it ‘crafts a lovely tweet’ do expect(tweet).to include ‘Today my BMI is...’ end end end

Slide 51

Slide 51 text

# spec/twitter_wrangler/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { FakePatient.new } before { patient.extend TweetingPatient } describe ‘#bmi_twitter_message’ do let(:tweet) { patient.bmi_twitter_message } it ‘crafts a lovely tweet’ do expect(tweet).to include ‘Today my BMI is...’ end end end

Slide 52

Slide 52 text

CONTEXT

Slide 53

Slide 53 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate < Struct.new(:patient) def call Queue.push tweeting_patient.bmi_twitter_message end private def tweeting_patient @tweeting_patient ||= patient.extend TweetingPatient end end end

Slide 54

Slide 54 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate < Struct.new(:patient) def call Queue.push tweeting_patient.bmi_twitter_message end private def tweeting_patient @tweeting_patient ||= patient.extend TweetingPatient end end end

Slide 55

Slide 55 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate < Struct.new(:patient) def call Queue.push tweeting_patient.bmi_twitter_message end private def tweeting_patient @tweeting_patient ||= patient.extend TweetingPatient end end end

Slide 56

Slide 56 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate < Struct.new(:patient) def call Queue.push tweeting_patient.bmi_twitter_message end private def tweeting_patient @tweeting_patient ||= patient.extend TweetingPatient end end end

Slide 57

Slide 57 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate < Struct.new(:patient) def call Queue.push tweeting_patient.bmi_twitter_message end private def tweeting_patient @tweeting_patient ||= patient.extend TweetingPatient end end end

Slide 58

Slide 58 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 59

Slide 59 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 60

Slide 60 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 61

Slide 61 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 62

Slide 62 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 63

Slide 63 text

# spec/twitter_wrangler/bmi_twitter_update_spec.rb describe BMITwitterUpdate do describe ‘#call’ do let(:patient) { mock() } let(:update) { BMITwitterUpdate.new(patient) } before { Queue.stubs(:push) } it ‘extends TweetingPatient’ do update.call expect(patient).to have_received(:extend).with TweetingPatient end it ‘queues the twitter message’ do update.call expect(Queue).to have_received(:push).with patient.bmi_twitter_message end end end

Slide 64

Slide 64 text

GLUE

Slide 65

Slide 65 text

# app/controllers/bmi_controller.rb class BmiController < ApplicationController def create bmi = patient.bmis.create(:bmi) Resque.enqueue :bmi_twitter_update_job, patient.id respond_with bmi end private def patient @patient ||= current_user.patients.find(:patient_id) end end

Slide 66

Slide 66 text

# app/jobs/bmi_twitter_update_job.rb class BmiTwitterUpdateJob @queue = :twitter def self.perform_job(patient_id) patient = Patient.find(patient_id) BMITwitterUpdate.new(patient).call end end

Slide 67

Slide 67 text

RECAP

Slide 68

Slide 68 text

Treat Your Lib Directory Like a Temple

Slide 69

Slide 69 text

Remove Configuration Nightmares

Slide 70

Slide 70 text

Protect Domain Interactions

Slide 71

Slide 71 text

Thanks!