Share
Embed
Learning to lib Again
Making the most of the lib directory in a Rails app
Brett Chalupa
February 02, 2016
Transcript
111 slides
-
Learning to lib Again Making the Most of the lib
Directory in a Rails App Brett Chalupa pdx.rb February 2016 -
pdx.rb, 2016-02-02 Learning to Lib Again 2 Intro Hi, my
name is Brett! -
pdx.rb, 2016-02-02 Learning to Lib Again 4 Intro • Programmer
for 10 years • Ruby and Rails programmer for 5 years • Organized the Burlington Ruby Conference for three years -
pdx.rb, 2016-02-02 Learning to Lib Again 5 Intro I recently
traded the snow for the rain. -
pdx.rb, 2016-02-02 Learning to Lib Again 6 Intro Thank you
so much for having me! -
pdx.rb, 2016-02-02 Learning to Lib Again 7 What the heck
is this talk about? Let's talk about the lib directory. -
pdx.rb, 2016-02-02 Learning to Lib Again 8 Why? For years
I thought the lib directory in Rails projects was just for rake tasks. -
pdx.rb, 2016-02-02 Learning to Lib Again 9 Why? These days,
the lib directory is where I spend most of my time when working with Rails. -
pdx.rb, 2016-02-02 Learning to Lib Again 10 Why? It has
changed the way I look at Rails, and it has made me a better Rubyist. -
pdx.rb, 2016-02-02 Learning to Lib Again 11 What lib is
for In a gem, lib is where the code goes. -
pdx.rb, 2016-02-02 Learning to Lib Again 12 What lib is
for A simple Ruby gem file structure: -
pdx.rb, 2016-02-02 Learning to Lib Again 13 What lib is
for class Jikan # Prints the time in a way that is not awful. # # Example: # >> Jikan.time # => The time is 6:14 PM # The date is February 7, 2013 def self.time puts "The time is #{Time.now.strftime("%I:%M %p")}." puts "The date is #{Time.now.strftime("%B %d, %Y")}." end end -
pdx.rb, 2016-02-02 Learning to Lib Again 14 What lib is
for In a new Rails project, the lib dir is for rake tasks and assets. -
pdx.rb, 2016-02-02 Learning to Lib Again 15 What lib is
for lib can be used for so much more than rake tasks and assets. -
pdx.rb, 2016-02-02 Learning to Lib Again 16 What lib is
for lib is great for code that does not fit Rails' paradigms. -
pdx.rb, 2016-02-02 Learning to Lib Again 17 What lib is
for Code that is not: • Models • Views • Controllers • Mailers • Jobs* -
pdx.rb, 2016-02-02 Learning to Lib Again 18 What lib is
for So… lib can be used for just about anything! -
pdx.rb, 2016-02-02 Learning to Lib Again 19 When to use
lib Here are some great times to put code in the lib directory. -
pdx.rb, 2016-02-02 Learning to Lib Again 20 Pre-gem development Put
code in lib that feels like it could be its own gem. -
pdx.rb, 2016-02-02 Learning to Lib Again 21 Pre-gem development Is
the code being written something that could potentially be used by other projects? -
pdx.rb, 2016-02-02 Learning to Lib Again 22 Pre-gem development Would
it make a good open source library? -
pdx.rb, 2016-02-02 Learning to Lib Again 23 Pre-gem development Let's
say an app needs to interface with a third- party API to send faxes, called Faxly. -
pdx.rb, 2016-02-02 Learning to Lib Again 24 Pre-gem development Faxly
is a new service and does not have a Ruby gem. -
pdx.rb, 2016-02-02 Learning to Lib Again 25 Pre-gem development It
would be nice there was a gem to make interacting with Faxly's API easier. -
pdx.rb, 2016-02-02 Learning to Lib Again 26 Pre-gem development For
example: faxly_client = Faxly::Client.new( key: 'some-key', secret: 'some-secret' ) faxly_client.send_fax( to: recipient.fax_number, body: 'Hey! Who faxes documents anymore?' ) -
pdx.rb, 2016-02-02 Learning to Lib Again 27 Pre-gem development This
is a great use for the lib directory! -
pdx.rb, 2016-02-02 Learning to Lib Again 28 Pre-gem development The
Faxly client code is external to the domain logic of this hypothetical app. -
pdx.rb, 2016-02-02 Learning to Lib Again 29 Pre-gem development Create
a directory in lib called faxly. Within that dir, it is just like building out a Ruby gem. -
pdx.rb, 2016-02-02 Learning to Lib Again 30 Pre-gem development #
lib/faxly/client.rb module Faxly class Client def initialize(key:, secret:) # get a token for making requests end def send_fax(to:, body:) # make the HTTP request to send the fax end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 31 Pre-gem development It
is easier to extract code and tests into a gem when they are independent of Rails. -
pdx.rb, 2016-02-02 Learning to Lib Again 32 Pre-gem development However,
beware of extracting the code into a gem too early. -
pdx.rb, 2016-02-02 Learning to Lib Again 33 Pre-gem development Well…
why not just start the building the Faxly client library as a gem from the start? -
pdx.rb, 2016-02-02 Learning to Lib Again 34 Pre-gem development The
overhead of maintaining a separate gem is usually not worth it from the start. -
pdx.rb, 2016-02-02 Learning to Lib Again 35 Pre-gem development Private
gems require a private gem server or a private Git repo. -
pdx.rb, 2016-02-02 Learning to Lib Again 36 Pre-gem development Public
gems take time and energy to support. -
pdx.rb, 2016-02-02 Learning to Lib Again 37 Pre-gem development Building
a more general use gem for anyone to use is a lot more time consuming and distracting than writing the code that you need. -
pdx.rb, 2016-02-02 Learning to Lib Again 38 Pre-gem development A
separate codebase for a gem means more steps to make changes and get them into the app. -
pdx.rb, 2016-02-02 Learning to Lib Again 39 Pre-gem development Extract
the code into a gem when it is stable, useful to others, and feels right! -
pdx.rb, 2016-02-02 Learning to Lib Again 40 Jobs Remember the
* that was next to Jobs before? -
pdx.rb, 2016-02-02 Learning to Lib Again 41 Jobs The app/jobs
dir is where jobs should go, especially with ActiveJob. -
pdx.rb, 2016-02-02 Learning to Lib Again 42 Jobs In our
app, users can connect the DropCube file sync service to automatically back-up their faxes before they get sent. -
pdx.rb, 2016-02-02 Learning to Lib Again 43 Jobs class BackupFaxJob
< ActiveJob::Base def perform(fax) fax.prep_for_pdf_transformation pdf = PDFWizard.new pdf.main_text = fax.body pdf.header = fax.recipient pdf.footer = fax.user fax_dir = “faxes/#{Date.today.strftime('%Y-%m-%d')}-#{SecureHex.random}.pdf” drop_cube_client = DropCube::Client.new('https://api.dropcube.cool/') drop_cube_client.put_file(pdf, dir: fax_dir) end end -
pdx.rb, 2016-02-02 Learning to Lib Again 44 Jobs Complex jobs
are a code smell. -
pdx.rb, 2016-02-02 Learning to Lib Again 45 Jobs Have complex
jobs delegate to classes in lib. -
pdx.rb, 2016-02-02 Learning to Lib Again 46 Jobs How would
one write tests for that? The #perform method is doing a lot, which makes it difficult to test and debug. -
pdx.rb, 2016-02-02 Learning to Lib Again 47 Jobs The job
should make use of action classes in lib to make the job simpler. class BackupFaxJob < ActiveJob::Base def perform(fax) pdf = FaxBackup::CreatesPDF.new(fax).create FaxBackup::SyncsToDropCube.new(pdf).sync end end -
pdx.rb, 2016-02-02 Learning to Lib Again 48 Jobs class BackupFaxJob
< ActiveJob::Base def perform(fax) fax.prep_for_pdf_transformation pdf = PDFWizard.new pdf.main_text = fax.body pdf.header = fax.recipient pdf.footer = fax.user fax_dir = “faxes/#{Date.today.strftime('%Y-%m-%d')}-#{SecureHex.random}.pdf” drop_cube_client = DropCube::Client.new('https://api.dropcube.cool/') drop_cube_client.put_file(pdf, dir: fax_dir) end end -
pdx.rb, 2016-02-02 Learning to Lib Again 49 Jobs class BackupFaxJob
< ActiveJob::Base def perform(fax) pdf = FaxBackup::CreatesPDF.new(fax).create FaxBackup::SyncsToDropCube.new(pdf).sync end end -
pdx.rb, 2016-02-02 Learning to Lib Again 50 Jobs Now testing
#perform is about expecting a class to create a PDF and another class to sync it. It is up to the tests of each of those classes to determine how that is handled. -
pdx.rb, 2016-02-02 Learning to Lib Again 51 Jobs Those two
files for backing up faxes could live at: • lib/fax_backup/creates_pdf.rb • lib/fax_backup/syncs_to_drop_cube.rb -
pdx.rb, 2016-02-02 Learning to Lib Again 52 Jobs Create action
classes to handle the work for non-trivial jobs. -
pdx.rb, 2016-02-02 Learning to Lib Again 53 SCFM There is
a way of thinking with Rails known as: Skinny controllers, fat models SCFM -
pdx.rb, 2016-02-02 Learning to Lib Again 54 SCFM The gist
of SCFM is that controllers in Rails apps can become very complex at times. SCFM is about removing the complexities of a controller and delegating them to a model. -
pdx.rb, 2016-02-02 Learning to Lib Again 55 SCFM However, SCFM
leads to models that become all-knowing, extremely complex, and hundreds of lines. -
pdx.rb, 2016-02-02 Learning to Lib Again 56 SCFM Instead of
putting a bunch of methods into a model, make use of… -
pdx.rb, 2016-02-02 Learning to Lib Again 57 SCFM You guessed
it, the lib dir! -
pdx.rb, 2016-02-02 Learning to Lib Again 58 SCFM Similar to
the complexities of jobs, controllers should delegate to action classes in lib to do the work. -
pdx.rb, 2016-02-02 Learning to Lib Again 59 Tickets::Completer module Tickets
class Completer def initialize(ticket) @ticket = ticket @project = ticket.project end def complete! return false if ticket.complete? ticket.complete! update_project_status end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 60 New paradigms in
app/ Try to avoid adding new paradigms in the app directory. -
pdx.rb, 2016-02-02 Learning to Lib Again 61 New paradigms in
app/ Some app/ paradigms I seen before: • Services • Presenters • Forms • Queries -
pdx.rb, 2016-02-02 Learning to Lib Again 62 New paradigms in
app/ They are all too generic for their own good. -
pdx.rb, 2016-02-02 Learning to Lib Again 63 New paradigms in
app/ Don't worry about fitting code into a generic noun. -
pdx.rb, 2016-02-02 Learning to Lib Again 64 New paradigms in
app/ Create action classes that specifically describe what the code does and the domain it encompasses. -
pdx.rb, 2016-02-02 Learning to Lib Again 65 New paradigms in
app/ Once more and more similar action classes get created, look into refactoring to a more generic approach. -
pdx.rb, 2016-02-02 Learning to Lib Again 66 New paradigms in
app/ When I started using Rails it took me years to understand MVC and what goes where. -
pdx.rb, 2016-02-02 Learning to Lib Again 67 New paradigms in
app/ New generic classifications of code are more difficult to understand than using specific classes. -
pdx.rb, 2016-02-02 Learning to Lib Again 68 When to use
lib Try putting code that doesn't fit nicely into the existing Rails paradigms in lib and see how it goes. -
pdx.rb, 2016-02-02 Learning to Lib Again 69 How to use
lib Using lib with Rails is pretty straightforward. -
pdx.rb, 2016-02-02 Learning to Lib Again 70 How to use
lib The various classes and modules could be required where needed, but that can be a bit tedious. -
pdx.rb, 2016-02-02 Learning to Lib Again 71 How to use
lib require 'secure_hash_generator' require 'fax_formatter' class Fax < ActiveRecord::Base def format FaxFormatter.new( SecureHashGenerator.new.generate ) end end -
pdx.rb, 2016-02-02 Learning to Lib Again 72 How to use
lib Add the following line to config/application.rb: config.paths.add 'lib', eager_load: true -
pdx.rb, 2016-02-02 Learning to Lib Again 73 How to use
lib That autoloads and eagerloads all of the code in the lib directory so it is accessible everywhere. -
pdx.rb, 2016-02-02 Learning to Lib Again 74 How to use
lib require 'secure_hash_generator' require 'fax_formatter' class Fax < ActiveRecord::Base def format FaxFormatter.new( SecureHashGenerator.new.generate ) end end -
pdx.rb, 2016-02-02 Learning to Lib Again 75 How to use
lib class Fax < ActiveRecord::Base def format FaxFormatter.new( SecureHashGenerator.new.generate ) end end -
pdx.rb, 2016-02-02 Learning to Lib Again 76 Organizing lib The
lib dir is a blank canvas at the start, which can be intimidating. -
pdx.rb, 2016-02-02 Learning to Lib Again 77 Organizing lib Create
directories (and namespaces) that correspond to different domains and concepts. -
pdx.rb, 2016-02-02 Learning to Lib Again 78 Organizing lib Most
importantly, don't sweat it. -
pdx.rb, 2016-02-02 Learning to Lib Again 79 Testing code in
lib Testing code in lib is very similar to models and other classes. -
pdx.rb, 2016-02-02 Learning to Lib Again 80 Testing code in
lib For RSpec, the spec for lib/faxly/client.rb would live at spec/lib/faxly/client_spec.rb -
pdx.rb, 2016-02-02 Learning to Lib Again 81 Testing code in
lib require 'rails_helper' describe Faxly::Client do subject { described_class.new(creds) } describe '#send_fax' do it 'makes an HTTP request' do expect(HTTPal).to receive(:make_request).with(some_json_blob) subject.send_fax( to: '1-800-123-4567', body: 'Serious business info' ) end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 82 Testing code in
lib With RSpec, running rails generate rspec:install creates two files: 1. spec/spec_helper.rb 2. spec/rails_helper.rb -
pdx.rb, 2016-02-02 Learning to Lib Again 83 Testing code in
lib The spec_helper is for general RSpec configuration that is not specific to Rails. -
pdx.rb, 2016-02-02 Learning to Lib Again 84 Testing code in
lib The rails_helper is for Rails specific RSpec configuration. It requires the spec_helper: require 'spec_helper' -
pdx.rb, 2016-02-02 Learning to Lib Again 85 Testing code in
lib Requiring rails_helper loads the entire Rails environment and every gem required in the Gemfile. -
pdx.rb, 2016-02-02 Learning to Lib Again 86 Testing code in
lib Gemfile: gem 'brakeman' – loads the gem automatically gem 'brakeman', require: false – does not load the gem automatically -
pdx.rb, 2016-02-02 Learning to Lib Again 87 Testing code in
lib require 'rails_helper' describe FaxesController do describe '#create' do it 'creates a fax record' do end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 88 Testing code in
lib Loading up the entire Rails environment is slow and unnecessary when testing code in lib. -
pdx.rb, 2016-02-02 Learning to Lib Again 89 Testing code in
lib When unit testing code in lib that does not depend on Rails, require the spec_helper instead. -
pdx.rb, 2016-02-02 Learning to Lib Again 90 Testing code in
lib require 'spec_helper' require 'faxly/client' describe Faxly::Client do describe '#send_fax' do it 'sends a fax' do end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 91 Testing code in
lib This means faster tests for that file. But how much faster? Let's see with: $ time bundle exec rspec spec/path/to/spec.rb * The app has 28 required gems in the Gemfile. -
pdx.rb, 2016-02-02 Learning to Lib Again 92 Testing code in
lib With rails_helper: ..... Finished in 0.07432 seconds (files took 5.6 seconds to load) 5 examples, 0 failures real 0m6.598s user 0m2.962s sys 0m0.597s -
pdx.rb, 2016-02-02 Learning to Lib Again 93 Testing code in
lib With spec_helper: ..... Finished in 0.04742 seconds (files took 0.29089 seconds to load) 5 examples, 0 failures real 0m0.964s user 0m0.830s sys 0m0.127s -
pdx.rb, 2016-02-02 Learning to Lib Again 94 Testing code in
lib ~ 6.8x faster. Pretty good, right? -
pdx.rb, 2016-02-02 Learning to Lib Again 95 Testing code in
lib With rails_helper with Spring: ..... Finished in 0.05533 seconds (files took 3.17 seconds to load) 5 examples, 0 failures real 0m3.875s user 0m2.720s sys 0m0.558s -
pdx.rb, 2016-02-02 Learning to Lib Again 96 Testing code in
lib Requiring rails_helper is a little better with Spring, but it is still ~4x slower. -
pdx.rb, 2016-02-02 Learning to Lib Again 97 Testing code in
lib When doing TDD, a 4x speed increase makes a big difference. -
pdx.rb, 2016-02-02 Learning to Lib Again 98 Testing code in
lib Note: requiring spec_helper does not make a difference when running the whole test suite because Rails has to be loaded anyways. -
pdx.rb, 2016-02-02 Learning to Lib Again 99 Testing code in
lib Code and gems need to be explicitly required when using spec_helper. -
pdx.rb, 2016-02-02 Learning to Lib Again 100 A few recent
uses Dropbox sync -
pdx.rb, 2016-02-02 Learning to Lib Again 101 A few recent
uses Client for internal API with no gem library -
pdx.rb, 2016-02-02 Learning to Lib Again 102 A few recent
uses Sortable table headers -
pdx.rb, 2016-02-02 Learning to Lib Again 103 A few recent
uses Light-weight AWS S3 client wrapper module Storage class S3File def initialize(key) bucket = ENV['some-bucket'] client = Aws::S3::Client.new @object = Aws::S3::Object.new(bucket_name: bucket, key: key, client: client) end def url object.presigned_url :get end def upload(source_path) object.upload_file source_path end end end -
pdx.rb, 2016-02-02 Learning to Lib Again 104 A few recent
uses Extract, Transform, Load (ETL) app -
pdx.rb, 2016-02-02 Learning to Lib Again 105 Open Source References
• https://github.com/chef/supermarket • https://github.com/bbatsov/rubocop & any gem -
pdx.rb, 2016-02-02 Learning to Lib Again 106 In conclusion Give
putting code in lib a try! -
pdx.rb, 2016-02-02 Learning to Lib Again 107 In conclusion It
encourages splitting up code into smaller action classes. -
pdx.rb, 2016-02-02 Learning to Lib Again 108 In conclusion It
feels a lot more like Ruby programming than most other aspects of Rails development. -
pdx.rb, 2016-02-02 Learning to Lib Again 109 In conclusion The
structure and style of code in lib is totally up to the developer, which can feel liberating. -
pdx.rb, 2016-02-02 Learning to Lib Again 111 Thank you! Brett
Chalupa @brettchalupa http://www.brettchalupa.com