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

The Design of Everyday Ruby

1a45b192d0bbaf167afb43a41859e313?s=47 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.

1a45b192d0bbaf167afb43a41859e313?s=128

Ju Liu

September 26, 2014
Tweet

Transcript

  1. THE DESIGN OF EVERYDAY RUBY

  2. ! Hi! I'm Ju, I really love Ruby, I work

    at AlphaSights, You can find me as @arkh4m.
  3. THE DESIGN OF EVERYDAY THINGS

  4. CATALOGUE OF UNFINDABLE OBJECTS

  5. COFFEEPOT FOR MASOCHISTS

  6. KNIFE FOR PEARS

  7. KANGAROO RIFLE

  8. SNOW BYCICLE

  9. ALL IN ONE

  10. THE DESIGN PSYCHOLOGY OF EVERYDAY THINGS

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

  12. None
  13. PLATE FOR PUSHING, HANDLE FOR PULLING.

  14. None
  15. HANDLE FOR PUSHING? OK...

  16. None
  17. WAT

  18. MENTAL MODELS

  19. THE DESIGNER

  20. The designer is given a problem. He thinks of a

    solution, a conceptual model. Then he implements this model and creates an object.
  21. THE USER

  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.
  23. DESIGNER MODEL != USER MODEL

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

    WORKS
  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
  26. A device which speaks clearly makes the difference between a

    great user experience and a horrible one.
  27. None
  28. None
  29. WHAT DOES THIS HAVE TO DO WITH ME?

  30. I'm not a designer.

  31. FALSE

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

  33. WRITING SOFTWARE IS AN ACT OF COMMUNICATION

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

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

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

  37. DESIGN PRINCIPLES APPLIED TO SOFTWARE WRITING

  38. VISIBILITY AFFORDANCES CONSTRAINTS NATURAL MAPPINGS

  39. VISIBILITY

  40. A device’s functions should be visible.

  41. Less frequently used functions should be hidden to reduce the

    apparent complexity of the device.
  42. At all times, it should be evident to the user

    what actions are available and how to perform them.
  43. AFFORDANCES

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

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

  46. LET'S SEE SOME CODE!

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

  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
  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
  50. USE PUBLIC METHODS TO MAKE THE API VISIBLE

  51. USE PRIVATE METHODS TO HIDE IMPLEMENTATION

  52. CONSTRAINTS

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

    of alternatives.
  54. The best way of making something easy to use is

    to restrict the possible choices.
  55. Make it impossible to do wrong.

  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
  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
  58. NATURAL MAPPINGS

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

    standards.
  60. A natural mapping leads to a clear conceptual model and

    is much easier to understand.
  61. Any design that requires labels, diagrams or instructions lacks a

    good natural mapping.
  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
  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
  64. COMMENTS SHOULD BE YOUR VERY LAST RESORT

  65. TRY TO WRITE TRUTHFUL METHODS NAMES INSTEAD

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

  67. CLARITY

  68. I concentrate on one component: making things that are understandable

    and usable. — Don Norman
  69. CONCENTRATE ON ONE COMPONENT

  70. WRITE SOFTWARE THAT IS UNDERSTANDABLE AND USABLE

  71. None
  72. Where are we going as an industry?

  73. THE HOLY GRAIL

  74. If you want to be a pro, all you have

    to do is just follow these set of rules...
  75. LOD LAW OF DEMETER

  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
  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
  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
  79. TDD TEST DRIVEN DEVELOPMENT

  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
  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
  82. class Author < ActiveRecord::Base has_many :posts end class AuthorPresenter <

    BasePresenter def posts_previews posts.map(&:preview) end end
  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
  84. class Author < ActiveRecord::Base has_many :posts end class AuthorPresenter <

    BasePresenter def posts_previews posts.published.latest_first.map(&:preview) end end
  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
  86. DRY DON'T REPEAT YOURSELF

  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
  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
  89. require 'ostruct' class Person < OpenStruct def can_drink? age >=

    21 end def age Date.today.year - birthday.year end end
  90. SOLID SRP, OPEN-CLOSED, LISKOV, IS, DIP

  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
  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
  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
  94. class EmployeesRepository def new_employee(*args) Biz::Employee.new(Employee.new(*args)) end def save_employee(employee) employee.save end

    end
  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
  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
  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
  98. None
  99. Following all the rules all the time...

  100. None
  101. THERE ARE NO RULES WITHOUT DRAWBACKS

  102. THINK ABOUT THE TRADEOFFS

  103. THINK ABOUT CLARITY

  104. INSTEAD OF SOLID, TRY

  105. AULIC

  106. AVOID

  107. USELESS

  108. LEVELS OF

  109. INDIRECTION

  110. COURAGEOUSLY

  111. AVOID USELESS LEVELS OF INDIRECTION COURAGEOUSLY

  112. THANKS, DISCUSSION TIME! ⚡ ALPHASIGHTS.COM/RUBY