Skip to content

Instantly share code, notes, and snippets.

@jamesyang124
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesyang124/03a9e374601758e3fcdc to your computer and use it in GitHub Desktop.
Save jamesyang124/03a9e374601758e3fcdc to your computer and use it in GitHub Desktop.
Review of the book - Confident Ruby

Review of Confident Ruby

Read API doc

  1. any? [{ |obj| block }] → true or false means could either accept 0 or 1 { |obj| block } block.

  2. send(symbol [, args...]) → obj means could either accept 0 or many argumetns. if symobol method accept block, then may also appen that block to it, e.g. define_method.

  3. Complex(x[, y]) → numeric means could either accept 0 or 1 y agrument.

  4. exec([env,] command... [,options]) means it amy specify the env or ignore it so env set by default.

  5. open(path [, mode [, perm]] [, opt]) → io or nil accepts either 0 or 1 mode, or opt argument. If mode specified, then perm may required. If 0 for mode, then rest arguments are as opt.

  6. spawn([env,] command... [,options]) → pid:

    pid = spawn({"FOO"=>"BAR"}, command, :unsetenv_others=>true) # FOO only

  7. [{block}] means could either apply 0 or 1 block.

  8. [{block}...] means could either apply 0 or many blocks.

  9. Internal or External Enumerator

Chp. 3

###Input collection

  1. Coerce objects into the roles we need them to play.

  2. Reject unexpected values which cannot play the needed roles.

  3. Substitute known-good objects for unacceptable inputs.

###Type of conversions

  1. Ruby doesn't call explicit conversions for core classes. But when we use string interpolation, Ruby implicitly uses #to_s ex: "String interpolation #{Time.now}".
Method Target Class Type Notes
#to_a Array Explicit
#to_ary Array Implicit
#to_c Complex Explicit
#to_enum Enumerator Explicit
#to_h Hash Explicit Introduced in Ruby2.0
#to_hash Hash Implicit
#to_i Integer Explicit
#to_int Integer Implicit
#to_io IO Implicit
#to_open IO Implicit Used by IO.open
#to_path String Implicit
#to_proc Proc Implicit
#to_r Rational Explicit
#to_regexp Regexp Implicit Used by Regexp.try_convert
#to_s String Explicit
#to_str String Implicit
#to_sym Symbol Implicit

3.2 Use built-in conversion protocols

def set_centrifuge_speed(new_rpm)
  new_rpm = new_rpm.to_int 
  puts "Adjusting {new_rpm} RPM"
end
  1. Here the implicit conversion call serves as an assertion, ensuring the method receives an argument which is either an Integer or an Object with a well-defined conversion to Integer.

3.4 Define your own conversion protocols

class Point
  attr_reader :x, :y, :name
  def initialize(x, y, name=nil) 
    @x, @y, @name = x, y, name
  end

  def to_coords 
    [x, y]
  end 
end

def method(point)
  point.respond_to?(:to_coords) ? point.to_coords : point.to_ary
end
  1. Provide your own conversion protocol :to_coords with respond_to? check.

3.6 Use built-in conversion functions

  1. Use Ruby's capitalized conversion functions, such as Integer() and Array() to ensure input must be an array-convertible data or raise error. Those methods such as Kernel#Array() has two advantages:

    1. They are idempotent. Calling it with an argument of the target type will simply return the unmodified argument.

    2. They can convert a wider array of input types than the equivalent #to_* methods.

items = ["a", 1, "b", 2, "c", 3]
Hash[*items]
# => {"a"=>1, "b"=>2, "c"=>3}
  1. Hash[] will produce a Hash where the first argument is a key, the second is a value, the third is a key, the fourth is its value, and so on.

3.8 Define conversion functions

Pair = Struct.new(:a, :b) do 
  def to_ary
    [a, b] 
  end
end
  1. The conversion function should be simple because we will call it a lot. And it should be idempotent.

  2. We may group the conversion functions to a module and set module_function for them:

module A
  def b; end
  module_function :b
end

class B
  include A
end

A.singleton_methods true
# => [:b]

B.new.private_methods.include? :b
# => true
  1. module_function set two things for the method. First, it marks all following methods as private. Second, it makes the methods available as singleton methods on the module.

3.9 Replace "string typing" with classes

class TrafficLight
  def signal
    case @state
    when "Stop" then turn_on_lamp(:red) 
    when "Caution" 
      turn_on_lamp(:yellow)
      ring_warning_bell
    when "proceed" then turn_on_lamp(:green) end
  end
end
# 1. Move String to object. Reuse it as Constant.

State = Struct.new(:name) do 
  def to_s
    name
  end
end

STOP = State.new("stop")
PROCEDE = State.new("procede")

VALIS_STATES = [STOP, PROCEDE]

class TrafficLight
  def change_to(state)
    raise ArgumentError unless VALID_STATES.include?(state)
    @state = state
  end

  def signal
    case @state
    when STOP then p @state # use#to_s to show current state.
    end
  end
end

# 2. Instead of instantiate object for each string, 
# inherit and provides method for it.

