Code should feel good

One of the pleasures of working with RubyMotion is the frequent occurrence of wrapping obtuse Objective-C code within abstractions. If we do our job right, these abstractions will be helpful. The sheer number of letters that your mind has to parse to read code is lowered, which is usually a win. However, this gain only materializes if your abstraction is any good.

Consider the following module:

module StoredObject

  SERVICE = 'YOUR_SERVICE'

  def save(key, val)
    storage[key] = val
  end

  def secure_save(key, val)
    SSKeychain.setPassword(val, forService: SERVICE, account: key)
  end

  def val(key)
    storage[key]
  end

  def secure_val(key)
    SSKeychain.passwordForService(SERVICE, account: key)
  end

  def delete(key)
    storage[key] = nil
  end

  def secure_delete(key)
    SSKeychain.deletePasswordForService(SERVICE, account: key)
  end

  def nil?(key)
    storage[key].nil? 
  end

  def secure_nil?(key)
    secure_val(key).nil?
  end

  private

  def storage
    @storage ||= NSUserDefaults.standardUserDefaults
  end

end

This code feels good to me.* I like the following things about this code:

How an object is stored is abstracted away from the logic of whatever is doing the storing.
The DSL feels more intuitive than the library it wraps around. It does what you would expect it to do, which is aspect of the beauty of code.

The 'Hash' like quality of NSUserDefaults has been hidden behind messages. This prevents the user from interacting with the information in NSUserDefaults like a specific data-type. We could change the implementation of NSUserdfaults without includers of this module caring.

Things worth considering:

Separating out Secure Saving into a separate module. I'm not feeling the pain of secure storage being included right now, but we should pay attention to that if it were to arise.
Test our memoization of @storage. Is it actually faster than calling NSUserDefaults.standardUserDefaults directly?
Is the secure_nil? method superfluous? Should we change that to just .nil?

*...right now. This might change the next time I look at this.


# Unsure if the following is worth anyone's def time. 

class Test
  def or_equals
    @foo ||= 'fo'
  end

  def if_and_return
    @foo = 'foo' unless @foo.nil? 
    @foo
  end

end

REPS = 10_000_000

time_1 = Time.now
REPS.times do 
  t = Test.new
  t.or_equals
  t.or_equals
end
time_2 = Time.now
REPS.times do 
  t = Test.new
  t.if_and_return
  t.if_and_return
end
time_3 = Time.now

puts "or_equals: #{time_2 - time_1}"      # => or_equals: 4.095663
puts "if_and_return: #{time_3 - time_2}"  # => if_and_return: 4.755961

end

Regardless of your opinion on Monkey Patching, it's something programmers have been doing for years. Like any other decision, it has its tradeoffs. It gives you extra functinality on top of your base classes, but you could run into namespacing issues, or suprise teammates through unexpected functionality on classes they're familliar with.

In RubyMotion, there's an additional cost: visibility into what you're actually editing isn't as clear as you might think.

➜  monkey-test  rake
    ...
  Simulate ./build/iPhoneSimulator-7.1-Development/monkey-test.app

(main)> Hash.ancestors
=> [Hash, NSMutableDictionary, NSDictionary, Enumerable, NSObject, Kernel]

What are the implications of this?

Let's hit a url with AFMotion, and see what we get back

class Film
    def Film.search(item, &block)
      url = "https://www.fandor.com/api/2/films.json?searchTerm=#{item}"
      AFMotion::JSON.get(url) do |response|
        puts response.object.class    # => Hash
      end
  end
end

RubyMotion and AFMotion report that we get back a hash. Let's test that.

class Hash
  def monkey_patch
    'ooh ooh ooh'
  end
end

...
AFMotion::JSON.get(url) do |response|
  puts response.object.class    # => Hash
  puts {}.monkey_patch          # => 'ooh ooh ooh'
  puts response.object.monkey_patch # => NoMethodError
end

But when this code gets run, an error gets thrown:

(main)> Hash
(main)> 'ooh ooh ooh'
2014-08-18 10:45:01.080 flow[16095:70b] film.rb:27:in `block in search:': undefined method `monkey_patch' for #<Hash:0x934b5b0> (NoMethodError)

That's because RubyMotion lies. Hash is built on NSMutableDictionary. AFMotion returns an NSmutableDictionary masquerading as a Hash. Our Monkey-Patch doesn't work, because we're patching Hash, not the Obj-C class.

Once we know this, the fix is easy:

class NSMutableDictionary
  def monkey_patch
    'ooh ooh ooh'
  end
end

...
  puts response.object.class    # => Hash
  puts {}.monkey_patch          # => 'ooh ooh ooh'
  puts response.object.monkey_patch # => 'ooh ooh ooh'

TL;DR - if you're monkey patching an Object in ruby, consider patching the superclass if it's an Obj-C class.