Monday, March 18, 2013

Bridges

I rather admire bridges - both the real ones that cross chasms, channels & all other manner of things, but also those in programming.



Why?

They both make it possible to solve difficult problems with very elegant solutions, but both can take some time to construct.  And, both are extremely useful.  For instance, in the IDE we use a bridge pattern to deal with Preferences on a platform specific basis - except for the small chunk of code that is the Preferences public API - nothing else in the IDE is even aware that there are underlying differences in how they are handled.  And therein lies the beauty of the bridge pattern.

So, how do you construct such a thing?

For something like Preferences it is reasonably straight forward. I know people would love to peek at the IDE code.  I'm not going to publish that, but I will give you a sample that is similar.

I've used a Module as a namespace to contain everything AND so I can actually have private classes that can be hidden away - they can be opened & reviewed, but they have no public API so the rest of your code can only use the one public class.

First things first - the public API - this is the module itself. (I'm going to trim some bits out to keep things short but you'll get the idea)

Module CrossPlatformPrefs

  Private Sub InitMPrefs()

    #if TargetWin32
      mPrefsImpl = new WindowsPrefs
    #elseIf TargetMacOS
      mPrefsImpl = new OSXPrefs
    #elseif TargetLinux
      mPrefsImpl = new LinuxPrefs
    #endif
 
  End Sub

  Function ReadPreferenceString(name as String) As String

    // may raise a "Preference not found exception"
 
    if mPrefsImpl is nil then
      InitMPrefs
    end if
 
    if mPrefsImpl is nil then
      raise new PrefsNotUsableException
    end if
 
    return mPrefsImpl.ReadPreferenceString( name )
  End Function

  Function ReadPreferenceStringArray(name as String) As String()

    // may raise a "Preference not found exception"
 
    if mPrefsImpl is nil then
      InitMPrefs
    end if
 
    if mPrefsImpl is nil then
      raise new PrefsNotUsableException
    end if
 
    return mPrefsImpl.ReadPreferenceStringArray( name )
  End Function

  Sub WritePreferenceString(name as string, value as String)  

    // may raise a "Preference not usable exception" - can't initialize
    // may raise a "Preference not found exception" - no such preference
 
    if mPrefsImpl is nil then
      InitMPrefs
    end if
 
    if mPrefsImpl is nil then
      raise new PrefsNotUsableException
    end if
 
    mPrefsImpl.WritePreferenceString( name, value )
 
  End Sub

  Sub WritePreferenceStringArray(name as string, value() as String)

    // may raise a "Preference not usable exception" - can't initialize
    // may raise a "Preference not found exception" - no such preference
 
    if mPrefsImpl is nil then
      InitMPrefs
    end if
 
    if mPrefsImpl is nil then
      raise new PrefsNotUsableException
    end if
 
    mPrefsImpl.WritePreferenceStringArray( name, value )
 
  End Sub

  Private mPrefsImpl As PrefsReaderWriter


End Module



You'll notice that the Module itself mostly just instantiates the right preferences implementation then passes any other method calls along to that implementor. And thats the beauty of the bridge pattern.

You can have many different implementations with one public API.

Each of the implementors implements a common interface - PrefsReaderWriter.

Protected Interface PrefsReaderWriter
  Function ReadPreferenceString(name as String) As String

  Function ReadPreferenceStringArray(name as String) As String()


  Sub WritePreferenceString(name as string, value as string)


  Sub WritePreferenceStringArray(name as string, value() as string)  

End Interface


(again for brevity I've not listed EVERY method just a handful for illustrative purposes)


This interface is IN the module and Private so only classes in the module can use it and nothing outside the module can use it. We want ALL access to Prefs to go through the methods in the module.

One implementor for OS X might look like (with the guts removed again to keep this article short)



Private Class OSXPrefs
Implements PrefsReaderWriter
  Function ReadPreferenceString(name as String) As String
  End Function

  Function ReadPreferenceStringArray(name as String) As string()       
  End Function

  Sub WritePreferenceString(name as string, value as String)
  End Sub

  Sub WritePreferenceStringArray(name as string, value() as String)   
  End Sub

End Class



Windows & Linux implementations would be similar but their internals would vary from the OS X one.  On Windows you'd read & write to the Registry. On Linux perhaps a configuration xml list or key value pair in a file that is prepended with a period so its hidden in most UI's and directory listings (this is very common). And, on OS X you'd used code to write to the property list - perhaps code from MacOSLib to read and write CFPreferences as Apple suggests.

Oh, and yes, I even included a couple of exceptions so you can tell what happened 



Private Class PrefsNotUsableException
Inherits RuntimeException

Private Class PrefsNotFoundException
Inherits RuntimeException

And all of this is private inside the module so the ONLY public API to the Preferences is through the API the module provides.

The bridge is a very handy pattern and really useful especially when you need to have a common API cross platform but need different implementations.

No comments: