Slide 1

Slide 1 text

Organizing an architecture of your Ruby on Rails app with Trailblazer 2.0 Yevhen Kuzminov, MobiDev

Slide 2

Slide 2 text

MobiDev:/$ whoami Yevhen Kuzminov |> Team Leader |> PHP 2009 |> Ruby 2014 |> Elixir 2016

Slide 3

Slide 3 text

What’s wrong with Rails Way™?

Slide 4

Slide 4 text

http://rwdtow.stdout.in

Slide 5

Slide 5 text

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/

Slide 6

Slide 6 text

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!

Slide 7

Slide 7 text

But actual goal of all these “sharp knives” is … to hide complexity and write less code in controller

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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”

Slide 10

Slide 10 text

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)

Slide 11

Slide 11 text

Rails or Ruby ? http://railshurts.com/quiz/

Slide 12

Slide 12 text

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” ?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

TDD is dead! I suppose S.O.L.I.D too ?

Slide 16

Slide 16 text

https://twitter.com/andrzejkrzywda/status/736265294516264960/photo/1

Slide 17

Slide 17 text

What I want is... ● configuration over confusion ● boilerplate over magic

Slide 18

Slide 18 text

Trailblazer at last, sorry for the long Rails Way intro, but you need to feel the pain! http://trailblazer.to, created by Nick Sutterer

Slide 19

Slide 19 text

https://twitter.com/wojtha/status/736999191487057920

Slide 20

Slide 20 text

Trailblazer… who uses it? http://bestgems.org/gems/trailblazer

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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).()

Slide 23

Slide 23 text

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
  • GUIDES
  • <% if signed_in? %> <% else %> SIGN IN <% end %>

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Reform - request form ● Specify params ● Data types ● Coersion ● Populaion ● Validation form = AlbumForm.new(model) form.validate form.save

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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))

Slide 28

Slide 28 text

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)

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Railway

Slide 33

Slide 33 text

Railway: mix of Either Monad and ROP

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Stop doing if/then/else Start doing encapsulation

Slide 37

Slide 37 text

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.

Slide 38

Slide 38 text

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)

Slide 39

Slide 39 text

API done right with Representable

Slide 40

Slide 40 text

Just reuse Operations + JSON (de)serialization

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Your own custom “Endpoints” - Controller def update result = ::User::Profile::Update.(params, user: current_user) render_json_operation_result(result) end

Slide 45

Slide 45 text

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 ...

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Tips and tricks to make Trailblazer a friend of Rails

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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 ]

Slide 50

Slide 50 text

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")

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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<

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

That’s all Yevhen Kuzminov [email protected] http://stdout.in @iJackUA --- More about Trailblazer http://trailblazer.to Go! Upgrade your Rails apps!