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. I maintain @copycopter.

Slide 3

Slide 3 text

I’m not so secretly using you for a sailing vacation.

Slide 4

Slide 4 text

The Duality of a Rails Developer

Slide 5

Slide 5 text

Ruby is driven by <3

Slide 6

Slide 6 text

Our best practices change with our sense of beauty

Slide 7

Slide 7 text

Our ecosystem is driven by the many ways to solve problems

Slide 8

Slide 8 text

Rails is driven by Conventions

Slide 9

Slide 9 text

Rails frees us from making many tedious decisions

Slide 10

Slide 10 text

The freedom from decisions allows us unparalleled productivity

Slide 11

Slide 11 text

/lib is a Battlefield

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Avoiding the Autoload Trap

Slide 17

Slide 17 text

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

Slide 18

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

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

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

Slide 21 text

Hide Your Credentials From Your Library

Slide 22

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

Slide 23 text

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

Slide 24

Slide 24 text

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

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

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

Slide 29 text

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

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

Slide 35

Slide 35 text

Keeping Your Domain Models Focused

Slide 36

Slide 36 text

Prescriptions Queueing Authorization Authentication Timeouts Caching BMI Appointments

Slide 37

Slide 37 text

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

Slide 38

Slide 38 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 39

Slide 39 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 40

Slide 40 text

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

Slide 41

Slide 41 text

# lib/twitter_wrangler/Contexts/BMITwitterUpdate.rb module TwitterWrangler::Contexts 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 42

Slide 42 text

# lib/twitter_wrangler/Contexts/BMITwitterUpdate.rb module TwitterWrangler::Contexts 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/Contexts/BMITwitterUpdate.rb module TwitterWrangler::Contexts 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/Contexts/BMITwitterUpdate.rb module TwitterWrangler::Contexts 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

# 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 46

Slide 46 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 47

Slide 47 text

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

Slide 48

Slide 48 text

Thanks!

Slide 49

Slide 49 text

Questions? I only take questions in the form of answers OR in haiku