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

I am not James Daniels!

Slide 4

Slide 4 text

Nor am I Joe Grossberg (I really <3 this GIF)

Slide 5

Slide 5 text

The Duality of a Rails Developer

Slide 6

Slide 6 text

Ruby is driven by <3

Slide 7

Slide 7 text

Our best practices change with our sense of beauty

Slide 8

Slide 8 text

Our ecosystem is driven by the many ways to solve problems

Slide 9

Slide 9 text

Rails is driven by Conventions

Slide 10

Slide 10 text

Rails frees us from making many tedious decisions

Slide 11

Slide 11 text

The freedom from decisions allows us unparalleled productivity

Slide 12

Slide 12 text

/lib is a Battlefield

Slide 13

Slide 13 text

I think it can benefit from some conventions So we can ship some more code

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 Models Use the DCI pattern at the glue points of your application code

Slide 17

Slide 17 text

Avoiding the Autoload Trap

Slide 18

Slide 18 text

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

Slide 19

Slide 19 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 20

Slide 20 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 21

Slide 21 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 22

Slide 22 text

Hide Your Credentials From Your Library

Slide 23

Slide 23 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 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

# lib/twitter_wrangler/configuration.rb module TwitterWrangler class Configuration attr_accessor :oauth_key def initialize oauth_key = nil end 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 attr_accessor :oauth_key def initialize oauth_key = nil end 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

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

Slide 36

Slide 36 text

Keeping Your Domain Models Focused

Slide 37

Slide 37 text

Prescriptions Queueing Authorization Authentication Timeouts Caching BMI Appointments

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

# lib/twitter_wrangler/roles/tweeting_patient.rb module TwitterWrangler::Roles::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 40

Slide 40 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 # spec/twitter_wrangler/roles/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { FakePatient.new } before { FakePatient.extend TweetingPatient } it ‘asserts some fantastic things’ do #GLORIOUS SPECS end end

Slide 41

Slide 41 text

# spec/twitter_wrangler/roles/tweeting_patient_spec.rb describe TweetingPatient do let(:patient) { OpenStruct.new(body_mass_index: 27, percent_of_body_mass_index_goal: “95%”, body_mass_index_goal: 25) } before { patient.extend TweetingPatient } it ‘asserts some fantastic things’ do #GLORIOUS SPECS end end

Slide 42

Slide 42 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate attr_reader: patient def initialize(patient) @patient = patient @patient.extend TweetingPatient end def call TwitterWranglerQueue.add @patient.bmi_twitter_message end end end

Slide 43

Slide 43 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate attr_reader: patient def initialize(patient) @patient = patient @patient.extend TweetingPatient end def call TwitterWranglerQueue.add @patient.bmi_twitter_message end end end

Slide 44

Slide 44 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate attr_reader: patient def initialize(patient) @patient = patient @patient.extend TweetingPatient end def call TwitterWranglerQueue.add @patient.bmi_twitter_message end end end

Slide 45

Slide 45 text

# lib/twitter_wrangler/bmi_twitter_update.rb module TwitterWrangler class BMITwitterUpdate attr_reader: patient def initialize(patient) @patient = patient @patient.extend TweetingPatient end def call TwitterWranglerQueue.add @patient.bmi_twitter_message end end end

Slide 46

Slide 46 text

# app/models/contexts/patient_bmi_observer.rb class PatientBmiObserver observe :patient def after_save(patient) TwitterWrangler::BMITwitterUpdate.new(patient).call @patient.extend TweetingPatient end end

Slide 47

Slide 47 text

# spec/models/patient_bmi_observer_spec.rb describe PatientBMIObserver do let(:bmi_update) { mock() } before do BMITwitterUpdate.stubs(:new).returns bmi_update bmi_update.stubs :call end it ‘sends a twitter update on mah BMI’ do # expect #call to have been called once end end

Slide 48

Slide 48 text

# spec/models/patient_bmi_observer_spec.rb describe PatientBMIObserver do let(:bmi_update) { mock() } before do BMITwitterUpdate.stubs(:new).returns bmi_update bmi_update.stubs :call end it ‘sends a twitter update on mah BMI’ do # expect #call to have been called once end end

Slide 49

Slide 49 text

Provides clear separation of Domain Model and outside code Creates isolatable tests Easy path forward for gem extraction Boilerplate :( YAGNI :(

Slide 50

Slide 50 text

Thanks!

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Questions?