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

Not the Rails Way. Again!

Not the Rails Way. Again!

Ruby on Rails architectural practices.

Ruby Russia 2019, September 28, 2019.

Igor Alexandrov

September 28, 2019
Tweet

More Decks by Igor Alexandrov

Other Decks in Technology

Transcript

  1. Tver.IO 11 meetups in 2019 > 80 people on each

    meetup Speakers from Evrone, Evil Martians, RAWG.io, Yandex, Mail.RU Audience > 2000 people • • • •
  2. About since 2008 >= Rails 2.1 SmallTalk => Ruby =>

    Crystal JetRockets CTO • • • •
  3. Community. How it was born? (2007) everyone came from somewhere

    (SmallTalk, Java, PHP); no developers who “were born with Rails”; • •
  4. Community. How it was born? (2007) everyone came from somewhere

    (SmallTalk, Java, PHP); no developers who “were born with Rails”; we needed to be unique and have something to unite us; • • •
  5. Community. How it was born? (2007) everyone came from somewhere

    (SmallTalk, Java, PHP); no developers who “were born with Rails”; we needed to be unique and have something to unite us; say “no” to “the architecture astronauts Java way” or the “PHP spaghetti way”; • • • •
  6. Community. How it was born? (2007) “no” to everything Java-like;

    XML → YAML; patterns → ; DDD movement → ; • • • •
  7. We’ve got ActiveRecord. We take the object from the database

    row and use it in all the three layers. Fat models or fat controllers? Whatever, let’s just not create new layers. » «
  8. What we had in The Rails Way? ActiveRecord objects everywhere,

    including the views; external gems used for most of the features; • •
  9. What we had in The Rails Way? ActiveRecord objects everywhere,

    including the views; external gems used for most of the features; non-trivial logic with the combination of filters, callbacks, conditional validations and tons of ; • • •
  10. What we had in The Rails Way? ActiveRecord objects everywhere,

    including the views; external gems used for most of the features; non-trivial logic with the combination of filters, callbacks, conditional validations and tons of ; metaprogramming; • • • •
  11. What we had in The Rails Way? ActiveRecord objects everywhere,

    including the views; external gems used for most of the features; non-trivial logic with the combination of filters, callbacks, conditional validations and tons of ; metaprogramming; only 3 layers - Models, Views, Controllers. • • • • •
  12. 619 729 Fat Models # app/models/document.rb # Callbacks 621 622

    before_validation :on => :create do 623 self.scorer = applicant 624 self.closing_status ||= 'likely' # … 728 # Class Methods
  13. Logic in Views # app/views/shared/header.html.erb <% if current_user %> <%=

    current_user.name %> <% else %> Guest <% end %>
  14. users_comments comments.select == params[:username] Controllers Hell class CommentsController < ApplicationController

    def posts = Post.all comments = posts.map(&:comments).flatten @user_comments = do |comment| comment.author.username end end end
  15. Rails Helpers Retrieve data from the DB visible_comments_for(@article) ; no

    dependency tracking; no inheritance; • • •
  16. Rails Helpers Retrieve data from the DB visible_comments_for(@article) ; no

    dependency tracking; no inheritance; html tags generation. • • • •
  17. Query Objects Scope that interacts with more than one column

    and/or joins in other tables; have single interface #call(relation) ; • •
  18. Query Objects Scope that interacts with more than one column

    and/or joins in other tables; have single interface #call(relation) ; return ActiveRecord::Relation ; • • •
  19. Query Objects Scope that interacts with more than one column

    and/or joins in other tables; have single interface #call(relation) ; return ActiveRecord::Relation ; accept query params as constructor arguments. • • • •
  20. reduce_loan_officer_id if loan_officer_id Query Objects require 'dry-initializer' class Crm::ApplicationsSearch extend

    Dry::Initializer option :loan_officer_id, optional: true option # ... def call(application_relation) @query = application_relation @query = # ... @query end end
  21. Web application — chain of services Request [*] [*] [*]

    [*] Response • • • • • •
  22. Web application — chain of services Request Nginx Application Server

    (Unicorn/Puma) Rails Application DB Response • • • • • •
  23. Web application — chain of services Request Nginx Application Server

    (Unicorn/Puma) Rails Application DB Response • • • • • •
  24. Service Objects responsibilities Place to define complex actions; processes multiple

    steps and interactions; remove callbacks from ActiveRecord models; place to define validations. • • • •
  25. Validator.check(contact).bind do |contact| Service Objects (AR validators) class Contacts::CreateContactFromDeal <

    BaseService def call(deal) contact = something_that_creates_contact(deal) contact.save! Success(contact) end end class Validator < ::ApplicationValidator validates :email, presence: true end end
  26. Service Objects (dry-rb) class Leads::CreateLead # … def call(lead, params)

    params = yield validate(lead, params) lead = yield persist(lead, params) yield notify(lead) Success(lead) end # … end
  27. yield Success(lead) Service Objects (dry-monads) class Leads::CreateLead # … def

    call(lead, params) params = validate(lead, params) lead = yield persist(lead, params) yield notify(lead) end # … end
  28. dry-validation Dry-schema to define reusable schemas; input sanitization, coercion and

    type-check; macros (beta); custom rules that don't break your head. • • • •
  29. detect_contract(params[:source]).call(params) Service Objects (dry-validation) class Leads::CreateLead def call(lead, params) #

    … end def validate(lead, params) result = if result.success? Success(result.to_h) else Failure(…) end end end
  30. ContactsContract HireUsContract Service Objects (dry-validation) class Leads::CreateLead # … class

    < Dry::Validation::Contract # … end class < Dry::Validation::Contract # … end end
  31. case result Service Objects (controllers) class LeadsController < ApplicationController def

    create command = Lead::CreateLead.new result = command.(resource_lead, params[:lead]) when Dry::Monads::Success render #… when Dry::Monads.Failure(JetRockets::ValidationError) render #… when Dry::Monads.Failure render #… end end end
  32. HireUsContract PostsContract Service Objects (RSpec) RSpec.describe Leads::CreateLead:: do subject(:command) do

    Leads::CreateLead::::HireUsContract.new end # operations end RSpec.describe Leads::CreateLead:: do # … end
  33. Service Objects conventions Avoid NounServices (e.g. UsersService, ProductService ); be

    specific (e.g. Users::ForgotPassword ); use namespaces based on domain concepts (e.g. /crm/documents/category/update_category ); • • •
  34. Service Objects conventions Avoid NounServices (e.g. UsersService, ProductService ); be

    specific (e.g. Users::ForgotPassword ); use namespaces based on domain concepts (e.g. /crm/documents/category/update_category ); focus on readability of your call method; • • • •
  35. Service Objects conventions Avoid NounServices (e.g. UsersService, ProductService ); be

    specific (e.g. Users::ForgotPassword ); use namespaces based on domain concepts (e.g. /crm/documents/category/update_category ); focus on readability of your call method; return results with state. • • • • •
  36. standard Wrapper on top of RuboCop; can be used separately;

    can included into Rubocop configuration; includes only rubocop and rubocop-performance Cops configuration. • • • •
  37. No, it is still Rails With modular approach; with composition

    of services; mature and stable; • • •
  38. No, it is still Rails With modular approach; with composition

    of services; mature and stable; maintainable. • • • •
  39. What to do right now? implement company/project-wide standard; follow these

    standards; don't be afraid to experiment; • • •
  40. What to do right now? implement company/project-wide standard; follow these

    standards; don't be afraid to experiment; start new project with Rails! • • • •