« | Main | »

December 01, 2009

resumep3 is a ruby/iphone project whose objective is to allow you to resume listening on one device to a song your paused on another device. So, there are n programs for n-many devices (iphone, laptop, etc), but for the most part each does the same thing.
  1. Check to see if you are playing music
  2. If you are:
    1. store your players state -- song info, time, etc -- remotely
    2. pause the player
  3. If you are NOT playing music:
    1. retrieve the last known player state remotely
    2. resume the player with this state
That really is basically all there is. The idea is that this is supposed to be anonymous,safe, but ultimately very easy to use. So at first I stored player state info (album,artist,track,time) IDed off the laptop's user name -- so, 'jeff' in my case. This worked, but there are probably more jeffs in the world. So, we need a way to have all your devices store locally a shared key. Of the only two programs that run this are a desktop ruby script and an iphone app, only the iphone has a unique id. So, the process of requesting a shared key differs between the two programs:
  • For the iphone, if we don't have a shared key we will request one from a remote URL, giving our unique id.
  • For the desktop, if we don't have a shared key we need to wait for an iphone to request one and then send it over the local network.
When I started making this change I noticed that entire desktop app was basically one file containing code from these two files:
  1. common.rb
  2. resumep3
Also, I thought it would be useful to have a iphone simulator program, too, so that I didn't have to run in XCode. I could also test the remote code using this. Instead of going through the needs of this program, let me point out the differences from the normal desktop app.
  1. It doesn't actually play and pause itunes
  2. and, it doesn't wait for a shared key, when it doesn't have one stored locally it requests one from get_id.php
I began using regular abstraction to pull out the common parts -- into common.rb -- and then have two subclasses -- one for the desktop app and one for the iphone simulator. And found that each the class implementating these programs would each essentially have three parts:
  1. The ability to pause the player and play tracks
  2. The ability to get the state of the player
  3. A means to retrieve a shared key, if one was not stored locally
But I could go further and take the command line aspect of the program out, and the argument parsing, help printing. And, what I decided to do was to completely decompose the program into one common interface: common_interface.rb, which had only seven methods that needed implementing, those were:
  1. run(has_options)
  2. find_track(info)
  3. find_info(track_id)
  4. read_info(user)
  5. play_track(track_id)
  6. pause_player
  7. find_the_shared_key
These methods basically fall into three interfaces:
  1. A Player implements:
    • read_info(user)
    • play_track(track_id)
    • pause_player
  2. A Library implements
    • find_track(info)
    • find_info(track_id)
  3. A KeyFinder implements
    • find_the_shared_key
Then a common base class could implement this interface solely by implementing run(has_options), because its implementation would be the same for both classes, and then delegating to the appropriate instance of Player, Library, or KeyFinder for the remaining six methods. At the end, I did use composition entirely and the two subclasses looked like this:
class DesktopComposedCommon < ComposedCommon

  def initialize(config)
    super(config,
          AppleScriptPlayer.new,
          ITunesBasedLibrary.new,
          WaitsForKey.new(config))
  end

end

class IPhoneComposedCommon < ComposedCommon

  def initialize(config)
    super(config,
          ConsolePlayer.new,
          ITunesBasedLibrary.new,
          RetrievesKeyFromSever.new(config,self))
  end

  def unique_id
    'asdkfjadflkjasdlfkjasdlfkjadsf02409234098234kjsdfksdf'
  end

end
This was kind of a pain, but it was fun decomposing the whole system and really maximizing both code and data reuse.

Posted by jeff at December 1, 2009 05:06 PM