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

Organizing an architecture of your Ruby on Rails app with Trailblazer 2.0

Organizing an architecture of your Ruby on Rails app with Trailblazer 2.0

CIklum Kharkiv Speakers' Corner
https://www.facebook.com/events/305615329893245

Yevhen "Eugene" Kuzminov

June 20, 2017
Tweet

More Decks by Yevhen "Eugene" Kuzminov

Other Decks in Programming

Transcript

  1. Organizing an architecture of your Ruby on Rails app with

    Trailblazer 2.0 Yevhen Kuzminov, MobiDev
  2. Most Rails developers do not write object-oriented Ruby code. They

    write MVC-oriented Ruby code by putting models and controllers in the expected locations. Most will add utility modules with class-methods in lib/, but that’s it. It takes 2-3 years before developers realize: “Rails is just Ruby. I can create simple objects and compose them in ways that Rails does not explicitly endorse! Mike Perham (the author and maintainer of Sidekiq) https://www.mikeperham.com/2012/05/05/five-common-rails-mistakes/
  3. Sharp Knives https://m.signalvnoise.com/provide-sharp-knives-cc0a22bf7934 Is it… ? • creating “simple solutions”

    for complicated issues, that caused by previous “simple solution” • thus each iteration is making entire system even more complex • but they call it a “sharp knife” and it’s all your fault to end up without 2 fingers!
  4. But actual goal of all these “sharp knives” is …

    to hide complexity and write less code in controller
  5. ActiveRecord callbacks Consequences: • Tight coupling • Fat Model •

    Indirect business logic flow (I know you write BL in Model :) ) Rails Way solution - SUPPRESS Notification.suppress do Relationship.create(follower: self, followed: user_to_follow) end https://medium.com/spritle-software/rails-5-activerecord-suppress-a-step-too-far-d7ec2e4ed027
  6. Strong Parameters with Nested Attrs Model contains multiple • Validation

    rules • accept_nested_attributes_for with conditions • Tight coupling of models Controller contains multiple • Strong params definitions per each action • Complex nested “require” and “permit”
  7. Monkey Patching, ActiveSupport Let’s rant one more time about 2.hours.ago

    Don’t get me wrong: • I like how it looks and how easy it is to write • What I don’t like ◦ Hidden internal complexity ◦ Shared state (timezone from current thread, internal call to Time.now) ◦ Architectural influence ◦ The need to “fix” this behaviour (via TimeCop gem)
  8. View Helpers • config.action_controller.include_all_helpers - The default configuration behavior is

    that all view helpers are available to each controller • How big is your “application_helper.rb” ?
  9. TDD is dead • If it does not play well

    with Rails Way - blame it • Unit tests are not about testing, but about code architecture • Unit test is the best independent consumer of your code • If you can’t unit test your code - smth is wrong in your code, but not with Unit tests idea • Capybara test are good, but slow and require refactoring as soon as your HTML/CSS changes http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html
  10. You wanted a banana but what you got was a

    gorilla holding the banana and the entire jungle Allusion to Unit test vs Application test Joe Armstrong (creator of Erlang)
  11. Trailblazer at last, sorry for the long Rails Way intro,

    but you need to feel the pain! http://trailblazer.to, created by Nick Sutterer
  12. Concept based project structure ├── app │ ├── concepts │

    │ ├── blog_post │ │ │ ├── cell │ │ │ │ ├── create.rb │ │ │ │ ├── ... │ │ │ ├── contract │ │ │ │ ├── create.rb │ │ │ │ └── ... │ │ │ ├── operation │ │ │ │ ├── create.rb │ │ │ │ ├── ... │ │ │ └── view │ │ │ ├── create.erb │ │ │ └── ...│ | ├── controllers │ │ └── blog_posts_controller.rb │ └── models │ ├── blog_post.rb
  13. Cells - reusable view model • an object that can

    render a template • faster than ActionView • has its properties “contract” Navigation.new({ active: 1 }, current_user: current_user).()
  14. class Navigation < Trailblazer::Cell property :active def signed_in? options[:current_user] and

    model.active end def email options[:current_user].email end def avatar_url hexed = Digest::MD5.hexdigest(email) "https://www.gravatar.com/avatar/#{hexed}?s=36" End def show render # renders app/navigation/view/show.erb end end #show.erb <nav class="top-bar"> <ul> <li>GUIDES</li> <li> <% if signed_in? %> <img src="<%= avatar_url %>"> <% else %> SIGN IN <% end %> </li> </ul> </nav>
  15. Testing Cell class NavigationCellTest < Minitest::Spec it "renders avatar when

    user provided" do html = Navigation.(nil, current_user: User.find(1)).() html.must_match "Signed in: [email protected]" end end
  16. Reform - request form • Specify params • Data types

    • Coersion • Populaion • Validation form = AlbumForm.new(model) form.validate form.save
  17. Reform - example class SongForm < Reform::Form property :title, default:

    “Untitled Song” property :written_at, type: Types::Form::DateTime property :country, writeable: false property :song_uuid, virtual: true validates :written_at, presence: true def title=(value) super sanitize(value) # value is raw form input. end end
  18. Reform - example #2 class AlbumForm < Reform::Form include Composition

    property :id, on: :album property :title, on: :album property :songs, on: :cd property :cd_id, on: :cd, from: :id validates :title, presence: true end form = AlbumForm.new(album: album, cd: CD.find(1))
  19. Operation - the heart of Trailblazer • Orchestrate business logic

    • Not a god object • Wires all dependencies • Reusable independent from context BlogPost::Create.(params, current_user: current_user)
  20. Really common Operation pattern class BlogPost::Create < Trailblazer::Operation extend Contract::DSL

    contract do # All that Reform stuff goes here with properties and validates end step Policy::Guard( :authorize! ) step Model( BlogPost, :new ) step Contract::Build() step Contract::Validate() step Contract::Persist() step :notify! def authorize!(options, current_user:, **) current_user.signed_in? end def notify!(options, current_user:, model:, **) BlogPost::Notification.(current_user, model) end end
  21. class BlogPost::Create < Trailblazer::Operation step :authorize! step :model! step :persist!

    step :notify! def authorize!(options, current_user:, **) current_user.signed_in? end def model!(options, **) options["model"] = BlogPost.new end def persist!(options, params:, model:, **) model.update_attributes(params) model.save end def notify!(options, current_user:, model:, **) options["result.notify"] = BlogPost::Notification.(current_user, model) end end
  22. Operation Result def update result = BlogPost::Create.(params, current_user: current_user) if

    result.success? do flash[:notice] = "#{result["model"].title} has been saved" return redirect_to blog_post_path(result["model"].id) end end
  23. Failure track module BlogPost class Create < Trailblazer::Operation success :hello_world!

    step :how_are_you? success :enjoy_your_day! failure :tell_joke! def hello_world!(options, *) end def how_are_you?(options, params:, **) params[:happy] == "yes" end def enjoy_your_day!(options, *) end def tell_joke!(options, *) end end end
  24. Testing Operation it do result = BlogPost::Create.( { name: "Hello"

    } ) expect(result.success?).to be_truthy # or smth like this expect(result["model"].seo_url).to eq “happy-1” end
  25. Cell builder class CommentCell < Cell::ViewModel include ::Cell::Builder builds do

    |model, options| if model.is_a?(Post) PostCell elsif model.is_a?(Comment) CommentCell end end end CommentCell.(Post.find(1)) #=> creates a PostCell.
  26. Operation builder class Update < Trailblazer::Operation step Nested( :build! )

    def build!(options, current_user:nil, **) current_user.admin? ? Create::Admin : Create::NeedsModeration end end Update.(params, current_user: current_user)
  27. Representable looks like ActiveModelSerializer, but... song_json = SongRepresenter.new(song).to_json #=> {"id":1,"title":"Fallout"}

    song = Song.new # empty Object song_object = SongRepresenter.new(song).from_json(song_json)
  28. Representable example class SongRepresenter < Representable::Decorator include Representable::JSON property :id,

    name: 'niceId' property :title, getter: ->(opts) { nice_title } property :artist do property :id property :name end property :studio, decorator: StudioRepresenter, class: Studio collection :mixes, decorator: SongMixRepresenter, class:SongMix end
  29. Trailblaser Endpoints (Work in progress) # Controller code result =

    Song::Create.( { title: "SVT" }, "current_user" => User.root ) Trailblazer::Endpoint.new.(result) do |m| m.success { |result| puts "Model #{result["model"]} saved" } m.unauthenticated { |result| puts "You ain't root!" } end http://trailblazer.to/gems/operation/2.0/endpoint.html
  30. Your own custom “Endpoints” - Controller def update result =

    ::User::Profile::Update.(params, user: current_user) render_json_operation_result(result) end
  31. Your own custom “Endpoints” - Concern def render_json_operation_result(result) if result.success?

    && result["model"].blank? return render_json_ok elsif result.success? && result["model"].present? return render_json_ok(result["model"]) elsif result.failure? && result["model"].blank? && result["model.action"] == :find_by return render_json_not_found ...
  32. Your own custom “Endpoints” - Concern ... elsif result.failure? &&

    result["result.contract.default"] && result["result.contract.default"].failure? errors = result['contract.default'].errors.full_messages << result['error'] errors_array = errors.uniq.reject(&:blank?) return render_json_request_error(errors_array) elsif result.failure? && result["model"].present? return render_json_request_error([result['error']]) else return render_json_error('Unknown operation result', 500) end end
  33. Duplicated operation steps step Contract::Build() step Contract::Validate() step :find_user step

    :send_instructions puts "PIPETREE:" puts self["pipetree"].inspect [ >operation.new >contract.build >contract.default.validate >find_user >send_instructions >contract.build >contract.default.validate >find_user >send_instructions ] Change Operation class and Class would be reloaded -> steps called one more time
  34. Duplicated operation steps - solution initialize_pipetree! step Contract::Build() step Contract::Validate()

    step :find_user step :send_instructions puts "PIPETREE:" puts self["pipetree"].inspect [ >operation.new >contract.build >contract.default.validate >find_user >send_instructions ]
  35. Require dependencies • Operations, Cells, Models etc. are loaded by

    trailblazer_loader gem • It has specific order • Models are loaded before Operations • If you need specific Operations loading order explicit require_dependency • It is needed if one Operation inherit from another one require_dependency("#{File.dirname(__FILE__)}/sign_up_form.rb")
  36. Operation Contract is Reform Form, that is in turn a

    Twin • Don't forget that Contract is a Disposable::Twin • All the great things like a Composition, default, nilify etc. could be used
  37. Shortcut helper # Controller code... def reset_password_form result = ForgotPassword::ResetForm.(params)

    @form, @model, @errors = form_model(result) if result.failure? flash[:error] = @errors end end module TrailblazerHelpers extend ActiveSupport::Concern def form_model(result) errors = result['contract.default'].errors.full_messages<<result['error'] errors_text = errors.uniq.join('. ') return [result["contract.default"], result["model"], errors_text] end end
  38. In a nutshell: what Trbr changes in my Rails app?

    • Model contains only Queries, Scopes and Relations. • No callbacks in Models! • Controller actions have only Operation calls and result render • No business logic in Controller, any data manipulation • No Strong Params used in Controller • All validation in only inside Operations • View helpers not used • All view logic placed in Cells • Shared view logic in Decorators • Mostly Cell and Operation builders used instead of if/then/else in Controller or view
  39. That’s all Yevhen Kuzminov [email protected] http://stdout.in @iJackUA --- More about

    Trailblazer http://trailblazer.to Go! Upgrade your Rails apps!