Object Pattern: the Object Switch

This time I want to talk about a great pattern of object usage which can be adapted into many scripting situations. If you’re not familiar with script objects, perhaps you should read that article first. I call this pattern the “Object Switch”. Here’s the situation: you’ve got a list of things that you’re going to loop over and based on some attributes of the object, do something a little different to each one. I can imagine that you’re thinking of this repeat statement that covers like five pages, filled with if… else if… else if… ad inifinitum. Impossible to maintain, difficult to read (not much nicer to write), and as exceptions or new cases get added it grows consuming your scripting patience and time. I’ve seen it in more scripts than I care to recall.

Let’s change that! Using objects we can transform that gigantic impenetrable mess into a purring little kitten. The example I’ll look at this time is a downloads folder assistant folder action (see also the macscripter faq). As files get downloaded the “adding folder items to… after recieving…” handler gets called. For certain kinds of files I want to define actions to occur, and for other files maybe just tell the finder to reveal it. This fits the above problem exactly; list of files, different actions per file attributes, and the cases we intend to handle is likely to grow or change over time. Let’s start by looking at the “adding folder items to” handler of the script:


on adding folder items to downloadsFolder after receiving someFiles
	repeat with eachFile in someFiles
		tell ObjectSwitch to dispatch(eachFile)
	end repeat
end adding folder items to

Instead of a huge “else if” structure I tell a script object (ObjectSwitch) to do all the work. Let’s look at the two objects now that make up the base of the “Object Switch” pattern: the switch (in this case ObjectSwitch), and the base case (AnyFile).


script AnyFile
	on match(fileAlias)
		return true --> over-ride me
	end match
	
	on actOn(fileAlias)
		return --> over-ride me
	end actOn
end script

script ObjectSwitch
	property caseList : {AnyFile}
	
	on dispatch(fileAlias)
		repeat with eachCase in caseList
			tell eachCase
				if match(fileAlias) then
					actOn(fileAlias)
					return
				end if
			end tell
		end repeat
	end dispatch
end script

Now to add a case, all I’d to is define a script object that may inherit from AnyFile and add it to ObjectSwitch’s caseList. In the most basic version of this pattern we only need match and actOn handlers, but for the next revision I’m going to add two more to show you the flexibility of this technique. Let’s look at a more complete version of the script with two special cases: AppleScriptFile, and WidgetArchiveFile. (pay attention to the highlighted bits…)


script AnyFile
	on match(fileAlias)
		return true --> over-ride me
	end match
	
	on shouldActOn(fileAlias)
		return true --> over-ride me
	end shouldActOn
	
	on actOn(fileAlias)
		tell application "Finder"
			reveal fileAlias
		end tell
		return --> over-ride me
	end actOn
	
	on didActOn(fileAlias)
		return --> over-ride me
	end didActOn
end script

script AppleScriptFile
	property parent : AnyFile
	property defaultApp : "Script Editor"
	
	on match(fileAlias)
		return (default application of (info for fileAlias)) is (application defaultApp)
	end match
	
	on shouldActOn(fileAlias)
		display dialog "Move " & (fileAlias as text) & " to scripts folder" buttons {"Don't move file", "Move file"} default button "Move file"
		return (button returned of result) is "Move file"
	end shouldActOn
	
	on actOn(fileAlias)
		move fileAlias to scripts folder
	end actOn
end script

script WidgetArchiveFile
	property parent : AnyFile
	
	on goodPosixPath(fileAlias)
		-- escape spaces
		
		set my posixPath to POSIX path of fileAlias
		set oldTids to text item delimiters
		set text item delimiters to " "
		set my posixPath to text items of my posixPath
		set text item delimiters to "\\ "
		set my posixPath to my posixPath as text
		set text item delimiters to oldTids
		
		return my posixPath
	end goodPosixPath
	
	on ensureDirectory()
		try
			do shell script "mkdir ~/Library/Widgets"
		on error
			--it already exists
		end try
	end ensureDirectory
	
	on match(fileAlias)
		set my shellScript to "unzip -l " & (goodPosixPath(fileAlias)) & " | grep wdgt\\/$"
		try
			do shell script my shellScript
		on error
			return false
		end try
		
		return result is not ""
	end match
	
	on actOn(fileAlias)
		ensureDirectory()
		
		set my shellScript to "unzip -qqo " & (goodPosixPath(fileAlias)) & " -d ~/Library/Widgets/"
		
		try
			do shell script my shellScript
		end try
		
	end actOn
	
	on didActOn(fileAlias)
		
		display dialog "Installed widget from " & (fileAlias as text) buttons {"Don't move archive to trash", "Move archive to trash"} default button "Move archive to trash"
		if (button returned of result) is "Move archive to trash" then
			move fileAlias to trash
		end if
		
	end didActOn
end script

script ObjectSwitch
	property caseList : {AppleScriptFile, WidgetArchiveFile, AnyFile}
	
	on dispatch(fileAlias)
		repeat with eachCase in caseList
			tell eachCase
				if match(fileAlias) and shouldActOn(fileAlias) then
					set actResult to actOn(fileAlias)
					didActOn(fileAlias)
					return
				end if
			end tell
		end repeat
	end dispatch
end script

on adding folder items to downloadsFolder after receiving someFiles
	repeat with eachFile in someFiles
		tell ObjectSwitch to dispatch(eachFile)
	end repeat
end adding folder items to

Here’s some of the reasons “Object Switch” kicks “else if”'s heinie.

Easier to read: The cases are all named, the parts of a case are even separated, and variables all exist in smaller separate scopes.
Easier to write: No getting lost in the nesting as you write, always know what you’re doing, what’s been done, and what’s left to do.
Easier to navigate: everything get’s added to our handy dandy navigation bar!
Easier to optimize: Just adjust the caseList order based on frequency.
Easier to extend: Just add script objects that inherit, even possible to create intermediate base cases to share handlers and properties between similar cases.
Easier to adapt: Adjust the dispatch method to create completely different scenarios, such as applying multiple cases by removing the “return” or adding methods to the base case as we saw.
Easier to maintain: Bugs can be easily pinpointed to specific cases, and within a case to specific parts. Cases can be enabled and disabled simply by adjusting the caseList.
Easier to re-use: Put the code from a case into a library, or even the “switch” object and it’s code.