class State; end

class Stop < State
  def next_state; Procede.new; end
  def color; "red"; end
  def signal(light)
    light.turn_on_lamp(color.to_sym) 
  end
end

class Procede < State
  def next_state; Stop.new; end 
  def color; "green"; end
  def signal(traffic_light)
    traffic_light.turn_on_lamp(color.to_sym) 
  end
end

class TrafficLight
  def turn_on_lamp(color)
    # ...
  end
  
  def signal
    @state.signal(self)
  end
end
  1. When dealing with string (or Symbol) values coming from the "borders" of our code, it can be tempting to simply ensure that they are in the expected set of valid values, and leave it at that.

  2. However, representing this concept as a class or set of classes can not only make the code less error-prone; it can clarify our understanding of the problem, and improve the design of all the classes which deal with that concept.


3.11 Use transparent adapters to gradually introduce abstraction

class Benchmark
  def log 
    case @sink
    when Cinch::Bot 
      @sink.dispatch(:log_info, message)
    else
      @sink << message
    end
  end
end

# 1. Improvement by warp it to an adapter. Now common interface << be called.

class IrcBot
  def initialize(bot); 
    @bot = bot; 
  end
  
  def <<(message)
     @bot.dispatch(:log_info, message)
  end
end

class Benchmark
  def initialize(sink)
    @case = case sink
            when Cinch::Bot then IrcBot.new(sink)
            end
  end
  
  def log 
    # ...
    @sink << message
  end
end

# 2. Above IrcBot may not support other function from Cinch::Bot.
#    This may cause problem for other functionality we want to achive in target class.
#    So we instead add a transparent adapter for it - Use delegator.

require 'delegate'

class Benchmark
  class IrcBotSink < DelegateClass(Cinch::Bot)
    def initialize(bot); 
      @bot = bot; 
    end
    
    def <<(message)
      @bot.dispatch(:log_info, nil, message)
    end 
  end
  
  def initialize(sink) 
    @sink = case sink
    when Cinch::Bot then IrcBotSink.new(sink) 
    else sink
    end
  end 
end

3.12 Reject unworkable values with preconditions

  1. One of the purposes of a constructor is to establish an object's invariant: a set of properties which should always hold true for that object.

  2. Decisively rejecting unusable values at the entrance to our classes can make our code more robust, simplify the internals, and provide valuable documentation to other developers.

# Instead of directly build a instance variable @hire_date, 
# use hire_date setter method to filter out unworkable cases.
# This filter method makes constructor still clean, and seperate 
# the logic out from building variables and filtering unworkable 
# condition for that variable.

def initialize(hire_date)
  self.hire_date = hire_date
end

def hire_date=(new_hire_date)
  raise TypeError, "Invalid hire date" unless
  new_hire_date.is_a?(Date) 
  @hire_date = new_hire_date
end

3.13 Use #fetch to assert the presence of Hash keys

  1. Instead of use [] operator to check keys, the #fetch will raise KeyError rather than nil if key does not present and block does not provide. This distinguish two conditions which are key presented but value is nil or missing key so return nil.

  2. fetch(key [, default] ) → obj if default argument is given, then return default.

  3. fetch(key) {| key | block } → obj If the optional code block is specified, then that will be run and its result returned.

# add block to custom fetch behavior of missing key.
def add_user(attributes)
  login = attributes.fetch(:login) 
  password = attributes.fetch(:password) do
    raise KeyError, "Password (or false) must be supplied" end
  # ...
end

3.14 Use #fetch for defaults

  1. Because #fetch only executes and returns the value of the given block if the specified key is missing, not just falsey.
# if key does not present, then create logger object which accept STDOUT as output source.
logger = options.fetch(:logger) { Logger.new($stdout) } 

if logger == false
  logger = Logger.new('/dev/null') 
end

3.17 Represent special cases as objects

  1. Before we execute a method, we should early return if the input case found should be guarded.

  2. Besides directly early return, we can wrap that special case as an object and behave like normal case but support all method calls with properly return result. (consider a session may include guest user, valid user, or invalid input.)

  3. We can extend this approach for presenting do-nothing object as customed null object.

  4. Sometimes, the object has many properties, but that input object may missing values for the property and cause method crashed. In that case we can supply a benign value - a known - good object that stands in for a missing input.

  5. Compare 4. and 5., null object is for the semantically null behavior and benign values is for supplying a valid object which missing some unnecessary properties for the object.

# Mock all valid input's method but return with properly result.
# So all other functions related to session can focus on its core function
# instead of guarding special cases with many effort.

class GuestUser
  def initialize(session)
    @session = session 
  end

  def name 
    "Anonymous"
  end
  
  def authenticated?
    false
  end
  
  def has_role?(role) 
    false
  end
  
  def visible_listings 
    Listing.publicly_visible
  end

  def last_seen_online=(time) 
    # NOOP
  end

  def cart 
    SessionCart.new(@session)
  end 
end

