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. ! Hi! I'm Ju, I really love Ruby, I work

    at AlphaSights, You can find me as @arkh4m.
  2. WAT

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

    solution, a conceptual model. Then he implements this model and creates an object.
  4. 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.
  5. 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
  6. A device which speaks clearly makes the difference between a

    great user experience and a horrible one.
  7. At all times, it should be evident to the user

    what actions are available and how to perform them.
  8. 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
  9. 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
  10. The best way of making something easy to use is

    to restrict the possible choices.
  11. 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
  12. 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
  13. 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
  14. 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
  15. If you want to be a pro, all you have

    to do is just follow these set of rules...
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. class Author < ActiveRecord::Base has_many :posts end class AuthorPresenter <

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

    BasePresenter def posts_previews posts.published.latest_first.map(&:preview) end end
  24. 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
  25. 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
  26. 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
  27. require 'ostruct' class Person < OpenStruct def can_drink? age >=

    21 end def age Date.today.year - birthday.year end end
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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