Slide 1

Slide 1 text

Lesscode Xuejie Xiao @defmacro

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

What this talk is about

Slide 4

Slide 4 text

Simplicity

Slide 5

Slide 5 text

What this talk is NOT about

Slide 6

Slide 6 text

Micro services

Slide 7

Slide 7 text

Perfomance

Slide 8

Slide 8 text

Shitty Rails Clone

Slide 9

Slide 9 text

Software is hard • Daily changed features • Unreasonable Deadlines

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Software is hard to make

Slide 14

Slide 14 text

How do we handle complexity?

Slide 15

Slide 15 text

What’s the simplest software?

Slide 16

Slide 16 text

No software at all! Zero complexity

Slide 17

Slide 17 text

But that’s unrealistic

Slide 18

Slide 18 text

What is simple? What is complex?

Slide 19

Slide 19 text

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!

Slide 20

Slide 20 text

Complexity • Essential Complexity • Determined by the problem • Accidental Complexity • Exists because our tools are not perfect

Slide 21

Slide 21 text

Easy <=> Essential Complexity Simple <=> Accidental Complexity

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Accidental Complexity • Simple • Untwisted • Complex • Twisted From Simplicity Matters @ RailsConf 2012 by Rich Hickey, https://www.youtube.com/watch?v=rI8tNMsozo0

Slide 25

Slide 25 text

Question: how many moving parts are needed for a normal site?

Slide 26

Slide 26 text

Moving Parts • PostgreSQL • Cache(Redis) • Ruby App

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Moving Parts • PostgreSQL • Cache(Redis) • Ruby App <= Required! <= Required! <= Required!

Slide 29

Slide 29 text

What do you think Redis is?

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

Redis as a database? R U kidding me?

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

So Redis as a database, how does that work?

Slide 34

Slide 34 text

Save a record HMSET User:1 email [email protected] name foo

Slide 35

Slide 35 text

What about model ID? INCR User:_id

Slide 36

Slide 36 text

Load a record HGETALL User:1

Slide 37

Slide 37 text

Update index SADD User:emails:[email protected] 1

Slide 38

Slide 38 text

Find by index SMEMBERS User:emails:[email protected] Then run HGETALL on each ID

Slide 39

Slide 39 text

Joint Query SINTERSTORE User:query:b0fca790 User:emails:[email protected] User:name:foo SMEMBERS User:query:b0fca790 Run whatever processing we need for each id DEL User:query:b0fca790

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Ohm http://ohm.keyvalue.org/

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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 # => #"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 # => []

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Moving Parts Now • Redis • Ruby App

Slide 46

Slide 46 text

Isn’t that simpler?

Slide 47

Slide 47 text

Let’s talk about tests

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

Ruby has gone too magical when it comes to tests

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

What does expect(…).to return?

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

This is CDD, not TDD

Slide 56

Slide 56 text

Copy(-paste) Driven Development

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Complex Behavior • expect(…).to returns a Matcher object • You don’t know Matcher unless you read RSpec source code

Slide 61

Slide 61 text

MiniTest: better, but still magical From https://github.com/seattlerb/minitest/blob/master/lib/minitest/spec.rb

Slide 62

Slide 62 text

Let’s face it: not everyone likes this syntax obj.must_equal “foo” expect(obj).to eq(“foo”) Or

Slide 63

Slide 63 text

What’s wrong with this? assert_equal obj, “foo”

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

How do we make the whole stack simple at CitrusByte

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

The whole stack is ~1556 lines of code • Read the source code! • Simple library, clear boundary • Easy to extend

Slide 71

Slide 71 text

AT&T M2X https://m2x.att.com/

Slide 72

Slide 72 text

Redis http://redis.io/

Slide 73

Slide 73 text

ChefsFeed http://www.chefsfeed.com/

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Is Rails simple?

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Example time!

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

Question: how many files are need for one request?

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

How big is your screen?

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

What about Cuba?

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

Views that queries and assembles data 1 2

Contact developer

3 4 6 7 8 10 11 13 {{ message.body }} 14 15 16 17 18

Slide 94

Slide 94 text

Is your screen nicer?

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

Choices in Rails

Slide 97

Slide 97 text

–David Heinemeier Hansson “Rails is omakase.”

Slide 98

Slide 98 text

People love to customize Rails • ActiveRecord vs. Sequel • Sprockets vs. Webpack/browserify • Disable Turbolinks • Concerns considered harmful • Rails API

Slide 99

Slide 99 text

sequel-rails From https://github.com/TalentBox/sequel-rails#using-sequel-rails

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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!

Slide 102

Slide 102 text

We could go on …

Slide 103

Slide 103 text

But something feels wrong

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

But things become messy when you try to extend

Slide 106

Slide 106 text

Why not aim for something much simpler?

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

Easiness doesn’t change, so let’s make our software simpler.

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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