A Pharo refactoring story: adding theme ability to a Morph

Morphs are visual objects that stay at the basis of the user interface framework in Pharo. Among other things, they also come with a mechanism that allows us to define look-and-feel themes for these morphs. Technically, this is achieved by having the morphs delegate the relevant rendering code to the UITheme and its subclasses.

This fine for a user as the only thing you have to do for choosing a new theme is to choose another subclass of UITheme. However, code-wise it does not work well: over time UITheme accumulated too many methods and it simply became too difficult to manage.

How can we improve this situation?

The problem

First, let’s take a closer look at UITheme. To give you an idea of how large this class became, executing:

(Object withAllSubclasses
     sorted: [ :a :b | a selectors size > b selectors size ])
     indexOf: UITheme

... says it is the second largest class in Pharo in terms of amount of methods. Indeed, it has 574 methods. That is pretty large.

Are these methods working with one another? The graph below shows all methods as gray nodes and attributes as blue nodes from the UITheme, and the edges show the self message sends and usages of instance variables.

Uitheme.png

If you are curious about how I produced the image, I simply used a Roassal script and the code model of Pharo. The code is a bit more complicated than it should be just because the the code model does not provide straightforward predicates, and in this case I had to recourse to explicit traversals of the abstract syntax tree:

view := ROMondrianViewBuilder new.
class := UITheme.
view shape ellipse withoutBorder; fillColor: Color blue.
view nodes: class instVarNames.
view shape ellipse withoutBorder; fillColor: Color gray.
view nodes: class methods.
view edges: class methods from: #yourself toAll: [ :each |
   each parseTree allChildren
      select: [ :n |
         n isMessage and: [
            n isSelfSend and: [
               class methods anySatisfy:[ :m | m selector = n selector ] ] ] ]
      thenCollect: [ :n | class methods detect: [:m | m selector = n selector ] ]].
view edges: class methods from: #yourself toAll: [ :each |
   each parseTree allChildren
      select: [ :n |
         n isVariable and: [
            class instVarNames includes: n name ] ]
      thenCollect: #name ].
view forceBasedLayout.
view open 

There are several things we can learn from this graph. The instance variables are only used in a couple of disparate methods. But, there appears to exist some communication between methods. On a closer inspection, we come to see that much of the graph connectivity happens due to only a few methods. As an exercise, we displayed the same class again, only by removing three methods: icons settings textFont.

Uitheme-selected.png

As you can see, the connectivity, and thus the class cohesion, goes down dramatically.

If these methods do not really work together, why did they get in this class? Essentially, they are actually related to the various morphs class, and the only reason to have them in the UITheme class is so that subclasses can provide different look and feels by overriding the methods.

For example, DialogWindow defines the fill style by delegating to the UITheme:

DialogWindow>>activeFillStyle
     ^self theme dialogWindowActiveFillStyleFor: self 
UITheme>>dialogWindowActiveFillStyleFor: aWindow
     ^aWindow paneColorToUse lighter 

Thus, subclasses of UITheme can override dialogWindowActiveFillStyleFor: and provide a different style. This design obviously does not scale well.

Another design would be to add another level of indirection by introducing the notion of a Themer. Essentially, rather then having the UITheme be responsible for holding the actual rendering code, we make it be a collection of strategy objects, each dedicated to a morph. Let me exemplify what I mean on a case study.

The Spotlight case study

Sean DeNigris recently created a rather beautiful incarnation of the Mac Spotlight in Morphic. It looks like this.

Spotlight.png

Now, let’s make it theme-able. Coloring is the primary target for theming. For example, the SpotlightItemMorph defines the color for the selected item like this:

SpotlightItemMorph>>onSelected
     self color: (Color r: 0.2627 g: 0.4588 b: 0.9333).

Let’s transform it. First, we add the themer object to UITheme:

UITheme>>spotlightThemer
     ^ SpotlightThemer new

Afterwards, the onSelected simply delegates to the themer that is specific to the current theme:

SpotlightItemMorph>>onSelected
     self theme spotlightThemer configureSelectedItemMorph: self 

And the default implementation remains the same:

SpotlightThemer>>configureSelectedItemMorph: aMorph
     aMorph color: (Color r: 0.2627 g: 0.4588 b: 0.9333)

At this point, if we want to provide another rendering, we can do it by subclassing SpotlightThemer and by overriding the spotlightThemer method in a UITheme subclass.

With this design, we still have the UITheme be in charge of producing a cohesive theme, but it does so by delegating the actual code to specialized themer objects. It should scale better.

To finish the job, we need to do the same for all the Spotlight methods that refer to Color. To do this, we write a query and use the GTInspector to guide us through the code:

(RPackageOrganizer default packageNamed: 'Spotlight')
     methodReferences asSet select: [ :each |
          (each literals collect: #value) anySatisfy: [:literal |
               literal = Color ] ]

Target-methods-referring-to-color.png

There are four methods in Spotlight that refer to Color, and theme-ing them is almost straightforward now that we have the tool to browse them.

Conclusion

Just because Pharo is a dynamic language, it does not mean that static analysis is useless. There is a wealth of information still left in the structure. The key thing is to ask the right question and know that there are powerful tools around to help. In my case, I used a query and a custom visualization to reason about a code problem, and I guided a specific refactoring by means of a query and a browser. The whole exercise took less than half an hour (and significantly longer to describe it here).

By the way, there are plenty of Morphs around that hardcode the reference to Color. How many? Inspect the result of this script:

(Morph withAllSubclasses 
     flatCollect: [ :c | c methods collect: #asRingDefinition ])
     select: [ :each |
          (each literals collect: #value) anySatisfy: [:literal |
               literal = Color ] ] 

In my image, it reveals 363 such methods.

Posted by Tudor Girba at 6 January 2014, 12:15 am with tags pharo, moose, assessment, spike, story link
|