Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Learning to lib Again

Learning to lib Again

Making the most of the lib directory in a Rails app

Brett Chalupa

February 02, 2016
Tweet

More Decks by Brett Chalupa

Other Decks in Programming

Transcript

  1. Learning to lib Again
    Making the Most of the lib
    Directory in a Rails App
    Brett Chalupa
    pdx.rb
    February 2016

    View Slide

  2. pdx.rb, 2016-02-02 Learning to Lib Again 2
    Intro
    Hi, my name is Brett!

    View Slide

  3. pdx.rb, 2016-02-02 Learning to Lib Again 3
    Intro
    I develop software at

    View Slide

  4. 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

    View Slide

  5. pdx.rb, 2016-02-02 Learning to Lib Again 5
    Intro
    I recently traded the snow for the rain.

    View Slide

  6. pdx.rb, 2016-02-02 Learning to Lib Again 6
    Intro
    Thank you so much for having me!

    View Slide

  7. pdx.rb, 2016-02-02 Learning to Lib Again 7
    What the heck is this talk about?
    Let's talk about the lib directory.

    View Slide

  8. 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.

    View Slide

  9. 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.

    View Slide

  10. 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.

    View Slide

  11. pdx.rb, 2016-02-02 Learning to Lib Again 11
    What lib is for
    In a gem, lib is where the code goes.

    View Slide

  12. pdx.rb, 2016-02-02 Learning to Lib Again 12
    What lib is for
    A simple Ruby gem file structure:

    View Slide

  13. 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

    View Slide

  14. 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.

    View Slide

  15. 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.

    View Slide

  16. 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.

    View Slide

  17. pdx.rb, 2016-02-02 Learning to Lib Again 17
    What lib is for
    Code that is not:
    ● Models
    ● Views
    ● Controllers
    ● Mailers
    ● Jobs*

    View Slide

  18. pdx.rb, 2016-02-02 Learning to Lib Again 18
    What lib is for
    So… lib can be used for just about anything!

    View Slide

  19. 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.

    View Slide

  20. 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.

    View Slide

  21. 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?

    View Slide

  22. pdx.rb, 2016-02-02 Learning to Lib Again 22
    Pre-gem development
    Would it make a good open source library?

    View Slide

  23. 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.

    View Slide

  24. 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.

    View Slide

  25. 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.

    View Slide

  26. 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?'
    )

    View Slide

  27. pdx.rb, 2016-02-02 Learning to Lib Again 27
    Pre-gem development
    This is a great use for the lib directory!

    View Slide

  28. 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.

    View Slide

  29. 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.

    View Slide

  30. 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

    View Slide

  31. 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.

    View Slide

  32. pdx.rb, 2016-02-02 Learning to Lib Again 32
    Pre-gem development
    However, beware of extracting the code into a
    gem too early.

    View Slide

  33. 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?

    View Slide

  34. 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.

    View Slide

  35. 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.

    View Slide

  36. pdx.rb, 2016-02-02 Learning to Lib Again 36
    Pre-gem development
    Public gems take time and energy to support.

    View Slide

  37. 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.

    View Slide

  38. 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.

    View Slide

  39. 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!

    View Slide

  40. pdx.rb, 2016-02-02 Learning to Lib Again 40
    Jobs
    Remember the * that was next to Jobs before?

    View Slide

  41. pdx.rb, 2016-02-02 Learning to Lib Again 41
    Jobs
    The app/jobs dir is where jobs should go,
    especially with ActiveJob.

    View Slide

  42. 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.

    View Slide

  43. 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

    View Slide

  44. pdx.rb, 2016-02-02 Learning to Lib Again 44
    Jobs
    Complex jobs are a code smell.

    View Slide

  45. pdx.rb, 2016-02-02 Learning to Lib Again 45
    Jobs
    Have complex jobs delegate to classes in lib.

    View Slide

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

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. 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

    View Slide

  50. 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.

    View Slide

  51. 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

    View Slide

  52. pdx.rb, 2016-02-02 Learning to Lib Again 52
    Jobs
    Create action classes to handle the work for
    non-trivial jobs.

    View Slide

  53. 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

    View Slide

  54. 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.

    View Slide

  55. 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.

    View Slide

  56. pdx.rb, 2016-02-02 Learning to Lib Again 56
    SCFM
    Instead of putting a bunch of methods into a
    model, make use of…

    View Slide

  57. pdx.rb, 2016-02-02 Learning to Lib Again 57
    SCFM
    You guessed it, the lib dir!

    View Slide

  58. 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.

    View Slide

  59. 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

    View Slide

  60. pdx.rb, 2016-02-02 Learning to Lib Again 60
    New paradigms in app/
    Try to avoid adding new paradigms in the app
    directory.

    View Slide

  61. pdx.rb, 2016-02-02 Learning to Lib Again 61
    New paradigms in app/
    Some app/ paradigms I seen before:
    ● Services
    ● Presenters
    ● Forms
    ● Queries

    View Slide

  62. pdx.rb, 2016-02-02 Learning to Lib Again 62
    New paradigms in app/
    They are all too generic for their own good.

    View Slide

  63. pdx.rb, 2016-02-02 Learning to Lib Again 63
    New paradigms in app/
    Don't worry about fitting code into a generic
    noun.

    View Slide

  64. 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.

    View Slide

  65. 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.

    View Slide

  66. 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.

    View Slide

  67. 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.

    View Slide

  68. 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.

    View Slide

  69. pdx.rb, 2016-02-02 Learning to Lib Again 69
    How to use lib
    Using lib with Rails is pretty straightforward.

    View Slide

  70. 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.

    View Slide

  71. 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

    View Slide

  72. 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

    View Slide

  73. 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.

    View Slide

  74. 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

    View Slide

  75. 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

    View Slide

  76. 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.

    View Slide

  77. pdx.rb, 2016-02-02 Learning to Lib Again 77
    Organizing lib
    Create directories (and namespaces) that
    correspond to different domains and concepts.

    View Slide

  78. pdx.rb, 2016-02-02 Learning to Lib Again 78
    Organizing lib
    Most importantly, don't sweat it.

    View Slide

  79. 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.

    View Slide

  80. 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

    View Slide

  81. 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

    View Slide

  82. 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

    View Slide

  83. 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.

    View Slide

  84. 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'

    View Slide

  85. 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.

    View Slide

  86. 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

    View Slide

  87. 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

    View Slide

  88. 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.

    View Slide

  89. 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.

    View Slide

  90. 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

    View Slide

  91. 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.

    View Slide

  92. 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

    View Slide

  93. 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

    View Slide

  94. pdx.rb, 2016-02-02 Learning to Lib Again 94
    Testing code in lib
    ~ 6.8x faster. Pretty good, right?

    View Slide

  95. 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

    View Slide

  96. 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.

    View Slide

  97. 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.

    View Slide

  98. 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.

    View Slide

  99. 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.

    View Slide

  100. pdx.rb, 2016-02-02 Learning to Lib Again 100
    A few recent uses
    Dropbox sync

    View Slide

  101. pdx.rb, 2016-02-02 Learning to Lib Again 101
    A few recent uses
    Client for internal API with no gem library

    View Slide

  102. pdx.rb, 2016-02-02 Learning to Lib Again 102
    A few recent uses
    Sortable table headers

    View Slide

  103. 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

    View Slide

  104. pdx.rb, 2016-02-02 Learning to Lib Again 104
    A few recent uses
    Extract, Transform, Load (ETL) app

    View Slide

  105. 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

    View Slide

  106. pdx.rb, 2016-02-02 Learning to Lib Again 106
    In conclusion
    Give putting code in lib a try!

    View Slide

  107. pdx.rb, 2016-02-02 Learning to Lib Again 107
    In conclusion
    It encourages splitting up code into smaller
    action classes.

    View Slide

  108. 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.

    View Slide

  109. 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.

    View Slide

  110. pdx.rb, 2016-02-02 Learning to Lib Again 110
    In conclusion
    Also, fast tests!

    View Slide

  111. pdx.rb, 2016-02-02 Learning to Lib Again 111
    Thank you!
    Brett Chalupa
    @brettchalupa
    http://www.brettchalupa.com

    View Slide