def current_user
  if session[:user_id]
    User.find(session[:user_id]) 
  else
    # special case as object.
    GuestUser.new(session) 
  end
end

# if current_user
#   @listings = current_user.visible_listings
# else
#   @listings = Listing.publicly_visible 
# end

# Now it can reduce if/else conditions of following code to just one line.
@listings = current_user.visible_listings

3.20 Use symbols as placeholder objects

  1. There are many other ways to make communicate missing-but-necessary values more clearly.

  2. Symbol is a unique and reusable object in system. Instead of provide a string for missing-but-necessary values, symbol are more light-weight and easier to check.

  3. Set nil as default missing-but-necessary values usually cannot help much to identify the information of that missing field.


3.21 Bundle arguments into parameter objects

  1. Sometimes your method accepted various but similar parameters. Instead of checking those extra parameters, we can wrap the essential parameters as an object and mutate (through the inheritance/mixin/delegation for that class/module) it as we need.

  2. By this mean, it reduces conditional tests for parameters, and properly define the type of each similar input.

  3. In further, you may define specific implementation for same behaviour(method).

Point = Struct.new(:x, :y) do # ...
  # specific implementation
  def draw_on(map) 
    # ...
  end 
end

class Map
  def draw_point(point)
    point.draw_on(self) 
  end

  def draw_line(point1, point2) 
    # now the detail of implementation is based on input object.
    point1.draw_on(self) 
    point2.draw_on(self)
    # draw line connecting points...
  end
end

# 2 Use delegation to extend extra inputs.
class FuzzyPoint < SimpleDelegator 
  def initialize(point, fuzzy_radius)
    super(point)
    @fuzzy_radius = fuzzy_radius 
  end

  def draw_on(map)
    super # draw the point
    # draw a circle around the point...
  end

  def to_hash
    super.merge(fuzzy_radius: @fuzzy_radius)
  end 
end

map = Map.new
# extra input @fuzzy_radius has merged to parameters hash.
p1 = FuzzyPoint.new(StarredPoint.new(23, 32), 100) 
map.draw_point(p1)

3.22 Yield a parameter builder object

  1. If you define a general parameter object, then it must have some parameters are not necessary for intialization. But user may want to add the values for it. In that case, yield parameter object to let user provides a block and able to change values for those parameters.
# magnitude and name parameters are not necessary.
Point < Struct.new(:x, :y, :name, :magnitude) do 
  def initialize(x, y, name='', magnitude=5)
    super(x, y, name, magnitude) 
  end

  def magnitude=(magnitude)
    raise ArgumentError unless (1..20).include?(magnitude) 
    super(magnitude)
  end
  # ...
end

class Map
  def draw_point(point_or_x, y=:y_not_set_in_draw_point)
    point = point_or_x.is_a?(Integer) ? Point.new(point_or_x, y) : point_or_x
    yield(point) if block_given?
    point.draw_on(self) 
  end
  
  def draw_starred_point(x, y, &point_customization) 
    draw_point(StarredPoint.new(x, y), &point_customization)
  end
  # ...
end

# Now use can dynamically set the parameters of magnitude and name.
map.draw_point(7, 9) do |point| 
  point.magnitude = 3
end

map.draw_starred_point(18, 27) do |point|
  point.name = "home base" 
end
  1. Combine with delegation, we can delegate the object when the extra parameter's setter method being called.
require 'delegate'

# general paramtere object
class PointBuilder < SimpleDelegator
  def initialize(point)
    super(point)
  end
  
  # The setter also change the parameter object from Point to FuzzyPoint.
  def fuzzy_radius=(fuzzy_radius)
    # __setobj__ is how we replace the wrapped object in a # SimpleDelegator
    __setobj__(FuzzyPoint.new(point, fuzzy_radius));
  end
  
  def point
    __getobj__ 
  end
end


map.draw_starred_point(7, 9) do |point| 
  point.name = "gold buried here" 
  point.magnitude = 15 
  # change Point object to FuzzyPoint
  point.fuzzy_radius = 50
end

# The problem left now is: 
# we can not sure which class is the exactly parameter object.
# But it must mixed in those classes(FuzzyPoint, Point).

3.22 Receive policies instead of data

  1. Different clients of a method want a potential edge case to be handled in different ways. For instance, some callers of a method that deletes files may want to be notified when they try to delete a file that doesn't exist. Others may want to ignore missing files.

  2. Sometimes you need user-defined policy for your edge case of input. To achieve this, insert customed policy from input(ex: hash with proc as value) instead directly set default policy.

  3. If possible, data should be filtered by the policy before entering the method.

def delete_files(files, options={}) 
  # if cannot find user-defined policy, then return default policy.
  error_policy = options.fetch(:on_error) { ->(file, error) { raise error } } 
  symlink_policy = options.fetch(:on_symlink) { ->(file) { File.delete(file) } } 
  files.each do |file|
    begin
      if File.symlink?(file)
        symlink_policy.call(file) 
      else
        File.delete(file) 
      end
    rescue => error 
      error_policy.call(file, error)
    end 
  end
end

Chp 4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment