On deep modules

One crucial piece of advice John Ousterhout gives in his software design book is about deep modules.

Module depth is a way of thinking about cost versus benefit. The benefit provided by a module is its functionality. The cost of a module (in terms of system complexity) is its interface.

A module's interface represents the complexity that the module imposes on the rest of the system: the smaller and simpler the interface, the less complexity that it introduces. The best modules are those with the greatest benefit (more functionality) and the least cost (smaller interface).

An example of brutal violation of this principle is the TestCase class in Python's standard library. It has a bunch of shallow methods like these:

assertEqual
assertNotEqual
assertTrue
assertFalse
assertIs
assertIsNot
...

In contrast, Pytest just uses a single assert statement for everything. The implementation is quite complex, but the interface is as small and simple as possible. That's deep!

Another example of a wide interface is Go's Regexp. Here's a sneak peak:

FindAllString(s string, n int) []string
FindAllStringIndex(s string, n int) [][]int
FindAllStringSubmatch(s string, n int) [][]string
FindAllStringSubmatchIndex(s string, n int) [][]int
FindString(s string) string
FindStringIndex(s string) (loc []int)
FindStringSubmatch(s string) []string
FindStringSubmatchIndex(s string) []int

Compare to a third-party regexp2 package:

FindStringMatch(s string) (*Match, error)
FindNextMatch(m *Match) (*Match, error)

But the one in the Go standard library is good for memory training; I'll give it that.

Going back to testing, yet another example is the testify/assert package. Similar to Python's TestCase, it has functions like Equal, NotEqual, EqualError, Error, NoError, ErrorAs, ErrorIs, etc. Even Greater and GreaterOrEqual!

While designing my own test assertions package, be, I tried to follow the deep modules principle. So, it only has three functions — Equal, Err and True:

Equal[T any](tb testing.TB, got T, wants ...T)
Err(tb testing.TB, got error, wants ...any)
True(tb testing.TB, got bool)

It turns out, that's all you really need. I've used this approach in three very different projects — a CLI tool, an API server, and a database engine — and it worked great every time, covering all my assertion needs. Honestly, even Equal and Err would be enough, but True seemed like a nice touch.

Make smaller interfaces. Stay deep.

★ Subscribe to keep up with new posts.