第1页
Lesscode
Xuejie Xiao @defmacro
第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
第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
Simple!
• 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
第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?
第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
第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”
Or
expect(obj).to eq(“foo”)
第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页
Cuba 3.4.0 has only 314 lines of code
0 Cuba 3.4.0
Sinatra 1.4.6
ActionPack 4.2.4
第69页
Gems normally used together with Cuba
Framework
|
LOC
---------------|---------------
mote
|
shield
|
scrivener
|
ohm | 647
protest
|
ost |
malone
|
nobi
|
clap
|
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页
“Rails is omakase.”
–David Heinemeier Hansson
第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
However, keep in mind the code for handling Turbolinks still exists!
From http://blog.steveklabnik.com/posts/2013-06-25-removing-turbolinks-from-rails-4
第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页
“Simplicity is the ultimate sophistication.”
–Leonardo da Vinci