Skip to content

Instantly share code, notes, and snippets.

@jamesyang124
Last active March 7, 2017 05:55
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/d4dbc4f46ca870e2e52ac8c3193d7b12 to your computer and use it in GitHub Desktop.
Save jamesyang124/d4dbc4f46ca870e2e52ac8c3193d7b12 to your computer and use it in GitHub Desktop.
Review of Effective Ruby - http://www.effectiveruby.com/

Review of Effective Ruby

Item 2

All Objects Could Be nil

All objects are mutable by default. It can be nil, using nil?, is_a?, to_a, etc. to help the value validaiton for method input.

Item 3

Avoid Cryptic Perlisms

Avoid using Perl styled perlisms. Use String#match instead of String#=~ with $1 varialbe. The ~= special oeprator put the match result to special global variable $1 ... $n. Those variables are actually scoped locally to the current thread and method. So outside of the method, the $1 will be nil if not assigned in outer scope yet.

We can requie('English') to use descriptive aliases instead.

Item 4

Constants Are Mutable

To make it immutable, freeze will lock the modification for it. But if it reference an collection object such as array or hash, you need to freeze its elements and keys(if that is also an object) for the constraint of deep copy level.

module Defaults
  SOME_CONFIG = ["val1", "val2", "val3"]
end

# only freeze array object reference, this array cannot add/remove, but can alter element value
SOME_CONFIG.freeze

# also lock element value.
SOME_CONFIG.map(:&.freeze).freeze

The constant maybe reassign to a new value, by this mean, we cannot lock it by constant.freeze, we need to define an outer namespace through module and freeze it in where thet defined in.

module ConfigConstants
  SOME_CONFIG = ["val1", "val2", "val3"]
end

ConfigConstants.freeze
ConfigConstants::SOME_CONFIG.map(:&.freeze).freeze

ConfigConstants::SOME_CONFIG.freeze
# => 5
ConfigConstants::SOME_CONFIG = 9
# (irb):9: warning: already initialized constant ConfigConstants::SOME_CONFIG
# (irb):6: warning: previous definition of SOME_CONFIG was here
# => 9

ConfigConstants.freeze
# => ConfigConstants
ConfigConstants::SOME_CONFIG = 9
# RuntimeError: can't modify frozen Module

The Array and String as module or class are actually constants.

The varialbe assignment is not locked yet until its top level such as class or module is frozen as well. Then no modification can performed in that class or module. Similar idea to the Array#freeze which lock its modification to add/remove.

We can conculde that the freeze is only constraint the object on shallow copy level.

Item 7

Different Behaviour of super

super without explicitly paranthesis will act like syntax sugar:

class Bae < Parent
  def speak(word, accent)
    # actually call Parent.speak(word, accent) assume with same parameter list of subclass
    super
  end
end

If your want to call superclass method without parameters safely, use suepr().

For the mixin module and class, calling super will stop at first match which is the method locate in module. In this case, use composition instead of inheritance to avoid tightly coupling.

Be aware not to override default method_missing in inheritance chain, If super cannot find the method, then it will call method_missing in inheritance chain which might lost detail for NoMethodError.

Furthermore, if the overriden method_missing is not defined in same class definfition, then it will not with details where it actually occurred.

Item 15

Prefe Class Instance Variables to Class Variables

https://ruby-doc.org/core-2.2.0/Class.html#method-c-new

When a new class is created, an object of type Class is initialized and assigned to a global constant (Name in this case).

class Class
  alias old_new new
  def new(*args)
    print "Creating a new ", self.name, "\n"
    old_new(*args)
  end
end

class Name
end

n = Name.new
# Creating a new Name

new keyword actually create an anonymous class object from the superclass. So we can access that object's instance variable seperately.

Creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant.

Item 16

Duplicate Collections Passed as Arguments before Mutating Them

clone has 2 features while dup does not contain:

  1. clone will keep frozen state of the receiver, while dup is not.
  2. If the reveiver has singleton methods, clone will also duplicate its singleont class.

Both clone and dup are shallow copy, so as Java's clone. You need to manually init object memebers if you want deep copy.

Item 17

Use the Array method to convert nil and Scalar Object into Arrays

Don't pass hash to array methods, it will convert to a nested array.

Item 18

Consider Set for Efficient Element Inclusion Checking

Using Set for element inclusion checking. O(logn) [set] vs O(n) [array].

Item 20

Know How to Fold Collections with reduce

Consider using default hash value:

default_value = 43
hash = Hash.new(default_value)

