The Zero-One-Many principle says that zero, one, and many are the three cases that make sense. (“A few” is more trouble than many.) This principle can guide you in splitting or expanding capabilities at multiple levels. You can use it at various levels: capabilities or user stories, system code, Test-Driven Development (TDD), and more. Today we’ll focus on TDD.
I learned this rule in the context of computer architecture. Part of writing a compiler is managing the use of the computer’s registers in generated code. If the machine has zero registers, register allocation is trivial. If it has one register, it’s reasonably straightforward. If you have many registers (a large enough supply), it’s also simple.
However, if you have a middling number of registers, especially ones with overloaded purposes, register allocation is very tricky. If R5 points to non-local scopes, and you can also gang it with R4 for extended-length operations, and who knows what other complexity, allocation is complicated.
The Zero-One-Many principle is also known as “Zero-One-Infinity” as well, and has been around for a while.
Zero-One-Many
In Test-Driven Development, Zero-One-Many says that the easiest case is (usually) zero of something, the next easiest is one, and the hardest and most general is many. This fits well with evolutionary design, where we grow a system from minimal to full form.
Using Zero-One-Many is not a deterministic approach: you usually have many choices for which dimension to generalize. How you work through those choices has consequences in the design you produce.
Zero-One-Many in a Data Viewer
I’ve been working on a data viewer application part-time for several months. One goal I have is that it be able to work with databases. That is big feature (or set of features) – especially when you’re working in 2-hour blocks.
Our starting point: nothing! Zero on all dimensions. We start from perfection (and go down from there).
From a system not capable of viewing anything, we move up to handling in-memory data. Even if you skip the “1” case and jump into an array, you still need to test zero, one, and many rows.
Even here, the system has roots of being useful. Admittedly, it’s a solution only a programmer could love, but you could pull in real data and turn it into code to initialize the data array.
With this structure in place, I explored many viewer capabilities: sorting, a bar column (that hides columns to its right), unique rows, rearranging columns.
Stepping Outside: Files
A natural next step for an in-memory version is to load its data from files. Files can be in many different formats, but I started with a simple one – tabbed file.
In that format, there’s a header row, followed by zero or more data lines of data. Each line has zero or more fields, separated by tabs. Each field has zero or more characters.
So, we moved forward by handling zero, one, or many lines, fields, and characters.
What files can we open? At first, there was only one: a text file stored in the “bundle” (the app itself).
Rather than explore where to put and find more files, I moved on to the database. This was driven by where I felt like I could learn the most.
Database
For working with a database, one decision is “which technology”? I wanted SQL support, and SQLite is readily available for iOS applications, so I started there.
Which database would I use? A hard-coded one – zero choices.
I skipped the zero-table case, and moved to one. Which table? A hard-coded table.
A table has fields – which ones? I may have started with one, but quickly moved to loading all the fields in a table.
Which rows? All the rows in the chosen table. (It’s easier to say “all” than to choose specific rows.) My code did a “big gulp” – loading the whole table into memory.
This clearly doesn’t scale, but it is enough to work with. Many interesting data sets have hundreds, not millions, of rows; that strategy works for them.
Data Sources | Tabbed File | Which database? | Table | Rows | |
0 | In-memory | Line/Field/Char | Hardcoded | N/A | – |
1 | Tabbed file | Line/Field/Char | Hardcoded | – | |
Many | Database (more later) | Line/Field/Char | Select 1 of N | All |
Scaling Up
The first database version used a non-scalable “load all” strategy. The next version moved to “load 1 row at a time” – a rare move from many to one. The code used the same basic query, but added a clause limiting the return to a single row.
Reading one row at a time is not a good solution for production use – it creates too much traffic between the app and the database. But it was a good intermediate step.
A cache helps with this problem: rather than re-fetching a row every time, you put a row in the cache and check there before going back out to the database.
Because of the way you use the system, you normally don’t need the whole database. You may only look at a screen’s worth of data before changing the query, so you don’t waste time pulling in all data only to ignore most of it.
One to Many (Cache)
From fetching one row at a time, we can make our queries return many rows (a tunable parameter). For example, we might load blocks of 50 rows at a time into our cache. Odds are good that if you access row N, you may need row N+1 next, and have decent odds of finding it ready.
In general, you want the cache to be smaller than the whole database. That brings you to another decision: how do you decide what rows to kick out (and reuse) when the cache gets full? We used the LRU policy (“Least Recently Used”): kick out the rows that haven’t been used in the longest time.
Can we work with data at scale? Yes, it’s much easier now. Even if the underlying data uses many tables, we could create a table or view (virtual table) that holds the data we want to look at. It can be arbitrarily large, and we’ll just load the parts we’re looking at, plus a bit of data we’re likely to need soon.
User Interface for More Choices
Until here, we’ve relied on a single hard-coded database with a specific table. The next step was to introduce a user interface that allows us to choose which database to use: from 0 to many choices.
Instead of one hard-coded table, we allowed selection of an arbitrary table. This table has a set of fields. By default, we load all of them. We then added a mechanism to “de-select” (or “re-select”) zero or more fields.
Now you could choose one database (from a directory), one table (from those in the database), and the fields you wanted (from the selected table). Our choices were opening up.
Multiple Tables and Joins
Requiring all data to be in one table isn’t very flexible, so we next added the ability to choose multiple tables.
If you’re not familiar with SQL, this may sound more useful than it is. A query with two tables, and no restrictions, results in the cross-product: every possible combination of any row from the first table paired with each row of the second table. Legitimate? Yes. Useful? Only rarely.
One way SQL lets you get more useful results is with joins – constraints that limit us to “interesting” results. There are several types of joins, but we’re going to restrict ourselves to inner joins – where a value can be taken from each table – and with an “equals” relationship, such as:
Class.instructor_fk = Person.id
We added support for one, then an arbitrary number of joins.
Caching | Incr. Load | UI – Choose DB | UI – Choose table | UI – Choose Fields | UI – Joins | |
0 | Load all => load 1 | “Big gulp” | Hardcoded | Hardcoded | Omit None (use all) | No joins |
1 | – | 1 at a time | Pick 1 | Pick 1 | – | 1 join |
Many | Unlimited then LRU | Tunable: N at a time | – | Pick N | Omit N | N joins |
Conclusion
We can easily imagine more things we want: more file formats, locations, brands of database, etc.
Zero-One-Many was a helpful guide in breaking down features and suggesting the natural next step.
When we want to do new things, we can add one at a time, growing from zero, to one, to many.
References
“From 0 to Composite (and Back Again)”, https://xp123.com/articles/from-0-to-composite-and-back-again/ – Using Zero-One-Many to help with evolutionary design.
“TDD Guided by ZOMBIES”, http://blog.wingman-sw.com/tdd-guided-by-zombies – James Grenning’s mnemonic for figuring out the next TDD test.
“Twenty Ways to Split Stories”, https://xp123.com/articles/twenty-ways-to-split-stories/ – Using Zero-One-Many at the story-splitting level.
“Zero one infinity rule”, https://en.wikipedia.org/wiki/Zero_one_infinity_rule – Retrieved 2020-12-07.