Tuesday, March 4, 2014

Modularity in Scala: Isolation of dependencies

Today at work, I said, "I wish I could express the right level of encapsulation here." Oh, but this is Scala! Many things are possible!

We have a class, an akka Actor, whose job is to keep an eye on things. Let's pretend its job is to clean up every so often: sweep the corners, wash the dishes, and wipe the TV screen. At construction, it receives all the tools needed to do these things.

class CleanerUpper(broomBroom
                   rag: Dishcloth, 
                   wiper: MicrofiberTowel, 
                   config: CleanConfig) ... {
...
  def work(...) {
    broom.sweep(config, corners)
    rag.wipe(config, dishes) 
    wiper.clear(tv)
  }
}

Today, we added reporting to the sweeping functionality. This made the sweeping part complicated enough to break out into its own class. At construction of the Sweeper, we provide everything that remains constant (from config) and the tools it needs (the broom). When it's time to sweep, we pass in the parts that vary each time (the corners).[1]

class CleanerUpper(broom: Broom
                   rag: Dishcloth, 
                   wiper: MicrofiberTowel, 
                   config: CleanConfig) ... {
  val sweeper = new Sweeper(configbroom)
...
  def work(...) {
    sweeper.sweep(corners)
    rag.wipe(config, dishes) 
    wiper.clear(tv)
  }
}

Looking at this, I don't like that broom is still available everywhere in the CleanerUpper. With the refactor, all broom-related functionality belongs in the Sweeper. The Broom constructor parameter serves only to construct the dependency. Yet, nothing stops me (or someone else) from adding a call directly to broom anywhere in CleanerUpper. Can I change this?

One option for is to construct the Sweeper outside and pass it in, in place of the Broom. Then construction would look like

new CleanerUpper(new Sweeper(configbroom), rag, wiper, config)

I don't like this because no one outside of CleanerUpper should have to know about the submodules that CleanerUpper uses. I want to keep this internal refactor from having so much impact on callers.

More importantly, I want to express "A Broom is needed to initialize dependencies of CleanerUpper. After that it is not available."

The solution we picked separates construction of dependencies from the class's functionality definition. I made the class abstract, with an uninitialized Sweeper field. The Broom is gone.

abstract class CleanerUpper
                   rag: Dishcloth, 
                   wiper: MicrofiberTowel, 
                   config: CleanConfig) ... {
  val sweeper: Sweeper
...
  def work(...) {
    sweeper.sweep(corners)
    rag.wipe(config, dishes) 
    wiper.clear(tv)
  }
}

Construction happens in the companion object. Its apply method accepts the same arguments as the original constructor -- the same objects a caller is required to provide. Here, a Sweeper is initialized.

object CleanerUpper {
  def apply(broom: Broom
            rag: Dishcloth,
            wiper: MicrofiberTowel, 
            config: CleanConfig): CleanerUpper = 
    new CleanerUpper(rag, wiper, config) {
      val sweeper = new Sweeper(config, broom)
    }
}

The only change to construction is use of the companion object instead of explicitly new-ing one up. Next time I make a similar refactor, it'll require no changes to external construction.

val cleaner = CleanerUpper(broom, rag, wiper, config)

I like this solution because it makes the dependency on submodule Sweeper explicit in CleanerUpper. Also, construction of that dependency is explicit.

There are several other ways to accomplish encapsulation of the broom within the sweeper. Scala offers all kinds of ways to modularize and break apart the code -- that's one of the fascinating things about the language. Modularity and organization are two of the biggest challenges in programming, and Scala offers many paths for exploring these.

-------------
[1] This example is silly. It is not my favorite kind of example, but all the realistic ones I came up with were higher-cognitive-load.

1 comment:

  1. So this seems to be a factory pattern, combined with an adapter pattern, in Java-speak?

    ReplyDelete