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 full-size slide

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

    View full-size slide

  3. THE DESIGN OF
    EVERYDAY THINGS

    View full-size slide

  4. CATALOGUE OF
    UNFINDABLE OBJECTS

    View full-size slide

  5. COFFEEPOT FOR
    MASOCHISTS

    View full-size slide

  6. KNIFE FOR PEARS

    View full-size slide

  7. KANGAROO RIFLE

    View full-size slide

  8. SNOW BYCICLE

    View full-size slide

  9. THE DESIGN PSYCHOLOGY OF
    EVERYDAY THINGS

    View full-size slide

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

    View full-size slide

  11. PLATE FOR PUSHING,
    HANDLE FOR PULLING.

    View full-size slide

  12. HANDLE FOR PUSHING? OK...

    View full-size slide

  13. MENTAL MODELS

    View full-size slide

  14. THE DESIGNER

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. DESIGNER MODEL != USER MODEL

    View full-size slide

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

    View full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. WHAT DOES THIS
    HAVE TO DO WITH ME?

    View full-size slide

  22. I'm not a designer.

    View full-size slide

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

    View full-size slide

  24. WRITING SOFTWARE IS AN
    ACT OF COMMUNICATION

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. DESIGN PRINCIPLES APPLIED TO
    SOFTWARE WRITING

    View full-size slide

  29. VISIBILITY
    AFFORDANCES
    CONSTRAINTS
    NATURAL MAPPINGS

    View full-size slide

  30. A device’s functions should be visible.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. LET'S SEE SOME CODE!

    View full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. USE PUBLIC METHODS TO
    MAKE THE API VISIBLE

    View full-size slide

  40. USE PRIVATE METHODS TO
    HIDE IMPLEMENTATION

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  43. Make it impossible to do wrong.

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. NATURAL
    MAPPINGS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

  52. COMMENTS SHOULD BE
    YOUR VERY LAST RESORT

    View full-size slide

  53. TRY TO WRITE TRUTHFUL
    METHODS NAMES INSTEAD

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. CONCENTRATE ON
    ONE COMPONENT

    View full-size slide

  57. WRITE SOFTWARE THAT IS
    UNDERSTANDABLE
    AND USABLE

    View full-size slide

  58. Where are we going as an industry?

    View full-size slide

  59. THE HOLY GRAIL

    View full-size slide

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

    View full-size slide

  61. LOD
    LAW OF DEMETER

    View full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

  65. TDD
    TEST DRIVEN DEVELOPMENT

    View full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

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

    View full-size slide

  69. 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 full-size slide

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

    View full-size slide

  71. 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 full-size slide

  72. DRY
    DON'T REPEAT YOURSELF

    View full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

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

    View full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. 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 full-size slide

  84. Following all the rules all the time...

    View full-size slide

  85. THERE ARE NO RULES
    WITHOUT DRAWBACKS

    View full-size slide

  86. THINK ABOUT THE TRADEOFFS

    View full-size slide

  87. THINK ABOUT CLARITY

    View full-size slide

  88. INSTEAD OF SOLID, TRY

    View full-size slide

  89. COURAGEOUSLY

    View full-size slide

  90. AVOID
    USELESS
    LEVELS OF
    INDIRECTION
    COURAGEOUSLY

    View full-size slide

  91. THANKS, DISCUSSION TIME!

    ALPHASIGHTS.COM/RUBY

    View full-size slide