Lesscode

 Lesscode

RubyConfChina 2015 talk

9c85527be213734ed23c2ff233b7b5b8?s=128

Xuejie "Rafael" Xiao

October 10, 2015
Tweet

Transcript

  1. Lesscode Xuejie Xiao @defmacro

  2. None
  3. What this talk is about

  4. Simplicity

  5. What this talk is NOT about

  6. Micro services

  7. Perfomance

  8. Shitty Rails Clone

  9. Software is hard • Daily changed features • Unreasonable Deadlines

  10. None
  11. None
  12. None
  13. Software is hard to make

  14. How do we handle complexity?

  15. What’s the simplest software?

  16. No software at all! Zero complexity

  17. But that’s unrealistic

  18. What is simple? What is complex?

  19. Simple vs. Easy • Simple • One fold/task • No

    interleaving • Defined by us • Easy • Near our capabilities • Defined by problems From Simplicity Matters @ RailsConf 2012 by Rich Hickey, https://www.youtube.com/watch?v=rI8tNMsozo0 Simple!
  20. Complexity • Essential Complexity • Determined by the problem •

    Accidental Complexity • Exists because our tools are not perfect
  21. Easy <=> Essential Complexity Simple <=> Accidental Complexity

  22. Easy <=> Essential Complexity (Hard) Simple <=> Accidental Complexity (Complex)

  23. We want to reduce accidental complexity! I.E., make software less

    complex
  24. Accidental Complexity • Simple • Untwisted • Complex • Twisted

    From Simplicity Matters @ RailsConf 2012 by Rich Hickey, https://www.youtube.com/watch?v=rI8tNMsozo0
  25. Question: how many moving parts are needed for a normal

    site?
  26. Moving Parts • PostgreSQL • Cache(Redis) • Ruby App

  27. Moving Parts • PostgreSQL • Cache(Redis) • Ruby App <=

    Required? <= Optional? <= Required?
  28. Moving Parts • PostgreSQL • Cache(Redis) • Ruby App <=

    Required! <= Required! <= Required!
  29. What do you think Redis is?

  30. None
  31. Redis as a database? R U kidding me?

  32. From https://twitter.com/antirez/status/634693291590725632

  33. So Redis as a database, how does that work?

  34. Save a record HMSET User:1 email foo@bar.com name foo

  35. What about model ID? INCR User:_id

  36. Load a record HGETALL User:1

  37. Update index SADD User:emails:foo@bar.com 1

  38. Find by index SMEMBERS User:emails:foo@bar.com Then run HGETALL on each

    ID
  39. Joint Query SINTERSTORE User:query:b0fca790 User:emails:foo@bar.com User:name:foo SMEMBERS User:query:b0fca790 Run whatever

    processing we need for each id DEL User:query:b0fca790
  40. Don’t forget you have Lua at your fingertip! 1 local

    ctoken = redis.call('HGET', KEYS[1], '_cas') 2 if (not ctoken) or ctoken == ARGV[2] then 3 local ntoken 4 if not ctoken then 5 ntoken = 1 6 else 7 ntoken = tonumber(ctoken) + 1 8 end 9 redis.call('HMSET', KEYS[1], '_sdata', ARGV[1], 10 '_cas', ntoken, '_ndata', ARGV[3]) 11 return ntoken 12 else 13 error('cas_error') 14 end
  41. Ohm http://ohm.keyvalue.org/

  42. 1 class Event < Ohm::Model 2 attribute :name 3 reference

    :venue, :Venue 4 set :participants, :Person 5 counter :votes 6 7 index :name 8 end 9 10 class Venue < Ohm::Model 11 attribute :name 12 collection :events, :Event 13 end 14 15 class Person < Ohm::Model 16 attribute :name 17 end
  43. 1 event = Event.create :name => "Ohm Worldwide Conference 2031"

    2 event.id 3 # => 1 4 5 # Find an event by id 6 event == Event[1] 7 # => true 8 9 # Update an event 10 event.update :name => "Ohm Worldwide Conference 2032" 11 # => #<Event:0x007fb4c35e2458 @attributes={:name=>"Ohm Worldwide Conference"}, @_memo={}, @id="1"> 12 13 # Trying to find a non existent event 14 Event[2] 15 # => nil 16 17 # Finding all the events 18 Event.all.to_a 19 # => [<Event:1 name='Ohm Worldwide Conference 2031'>]
  44. 1 event.attendees.add(Person.create(name: "Albert")) 2 3 # And now... 4 event.attendees.each

    do |person| 5 # ...do what you want with this person. 6 end
  45. Moving Parts Now • Redis • Ruby App

  46. Isn’t that simpler?

  47. Let’s talk about tests

  48. None
  49. None
  50. Ruby has gone too magical when it comes to tests

  51. From https://github.com/rspec/rspec-core/issues/2067

  52. What does expect(…).to return?

  53. From https://relishapp.com/rspec/rspec-expectations/docs

  54. Wait a sec, all you give me is an example?

  55. This is CDD, not TDD

  56. Copy(-paste) Driven Development

  57. More documentation From https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations

  58. RubyDoc to the rescue From http://www.rubydoc.info/gems/rspec-expectations/RSpec/Expectations/ExpectationTarget#to-instance_method

  59. Huh, Matcher object does what? From http://www.rubydoc.info/gems/rspec-expectations/RSpec/Matchers/DSL/Matcher

  60. Complex Behavior • expect(…).to returns a Matcher object • You

    don’t know Matcher unless you read RSpec source code
  61. MiniTest: better, but still magical From https://github.com/seattlerb/minitest/blob/master/lib/minitest/spec.rb

  62. Let’s face it: not everyone likes this syntax obj.must_equal “foo”

    expect(obj).to eq(“foo”) Or
  63. What’s wrong with this? assert_equal obj, “foo”

  64. Cutest https://github.com/djanowski/cutest 118 LOC

  65. Tests cannot be simpler 1 setup do 2 {:a =>

    23, :b => 43} 3 end 4 5 test "should receive the result of the setup block as a parameter" do |params| 6 assert params == {:a => 23, :b => 43} 7 end 8 9 test "should evaluate the setup block before each test" do | params| 10 params[:a] = nil 11 end 12 13 test "should preserve the original values from the setup" do | params| 14 assert 23 == params[:a] 15 end
  66. How do we make the whole stack simple at CitrusByte

  67. Our choice: Cuba http://cuba.is/

  68. 0 3000 6000 9000 12000 Cuba 3.4.0 Sinatra 1.4.6 ActionPack

    4.2.4 Cuba 3.4.0 has only 314 lines of code
  69. Gems normally used together with Cuba Framework | LOC ---------------|---------------

    mote | 33 shield | 98 scrivener | 97 ohm | 647 protest | 118 ost | 59 malone | 93 nobi | 127 clap | 28 gs | 43 dep | 213 Notice this is never about LOC, it’s about one library fulfills one purpose only
  70. The whole stack is ~1556 lines of code • Read

    the source code! • Simple library, clear boundary • Easy to extend
  71. AT&T M2X https://m2x.att.com/

  72. Redis http://redis.io/

  73. ChefsFeed http://www.chefsfeed.com/

  74. Red Stamp https://www.redstamp.com/

  75. Is Rails simple?

  76. You might say: Rails feels easy to me! • Complicated

    constructs can be: • Familiar • Ready to use • But they are still complex, meaning they are: • Interleaved • Brings accidental complexity, which will bite you
  77. Example time!

  78. Hound https://houndci.com/ https://github.com/thoughtbot/hound

  79. Punchgirls Job Board https://jobs.punchgirls.com https://github.com/punchgirls/job_board

  80. Question: how many files are need for one request?

  81. Route 1 Houndapp::Application.routes.draw do 2 # ... 3 4 resources

    :repos, only: [:index] do 5 with_options(defaults: { format: :json }) do 6 resource :activation, only: [:create] 7 resource :deactivation, only: [:create] 8 resource :subscription, only: [:create, :destroy] 9 end 10 end 11 12 # ... 13 end
  82. Controller 1 class ActivationsController < ApplicationController 2 class FailedToActivate <

    StandardError; end 3 class CannotActivatePaidRepo < StandardError; end 4 5 before_action :check_repo_plan 6 7 def create 8 if activator.activate 9 analytics.track_repo_activated(repo) 10 render json: repo, status: :created 11 else 12 analytics.track_repo_activation_failed(repo) 13 render json: { errors: activator.errors }, status: 502 14 end 15 end 16 17 private
  83. Controller (cont.) 1 def check_repo_plan 2 if repo.plan_price > 0

    3 raise CannotActivatePaidRepo 4 end 5 end 6 7 def activator 8 @activator ||= RepoActivator.new(repo: repo, github_token: github_token) 9 end 10 11 def repo 12 @repo ||= current_user.repos.find(params[:repo_id]) 13 end 14 15 def github_token 16 current_user.token 17 end 18 end
  84. Related action in ApplicationController 1 class ApplicationController < ActionController::Base 2

    protect_from_forgery 3 4 before_action :force_https 5 before_action :capture_campaign_params 6 before_action :authenticate 7 8 helper_method :current_user, :signed_in? 9 10 private 11 # ... 12 13 def analytics 14 @analytics ||= Analytics.new(current_user, session[:campaign_params]) 15 end 16 17 # ... 18 end
  85. Service Object 1 class RepoActivator 2 attr_reader :errors 3 4

    def initialize(github_token:, repo:) 5 @github_token = github_token 6 @repo = repo 7 @errors = [] 8 end 9 10 def activate 11 activated = activate_repo 12 13 if activated 14 enqueue_org_invitation 15 end 16 17 activated 18 end 19 # ... 20 end
  86. View or Serializer 1 class RepoSerializer < ActiveModel::Serializer 2 attributes(

    3 :active, 4 :full_github_name, 5 :full_plan_name, 6 :github_id, 7 :id, 8 :in_organization, 9 :price_in_cents, 10 :private, 11 :stripe_subscription_id, 12 ) 13 # ... 14 end
  87. How big is your screen?

  88. I thought I was writing Ruby, not Objective-C?

  89. Luckily, the action we showed is simple • No helpers

    • No so-called presenter or whatever objects • We assume you already know models • before_action is not abused
  90. What about Cuba?

  91. Routes that perform actions 1 on "application/:id/contact" do |id| 2

    application = Application[id] 3 on application && company.posts.include?(application.post) do 4 on post, param("message") do |params| 5 mail = Contact.new(params) 6 if mail.valid? 7 message = JSON.dump(application_id: id, 8 subject: params["subject"], body: params["body"]) 9 Ost[:contacted_applicant].push(message) 10 res.redirect "/post/#{application.post.id}/applications" 11 else 12 session[:error] = "All fields are required" 13 render("company/post/contact", 14 title: "Contact developer", 15 application: application, message: mail) 16 end 17 end 18 end 19 end
  92. Filters that validates requests 1 class Contact < Scrivener 2

    attr_accessor :subject, :body 3 4 def validate 5 assert_present :subject 6 assert_present :body 7 end 8 end
  93. Views that queries and assembles data 1 <section id="contact-applicant"> 2

    <h2>Contact developer</h2> 3 4 <form action="/application/{{ application.id }}/contact" 5 method="POST"> 6 <!-- ... --> 7 8 <input type="text" name="message[subject]" 9 value="{{ message.subject }}" placeholder="Subject"> 10 11 <textarea name="message[body]" 12 placeholder="This mail will be sent to the developer"> 13 {{ message.body }} 14 </textarea> 15 16 <!-- ... --> 17 </form> 18 </section>
  94. Is your screen nicer?

  95. We do use helpers, but it’s more general 1 module

    DeveloperHelpers 2 # ... 3 4 def mote_vars(content) 5 super.merge(current_developer: current_developer) 6 end 7 8 def notfound(msg) 9 res.status = 404 10 res.write(msg) 11 halt(res.finish) 12 end 13 14 # ... 15 end
  96. Choices in Rails

  97. –David Heinemeier Hansson “Rails is omakase.”

  98. People love to customize Rails • ActiveRecord vs. Sequel •

    Sprockets vs. Webpack/browserify • Disable Turbolinks • Concerns considered harmful • Rails API
  99. sequel-rails From https://github.com/TalentBox/sequel-rails#using-sequel-rails

  100. webpack with Rails From https://medium.com/brigade-engineering/setting-up-webpack-with-rails-c62aea149679

  101. Turbolinks From http://blog.steveklabnik.com/posts/2013-06-25-removing-turbolinks-from-rails-4 However, keep in mind the code for

    handling Turbolinks still exists!
  102. We could go on …

  103. But something feels wrong

  104. It’s good if you want to JUST use Rails.

  105. But things become messy when you try to extend

  106. Why not aim for something much simpler?

  107. Don’t worry about bootstrapping speed! From “Simple Made Easy” at

    QCon London 2012 by Rich Hickey, http://www.infoq.com/presentations/Simple-Made-Easy-QCon-London-2012
  108. Easiness doesn’t change, so let’s make our software simpler.

  109. Lesscode • http://lesscode.is/ • Less Code track at RubyConf 2015

  110. –Leonardo da Vinci “Simplicity is the ultimate sophistication.”