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

Lesscode

 Lesscode

RubyConfChina 2015 talk

Xuejie "Rafael" Xiao

October 10, 2015
Tweet

More Decks by Xuejie "Rafael" Xiao

Other Decks in Programming

Transcript

  1. 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!
  2. Complexity • Essential Complexity • Determined by the problem •

    Accidental Complexity • Exists because our tools are not perfect
  3. Accidental Complexity • Simple • Untwisted • Complex • Twisted

    From Simplicity Matters @ RailsConf 2012 by Rich Hickey, https://www.youtube.com/watch?v=rI8tNMsozo0
  4. 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
  5. 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
  6. 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'>]
  7. 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
  8. Complex Behavior • expect(…).to returns a Matcher object • You

    don’t know Matcher unless you read RSpec source code
  9. 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
  10. 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
  11. 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
  12. The whole stack is ~1556 lines of code • Read

    the source code! • Simple library, clear boundary • Easy to extend
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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>
  24. 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
  25. People love to customize Rails • ActiveRecord vs. Sequel •

    Sprockets vs. Webpack/browserify • Disable Turbolinks • Concerns considered harmful • Rails API
  26. 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