# default value
hash[:not_exist]
# 43

But the default value will not populate nonexistent key:

# still empty, thogh has default value
hash.keys 
[]

Intead of simply setting default value, try create hash with initialization block, this will not share the deafult value anymore, each nonexistent keys's value are independently.

# default value, share to all nonexistent keys
hh = Hash.new([])
hh[:non_exist1] << 1
# [1]

hh[:non_exist2] << 1
# [1, 1]

hh[:non_exist3]
# [1, 1]

# use init block
h = Hash.new do |k, v|
	[]
end

h[:weekendays]
# []

Item 21

Prefer Delegation over Inheritance from Collection Classes

require 'forwardable'

class SomeHash
	extend(Forwardable)
    
    # foward instance method to it
    def_delegators(:@hash, :[], :[]=, :empty?)
    
    # forward singleton method erase! to hash instance method
    def_delegator(:@hash, :delete, :erase!)
    
    # default_proc is the init block given to Hash.new
    def replace!(hash)
    	hash.default_proc = @hash.default_proc
        @hash = hash
    end
end

If you want to set SomeHash as frozen, then you need to freeze @hash as well, it could either override freeze method, or use taint to the same way. untaint is the contrary to taint method.

Item 22

Prefer Custom Exceptions to Raising Strings

Prefer using custom exception message.

# simply return self exception without do anything
raise CustomException

# with single argument return as message
raise CustomException, "error message"

Item 24

Manage Resouces with Blocks and ensure

begin create a new scope for exceptions, use ensure to release the resource. The scope of ensure keep in the same as begin body. Variables defined in begin will also available to ensure block. That is why File.open usually suggest use default block to make sure its IO is ensured to closed.

Item 25

Exit ensure Clauses by Flowing Off the End

Make sure to put ensure at the end, instead of just rescue, basically rescue will discard any exceptions which leads the error untrackable. So below pattern is often to use:

def explicit

	return 'horses'
rescue SomeError => e
	# recover from this type of exception.
	return 'ponies'
ensure
	# resource releasing.
    return result
end

Note that return output will alwasy be the result from ensure block, not horses.

Item 27

Prefer throw to raise for Jumping Out of Scope

Use catch...throw instead of raise.

In below example, throw is used in catch block, it will terminate that block and return its given value [another_result]:

match = catch(:jump) do 
	if not_right
    	# jump back to where that label locate.
    	throw(:jump, [another_result])
    end
end

Item 28

Familiarize Yourself with Module and Class Hooks

Extend a class trigger self.extended hook method. This is happened after all the definitions(all extend modules) in a class are loaded.

module ExtendHook
	# expect first arg as class or other mod, 
    # depends on how extend keyword received its first argument.
	def self.extended(klass)
    	require("other-modules")
        klass.extend(Forwardable)
    end
end

module UseCase
	extend ExtendHook
end

https://ruby-doc.org/core-2.2.0/Module.html#method-i-extended

included, prepended, extended are all similar mechanisms. inherited to intercept a class definition during class inhertiance. We can inject the exception handler for its inheritance behvaior.

Also, method added, removed, indefined could trigger below hooks:

class InstanceMethodMonitor
	def self.method_added(m); end
	def self.method_removed(m); end
	def self.method_undefined(m); end

	# trigger method_added
	def hello; end
    
    # trigger method_removed
    remove_method(:hello)
    
    # tigger method_undefined
    undef_method(:hello)
end

In singleton class level, consider these hooked methods:

class SingletonMethodsMonitor
	def self.singleton_method_added(m); end
	def self.singleton_method_removed(m); end	
    def self.singleton_method_undefined(m); end
    
    def self.hello; end
    
    class << self; remove_method(:hello); end
    
    def self.hello; end
    
    class << self; undef_method(:hello); end
end

Item 30

Prefer define_method to method_missing

method_missing is untrackable when user call obj.respond_to? :method or obj.public_methods(false):

class HashProxy
	def initialize
    	@hash = {}
    end

	def method_missing(name, *args &block)
    	if @hash.response_to?(name)
        	@hash.send(name, *args, &block)
        end
    end
end

h = HashProxy.new

h.size
# 0

h.respond_to? :size
# false

Now, when we want to use decorator class to forward all methods to decorated object, method_missing will intercept and call at decorator level, but we should pass the calling of existed methods in decorator class to decorated object and let it decide. This is where define_method comes in, following class use anonymous module:

