technology from back to front

Breaking inter-package dependencies through Squeak’s pragma preferences

The Preferences class provides a common place for all parts of a Squeak Smalltalk image to register their switches: Which update stream do we want to follow? What colour do we want our Browsers? Do we allow assignments to block parameters? Do we allow underscores in selector names? Preferences range from low level things that affect the language’s grammar, all the way to trivial things.

In the old days we would create a preference by adding a getter/setter pair that would expose a key-value pair in a dictionary. That sounds great. It’s certainly simple. It’s only when a system evolves organically over decades that we realise the trap: such a dictionary is a giant chain, coupling together otherwise independent packages that simply wish to store something configurable. Even though we can store the preference accessors within each package, the packages still share the backing dictionary. It’s a recipe for trouble. Let’s see how we can untangle the chain.

As it happens, we’ve had the solution for a while. (Having the solution, and having people implement the solution, are not the same thing.) The trick is this: describe preferences using a pragma. To keep things concrete, let’s work through an example. If the #updateFromServerAtStartup preference is true, Squeak presents the user with a dialog, asking if she’d like to update the image now, later, or never. The AutoStart class uses it:

AutoStart >> processUpdates
    "Process update files from a well-known update server.  This method is called at system startup time.
     Only if the preference #updateFromServerAtStartup is true is the actual update processing undertaken automatically"

    | choice |
    (Preferences valueOfFlag: #updateFromServerAtStartup) ifTrue:
        [choice := UIManager default chooseFrom: #('Yes, Update' 'No, Not now' 'Don''t ask again')
            title: 'Shall I look for new code\updates on the server?' withCRs.
        choice = 1 ifTrue: [
            Utilities updateFromServer].
        choice = 3 ifTrue: [
            Preferences setPreference: #updateFromServerAtStartup toValue: false.
            self inform: 'Remember to save you image to make this setting permanent.']].
    ^false

Looks fine, except that it uses Utilities, which as we’ve seen before needs to go. This references induces a dependency from the relatively low level System package to the higher level MonticelloConfigurations package. It has to go. Really, we want updating functionality to live in MCMcmUpdater, the class responsible for, well, updating. It makes sense to store preferences controlling this updating to live in the same class.

We won’t worry about how we move the functionality out of Utilities. That’s pretty obvious. (It’s the functionality that directly causes the inter-package dependency by referencing a class in the MonticelloConfiguration package. However, if we stop after moving the functionality, we have harder-to-find cruft in a blob of global state.) Let’s look instead at the preference side of things.

I’d mentioned “pragma” earlier. A Smalltalk pragma is just like a C# attribute, except the metadata it supplies is within the method source. For instance:

MCMcmUpdater >> promptForUpdatesAtStartup
    <preference: 'Update from server at startup'
        category: 'updates'
        description: 'If enabled, will ask you if you wish to update from the server'
        type: #Boolean>
    ^ self updateFromServerAtStartup.

Here we see a pragma called #preference:category:description:type containing some data. We might consider the pragma as a record type. Pragmas differ from C# attributes in that pragmas may only contain literal data. They can not, in particular, execute any code. The act of saving this method causes the SystemChangeNotifier to notify interested parties of the new or changed method. The Preferences browser registers for such events, and hence we have a dynamically constructed list of preferences.

This preference-in-a-package replaces the existing in-the-Preferences-class preference, Preferences class >> #updateFromServerAtStartup:

Preferences class >> updateFromServerAtStartup
    ^ self
        valueOfFlag: #updateFromServerAtStartup
        ifAbsent: [false]

But so far we’ve just added a duplicate. Not only do we need to remove the old preference, we need to remove the preference from people’s images when they update. (Remember, code loading is data migration in a typical Smalltalk update stream.). The easiest place to do this is at load time:

Preferences class >> initialize
    "Preferences initialize"
    self registerForEvents.
    "Remove obsolete preferences, storing their values in the new preferences as necessary"
    MCMcmUpdater updateFromServerAtStartup: (self valueOfFlag: #updateFromServerAtStartup ifAbsent: [false]).
    self removePreference: #updateFromServerAtStartup.
    self registerForEvents.

When our image updates, the load mechanism sees that the class’ #initialize has changed, and reruns it, removing the reference. A later version of the package can then remove the removal of the old preference.

Driving the preferences into pragmas serves a valuable purpose: packages add or remove their own preferences, and so the Preferences browser does not need to know about random packages.

by
Frank Shearar
on
31/07/13
 
 


seven + 3 =

2000-14 LShift Ltd, 1st Floor, Hoxton Point, 6 Rufus Street, London, N1 6PE, UK+44 (0)20 7729 7060   Contact us