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

The Design of Everyday Ruby

Ju Liu
September 26, 2014

The Design of Everyday Ruby

Everyday we read about LoD, TDD, DRY and SOLID. We are not only discussing about these concepts, but also we use them as canons to design, build and maintain our applications. In this talk, I want to explore the possible dangers in treating these concepts as sacred principles: in our everyday life, it pays off to treat these concepts as general guidelines, which can and should be challenged on a daily basis. In doing so, we can improve our understanding of these concepts and become better programmers.

Ju Liu

September 26, 2014
Tweet

More Decks by Ju Liu

Other Decks in Programming

Transcript

  1. THE DESIGN OF
    EVERYDAY RUBY

    View Slide

  2. !
    Hi! I'm Ju,
    I really love Ruby,
    I work at AlphaSights,
    You can find me as @arkh4m.

    View Slide

  3. THE DESIGN OF
    EVERYDAY THINGS

    View Slide

  4. CATALOGUE OF
    UNFINDABLE OBJECTS

    View Slide

  5. COFFEEPOT FOR
    MASOCHISTS

    View Slide

  6. KNIFE FOR PEARS

    View Slide

  7. KANGAROO RIFLE

    View Slide

  8. SNOW BYCICLE

    View Slide

  9. ALL IN ONE

    View Slide

  10. THE DESIGN PSYCHOLOGY OF
    EVERYDAY THINGS

    View Slide

  11. YOU'LL NEVER SEE A DOOR IN THE SAME WAY !

    View Slide

  12. View Slide

  13. PLATE FOR PUSHING,
    HANDLE FOR PULLING.

    View Slide

  14. View Slide

  15. HANDLE FOR PUSHING? OK...

    View Slide

  16. View Slide

  17. WAT

    View Slide

  18. MENTAL MODELS

    View Slide

  19. THE DESIGNER

    View Slide

  20. The designer is given a problem.
    He thinks of a solution, a conceptual model.
    Then he implements this model and creates an object.

    View Slide

  21. THE USER

    View Slide

  22. The user is given an object.
    He looks at it and tries to use it.
    He builds a mental model of how the object works.

    View Slide

  23. DESIGNER MODEL != USER MODEL

    View Slide

  24. THE DESIGNER CANNOT EXPLAIN TO
    THE USER HOW THE OBJECT WORKS

    View Slide

  25. Design is an act of communication
    between the designer and the user,
    except that all the communication
    has to come from the device itself.
    — Don Norman

    View Slide

  26. A device which speaks clearly makes the difference
    between a great user experience and a horrible one.

    View Slide

  27. View Slide

  28. View Slide

  29. WHAT DOES THIS
    HAVE TO DO WITH ME?

    View Slide

  30. I'm not a designer.

    View Slide

  31. FALSE

    View Slide

  32. We spend our days reading, writing and obsessing over software.

    View Slide

  33. WRITING SOFTWARE IS AN
    ACT OF COMMUNICATION

    View Slide

  34. When we read software, we're Users.

    View Slide

  35. When we write software, we're Designers.

    View Slide

  36. When we hate software, we're Angry Users.

    View Slide

  37. DESIGN PRINCIPLES APPLIED TO
    SOFTWARE WRITING

    View Slide

  38. VISIBILITY
    AFFORDANCES
    CONSTRAINTS
    NATURAL MAPPINGS

    View Slide

  39. VISIBILITY

    View Slide

  40. A device’s functions should be visible.

    View Slide

  41. Less frequently used functions should be hidden
    to reduce the apparent complexity of the device.

    View Slide

  42. At all times, it should be evident to the user
    what actions are available and how to perform them.

    View Slide

  43. AFFORDANCES

    View Slide

  44. Affordances are visual clues that suggest
    how to manipulate an object.

    View Slide

  45. Plates on doors afford pushing,
    Handles on doors afford pulling.

    View Slide

  46. LET'S SEE SOME CODE!

    View Slide

  47. No wait, let's try the Squint Test..

    View Slide

  48. class Anagram
    attr_reader :word
    def initialize(word)
    @word = word.downcase
    end
    def matches(candidates)
    candidates.select{ |candidate| is_anagram?(candidate) }
    end
    def is_anagram?(candidate)
    word != candidate.downcase &&
    word_distribution == distribution_for(candidate)
    end
    def word_distribution
    @word_distribution ||= distribution_for(word)
    end
    def distribution_for(word)
    word.downcase.chars.each_with_object(Hash.new(0)) do |c, result|
    result[c] += 1
    end
    end
    end

    View Slide

  49. class Anagram
    attr_reader :word
    def initialize(word)
    @word = word.downcase
    end
    def matches(candidates)
    candidates.select{ |candidate| is_anagram?(candidate) }
    end
    private
    def is_anagram?(candidate)
    word != candidate.downcase &&
    word_distribution == distribution_for(candidate)
    end
    def word_distribution
    @word_distribution ||= distribution_for(word)
    end
    def distribution_for(word)
    word.downcase.chars.each_with_object(Hash.new(0)) do |c, result|
    result[c] += 1
    end
    end
    end

    View Slide

  50. USE PUBLIC METHODS TO
    MAKE THE API VISIBLE

    View Slide

  51. USE PRIVATE METHODS TO
    HIDE IMPLEMENTATION

    View Slide

  52. CONSTRAINTS

    View Slide

  53. Affordances suggest the range of possibilities,
    Constraints limit the number of alternatives.

    View Slide

  54. The best way of making something easy to use
    is to restrict the possible choices.

    View Slide

  55. Make it impossible to do wrong.

    View Slide

  56. module RasmusString
    def strpos(haystack, needle, offset = 0)
    end
    def stristr(haystack, needle, before_needle = false)
    end
    end
    module RasmusArray
    def in_array(needle, haystack, strict = nil)
    end
    def array_search(needle, haystack, strict = nil)
    end
    end

    View Slide

  57. module RasmusString
    def strpos(haystack:, needle:, offset: 0)
    end
    def stristr(haystack:, needle:, before_needle: false)
    end
    end
    module RasmusArray
    def in_array(haystack:, needle:, strict: nil)
    end
    def array_search(haystack:, needle:, strict: nil)
    end
    end

    View Slide

  58. NATURAL
    MAPPINGS

    View Slide

  59. Your design should take advantadge of
    physical analogies and cultural standards.

    View Slide

  60. A natural mapping leads to a clear conceptual
    model and is much easier to understand.

    View Slide

  61. Any design that requires labels, diagrams or
    instructions lacks a good natural mapping.

    View Slide

  62. class User < ActiveRecord::Base
    before_save :geocode
    def geocode
    Geocoder.new(self).process
    # if Geocoder returns false, it will halt the
    # callback chain and Rails won't save
    true
    end
    end

    View Slide

  63. ActiveRecord::Base.class_eval do
    def without_halting_save
    yield
    true
    end
    end
    class User < ActiveRecord::Base
    before_save :geocode
    def geocode
    without_halting_save { Geocoder.new(self).process }
    end
    end

    View Slide

  64. COMMENTS SHOULD BE
    YOUR VERY LAST RESORT

    View Slide

  65. TRY TO WRITE TRUTHFUL
    METHODS NAMES INSTEAD

    View Slide

  66. So, what's the point of it all?

    View Slide

  67. CLARITY

    View Slide

  68. I concentrate on one component:
    making things that are
    understandable and usable.
    — Don Norman

    View Slide

  69. CONCENTRATE ON
    ONE COMPONENT

    View Slide

  70. WRITE SOFTWARE THAT IS
    UNDERSTANDABLE
    AND USABLE

    View Slide

  71. View Slide

  72. Where are we going as an industry?

    View Slide

  73. THE HOLY GRAIL

    View Slide

  74. If you want to be a pro,
    all you have to do is just
    follow these set of rules...

    View Slide

  75. LOD
    LAW OF DEMETER

    View Slide

  76. class Sport < ActiveRecord::Base
    has_many :leagues
    end
    class League < ActiveRecord::Base
    belongs_to :sport
    has_many :teams
    end
    class Team < ActiveRecord::Base
    belongs_to :league
    end

    View Slide

  77. class Sport < ActiveRecord::Base
    has_many :leagues
    end
    class League < ActiveRecord::Base
    belongs_to :sport
    has_many :teams
    end
    class Team < ActiveRecord::Base
    belongs_to :league
    def sport_name
    league.sport.name
    end
    end

    View Slide

  78. class Sport < ActiveRecord::Base
    has_many :leagues
    end
    class League < ActiveRecord::Base
    belongs_to :sport
    has_many :teams
    def sport_name
    sport.name
    end
    end
    class Team < ActiveRecord::Base
    belongs_to :league
    has_many :players
    def sport_name
    league.sport_name
    end
    end

    View Slide

  79. TDD
    TEST DRIVEN DEVELOPMENT

    View Slide

  80. require 'ostruct'
    class Person < OpenStruct
    def age
    Date.today.year - birthday.year
    end
    end
    describe Person do
    let(:ju) { Person.new(birthday: Date.new(1986)) }
    context "#age" do
    it "returns its age" do
    Timecop.travel(Date.new(2014)) do
    expect(ju.age).to eq(28)
    end
    end
    end
    end

    View Slide

  81. require 'ostruct'
    class Person < OpenStruct
    def age(now = Date.today)
    now.year - birthday.year
    end
    end
    describe Person do
    let(:ju) { Person.new(birthday: Date.new(1986)) }
    context "#age" do
    it "returns its age" do
    expect(ju.age(Date.new(2014))).to eq(28)
    end
    end
    end

    View Slide

  82. class Author < ActiveRecord::Base
    has_many :posts
    end
    class AuthorPresenter < BasePresenter
    def posts_previews
    posts.map(&:preview)
    end
    end

    View Slide

  83. describe AuthorPresenter do
    let(:post) { Post.new(preview: "I love Ruby") }
    let(:ju) { double(posts: [post]) }
    let(:presenter) { AuthorPresenter.new(ju) }
    context "#posts_previews" do
    it "returns posts previews" do
    expect(presenter.posts_previews).to eq(["I love Ruby"])
    end
    end
    end

    View Slide

  84. class Author < ActiveRecord::Base
    has_many :posts
    end
    class AuthorPresenter < BasePresenter
    def posts_previews
    posts.published.latest_first.map(&:preview)
    end
    end

    View Slide

  85. describe AuthorPresenter do
    let(:post) { Post.new(preview: "I love Ruby") }
    let(:ju) do
    double(
    posts: double(
    published: double(
    latest_first: [post]
    )
    )
    )
    end
    let(:presenter) { AuthorPresenter.new(ju) }
    context "#posts_previews" do
    it "returns posts previews" do
    expect(presenter.posts_previews).to eq(["I love Ruby"])
    end
    end
    end

    View Slide

  86. DRY
    DON'T REPEAT YOURSELF

    View Slide

  87. require 'ostruct'
    class Person < OpenStruct
    def can_drink?(now = Date.today)
    age(now) >= 21
    end
    def age(now = Date.today)
    now.year - birthday.year
    end
    end

    View Slide

  88. require 'ostruct'
    class Person < OpenStruct
    def can_drink?(now = date_today)
    age(now) >= 21
    end
    def age(now = date_today)
    now.year - birthday.year
    end
    private
    def date_today
    Date.today
    end
    end

    View Slide

  89. require 'ostruct'
    class Person < OpenStruct
    def can_drink?
    age >= 21
    end
    def age
    Date.today.year - birthday.year
    end
    end

    View Slide

  90. SOLID
    SRP, OPEN-CLOSED, LISKOV, IS, DIP

    View Slide

  91. class EmployeesController < ApplicationController
    def create
    @employee = Employee.new(params[:employee])
    if @employee.save
    redirect_to @employee, notice: "Employee #{@employee.name} created"
    else
    render :new
    end
    end
    end

    View Slide

  92. class EmployeesController < ApplicationController
    def create
    CreateRunner.new(self, EmployeesRepository.new).run(params[:employee])
    end
    def create_succeeded(employee, message)
    redirect_to employee, notice: message
    end
    def create_failed(employee)
    @employee = employee
    render :new
    end
    end

    View Slide

  93. class CreateRunner
    attr_reader :context, :repo
    def initialize(context, repo)
    @context = context
    @repo = repo
    end
    def run(employee_attrs)
    @employee = repo.new_employee(employee_attrs)
    if repo.save_employee
    context.create_succeeded(employee, "Employee #{employee.name} created")
    else
    context.create_failed(employee)
    end
    end
    end

    View Slide

  94. class EmployeesRepository
    def new_employee(*args)
    Biz::Employee.new(Employee.new(*args))
    end
    def save_employee(employee)
    employee.save
    end
    end

    View Slide

  95. require 'delegate'
    module Biz
    class Employee < SimpleDelegator
    def self.wrap(employees)
    employees.wrap { |e| new(e) }
    end
    def class
    __getobj__.class
    end
    # Biz logic ... AR is only a data-access object
    end
    end

    View Slide

  96. class EmployeesController < ApplicationController
    def create
    @employee = Employee.new(employee_params)
    if @employee.save
    redirect_to @employee, notice: "Employee #{@employee.name} created"
    else
    render :new
    end
    end
    end

    View Slide

  97. class EmployeesController < ApplicationController
    def create
    CreateRunner.new(self, EmployeesRepository.new).run(params[:employee])
    end
    def create_succeeded(employee, message)
    redirect_to employee, notice: message
    end
    def create_failed(employee)
    @employee = employee
    render :new
    end
    end
    class CreateRunner
    attr_reader :context, :repo
    def initialize(context, repo)
    @context = context
    @repo = repo
    end
    def run(employee_attrs)
    @employee = repo.new_employee(employee_attrs)
    if repo.save_employee
    context.create_succeeded(employee, "Employee #{employee.name} created")
    else
    context.create_failed(employee)
    end
    end
    end
    class EmployeesRepository
    def new_employee(*args)
    Biz::Employee.new(Employee.new(*args))
    end
    def save_employee(employee)
    employee.save
    end
    end
    require 'delegate'
    module Biz
    class Employee < SimpleDelegator
    def self.wrap(employees)
    employees.wrap { |e| new(e) }
    end
    def class
    __getobj__.class
    end
    # Biz logic ... AR is only a data-access object
    end
    end

    View Slide

  98. View Slide

  99. Following all the rules all the time...

    View Slide

  100. View Slide

  101. THERE ARE NO RULES
    WITHOUT DRAWBACKS

    View Slide

  102. THINK ABOUT THE TRADEOFFS

    View Slide

  103. THINK ABOUT CLARITY

    View Slide

  104. INSTEAD OF SOLID, TRY

    View Slide

  105. AULIC

    View Slide

  106. AVOID

    View Slide

  107. USELESS

    View Slide

  108. LEVELS OF

    View Slide

  109. INDIRECTION

    View Slide

  110. COURAGEOUSLY

    View Slide

  111. AVOID
    USELESS
    LEVELS OF
    INDIRECTION
    COURAGEOUSLY

    View Slide

  112. THANKS, DISCUSSION TIME!

    ALPHASIGHTS.COM/RUBY

    View Slide