Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save therod/70b943640a2caa9580459470f02ea5f2 to your computer and use it in GitHub Desktop.
Save therod/70b943640a2caa9580459470f02ea5f2 to your computer and use it in GitHub Desktop.

Smalltalk Best Practice Patterns in Ruby

1 – INTRODUCTION

  • We aren’t always good at guessing where responsibilities should go. Coding is where our design guesses are tested. Being prepared to be flexible about making design changes during coding results in programs that get better and better over time.

  • If you’re programming along, doing nicely, and all of a sudden your program gets balky, makes things hard for you, it’s talking. It’s telling you there is something important missing.

  • Some of the biggest improvements come from figuring out how to eliminate:

    • Duplicate code (even little bits of it)
    • Conditional logic
    • Complex methods
    • Structural code (where one object treats another as a data structure)
  • When you program, you have to think about how someone will read your code, not just how a computer will interpret it.

Style

  • In a program written with good style, everything is said once and only once.

  • Lots of little pieces - Good code invariably has small methods and small objects. When you are doing this, you must always be certain that you communicate the big picture effectively.

  • Replacing Objects: When you can extend a system solely by adding new objects without modifying any existing objects, then you have a system that is flexible and cheap to maintain.

  • Moving Objects - Ship a couple of systems first. Then if you have a system built with lots of little pieces, you will be able to make the necessary modifications and generalizations fairly easy.

2 – PATTERNS

  • Mature engineering disciplines capitalize on the repetitive nature of development by collecting handbooks of adequate solutions to recurring problems. A bridge designer designs using the solutions in the handbook, like I-beams, not the laws of physics.

  • The next bottleneck in software engineering is human communication.

  • Patterns are a literary form for capturing and transmitting common practice.

3 – BEHAVIOR

  • Object model the world through behavior and state. Behavior is the dynamic, active, computational part of the model. Stat is what is left after behavior is done, how the model is represented before, after and during a computation.

  • Methods must do the work they are supposed to do but they must also communicate the intent of the work to be done.

  • How your program is broken into methods (as well as how big those methods are) is one of the most important decisions you will make as you refine your code so that it communicates as clearly as possible.

  • The opportunity to communicate through intention revealing message names is the most compelling reason to keep methods small.

  • Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. This will naturally result in programs with many small methods, each a few lines long.

  • Any time you are sending two or more messages from one object to another in a single method, you may be able to create a Composed Method in the receiver that combines those messages.

  • “What does it take to create an instance?” As a class provider, you’d like the answer to this question to be as simple as possible.

3.1 COMPOSED METHOD

Q: How do you divide a program into methods?

A: Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. This will naturally result in program swith many small methods, each a few lines long.

  • The opportunity to communicate through intention revealing message names is the most compelling reason to keep methods small.

  • Any time you are sending two or more messages from one object to another in a single method, you may be able to create a Composed Method in the receiver that combines those messages.

class Controller
  def control_activity
    control_initialize
    control_loop
    control_terminate
  end
end

Source: https://github.com/avdi/sbpprb/blob/master/01_composing_method.rb

3.2 CONSTRUCTOR METHOD

Q: How do you represent instance creation?

A: Provide methods that create well-formed instances. Pass all required parameters to them.

  • “What does it take to create an instance?” As a class provider, you’d like the answer to this question to be as simple as possible.
class Point
  attr_accessor :x, :y

  # Called as Point.new_polar(radius, theta)
  #
  # Ruby doesn't really have the syntax for:
  #
  #   Point r: a theta: b
  #
  def self.new_polar(radius, theta)
    allocate.tap do |p|
      p.x = radius * Math.cos(theta)
      p.y = radius * Math.sin(theta)
    end
  end
end

# From dbrock:
# Except you could actually do this:

def Point(options)
  Point.new_polar(options[:r], options[:theta])
end

# And then with Ruby 1.9 you could call it like this:

#
#  Point r: a, theta: b
#
# :DDDDD

Source: https://github.com/avdi/sbpprb/blob/master/02_constructor_method.rb

3.3 CONSTRUCTOR PARAMETER METHOD