class SomeDecorator
	def initialize(object)
    	@obj = object
        @logger = Logger.new($stdout)

    
    	mod = Module.new do
	    	object.public_instance_methods.each do |name|
            	# this block in current SomeDecorator class scope
    			define_method(name) do |*args, name, &block|
            		@logger.info("some info")
                	@obj.send(name, )
            	end
    		end
    	end
    
    	extend(mod)
    end
end

Regarding the scope, closure for define_method, it use instance_eval:

http://stackoverflow.com/questions/10206193/ruby-define-method-and-closures

So, can we return from a Proc the “normal” way? Yes, if we use the next keyword, instead of return. We can re-write method_b so that it’s functionally the same to method_a:

def method_b
  res = proc { next "return from proc" }.call
  return  "method b returns #{res}"
end


puts method_b

Since the block won't create a scope, so it is a closure, so does proc. But lambda create a scope and memoized its reference vars:

outer = 1

def m a_var
        inner = 99
        puts "inner var = #{inner}"
        # a_var is memoized
        lambda {inner + a_var}
end

p = m(outer)
puts "p is a #{p.class}"

outer = 0
puts "changed outer to #{outer}"

puts "result of proc call: #{p.call}"

#=> inner var = 99
#=> p is a Proc
#=> changed outer to 0
#=> result of proc call: 100

https://www.sitepoint.com/closures-ruby/

Item 31

Know the Difference between the Variants of eval

Beward of isntance_eval, eval, and class_eval(module_eval).

instance_eval opent an instance's singleton class and inject instance method in singleton class, treat as singleton_method for the class.

For class_eval it open that class in lexical scope and inject the code snippet for it.

Item 32

Consider Alternatives to Monkey Patching

Because class, objects, and modules are always open in Ruby, so may have patch collision issue when you monkey patching in same class but different run time execution phase. We can use new feature refinements from Ruby 2.1, as follow:

module OnlySpace
	refine(String) do
    	def only_space?
        	# ...
        end
    end
end

# the use using in pachted module

class Person
	using(OnlySpace)
    
    def initialize
    	@name = name
    end
    
    def valid?
    	!@name.only_space?
    end
    
    def display(io=$stdout)
    	io.puts @name
    end
end

Note that refinements only activated in current lexical scope, out of that module or class, refinements will automatically deactivated. So the string(@name) passed to puts in display method is actually leave current lexical scope, then only_space? refinement will deactivated.

Once the control leaves display and enter puts, the refinements defined in OnlySpace are deactivated.

When we mention the constraint about lexical scope, it means the inherited subclass will not be able to use that refinements from superclass as well.

Item 34

Consider Supporting Differences in Proc Arity

Block may have different arity with different behavior, consider supporting this difference in Proic.arty:

def stream(&block)
	loop do
    	start = Time.now
        data = @io.read(@chunk)
        return if data.nil?
        
        arg_count = block.arity
        arg_list = [data]
        
        if arg_count == 2 || ~arg_count == 2
        	# arg_list << ...
        end
        
        block.call(*arg_list)
    end
end

The ~ unary complement operator will turn the optional arguments into the number of requirement arguments:

func = ->(x, y=1) { x + y}
# => #<Proc:0x007fa2c3108980@(irb):1 (lambda)>

# if have optional arguments, turn it to negative from one requirement + at least one optional as requirement 
func.arity
# => -2

~func.arity
# => 1

func = ->(x, z, y=1, g=1) { x + y}

func.arity
# -3

Item 35

Think Carefully Nefore Using Module Prepending

Think carefully for both the prepend order in syntax, and its actual inheritance chain:

module A; end
module B; end

class C
	include(A)
    include(B)
end

C.ancestors
# [C, B, A, Object, Kernal, BasicObject]

class D
	prepend(A)
    prepend(B)    
end

D.ancestors
# [A, B, D, Object, Kernal, BasicObject]

It reads the prepend and include from buttom-up manor [B, A], then append by this order, or prepend by reverse order.

Item 36

Familiarize YOurself with MiniTest Unit Testing

