I feel there's a tendency to think of data as being transport-only. But often there is data inside a program that isn't designed at all. Sometimes there are comments written about it, but most of the time the data is implicitly known. The lack of explicitness allows for an undefined black hole that's usually coupled with code inside a function somewhere.
Explicitness starts with writing it down. One reason why people write things down is to put that knowledge down for others to read. This helps people contribute because others (and even yourself, later on) can know the flow of your program.
Another reason is that act of writing those things down can actually help us learn by strengthening internal connections and by getting facts to a single place.
Explicitly defining the data and how it flows in a program and also designing around it gives three benefits. The first is that there is a clear definition to reference. The second (arguably the most important) is that we can now reason about that definition to find edge-cases. And finally, it gives us the ability to change the data arbitrarily as long as it fits within that definition.
Data-driven programming doesn't require an input source
A common belief is that if a program isn't reading data, there's no use for a data-driven design. More often than not, there is data being read, just not as input from the outside.
How do we find the hidden data inside a program? A good bet is to look at functions which see a lot of use, each time with different arguments. User-interace functions like
draw_rect are often the culprit and may get called hundreds of times throughout rendering.
I'm a firm believer that good examples should be used over "Don't do this!" bad examples. I feel that a good example of data-driven design is this calculator app for iOS written in Scheme.
While the following may look foreign to those unfamiliar to Scheme (or Lisp), it is a lot less complicated than it seems at first glance.
(define keypad `(( ( (#\A "AC") (#\M "MC") #\C (,delchar ,glgui_keypad_delete.img) ) ( (#\a "Ans") (#\m "Mem") (#\p "M+") (#\q "M-") ) ( #\( #\) (#\S ,sqrt.img) #\/ ) ( #\7 #\8 #\9 #\* ) ( #\4 #\5 #\6 #\- ) ( #\1 #\2 #\3 #\+ ) ( (#\0 "0" 2.) #\. (#\= "=" 1. ,DarkOrange)) )))
The design is straight-forward with each row of the keypad having four buttons. Each button can have a short or a long form where the short form just describes a simple key and the long form allows overriding defaults, e.g. use two columns instead of one.
symbol ; short form (symbol [ui-representation [column-count [color]]]) ; long form
I feel it's a good example because we can change the keypad without much deliberation about what else might change in the program. If we wanted to inverse the keypad (1, 2, 3 on top; 7, 8, 9 on bottom) it really is as simple as just editting the structure above.
The meat is in the symbol of the short form (or in the first element of the long form).
#\1 is actually a symbolic notation for the character
1. Each button has a single character attached to it which isn't necessarily what is displayed, e.g. the
#\M button is displayed as "MC".
Not only is form represented as data, but function is too.
Take this ludicrous example: imagine this calculator having a paid and free version. The free version has two less functions than the paid version. So, I would take those two functions, make sure the
ui-representation remains the same but the
symbol changes to one that means "You need the paid version for this function."
( #\$ "+" ) ( #\$ "*" )
This calculator example is a tad contrived but should drive home the point that fundamental changes in the flow of a program should not always require fundamental changes in the core of the code. If the program is designed around the data inside, then changing the program is as easy as changing the data.