Q: How do you set instance variables from the parameters to a Constructor Method? A: Code a single method that sets all the variables. Preface its name with “set” then the names of the variables

# First example:

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    self.x = x
    self.y = y
  end
end

Source: https://github.com/avdi/sbpprb/blob/master/03_constructor_parameter_method.rb

3.4 SHORTCUT CONSTRUCTOR METHOD

Q: What is the external interface for creating a new object when a constructor method is too wordY?

A: Represent object creation as a message to one of the arguments to the Constructor Method. Add no more than three of these Shortcut Constructor Methods per system you develop.

# This pattern is common in ActiveSupport, e.g.
#
#   2.days.ago
#

class Point
  attr_accessor :x, :y
end

class Integer
  def by(y)
    Point.new.tap do |p|
      p.x = self
      p.y = y
    end
  end
end

4.by(5) # => #<Point:0x90eb420 @x=4, @y=5>

Source: https://github.com/avdi/sbpprb/blob/master/04_shortcut_constructor_method.rb

3.5 CONVERSION

Q: How do you convert information from one object’s format to another’s?

A: Convert from one object to another rather than overwhelm any one object’s protocol.

3.6 CONVERTER METHOD

Q: How do you represent simple conversion of an object to another object with the same protocol but different format?

A: Provide a method in the object to be converted that converts to the new object. Name the method by prepending “as” ot the class of the object returned.

  • Put Converter Methods in a method protocol called “private”
# Idiomatic Ruby uses "to_" instead of "as"
require 'set'
[1,2,1,3].to_set                # => #<Set: {1, 2, 3}>
23.to_f                         # => 23.0

Source: https://github.com/avdi/sbpprb/blob/master/06_converter_method.rb

3.7 CONVERTER CONSTRUCTOR METHOD

Q: How do you represent the conversion of an object to another with different protocol?

A: Make a Constructor Method that takes the object to be converted as an argument.

# Staying close to the example we get something like this:
class Date
  def self.from_string(string)
    # ...
  end
end

# But the built-in Date.parse() is basically the same thing:
require 'date'
Date.parse('2011-09-22')

# There is another Ruby idiom which may also qualify as embodying this
# pattern:
require 'pathname'
String(23)         # => "23"
Array(23)          # => [23]
Pathname(__FILE__) # => #<Pathname:->

Source: https://github.com/avdi/sbpprb/blob/master/07_converter_constructor_method.rb

3.8 QUERY METHOD

Q: How do you represent testing a property of an object?

A: Provie a method that returns a Boolean. Name it by prefacing the property name with a form of “be” -is, was, will, etc.

# Query Method
# Ruby has a syntactical edge on this one: there is a clear and
# well-known idiom for naming predicate methods.

class Switch
  def on?
    # ...
  end
end

Source: https://github.com/avdi/sbpprb/blob/master/08_query_method.rb

3.9 COMPARING METHOD

Q: How do you order objects with respect to each other?

A: Implement “<=“ to return true if the receiver should be ordered before the argument

# Comparing Method
class Event
  include Comparable
  def <=>(other)
    timestamp <=> other.timestamp
  end
end

Source: https://github.com/avdi/sbpprb/blob/master/09_comparing_method.rb

3.10 REVERSING METHOD

Q: How do you code a smooth flow of messages?

A: Code a method on the parameter. Derive its name from the original message. Take the original receiver as a parameter to the new method. Implement the method by sending the original message to the original receiver.

# However, for the specific case of handling output, Ruby has a
# solution which is concise and functionally equivalent to the
# Smalltalk example:

class Point
  def print_on(io)
    io << x << ' @ ' << y
  end
end

puts # blank line
p.print_on($stdout)

Source: https://github.com/avdi/sbpprb/blob/master/10_reversing_method.rb

3.11 METHOD OBJECT

  • Q: How do you code a method where many lines of code share many arguments and temporary variables?

  • A: Create a class named after the method. Give it an instance variable for the receiver of the original method, each argument, and each temporary variable. Give it a Constructor MEthod that takes the original receiver and the method arguments. Give it one instance method, #compute, implemented by copying the body of the original method. Replace the method with one that creates an instance of the new class and sends it #compute.

Source: https://github.com/avdi/sbpprb/blob/master/11_method_object.rb

3.12 EXECUTE AROUND METHOD

  • Q: How do you represent pairs of actions that have to be taken together?

  • A: Code a method that takes a Block as an argument. Name the method by appending “During: aBlock” to the name of the first method that needs to be invoked. In the body of the Execute Around Method, invoke the first method, evaluate the block, then invoke the second method.

# Cursor example:
class Cursor
  def show_while
    old = Cursor.current_cursor
    show
    yield
    old.show
  end
end

Cursor.new.show_while do { |cursor| # Code here }

Source: https://github.com/avdi/sbpprb/blob/master/12_execute_around_method.rb

3.13 DEBUG PRINTING METHOD

  • Q: How do you code the default printing method?

  • A: Override printOn: to provide information about an object’s structure to the programmer.

# For better or for worse, though, telling objects how to dump
# themselves to a stream isn't really Rubyish. We tend to tell objects
# how to stringify themselves instead.

class Association
  attr_accessor :key, :value

  def inspect
    "#{key}->#{value}"
  end
end

Source: https://github.com/avdi/sbpprb/blob/master/13_debug_printing_method.rb

3.14 METHOD COMMENT

  • Q: How do you comment methods?

  • A: Communicate important information that is not obvious from the code in a comment at the beginning of the method.

Source: https://github.com/avdi/sbpprb/blob/master/14_method_comment.rb

3.15 MESSAGE

  • Q: How do you invoke computation?

  • A: Send a named message and let the receiving object decide what to do with it.

NOTE: “Tell don’t ask”

Reference: [https://robots.thoughtbot.com/tell-dont-ask]

3.16 CHOOSING MESSAGE

  • Q: How do you execute one of several alternatives?

  • A: Send a message to one of several different kinds of objects, each of which executes one alternative.

Note: When you begin a program, you won’t be able to anticipate the variations. As your program matures, you will see explicit conditional logic creep in. When you can see the same logic repeated in several places, it is time to find a way to represent the alternatives as objects and invoke them with a choosing message.

Source https://github.com/avdi/sbpprb/blob/master/16_choosing_message.rb

3.17 DECOMPOSING MESSAGE

  • Q: How do you invoke parts of a computation?

  • A: Send several messages to “self”

# Decomposing Message

class Controller
  def control_activity
    control_initialize
    control_loop
    control_terminate
  end
end

Source https://github.com/avdi/sbpprb/blob/master/17_decomposing_message.rb

3.18 INTENTION REVEALING MESSAGE

  • Q: How do you communicate your intent when the implementation is simple?

  • A: Send a message to “self”. Name the message so it communicates what is to be done rather than how it is to be done. Code a simple method for the message.

Note: Intention Revealing Messages are the most extreme case of writing for readers instead of the computer.. ..The one that separates intention (what you want done) from implementation (how it is done) communicates better to a person.

Source: https://github.com/avdi/sbpprb/blob/master/18_intention_revealing_message.rb

3.19 INTENTION REVEALING SELECTOR

  • Q: What do you name a method?

  • A: Name methods after what they accomplish and not what it does

Source: https://github.com/avdi/sbpprb/blob/master/19_intention_revealing_selector.rb

3.20 DISPATCHED INTERPRETATION

  • Q: How can two objects cooperate when one wishes to conceal its representation?

  • A: Have the client send a message to the encoded object. Pass a parameter to which the encoded object will send decoded messages.

Trivia: Back in the days when data was separated form computation, and seldom the wain should meet, encoding decisions were critical. Any encoding decision you made was propagated to may different parts of the computation. If you got the encoding wrong, the cost of change was enormous. The longer it took to find the mistake, the more ridiculous the bill.

Source: https://github.com/avdi/sbpprb/blob/master/20_dispatched_interpretation.rb

3.21 DOUBLE DISPATCH

  • Q: How can you code a computation that has many cases, the cross product of two families of classes?

  • A: Send a message to the argument. Append the class name of the receiver to the selector. Pass the receiver as an argument.

3.22 MEDIATING PROTOCOL

  • Q: How do you code the interaction between two objects that need to remain independent?

  • A: Refine the protocol between the objects so the words used are consistent.

“Sometimes, you will not have consistent opposites in the messages, as in #show being the opposite of #makeInvisible. Sometimes, you will not have consistently made selectors plural, as in #addEmployees:being the opposite of #removeAllEmployees

3.23 SUPER

  • Q: How can you invoke superclass behavior?
  • A: Invoke code in a superclass explicitly by sending a message to “super” instead of “self”. The method corresponding to the message will be found in the superclass of the class implementing the sending method.

“What if the subclass method wants some aspect of the superclass method? Good style boils down to one rule: say things once and only once. If the subclass method were to contain a copy of the code from the superclass method, the result would no longer be easy to maintain. We would have to remember to update both or (potentially) many copies at once.”

3.24 EXTENDING SUPER

  • Q: How do you add to a superclass implementation of a method?

  • A: Override the method and send a message to “super” in the overriding method.

3.25 MODIFYING SUPER

  • Q: How do you change part of the behavior of a superclass’ method without modifying it?

  • A: Override the method and invoke “super”, then execute the code to modify the results.

class SuperFigure
  def initialize
    @color = “white”
    @size = 0.0
  end
end

class SubFigure << SuperFigure
  def initialize
    super
    @color = “beige”
  end
end

3.26 DELEGATION

Q: How does an object share implementation without inheritance?

A: Pass part of its work on to another object.

“For example, since many objects need to display, all objects in the system delegate to a brush-like object for the display. That way, all the detailed display code can be concentrated in a single class and the rest of the system can have a simplified view of displaying.

3.27 SIMPLE DELEGATION (???)

Q: How do you invoke a disinterested delegate?

A: Delegate messages unchanged

https://blog.lelonek.me/how-to-delegate-methods-in-ruby-a7a71b077d99#.w1vo1janv

3.28 SELF DELEGATION

Q: How do you implement delegation to an object that needs reference to the delegating object?

A: Pass along the delegating object (i.e. ‘self’) in an additional parameter called ‘for’:

class Post
  belongs_to :user

  def name
    # let's use try to bypass nil-check
    user.try(:name)
  end
end

class Post
  belongs_to :user
  delegate :name, :to => :user, :allow_nil => true
end

3.29 PLUGGABLE BEHAVIOR

Q: How do you parameterize the behavior of an object?

A: Issues you need to consider:

* How much flexibility do you need?
* How many methods will need to vary dynamically?
* How hard is it to follow the code?
* Will clients need to specify the behavior to be plugged, or can it be hidden within the plugged object?
* Add a variable that will be used to trigger different behavior.

3.30 PLUGGABLE SELECTOR

Q: How do you code simple instance specific behavior?

A: Add a variable that contains a selector to be performed. Append ‘Message’ to the Role Suggesting Instance Variable Name. Create a Composed Method that simply performs the selector.

SleepTimer = Struct.new(:minutes, :notifier, :notify_message) do
  def start
    sleep minutes * 60
    notifier.public_send(notify_message, 'Tea is ready!')
  end
end

timer = SleepTimer.new(0.1, $stdout, :puts)
timer.start

<Ruby Tapas Episode 19>

3.31 PLUGGABLE BLOCK

Q: How do you code complex Pluggable Behavior that is not quite worth its own class?

A: Add an instance variable to store a Block. Append ‘Block’ to the Role Suggesting Instance Variable Name. Create a Composed Method to evaluate the Block to invoke the Pluggable Behavior.

3.32 COLLECTING PARAMETER

Q: How do you return a collection that is the collaborative result of several methods?

A: Add a parameter that collects their results to all of the submethods

https://github.com/ruben/smalltalk-best-practice-patterns/blob/master/3-behaviour/2-messages/13-collecting_parameter.rb

4 – STATE

  • Most state-related decisions have more to do with modeling and less with coding, so the patterns here don't tell anything like the whole story. However the tactical decisions you make about representation will have an important impact on how well your code communicates with others.

  • This section only talks about two kinds of state: instance variables and temporary variables.

Instance Variables

4.1 COMMON STATE

  • How do you represent state, different values for which will exist in all instances of a class?

  • In spite of their conceptual and mathematical elegance, functional programming languages never caught on for commercial software. They problem is that programmers think and model in terms of state. State is a pretty darn good way to think about the world.

  • Instance variables have a very important communicative role to play. They say once and for all and out in the open, "Here's what I'm modeling with this object".

class CartesianPoint
  def initialize(x, y)
    @x, @y = x, y
  end
end

source https://github.com/ruben/smalltalk-best-practice-patterns/blob/master/3-behaviour/3-state/1-common_state.rb

  • Be sure to declare instance variables in order of importance in the class definition.

4.2 VARIABLE STATE

Q: How do you represent state whose presence varies from instance to instance?

A: Put variables that only some instances will have in a Dictionary stored in an instance variable called 'properties'. Implement 'propertyAt: aSymbol' and 'propertyAt: aSymbol put:anObject' to access properties.

4.3 EXPLICIT INITALIZATION

Q: How do you initialize instance variables to their default value?

A: Implement a method 'initialize' that sets all the values explicitly. Override the class message 'new' to invoke it on new instances.

class Timer
  # Default value method
  def default_millisecond_period
    1000
  end

  def initialize
    @period = default_millisecond_period
    @count = 0
  end
end

4.4 LAZY INITIALIZATION

Q: How do you initialize an instance variable to its default value?

A: Write a Getting Method for the variable. Initialize it if necessary with a default Value Method.

Note: Some people sear by Lazy Initialization. They go so far as to say that all initialization should be done this way. I actually lean towards Explicit Initialization. Too many times, I've bent over backwards to supply lots of flexibility that is never used. I'd rather do it the simple way first, then fix it if it's a problem. However, if I know I am writing code that is likely to be subclassed by others, I'll go ahead and use Lazy Initialization from the first.

# If we implement the Smalltalk example 1:1
class Timer
  # Default value method
  def default_count
    0
  end

  def default_period
    1000
  end

  def count
    @count = default_count if @count.nil?
    @count
  end

  def period
    @period.tap do = default_period if @period.nil?
    @period
  end
end

# Of course we can use Keyword arguments in Ruby 2.0
class Timer
  attr_accessor :count, :period

  def initialize(count: 0, period: 1000)
    @count = count
    @period = period
  end
end

More https://robots.thoughtbot.com/ruby-2-keyword-arguments

4.5 DEFAULT VALUE METHOD

Q: How do you represent the default value of a variable?

A: Create a method that returns the value. Preprend 'default' to the name of the variable as the name of the method.

class Book
  def default_synopsis
    ""
  end
end

4.6 CONSTANT METHOD

Q: How do you code a constant?

A: Create a method that returns the constant.

Note: I would rather limit the visibility of constants to a single class. Then, that class can provide meaningful behavior to other objects. If you can refactor your code so that only a single class cares about a constant, you can then represent that constant as a method.

class ListPane
  def single_select
    self.selection_policy = 15
  end
end

# If other methods need to know that 15 is the magic constant for single selection
# we create a method for it

class ListPanel
  def single_select_policy
    15
  end

  def single_select
    self.selection_policy = single_select_policy
  end
end

4.7 DIRECT VARIABLE ACCESS

Q: How do you get and set an instance variable's value?

A: Access and set the variable directly

Note: The simple, readable way to get and set the value of instance variables is to use the variables directly in all the methods that need their values. The alternative requires that you send a message every time you need to use or change an instance variable's value.

Note: When you write classes that only have a handful of methods, adding a getting and a setting method can easily double the number of methods in your class - twice as many methods to buy you flexibility than you may never use.

class Point
  def set_coordinates(x, y)
    @x, @y = x, get_y
  end

  def x
    @x
  end

  def y
    @y
  end
end

# vs

class Point
  attr_accessor :x, :y

  def set_coordinates(x, y)
    @x, @y = x, y
  end
end

4.8 INDIRECT VARIABLE ACCESS

Q: How do you get and set an instance variable's value?

A: Access and set its value only through a Getting Method and Setting Method.

# Defining attr_accessor should be enought in Ruby

class Point
  attr_accessor :x, :y
end

4.9 GETTING METHOD

Q: how do you provide access to an instance variable?

A: Provide a method that returns the value of the variable. Give it the same name as the variable.

# Using Ruby attr_reader again
class Book
  private
  attr_reader :title, :author
end

# Only make it public if strictly needed
class Book
  attr_reader :title, :author
end

4.10 SETTING METHOD

Q: How do you change the value of an instance variable?

A: Provide a method with the same name as the variable. Have it take a single parameter, the value to be set.

# Using Ruby's attr_writer now
class Book
  attr_writer :title, :author
end

4.11 COLLECTION ACCESSOR METHOD

Q: How do you provide access to an instance variable that holds a collection?

A: Provide methods that are implemented with Delegation to the collection. To name the methods, add the name of the collection to the collection messages.

class Department
  def total_salary
    if @total_salary.nil?
      @total_salary = compute_total_salary
    end
    @total_salary
  end

  def compute_total_salary
     @employees.inject { |sum, n| sum + n }
  end

  def clear_total_salary
    @total_salary = nil
  end

  def add_employee employee
    clear_total_salary
    @employees << employee
  end

  def remove_employee employee
    clear_total_salary
    @employees.delete(employee)
  end

  def employs employee
    @employees.include? employee
  end
end

4.12 ENUMERATION METHOD

Q: How do you provide safe, general access to collection elements?

A: Implement a method that executes a Block for each element of the collection. Name the method by concatenating the name of the collection and 'Do:..'

class Department
  def each_employee &block
    employees.each do |employee|
      yield employee
    end
  end
end

# Getting all employees from a company
class Company
  def all_employees
    result = Array.new
    all_departments.each do |department|
    department.each_employee |employee|
        result << employee
    end
    result
  end
end

# If a department can contain not only employees but other departments (Composite modeling pattern)
# This way employees contains employees and departments.

class Department
  def each_employee &block
    employees.each_employee do |employee|
      employee.each_employee &block
    end
  end
end

class Employee
   def each_employee &block
     yield self
   end
end

4.13 BOOLEAN PROPERTY SETTING METHOD

Q: How do you set a boolean property?

A: Create two methods beginning with 'be'. One has property name, the other the negation. Add 'toggle' if the client doesn't want to know about the current state.

# Ruby uses the ! notion instead of the be keyword


class ListPane
  attr_accessor :visible

  def initialize
    @visible = true
  end

  def show!
    @visible = true
  end

  def hide!
    @visible = false
  end

  def toggle_visibility!
    @visible = !@visible
  end
end

4.14 ROLE SUGGESTING INSTANCE VARIABLE NAME

Q: What do you name an instance variable?

A: Name instance variables for the role they play in the computation. Make the name plural if the variable will hold a Collection.

Note: Typically, when you read code you have a purpose in mind. If you understand the role of a variable and it is unrelated to your purpose, you can quickly skim over irrelevant code that uses that variable. Likewise, if you see a variable that is related to your purpose, you can quickly narrow your reading to relevant code by looking for that variable.

If the variable in Point were called 't1' and 't2' instead of 'x' and 'y' you'd have a lot of reading to do before you could tell which was the horizontal component and which the vertical. Naming the variables by their roles gives you that information directly.

4.15 TEMPORARY VARIABLE

Q: How do you save the value of an expression for later use within a method?

A: Create a variable whose scope and extent is a single method. Declare just below the method selector. Assign it as soon as the expression is valid.

# This method is more easily read...
class Rectangle
  def bottom_rigth
    right = left + width
    bottom = top + height
    Point.new(right, bottom)
  end
end

# ...than this one
class Rectangle
  def bottom_rigth
    Point.new(left + width, top + height)
  end
end

4.16 COLLECTING TEMPORARY VARIABLE

Q: How do you gradually collect values to be used later in a method?

A: When you need to collect or merge objects over a complex enumeration, use a temporary variable to hold the collection or merged value.

# Often, the right set of enumeration protocol eliminates the need of temporary variables.
# Code like
class Klass
  def size
    sum = 0
    children.each do |child|
      sum += child.size
    end
    sum
  end
end

# Can be rewritten as
class Klass
  def size
    children.inject { |sum, child| sum += child.size }
  end
end

# Merges two collections together so that you have one element from a and one element from b, etc.
class Klass
  def merge_collections left, right
    result = Array.new
    left.zip(right).each do |l, r|
      result << l
      result << r
    end
   result
  end
end

4.17 CACHING TEMPORARY VARIABLE

Q: How do you improve the performance of a method?

A: Set a temporary variable to the value of the expression as soon as it is valid. Use the variable instead of the expression in the remainder of the method.

# If calculating the bounds is expensive, you can transform...
class Klass
  def bounds
    # Expensive calculation
  end

  def children_calculation
     children.each do |child|
       child.calculation * self.bounds
     end
  end
end

# ...in
class Klass
  def bounds
    # Expensive calculation
  end

  def children_calculation
    bounds = self.bounds
    children.each do |child|
      child.calculation * bounds
    end
  end
end

4.18 EXPLAINING TEMPORARY VARIABLE

Q: How do you simplify a complex expression within a method?

A: Take a subexpression out of the complex expression. Assign its value to a temporary variable before the complex expression. Use the variable instead in the complex expression.

class LinearHashTable
  def find_index element, client
    last_index = self.size - 1 # In Smalltalk self.size
    # ... Rest of the method
  end
end

4.19 REUSING TEMPORARY VARIABLE

Q: How do you use an expression several places in a method when its value may change?

A: Execute the expression once and set a temporary variable. Use the variable instead of the expression in the remainder of the method.

# If you match the value returned from a stream against a list of keywords...
class Klass
  def stream
  end

  # The evaluation of stream.gets causes the stream to change, so this is not likely what you mean...
  def read_from_stream
    if stream.gets == a
    end
    if stream.gets == b
    end
    if stream.gets == c
    end
  end
end

# Instead...
class Klass
  def stream
  end

  def read_from_stream
    token = stream.gets
    if token == a
    end
    if token == b
    end
    if token == c
    end
  end
end

4.20 ROLE SUGGESTING TEMPORARY VARIABLE NAME

Q: What do you call a temporary variable?

A: Name a temporary variable for the role it plays in the computation.

Note: Use variable naming as an opportunity to communicate valuable tactical information for future readers.

# This always helps me when doing Ruby loops

collection.each do |x|
  puts x.name
end

# In the above case we have no idea what the collection contains.

collection.each do |user|
  puts user.name
end

# This example is more clearer even though the 'collection' is still not concrete.

users.each do |user|
  puts user.name
end

5 – COLLECTIONS

Smalltalk          -> Ruby

FileStream         -> File
ReadWriteStream    -> IO (or other things that duck type like it)
Set                -> require 'set', then use the Set class
Dictionary         -> Hash
OrderedCollection  -> Array
SortedCollection      nothing similar (Array?)
Bag                   nothing similar (Hash?)
Interval           -> Range
Array                 Ruby has no fixed-length collection class.

5.1 - ORDERED COLLECTION

Q: How do you code Collections whose size can't be determined when they are created? A: Use an OrderedCollection as your default dynamically sized Collection.

Ruby has no OrderedColletion AFAIK

5.2 - RUN ARRAY

Q: How do you compactly code an OrderedCollection or Array where you have the same element many times in a row?

A: Use a RunArray to compress long runs of the same element.

Ruby does not implement RunArray AFAIK

5.3 - SET

Q: How do you code a Collection whose elements are unique?

A: Use a Set

# Suppose you have a collection of Accounts and want to send statements to all of the Owners.
# The naive code doesn't work right
# We cant get multiple copies of an owner.
class Statement
  def owners
    result = Array.new
    accounts.each do |account|
      result << account.owner
    end
    result
  end
end

# We can solve this checking if the owner was in the result before adding it
class Statement
  def owners
    result = Array.new
    accounts.each do |account|
      owner = account.owner
      result << owner unless result.include? owner
    end
    result
  end
end

# Set solves the problem by ignoring add if the object is already included.
class Statement
  def owners
    result = Set.new
    accounts.each do |account|
      result << account.owner
    end
    result
  end
end

5.4 - EQUALITY METHOD

Q: How do you code equlaity for new objects?

A: If you will be putting objects in a Set, using them as Dictionary keys, or otherwise using them with other objects that define equality, define a method called '='. Protect the implementation of '=' so only objects of compatible classes will be fully tested for equality.

# The default implementation of equality is identity
# Two objects are equal if they are the same object
class Object
  def ==(object)
    self == object
  end
end

# The most important reason for implementing equality
# is because you are going to put objects in a Collection
# and want to be able to test for inclusion, remove elements
# or eliminate duplicate in tests without having to have the
# same instance.

# Lets say we are working in a library program.
# Two books are equal if the author and title are equal
class Book
  def ==(book)
    self.title == book.title && self.author == book.author
  end
end

# A Library has an OrderedCollection of Books
class Library
  def has_author_and_title(author, title)
    book = Book.new(author, title)
    books.include? book
  end
end

# If the Library can contain different classes of objects
class Book
  def ==(book)
    return false unless book.class == self.class
    self.title == book.title && self.author == book.author
  end
end

5.5 - HASHING METHOD

Q: How do you ensure that new objects work correctly with hashed Collections?

A: If you override '=' and use the object with a hashed Collection, override 'hash' so that two objects that are equal return the same hash value.

# To define the hash we xor the two attributes hash values.
class Book
  def hash
    title.hash ^ author.hash
  end
end

5.6 - DICTIONARY

Q: How do you map one kind of object to another?

A: Use a Dictionary

5.7 - SORTED COLLECTION

Q: How do you sort a collection?

A: Use a SortedCollection. Set its sort block if you want to sort by some criteria other than "<=."

5.8 - ARRAY

Q: How do you code a collection with a fixed number of elements?

A: Use an Array. Create it with 'new:anInteger' so that it has space for the number of elements you know it needs

5.9 - BYTE ARRAY

OMITTED

5.10 - INTERVAL

Q: How do you code a collection of numbers in sequence?

A: Use an interval with start, stop and an optional step value. The Shortcut Constructor Methods Number>>to: and to:by build intervals for you.

# In Ruby you can use .times on integers

10.times do |iteration|
  puts "Currently at iteration #{iteration}"
end

5.11 - ISEMPTY

Q: How do you test if a collection is empty?

A: Send isEmpty to test whether a collection is empty (has no elements).

[].empty?

5.12 - INCLUDES

Q: How do you search for a particular element in a collection?

A: Send includes: and pass the object to be searched for.

"Test String".includes?("Test")

5.13 - CONCATENATION

Q: How do you put two collections together?

A: Concatenate two collections by sending ',' to the first with the second as an argument.

# In ruby you can just use this
[1, 3, 4] + [2, 3, 5] # => [1, 3, 4, 2, 3, 5]

# Or use the concat method on the first array
[1, 3, 4].concat [2, 3, 5] # => [1, 3, 4, 2, 3, 5]

5.14 - ENUMERATION / DO

Q: How do you execute code across a collection?

A: Use the enumeration message to spread a computation across a collection

['John', 'Frieda', 'Moritz', 'Hans'].each do |name|
  puts "Hello #{name}!"
end

5.15 - COLLECT

Q: How do you operate on the result of a message sent to each object in the collection?

A: Use collect: to create a new collection whose elements are the results of evaluating the block passed to collect: with each element of the original collection. Use the new collection.

arr = [1, 2, 3, 4]
arr.collect { |x| x * 2 } # => [2, 4, 6, 8]

5.16 - SELECT / REJECT

Q: How do you filter out part of a collection?

A: Use select: and reject: to return new collections containing only elements of interest. Enumerate the new collection. Both take a one argument Block that returns a Boolean. Select: gives you elements for which the Block returns true, reject: gives you elements for whcih the Block returns false.

# Ruby select
[1,2,3,4,5].select { |num|  num.even?  }   #=> [2, 4]

# Ruby reject
[1, 2, 3, 4, 5].reject { |num| num.even? } #=> [1, 3, 5]

5.17 - DETECT

Q: How do you search a collection?

A: Search a collection by sending it detect: The first element for which the block argument evaluates to true will be returned.

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