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.