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

I don't like Rails anymore: an exploration of web application architecture design with Ruby

Damir
April 26, 2014

I don't like Rails anymore: an exploration of web application architecture design with Ruby

It’s exciting to start a new application with Ruby on Rails. So exciting that it’s, in fact, common for developers to start developing strong emotions towards the framework. However, after working on a single Rails project for few years you notice it’s getting harder to maintain your project: models are getting huge, code in controllers is mostly untested and trying to fix these issues can start a fight with the framework. But it’s not a reason to abandon Ruby! We’ll explore some alternatives and see whether they can provide a relief.

Damir

April 26, 2014
Tweet

More Decks by Damir

Other Decks in Programming

Transcript

  1. I don’t like Rails anymore an exploration of web application

    architecture design with Ruby Damir Zekić @sidonath
  2. Ruby on Rails "Ruby on Rails is an open-source web

    framework that's optimized for programmer happiness and sustainable productivity."
  3. Speed Time When is honeymoon over? Adding a feature
 takes

    more than a day A feature change
 causes a headache You wait more than
 60s for tests to run or…
  4. class CheckoutsController < ApplicationController def create ActiveRecord::Base.transaction do ! cancel_abandoned_checkouts

    return unless new_checkout_from_params @checkout.save! ! if params[:email_address_set_membership] && params[:email_address_set_membership][:email_address_set_id].present? @checkout.meta( :email_address_set_join_intention, params[:email_address_set_membership][:email_address_set_id] ) end ! # Track checkout creation mixpanel.track_event('Payment', { source: params[:checkout][:source], user: current_user.id, method: params[:checkout][:service] }) ! mixpanel.track_event('Retailer Link', { source: params[:checkout][:source], providerId: @checkout.ownerships.reduce([]){|pids, o| pids << o.product.provider.id }.uniq, itemId: @checkout.item_ids, embedded: params[:checkout][:embedded], url: request.referer ? request.referer.encode('UTF-8', invalid: :replace, undef: :replace) : nil }) ! payment_service = params[:checkout][:service] current_user.meta(:most_recent_payment_service, payment_service) key = (payment_service + '_payment_service_usage_count').to_sym if current_user.meta(key).nil? current_user.meta(key, 1) else current_user.meta(key, current_user.meta(key) + 1 ) end ! if payment_service == 'PayLater' && @checkout.pay_later_allowed? if @checkout.gift && [email protected]_address respond_to do |format| msg = "Provide the full billing address." format.html do flash[:error] = Notification.new(msg, "You tried to purchase a gift using 'pay later' option, but no billing address was specified." + "<br>Contact us for more info: <strong><a href='mailto:help&#64;example.com'>help&#64;example.com</a></strong>") redirect_to checkout_path and return end format.json { render json: {message: msg}, status: :unprocessable_entity and return} end end ! @checkout.complete_without_payment! if current_user.existant_ea PurchaseMailer.delay.purchase_confirmation(current_user.existant_ea, @checkout) end ! respond_to_successfully_completed_checkout(@checkout, payment_service) and return end ! collect_mailing = case params[:checkout][:collect_mailing] when 'true' then true when 'false' then false else false end ! allow_guest_checkout = case params[:checkout][:pp_mode] when 'cc' then true when 'account' then false else true end ! invoice_items = nil ! invoice_items = @checkout.ownerships.inject([]) do |a, e| i = e.product_item if i.custom_invoice_items a += i.custom_invoice_items else a << { name: i.product.description, amount: i.price * 100, description: i.description } end a end if invoice_items.map{|x| x[:amount] }.sum != @checkout.amount * 100 invoice_items = nil end ! begin payment_creation_options = { user_id: current_user.id, checkout_id: @checkout.id, amount: @checkout.amount, service: payment_service, collect_mailing: collect_mailing, allow_guest_checkout: allow_guest_checkout } payment_creation_options[:invoice_items] = invoice_items if invoice_items payment_creation_options[:pp_page_style] = params[:checkout][:pp_page_style] if params[:checkout][:pp_page_style] ! payment_creation_options[:amazon_cobranding_url] = params[:checkout][:amazon_cobranding_url] if params[:checkout][:amazon_cobranding_url] ! @payment = Payment.create( payment_creation_options ) rescue ActiveResource::ResourceInvalid, StandardError notify_airbrake($!) flash[:error] = Notification.new(UX::PAYMENTS[:creation_failed], UX::PAYMENTS[:help], false) respond_to do |format| format.html { redirect_to checkout_path and return } format.json { render json: {message: UX::PAYMENTS[:creation_failed]}, status: 422 and return } end else @checkout.update_attributes({ payment_id: @payment.id }, without_protection: true) ! respond_to do |format| format.html { redirect_to( @payment.get(:authorization_url)['value'] ) and return } format.json { render json: { payment_id: @payment.id } and return } end end ! end # ActiveRecord::Base.transaction end end Your code looks like this (this is a single
 controller action)
  5. The Technical Issues an ActiveRecord model many responsibilities no interface

    segregation an ActionController controller many responsibilities can't be extended for new features
  6. Ruby on Rails "Ruby on Rails is an open-source web

    framework that's optimized for programmer happiness and sustainable productivity."
  7. How to Manage? Read about design patterns Refactor using design

    patterns Design domain model thinking
 about interactions first
  8. Alternatives to Rails For the brave: go without a framework


    and focus on designing your application first Adam Hawkins" Joy of Design Application First, Frameworks Second
  9. ➜ lotus-controller git:(master) bundle exec rake Run options: --seed 24213

    ! # Running: ! ................................................................. ................................................................. .............. ! Finished in 0.063052s, 2283.8292 runs/s, 4187.0202 assertions/s. ! 144 runs, 264 assertions, 0 failures, 0 errors, 0
  10. ➜ lotus-model git:(master) bundle exec rake Run options: --seed 33698

    ! # Running: ! ................................................................. ................................................................. ................................................................. ................................................................. ............ ! Finished in 0.633141s, 429.6041 runs/s, 546.4817 assertions/s. ! 272 runs, 346 assertions, 0 failures, 0 errors, 0 skips ➜ lotus-model git:(master)
  11. class RoomsController action 'Create' do expose :form ! def initialize(repository:

    RoomRepository, router: Application.router) @repository = repository @router = router end ! def call(params) @form = FormFactory.new_room room = @form.populate(params.fetch(:room), self) @repository.persist(room) redirect_to @router.path(:rooms) end ! def form_invalid throw 422 end end end Room Reservation (a Lotus greenfield app) https://github.com/sidonath/room-reservation
  12. Recommended Materials Matt Wynne
 Hexagonal Rails (a talk)" Erich Gamma,

    Richard Helm, Ralph Johnson, John Vlissides
 Design Patterns: Elements of Reusable Object-Oriented Software" Elisabeth Freeman, Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson
 Head First Design Patterns Martin Fowler
 Patterns of Enterprise Application Architecture" Avdi Grimm
 Objects on Rails (free ebook) Sandi Metz
 Practical Object-Oriented Design in Ruby