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

How Trailblazer and Rails Engines can save your Rails monolith application!

How Trailblazer and Rails Engines can save your Rails monolith application!

rails new foobar is always a green cool field, then it starts to get bigger and bigger, and messy, and ugly!

how can we handle that? usually the rails way give us 3 "boxes" to organize all our code M, V and C... to help organize our architecture we use namespace to isolate and our app become huge, but wait, we can isolate in microservices, many deployments, refactor is now a impossible work! And rails is now a brown messy ugly and boring field, but using trailblazer and components we can save it! \o/

In this talk we are gonna see a real use case where a huge application was split into small components, for authentication, template, entities, financial (all these can be reused into another applications), and these components are organized into layers for developer happiness, the best part, if you wanna go this way you don't need to break up with Rails, you can refactor small pieces, to start grow in a sustainable way and get that green field again :)

Celso Fernandes

September 22, 2015
Tweet

More Decks by Celso Fernandes

Other Decks in Programming

Transcript

  1. A visão hermenêutica do desenvolvimento de software; O significado de

    um projeto de software está indissociavelmente ligado às pessoas que trabalharam no mesmo. Avdi Grimm
  2. * Empresa de 5 anos * 2~4 Desenvolvedores * Ruby

    -> 2010 (com Puppet) * Experimentação!
  3. * Aplicação Multi Tenancy * On Premise * Sistema White

    Label * Provisionar VMs, e-mails, … * Gerenciar auto scaling, LB…
  4. Model View Controller concerns? concerns? persistance! callbacks! validations! query interface!

    helpers! helpers! helpers! helpers! helpers! helpers! helpers! helpers!
  5. 7 Patterns to Refactor Fat ActiveRecord Models Bryan Helmkamp 1.

    Extract Value Objects 2. Extract Service Objects 3. Extract Form Objects 4. Extract Query Objects 5. Introduce View Objects 6. Extract Policy Objects 7. Extract Decorators
  6. The puristic Rails Way isn’t appropriate for projects with a

    complexity greater than a 5- minute blog. Full stop. Nick Sutterer
  7. class  CommentsController  <  AppController      before_filter  :authenticate!    

     def  create          @comment  =  Comment.new(params)          if  signed_in?              @comment.user  =  current_user          end          unless  @comment.save              notify(@comment)          end          respond_to  do  |format|              format.json  {  render  json:  @comment  }          end      end   end  
  8. class  CommentsController  <  AppController      before_filter  :authenticate!    

     def  create          @comment  =  Comment.new(params)          if  signed_in?              @comment.user  =  current_user          end          unless  @comment.save              notify(@comment)          end          respond_to  do  |format|              format.json  {  render  json:  @comment  }          end      end   end   * * * * *
  9. class    before_filter              @comment

                         @comment.user                              notify(@comment)                  respond_to              format.json  {  render               end      before_filter  :authenticate!  
  10. class    before_filter              @comment

                         @comment.user                              notify(@comment)                  respond_to              format.json  {  render               end          @comment  =  Comment.new(params)  
  11. class    before_filter              @comment

                         @comment.user                              notify(@comment)                  respond_to              format.json  {  render               end          if  signed_in?              @comment.user  =  current_user          end  
  12. class    before_filter              @comment

                         @comment.user                              notify(@comment)                  respond_to              format.json  {  render               end          unless  @comment.save              notify(@comment)          end  
  13. class    before_filter              @comment

                         @comment.user                              notify(@comment)                  respond_to              format.json  {  render               end          respond_to  do  |format|              format.json  {  render  json:  @comment  }          end  
  14. class  CommentsController  <  AppController      before_filter  :authenticate!    

     def  create          @comment  =  Comment.new(params)          if  signed_in?              @comment.user  =  current_user          end          unless  @comment.save              notify(@comment)          end          respond_to  do  |format|              format.json  {  render  json:  @comment  }          end      end   end   * * * * *
  15. class  Comment  <  ActiveRecord::Base      attr_accessible  :author_attributes,  :body  

             accepts_nested_attributes_for  :author      validates  :author,  :body,          :presence  =>  true,  :unless  =>  :notification?      before_validation  :no_create_when_closed,          :on  =>  :create,          :if  =>  :forum?   end
  16. class  Comment  <  ActiveRecord::Base      attr_accessible  :author_attributes,  :body  

             accepts_nested_attributes_for  :author      validates  :author,  :body,          :presence  =>  true,  :unless  =>  :notification?      before_validation  :no_create_when_closed,          :on  =>  :create,          :if  =>  :forum?   end * * * *
  17. class    attr_accessible            accepts_nested_attributes_for  

       validates              before_validation                   end    attr_accessible  :author_attributes,  :body
  18. class    attr_accessible            accepts_nested_attributes_for  

       validates              before_validation                   end          accepts_nested_attributes_for  :author  
  19. class    attr_accessible            accepts_nested_attributes_for  

       validates              before_validation                   end          validates  :author,  :body,          :presence  =>  true,  :unless  =>  :notification?  
  20. class    attr_accessible            accepts_nested_attributes_for  

       validates              before_validation                   end    before_validation  :no_create_when_closed,          :on  =>  :create,          :if  =>  :forum?  
  21. class  Comment  <  ActiveRecord::Base      attr_accessible  :author_attributes,  :body  

             accepts_nested_attributes_for  :author      validates  :author,  :body,          :presence  =>  true,  :unless  =>  :notification?      before_validation  :no_create_when_closed,          :on  =>  :create,          :if  =>  :forum?   end * * * *
  22. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment) .comment      .text          =  textilize(@comment.body).safe_html      .author          -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment)
  23. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment) .comment      .text          =  textilize(@comment.body).safe_html      .author          -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment) * * * *
  24. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment)        =  textilize(@comment.body).safe_html  
  25. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment)        -­‐  type  =  comment.staff?  ?  :thumb  :  :default  
  26. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment)                =  image_tag  comment.image.url(type)  
  27. .comment            =  textilize(@comment.body).safe_html    

             -­‐  type  =  comment.staff?  ?  :thumb  :  :default          =  image_tag  comment.image.url(type)          =  render_comment_link(@comment)        =  render_comment_link(@comment)
  28. Trailblazer is a thin layer on top of Rails. It

    gently enforces encapsulation, an intuitive code structure and gives you an object- oriented architecture.
  29. class  CommentsController  <  AppController      def  create    

         run  Comment::Create      end   end  
  30. class  Comment::Cell  <  Cell::ViewModel      property  :body    

     def  show          render      end      private      def  avatar          image_tag  model.image.url      end   end  
  31. .comment      .text          =  body

         .author          =  avatar
  32. class  Comment::Cell  <  Cell::ViewModel      property  :body    

     def  show          render      end      private      def  avatar          image_tag  model.image.url      end   end   .comment      .text          =  body      .author          =  avatar
  33. class    property              render

         end    private            image_tag  model.image.url       end   .comment      .text          =  body      .author          =  avatar    def  show          render      end  
  34. class    property              render

         end    private            image_tag  model.image.url       end   .comment      .text          =  body      .author          =  avatar    property  :body  
  35.    def  avatar          image_tag  model.image.url  

       end   class    property              render      end    private            image_tag  model.image.url       end   .comment      .text          =  body      .author          =  avatar
  36. class  Comment  <  ActiveRecord::Base      class  Create  <  Trailblazer::Operation

             contract  do              property  :body              validates  :body,  presence:  true          end          def  process(params)              validate  do                  model.save                  notify!              end          end      end   end  
  37. class  CommentsController  <  AppController      def  create    

         run  Comment::Create      end   end  
  38. class            contract      

           property              validates                              validate                  model.save                  notify!                           end          contract  do              property  :body              validates  :body,  presence:  true          end  
  39. class  CommentsController  <  AppController      def  create    

         run  Comment::Create.({comment:{                                            body:  “RubyConf!”                                        }})        end   end  
  40. class            contract      

           property              validates                              validate                  model.save                  notify!                           end              validate  do              end  
  41. class            contract      

           property              validates                              validate                  model.save                  notify!                           end          def  process(params)          end  
  42. class  Comment  <  ActiveRecord::Base      class  Index  <  Trailblazer::Operation

             include  Collection          def  model!(params)              Company.all          end      end   end
  43. class            contract      

           property              validates                              validate                  model.save                  notify!                           end                  model.save  
  44. class            contract      

           property              validates                              validate                                         end                Comment.create  body:  params[:body]  
  45. class            contract      

           property              validates                               end   if  validate      Comment.create  body:  params[:body]   else      InvalidComment.create!   end  
  46. class            contract      

           property              validates                              validate                  model.save                  notify!                           end                  notify!
  47. class            contract      

           property              validates                              validate                           end   model.author  =  User.find(1)   contract.save
  48. class            contract      

           property              validates                              validate                           end   genre  =  Genre.compute(model)
  49. class            contract      

           property              validates                              validate                     genre! def  genre!      @genre  =  Genre.compute(model)   end
  50. op  =  Comment::Create.(      comment:  {      

       body:  "RubyConf!",      }   )
  51. op.to_json   {      “comment”:      {  

           "body":  "RubyConf!"      }   }
  52. class  Comment  <  ActiveRecord::Base      class  Create  <  Trailblazer::Operation

             include  Representer            representer  do              property  :id              property  :body              link(:self)  {  comment_path(model)  }            end      end   end
  53. op.to_json   {      "comment":      {  

           "id":  1,          "body":  "RubyConf!"          "links":          {              "self":  "/comments/1"          }      }   }
  54. class  Update  <  Create      contract  do    

         property  :body,  readonly:  true      end   end
  55. class  Update  <  Create      policy  do    

         Permit.can?(:update,  model)      end   end
  56. class  Update  <  Create      policy  do    

         model.owner  ==  current_user      end   end
  57. class  Comment  <  ActiveRecord::Base      builds  do  |params|  

           Admin  if  params[:current_user].admin?      end      def  process(params)          #  user  action      end            class  Admin  <  self          def  process(params)              #  admin  action          end      end   end
  58.            def  setup!(params)      

               setup_params!(params)                  build_model!(params)              end
  59. Controller class  CommentsController  <  AppController      def  create  

           run  Comment::Create      end   end
  60. Workers class  BackgroundJob  <  Sidekiq::Worker      def  process  

           Comment::Create.(..)      end   end
  61. Tests describe  Comment      let(:comment)  do      

       Comment::Create.(..)      end   end
  62. Moving to microservices adds overheads: communication, deploy, logging, monitoring, etc.

    After you abstract something to a service, it is very hard to make it come back to the monolithic application. Have you considered using micro services in your startup? Fabiano Bezelga
  63. theme api financial sales shop business browser api-spec api specs

    using dredd common auth web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /*
  64. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* browser web
  65. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* browser web single deploy
  66. browser web browser web #  Gemfile  (web)   gem  'api'

    #  Gemfile  (web)   gem  'api',  github:  'user/api' #  Gemfile   path  'components'  do      gem  'api'      gem  'financial'      gem  'shop'      gem  'sales'   end
  67. browser web browser web require  'manager_constraint'   require  'tenant_constraint'  

    Rails.application.routes.draw  do      constraints(ManagerConstraint.new)  do          root  'manager#index',  as:  :manager_root          resources  :tenants,  only:  [:index,  :create,  :put]      end      constraints(TenantConstraint.new)  do          get  'admin',  to:  "tenant#admin",  as:  :tenant_admin          root  'tenant#index',  as:  :tenant_root      end   end
  68. browser web browser web require  'manager_constraint'   require  'tenant_constraint'  

    Rails.application.routes.draw  do      constraints(ManagerConstraint.new)  do          root  'manager#index',  as:  :manager_root          resources  :tenants,  only:  [:index,  :create,  :put]      end      constraints(TenantConstraint.new)  do          get  'admin',  to:  "tenant#admin",  as:  :tenant_admin          root  'tenant#index',  as:  :tenant_root      end   end ember:manager ember:admin ember:web
  69. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* browser web
  70. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* /manager browser web manager
  71. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* /admin browser web admin
  72. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* / /* web web views
  73. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* / /* web web views api /api
  74. theme api browser web web views manager admin / /admin

    /manager /* / /* web web views api #  api/config/routes.rb   Rails.application.routes.draw  do      namespace  :api  do          namespace  :v1  do              resources  :invoices              resources  :things          end      end   end
  75. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* / /* web web views api /api/oauth auth
  76. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager / /shop web web views api /api/shop business common shop
  77. theme api financial sales shop business browser api-spec common auth

    web web views manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /* /admin/sales web api /api/sales business sales common admin
  78. theme api financial sales shop business browser api-spec common auth

    web web manager admin /api /api/oauth /api/financial /api/sales /api/shop / /admin /manager /manager/financial web api /api/financial financial business common manager views
  79. #  api/config/routes.rb   Rails.application.routes.draw  do      namespace  :api  do

             namespace  :v1  do              resources  :invoices              resources  :things          end      end   end api
  80. #  api/config/routes.rb   Rails    namespace        

     namespace              resources              resources               end #  api/controllers/api/v1/invoices_controller.rb   class  Api::V1::InvoicesController  <  ApiController      def  create          run  Financial::Invoice  #  from  `invoices`  gem      end   end            resources  :invoices   api financial
  81. #  api/config/routes.rb   Rails    namespace        

     namespace              resources              resources               end #  api/controllers/api/v1/things_controller.rb   class  Api::V1::ThingsController  <  ApiController      def  create          run  Business::Thing::Create      end   end            resources  :things   api business
  82. #  api/config/routes.rb   Rails    namespace        

     namespace              resources              resources               end class  Business::Thing  <  ActiveRecord::Base      class  Create          def  process(params)              #  here  we  call  different  objects  like              #  cart_params  =  Shop::Cart.buy!              #  Sales::Sale.create(cart_params)          end      end   end            resources  :things   api sales shop business
  83. Travis CI consists of many different sub-projects. The main ones

    are: travis-api travis-api is the Sinatra app that's responsible for serving our API. It responds to different HTTP endpoints and runs services in travis-core. Very little logic is in this repository. travis-build travis-build creates the build script for each job. It takes the configuration from the .travis.yml file and creates a bash script that is then run in the build environment by travis-worker. travis-core travis-core holds most of the logic for Travis CI. This repository is shared across several other apps and holds the models, services, and other things that these apps need. travis-cookbooks travis-cookbooks holds the Chef cookbooks that are used to provision the build environments. travis-hub travis-hub collects events from other apps and notifies other apps about the events. For example, it notifies travis-tasks about builds starting and finishing so notifications can be sent out. travis-hub is also responsible for enqueueing jobs that have been created and enforcing the Quality of Service restrictions, such as the number of concurrent builds per user. travis-listener travis-listener receives notifications from GitHub whenever commits are pushed or pull requests are opened. They are then pushed onto RabbitMQ for other apps to process. travis-logs travis-logs receives log updates from travis-worker, saves them to the database and pushes them to the web client. When a job is finished, travis-logs is responsible for pushing the log to Amazon S3 for archiving. travis-support travis-support holds shared logic for the different Travis CI apps. It is different from travis-core in that it holds more generic things, like how to run an async job or how to handle exceptions. travis-tasks travis-tasks receives notifications from travis-hub and sends out notifications to the different notification providers as needed. travis-web travis-web is our main Web client. It is written using Ember and communicates with travis-api to get information and gets live updates from travis-hub and travis-logs through Pusher. travis-worker travis-worker is responsible for running the build scripts in a clean environment. It streams the log output to travis-logs and pushes state updates (build starting/finishing) to travis-hub.
  84. Travis CI consists of many different sub-projects. The main ones

    are: travis-api travis-api is the Sinatra app that's responsible for serving our API. It responds to different HTTP endpoints and runs services in travis-core. Very little logic is in this repository. travis-build travis-build creates the build script for each job. It takes the configuration from the .travis.yml file and creates a bash script that is then run in the build environment by travis-worker. travis-core travis-core holds most of the logic for Travis CI. This repository is shared across several other apps and holds the models, services, and other things that these apps need. travis-cookbooks travis-cookbooks holds the Chef cookbooks that are used to provision the build environments. travis-hub travis-hub collects events from other apps and notifies other apps about the events. For example, it notifies travis-tasks about builds starting and finishing so notifications can be sent out. travis-hub is also responsible for enqueueing jobs that have been created and enforcing the Quality of Service restrictions, such as the number of concurrent builds per user. travis-listener travis-listener receives notifications from GitHub whenever commits are pushed or pull requests are opened. They are then pushed onto RabbitMQ for other apps to process. travis-logs travis-logs receives log updates from travis-worker, saves them to the database and pushes them to the web client. When a job is finished, travis-logs is responsible for pushing the log to Amazon S3 for archiving. travis-support travis-support holds shared logic for the different Travis CI apps. It is different from travis-core in that it holds more generic things, like how to run an async job or how to handle exceptions. travis-tasks travis-tasks receives notifications from travis-hub and sends out notifications to the different notification providers as needed. travis-web travis-web is our main Web client. It is written using Ember and communicates with travis-api to get information and gets live updates from travis-hub and travis-logs through Pusher. travis-worker travis-worker is responsible for running the build scripts in a clean environment. It streams the log output to travis-logs and pushes state updates (build starting/finishing) to travis-hub. api business common web componente componente