Save 37% off PRO during our Black Friday Sale! »

Object Oriented Design, Rails and Why You Should Think Twice Before Leaving the Rails Way

Object Oriented Design, Rails and Why You Should Think Twice Before Leaving the Rails Way

The Ruby community has been bombarded by a plethora of discussions about software design best practices. Acronyms such as SOLID, DCI and SOA are up in the community and all them promise to materialize the ghost of maintainability. What few of these discussions present is that applying these principles and techniques fervently also impacts the software maintenance.
In this talk I will present some of these concepts and show the symptoms that they can cause in yours projects if applied incorrectly.

0525b332aafb83307b32d9747a93de03?s=128

Rafael França

March 06, 2015
Tweet

Transcript

  1. Object Oriented Design and Rails

  2. Why You Should Think Twice Before Leaving the Rails Way

  3. Rafael França @rafaelfranca

  4. Core Team Member

  5. None
  6. Consultant

  7. Member of the Legacy Code department

  8. Worked in a dozen of different projects

  9. Many had problems

  10. Get away from Rails

  11. What is The Rails Way?

  12. MVC

  13. None
  14. Folders structure

  15. None
  16. Fat models / Skinny controllers

  17. # app/models/person.rb class Person < ActiveRecord::Base has_one :address end

  18. <!-- app/views/people/index.html.erb --> <% @people.each do |person| %> <div id="person-<%=

    person.new_record? ? "new" : person.id %>"> <span class="name"> <%= person.last_name %>, <%= person.first_name %> </span> <span class="age"> <%= (Date.today - person.birthdate) / 365 %> </span> </div> <% end %>
  19. # app/controllers/people_controller.rb class PeopleController < ActionController::Base def index @people =

    Person.where("added_at > ? and deleted = ?", Time.now.utc, false) .order("last_name, first_name") @people = @people.reject { |p| p.address.nil? } end end
  20. # app/models/person.rb class Person < ActiveRecord::Base has_one :address def name

    "#{last_name}, #{first_name}" end def age (Date.today - person.birthdate) / 365 end def pseudo_id new_record? ? "new" : id end end
  21. <!-- app/views/people/index.html.erb --> <% @people.each do |person| %> <div id="person-<%=

    person.pseudo_id %>"> <span class="name"><%= person.name %></span> <span class="age"><%= person.age %></span> </div> <% end %>
  22. # app/models/person.rb class Person < ActiveRecord::Base has_one :address def self.find_recent

    people = where("added_at > ? and deleted = ?", Time.now.utc, false). order("last_name, first_name") people.reject { |p| p.address.nil? } end def name "#{last_name}, #{first_name}" end def age (Date.today - person.birthdate) / 365 end def pseudo_id new_record? ? "new" : id end end
  23. # app/models/person.rb class Person < ActiveRecord::Base has_one :address def self.find_recent

    people = where("added_at > ? and deleted = ?", Time.now.utc, false). order("last_name, first_name") people.reject { |p| p.address.nil? } end def name "#{last_name}, #{first_name}" end def age (Date.today - person.birthdate) / 365 end def pseudo_id new_record? ? "new" : id end end
  24. # app/controllers/people_controller.rb class PeopleController < ActionController::Base def index @people =

    Person.find_recent end end
  25. # app/controllers/people_controller.rb class PeopleController < ActionController::Base def index @people =

    Person.find_recent end end
  26. Callbacks

  27. # app/controllers/people_controller.rb class PeopleController < ActionController::Base before_action :build_person def create

    if @person.save head :created else render :new end end private def build_person @person.new(params[:person]) end end
  28. # app/models/comment.rb class Comment < ActiveRecord::Base belongs_to :commentable, polymorphic: true

    after_create -> { Notification.create! comment: self, recipients: commendable.recipients } end
  29. The exception must carry the weight of its own work

  30. # app/models/comment.rb class Comment < ActiveRecord::Base belongs_to :commentable, polymorphic: true

    after_create -> { Notification.create! comment: self, recipients: commendable.recipients } end
  31. # app/models/concerns/copyable.rb module Copyable def copy_to(destination) Notification.suppress do # Copy

    logic that creates new comments that we do not want # triggering Notifications end end end
  32. https://github.com/rails/rails/pull/18910

  33. break them apart into a higher level object (CreateCommentWithNotification, for

    instance) so that I had the freedom to only create the comment if I wanted to.
  34. break them apart into a higher level object (CreateCommentWithNotification, for

    instance) so that I had the freedom to only create the comment if I wanted to.
  35. break them apart into a higher level object (CreateCommentWithNotification, for

    instance) so that I had the freedom to only create the comment if I wanted to.
  36. break them apart into a higher level object (CreateCommentWithNotification, for

    instance) so that I had the freedom to only create the comment if I wanted to.
  37. None
  38. The general principle I apply for this design is "the

    exception must carry the weight of its own work". If you create a new CreateCommentWithNotifications object, you're pushing the weight of the work onto the default case. That's not proportional in my book.
  39. The general principle I apply for this design is "the

    exception must carry the weight of its own work". If you create a new CreateCommentWithNotifications object, you're pushing the weight of the work onto the default case. That's not proportional in my book.
  40. The general principle I apply for this design is "the

    exception must carry the weight of its own work". If you create a new CreateCommentWithNotifications object, you're pushing the weight of the work onto the default case. That's not proportional in my book.
  41. Global helpers

  42. <!-- app/views/people/index.html.erb --> <div id="person-<%= pseudo_id_for(person) %>"> <!-- app/views/comments/index.html.erb -->

    <div id="comments-<%= pseudo_id_for(comment) %>"> # app/helpers/person_helper.rb module PersonHelper def pseudo_id_for(person) person.new_record? ? "new" : person.id end end # app/helpers/person_helper.rb module CommentHelper def pseudo_id_for(comment) comment.new_record? ? "new_comment" : comment.uuid end end
  43. Why get away from Rails?

  44. The Rails way is bad

  45. • Models too fat • Callbacks hard to avoid and

    test • Lots of conditionals in the code • Domain logic coupled with the framework
  46. Principles of OOD

  47. SOLID

  48. SOLID • Single responsibility principle • Open/closed principle • Liskov

    substitution principle • Interface segregation principle • Dependency inversion principle
  49. Small objects communicating with each other

  50. Alternative Architectures

  51. • Hexagonal • DCI • Microservices • Trailblazer

  52. Hexagonal

  53. Application driven equally from different sources

  54. Isolates the application from dependencies

  55. Application

  56. Application

  57. Application

  58. Application

  59. DCI

  60. Data Context Interaction

  61. None
  62. None
  63. None
  64. Microservices

  65. Break your application in smaller services

  66. • Componentization via Services • Organized around Business Capabilities •

    Smart endpoints and dumb pipes • Decentralized Governance • Decentralized Data Management • Design for failure • Evolutionary Design
  67. Catalog Payment Comments Store

  68. Trailblazer

  69. apotonick/trailblazer

  70. In a nutshell: Trailblazer makes you write logicless models that

    purely act as data objects, don't contain callbacks, nested attributes, validations or domain logic. It removes bulky controllers and strong_parameters by supplying additional layers to hold that code and completely replaces helpers.
  71. Concepts

  72. None
  73. None
  74. https://leanpub.com/trailblazer

  75. It solves many problems that have been around for years

    with a cleanly layered architecture. Only use what you like.
  76. While Trailblazer offers you abstraction layers for all aspects of

    Ruby On Rails, it does not missionize you. Wherever you want, you may fall back to the "Rails Way" with fat models, monolithic controllers, global helpers, etc. This is not a bad thing
  77. There is no silver bullet

  78. Software is not just objects and interactions

  79. A lot of things influence architecture

  80. Team maturity

  81. Ramp up on the project

  82. Team organization

  83. Personal preferences

  84. Dictatorship of pragmatism

  85. Dictatorship of best practices

  86. Create patterns, not standards

  87. Guidelines, not rules

  88. Cost of the indirection

  89. Cost of the chaos

  90. Don’t take automatic decisions

  91. Think with your editor Xavier Noria - @fxn

  92. Thanks Rafael França @rafaelfranca