prefer using suffix _test for test file name. require('minitest/autorun' will load unit testing, spec testing, and mocking 3 components automatically. To define an unit test, inherit MiniTest::Unit::TestCase and prefix test_ in each testing instance method:

class SomeTest < MiniTest::UnitTest::TestCase
	def test_something
        # assert something
    end
end

Minitest randomizes the methods before executing each test case. We can explore assertion methods from MiniTest::Assertions module documentation. For example assert_equal(expect_value, actual_value, failure_message)

MiniTest only invoke methods which prefix with test_, so you can add helper methods without that prefix.

Item 37

Familiarize Yourself with Minitest Spec Testing

Each describe keyword inherited MiniTest::Spec class, the argument can be any object, it convert to string internally as label for the new, anonymous class. A test is defined by it keyword inside describe block. In spec testing, assertions have been replaced with monkey patching that allows you to call must_equal to any object.

  1. Prefix assert_ in unit testing is similar as must_ prefix in spec testing.
  2. Prefix wont_ in unit testing is similar as refute_ prefix in spec testing.

Item 38

Simulate Determinism with Mock Objects

To mock the object in MiniTest, create a blank object by MiniTest::Mock::new which is eager to prepend to be any other object. Then target on mocked object's method to add expect method symbol and return value to mock the behvior:

class Monitor
	def is_alive?
    	resp = get(echo)
        resp.success? && resp.body == echo
    end

	def get(echo); 
    	# return a response ...	
    end
end

def test_successful_monitor
	monitor = Monitor.new("target.object.com")
    response = MiniTest::Mock.new
    
    # mocking get method's behavior, and its return value
    monitor.define_singleton_method(:get) do |echo|
    	# return mocked object instead.
        response.expect(:success?, true)
        response.expect(:body, echo)
        response
    end
    
    response.verify
end

The verify method will check whether any of the expected methods were called. It will raise exception which cause test fail if not all methods were called.

Item 39

Strive for Effectively Tested Code

Testing should not be only positive, add fuzzy testing and property testing to make your code robust. Both are feeding random data to ensure the executed functionality still work as expected. Fuzzy testing focus on finding out the crashes or security hole.

The fuzzy test should made up of two parts, the generator and test, ex fuzzybert gem:

https://github.com/krypt/FuzzBert

require 'fuzzbert'

fuzz('URI::HTTP.build') do 
	data('random server names') do 
    	FuzzBert::Generators.random
    end
    
    deploy do |data|
    	URI::HTTP.build(host: data, path: '/')
    end
end

Fuzzy testing is usually not terminated automatically, so the longer the test running, the code is more confident to be secured. For fuzzy testing, you should leave the test for several days to make some degree of confidence for your program.

Property testing:

http://www.scalatest.org/user_guide/property_based_testing

where a property is a high-level specification of behavior that should hold for a range of data points. For example, a property might state that the size of a list returned from a method should always be greater than or equal to the size of the list passed to that method. This property should hold no matter what list is passed.

The difference between a traditional test and a property is that tests traditionally verify behavior based on specific data points checked by the test. A test might pass three or four specific lists of different sizes to a method under test that takes a list, for example, and check the results are as expected. A property, by contrast, would describe at a high level the preconditions of the method under test and specify some aspect of the result that should hold no matter what valid list is passed.

We can use MrProper ruby gem :

require 'mrproper'

class Version
	def initialize(version)
    	@major, @minor, @patch = version.split(".").map(&:to_i)
    end
    
    def to_s
    	[@major, @minor, @patch].join(".")
    end
    
    # we can have assumption @major must > 1 or some rules
    # these rules as assumptions can be examined by property testing
    # not just presumed data ponits feed in test then expect presumed output correct only.
end

propertties("Verison") do
	data([Integer, Integer, Integer])
    
    property("new(str).to_s should always equal to str") do |data|
    	str = data.join('.')
        assert_equal(str, Version.new(str).to_s)
    end
end

Item 44

Familiarize Yourself with Ruby's Garbage Collector

Mark & Sweep
  1. Ruby use mark and sweep with generational garbage collector.

  2. First phase is traverse object graph and mark alive objects.

  3. Second phase will sweep away unmarked objects and release memory back to Ruby memory pool then possibly release to operating system.

  4. Mark phase hase major and minor mode:

    • major is expensive, all objects are considered for marking. The generation will not considered in this phase.
    • minor marking only consider young objects and automatically mark old objects without checking to see if they were reachable. This means old object can only be swept away in major marking.
    • The garbage collector has some threshold to trigger major marking.
  5. Sweep phase hase immediate and lazy two phases:

    • immediate phase will free all unmarked objects.
    • lazy free minimum of objects, each time you create ruby object, ruby might trigger lazy sweeping to free some space.
Generational Garbage Collector
  1. Objects are divided into young and old two generations by its surviving time.
  2. If old objects is marked as reachable, then skipping entire sections of the object graph while traversing.
Memory Pool, Pages, and Slots
  1. Ruby's memory pool a.k.a heap(not data structure), which divided into pages and subdivided into slots.
  2. Each slot holds a single ruby object.
  3. Ruby only ask the operating system for memory when pool is empty.
  4. If ruby cannot find empty slot for storing new object, it will attempt a lazy sweeping. If it still cannot find empty slot, then allocate a new page to heap. During sweeping phase, if all slots in page are released, it might return the page to operating system. The memory footprint of a ruby process will appear to grow and shrink over time. From the inside, a ruby process grow and shrink by pages and not individual objects.

Item 45

Create Resource Safety Nets with Finalizers

Revisit closure:

http://www.skorks.com/2010/05/closures-a-simple-explanation-using-ruby/

How do they retain the values of the variables that were in scope when the closure was defined? This must be supported by the language and there are two ways to do that.

  1. The closure will create a copy of all the variables that it needs when it is defined. The copies of the variables will therefore come along for the ride as the closure gets passed around.

  2. The closure will actually extend the lifetime of all the variables that it needs. It will not copy them, but will retain a reference to them and the variables themselves will not be eligible for garbage collection (if the language has garbage collection) while the closure is around.

Point 2 is how ruby does things.

Scope and Closure

https://aprescott.com/posts/variables-closures-and-scope

Since foo opens a new level of scope, x has no associated value inside foo. Scope can be understood as a set of identifier-value associations which are visible within some hierarchy in the program. Any method definition in Ruby opens up its own scope, clearing out the identifiers currently visible within the method body.

def foo
  x
end

def bar(b)
  b.eval("x")
end

x = 1
current_binding = binding

foo                  #=> NameError, undefined local variable or method `x'
bar(current_binding) #=> 1

By passing around a Binding object explicitly, we can keep a hold of associations and use them in place of some other set of associations. That is what’s happening in bar.

Binding is actually ref to that associated enviroment variables:

x = 1
@l = lambda { x }

def foo
  # opens a new scope to make the point about which
  # binding we're changing
  x = 2
  puts x
  puts @l.binding.eval("x")
  @l.binding.eval("x = 50")
  puts x
end

x   #=> 1

foo	#=> 2
    #   1
    #   2

x   #=> 50

Scope will be created when:

  1. Reach the beginning of your scope (a def/class/module/do-end block)
  2. Reach the code that does the assignment to that local variable.

https://www.sitepoint.com/understanding-scope-in-ruby/

A closure is simply code containing behavior that can:

  1. be passed around like an object (which can be called later)
  2. remember the variables that were in scope when the closure (lambda in this example) was defined.
def foo
  x = 1
  lambda { x }
end

x = 2

p foo.call
# 1

http://awaxman11.github.io/blog/2013/08/05/what-is-the-difference-between-a-block/

Item 46

Be Aware of Ruby Profiling Tools

Profiling tools: objspace, stackprof, or memory_profiler.

Item 47

Avoid Object Literals in Loops

# actually create 3 string object + 1 array => 4 objects
# 4 * n errors
errors.any? { |e| %w(F1, F2, F3) }

# more efficient way
"F1 F2 F3".split

Better store the string literals into a constant oust side of loops, which only create once:

FATAL = %w(F1 F2 F3).map(&:freeze).freeze

errors.any? { |e| FATAL }

The frezze guarantee string literal will never mutate. In ruby 2.1, we can use frezze directly to string literal object only, which equivalent to constant. This freeze trick is not work on arbitrary other string objects.

errors.any? { |e| "FATAL".freeze }

Item 48

Consider Memoizing Expensive Computations

Consider using ||= to memoize your expansive operation, but if side effect occurs in later execution, we need to reconsider it:

# memo db orm operation
@memo ||= User.find(:some_id)

# if memo is updated(side effect), then you need to reload to get updated data
@memo.reload

@memo ||= expression

# syntax sugar as 
@memo || @memo = expression

Reference

https://ruby-doc.org/core-2.2.0/Class.html#method-c-new
https://ruby-doc.org/core-2.2.0/Module.html#method-i-extended
http://stackoverflow.com/questions/10206193/ruby-define-method-and-closures
https://www.sitepoint.com/closures-ruby/
https://github.com/krypt/FuzzBert
http://www.scalatest.org/user_guide/property_based_testing
http://www.skorks.com/2010/05/closures-a-simple-explanation-using-ruby/
https://aprescott.com/posts/variables-closures-and-scope
https://www.sitepoint.com/understanding-scope-in-ruby/
http://awaxman11.github.io/blog/2013/08/05/what-is-the-difference-between-a-block/

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