Going off the rails

Bundler

  • https://quiz.bundlertv.com/
  • import data from (30 MB) excel
  • quiz collects user taste in TV shows
  • algorithm bundles channels and internet packages according to user taste
  • checkout for buying over affiliate links
  • admin interface

Topic-based organisation

  • the default Rails way uses topic-based file structure
    • all controllers go in app/controllers
    • all models go in app/models
    • all topics go in app/topics
  • where are domain classes?
    • UserForm, OrderCreator, PaymentGatewayClient, CorridorSearchResponse, StatsMarker, NilCompany, CorridorSearchData, CouponsImporter, LastWeekRanks
  • application controller lives in app/controllers directory
    • isn't _controller.rb unnecessary?
    • what is the real reason for suffix?

Component-based organisation

  • domain is in app/components
  • use module(not class) for component namespace, especially when component spans through multiple files
  • use pluralized model name to avoid collision with model class
    • user sign up form lives in app/components/userS/form/sign_up.rb
  • framework shell remains in default directories, controllers, mailers, models, views...
  • related project https://github.com/NullVoxPopuli/drawers

Truncated TDD

  • TDDist Red => Green => Refactor
  • JIT (feature, feature, ...., feature, specs, commit)
  • Kamikaze

What is your distribution of time when coding features?

RSpec.describe Orders::Operation::Create do
  context 'success' do
    it 'is successful'
    it 'creates order'
    it 'creates line items'
  end

  context 'failure' do
    it 'is not successful'
    it 'contains order errors'
  end
end

< 100% test coverage

  • code metrics should not drive your development process
  • quality over quantity
  • but, make sure you write tests that matter
module StarCount # :nodoc:
  def self.coverage(score)
    case score
    when 0..15  then 1
    when 15..30 then 2
    when 30..51 then 3
    when 51..71 then 4
    else             5
    end
  end
end

Decoupling from Rails - Repository

  • a simple module encapsulates direct access to model
  • pros
    • fetching logic is extracted to a single place
    • fetching logic can be changed without impact on clients
  • cons
    • adding simple methods(eg. #find) seems like overkill
    • do you test repository?
  • other reasons
    • direct access to DB(eg. from controller, anywhere)

Decoupling from Rails - Distributing logic

  • Business/Domain => Components
  • Presentation => Decorators
  • Load/Fetch => Repositories
  • Persistance => Models
class ContentAddon < ActiveRecord::Base # :nodoc:
  def single_channel?
    addon_channels.count == 1
  end
end

class BundlePackage < ActiveRecord::Base # :nodoc:
  def double_play?
    package_type == 'double_play' || package_type == 'broadband'
  end
end

class Sport < ActiveRecord::Base # :nodoc:
  def mark_preincluded(channel)
    addon_channels.find_by!(channel: channel)
                  .update(preincluded: true)
  end

  def top_show_channel
    addon_channels.first.channel
  end
end

class Order < ActiveRecord::Base # :nodoc:
  def line_items_amount
    line_items.selected.map(&:item).sum(&:amount)
  end
end
class Profile < ActiveRecord::Base # :nodoc:
  validate :favorites_dont_overlap_dislikes

  def neutral_buckets
    UserBucket.where.not(id: favorite_buckets + disliked_buckets)
  end

  def subscription_channels
    Repository::Subscription.load(subscriptions)
  end

  private

  def favorites_dont_overlap_dislikes
    return if favorite_buckets
      .none? { |bucket| disliked_buckets.include?(bucket) }

    errors.add(
      :base, :overlapping_user_buckets,
      values: overlapping_user_buckets
    )
  end

  def overlapping_user_buckets
    included_in_both(favorite_buckets, disliked_buckets)
      .map(&:name).join(', ')
  end

  def included_in_both(first, second)
    first.select { |e| second.include?(e) }
  end
end

Algorithm in Rails Ruby

  • the algorithm will surely use data from ActiveRecord models
  • where do algorithmic methods live? things to consider:
    • is solution compliant with SRP?
    • excessive DB queries(ensure data is preloaded)
  • not-AR solution => algorithm entity objects
    • data (de)serialization logic
    • identity(borrowed id from AR model)
    1. API call
    2. Preload models needed for calculation and serialize them to entity objects
    3. Run algorithm
    4. Deserialize entity objects to models and present to user

Optimise...later

  • leave that task to mature
  • ready? not yet...
  • now? getting close...
  • it is easy to optimise understandable well-structured code base(and preferably with green tests)
  • complex code benefits from documentation(YARD)

Dependency injection

  • Is my class following SRP?
  • Am I more complex from manufacturing collaborators?
  • Do I require additional dependencies to build collaborator?
  • Am I responsible for creating collaborator?
  • What is a good place for factory logic?
  • client recursive approach:
    • client is responsible for constructing collaborating object(s)
    • in web application this means controller is responsible to construct all dependencies(transitive also)
      •  BAD IDEA!
      • use (Abstract)Factory pattern
    • on large enough scale it comes down to using DI library
  • related projects:

I am the last slide!

Questions?

Going off the rails

By vrabac

Going off the rails

  • 583