Deep and narrow classes

We are doomed to write convoluted code when interacting with classes that have poor public methods. On the contrary, we are brought to write readable code when interacting with classes that have great public methods. Let's consider the following play public method for the FizzBuzz game.

class FizzBuzz {

  fun play(number: Int): String {
  
    if (number % 5 == 0 && number % 3 == 0)
      return "fizz buzz"
  
    if (number % 5 == 0)
      return "buzz"

    if (number % 3 == 0)
      return "fizz"

    return number.toString()
  }
}

The above signature of the play method leads to the following code when playing with the numbers between 1 and 100.

class App {

  fun main() {
 
    val fizzBuzz = FizzBuzz()    
    val result = ArrayList<String>()
    
    for (number in 1..100)
      result.add(fizzBuzz.play(number))
  }
}

Now, let's change the signature of the play method like follows.

class FizzBuzz {

  fun play(from: Int, to: Int): List<String> {
  
    val result = ArrayList<String>()
    
    for (number in from..to)
      result.add(playSingleNumber(number))
    
    return  result
  }

  private fun playSingleNumber(number: Int): String {
  
    if (number % 5 == 0 && number % 3 == 0)
      return "fizz buzz"
    
    if (number % 5 == 0)
      return "buzz"
    
    if (number % 3 == 0)
      return "fizz"
  
    return number.toString()
  }
}

The above signature of the play method leads to the following code when playing with the numbers between 1 and 100.

class App {

  fun main() {
  
    val fizzBuzz = FizzBuzz()
    val result = fizzBuzz.play(1, 100)        
  }
}

Now the main method is more readable. The for-loop has not disappeared, it just moved from App to FizzBuzz.
However, such a shift becomes remarkable if applied to a codebase with many classes:

  • In the first approach, the for-cycle is repeated every time a piece of code interacts with FizzBuzz. In the second approach, we are guaranteed the for-cycle is written only once: inside FizzBuzz.
  • If main interacted with 4 classes each one using the first approach of FizzBuzz, main would contain 4 for-loops. If the 4 classes were to use the second approach, main will contain zero for-loops.

To summarise with a catchphrase from John Ousterhout, classes should be narrow and deep:

  • narrow means few public methods with few input parameters
  • deep means public methods get a lot of things done for the caller

Recommended reads

Teach me back

I really appreciate any feedback about the book and my current understanding of software design.