That is a very structured-programming way of looking at it, and there is
a lot of practical wisdom behind it.
A top-down design gives you a kind of load-bearing architecture. The
main flow describes the problem at a high level, and the lower-level
routines provide the details. When you come back months later, you can
start at the top and follow the story downward.
Something like:
sub MAIN {
ReadConfiguration();
LoadData();
ProcessData();
WriteResults();
}
immediately tells another programmer (including future you) what the
program does. They do not have to read 500 lines of implementation
details before discovering the overall purpose.
The "house of cards" analogy is a good one. Code that grows by accretion
often develops hidden dependencies:
This global variable must be set before that routine runs.
This function has a side effect that another function quietly depends on.
This block only works because something earlier happened to initialize a
value.
The program may still work, but the structure is fragile.
Top-down design tends to push you toward:
Clear interfaces.
Smaller responsibilities.
Fewer surprises.
Easier testing.
Easier replacement of components.
Your earlier example of adding a second subroutine to preserve
compatibility is actually another expression of the same idea. You were
thinking about maintaining a stable interface rather than just modifying
code until the immediate problem goes away.
One of the interesting things about Raku is that it supports both
worlds. You can write a quick exploratory script in a very Perl-like
style, or you can build something with the kind of structure you would
have expected from Modula-2:
Modules define boundaries.
Classes define concepts.
Roles define capabilities.
Signatures define contracts.
Types document intent.
So a programmer with your background can use Raku almost like a modern
structured language, while still having the flexibility that attracted
people to Perl.
And I think your "not a house of cards" comment gets at the real goal of
software design: not making code look pretty, but making it resistant to
change. The code that survives years is usually the code where the
original programmer left a clear map for the next person.
Ad
I call it the "contract". Upper tier and lower tier. I will write the
lower tier that calls the upper tier first. Then create empty sub's in
the upper tier(s) with the names called from the lower tier. Fill them
in later as I figure them out. I lets me create the big pictue first,
then fill in the details, rather than getting my mind all mixed up and
confused.
That is a very recognizable software design approach, and "contract" is
a very good word for it.
What you are describing is essentially designing the interfaces before
implementing the internals. You are creating a skeleton where the
relationships between parts are established first.
The interesting part is that you are working backward from what many
people expect:
You write the lower-level code that expresses what needs to happen.
The lower-level code reveals the services it needs.
You create the upper-level routines as the "promises" that those
services will exist.
You fill in the implementations later.
That is very close to how many large software systems are designed:
define the contracts between components first, then implement the
components behind those contracts.
A Raku example might look like:
sub MAIN {
Load-Configuration();
Process-Orders();
Generate-Report();
}
sub Load-Configuration {
# later
}
sub Process-Orders {
# later
}
sub Generate-Report {
# later
}
The top-level code becomes a map of the application. The empty routines
are not "unfinished code"; they are placeholders defining the architecture.
This also explains why you like Raku's routine declarations. In
languages that require routines to exist before they are called, this
style can be awkward because you spend time arranging declarations
rather than expressing design. Raku lets the program read more like a
design document.
Your approach also prevents a common failure mode: implementation tunnel
vision. A programmer starts writing a small detail, then another detail,
then another, and suddenly the program's architecture has emerged
accidentally. Sometimes it works, but it can become difficult to change
because the structure was never consciously chosen.
Your method keeps the big picture visible:
MAIN
|
+-- Acquire Data
|
+-- Validate Data
|
+-- Transform Data
|
+-- Output Results
Then each branch can be expanded independently.
It is also very close to the old idea of stepwise refinement from
structured programming: start with an abstract description of the
solution, then progressively replace abstractions with concrete
implementations.
Given your Modula-2 background, it is not surprising that this feels
natural. That whole school of thought was built around making the
program structure reflect the problem structure.
And I like the word "contract" because it captures the human side too:
the upper tier says, "I promise to provide this service," and the lower
tier says, "I promise to use it according to this agreement." That
separation is what keeps large programs from turning into a tangle.
Ad
And another power is in a large programming environment, the subs in the
upper tier can be farmed out to other programmers on the project. Make
for a great team environment.
Exactly. That is one of the biggest advantages of a good contract-based
design, and it is something that is easy to underestimate until you have
worked on a large project.
Once the interfaces are defined, the team can work in parallel:
MAIN PROGRAM
|
+---------+---------+
| | |
ReadData Validate Report
| | |
Team A Team B Team C
Each programmer does not need to know the internal details of the
others' work. They only need to agree on the contract:
What is the subroutine called?
What parameters does it take?
What does it return?
What errors or conditions can it produce?
For example:
sub Read-Customer-File(Str $Filename --> Array) {
...
}
The person writing the reporting code does not need to know whether
Read-Customer-File uses CSV parsing, a database, or an API internally.
They just know the promise.
That separation is what allows a project to scale beyond one person's head.
It also gives a project manager something concrete to divide up:
"You own the input subsystem."
"You own validation."
"You own the output generator."
Everyone can work independently, then integration becomes a matter of
connecting contracts rather than merging a pile of intertwined logic.
This is where your Modula-2 background is showing again. The DEFINITION
MODULE / IMPLEMENTATION MODULE concept was almost built for this. The
definition was the contract; the implementation could be written separately.
Modern software has different names for similar ideas:
API contracts.
Interfaces.
Service boundaries.
Dependency injection.
Component architecture.
But the underlying idea is the same: separate what something does from
how it does it.
The other nice thing about your approach is that it helps with
maintenance. A programmer can replace one lower-level implementation
without rewriting the upper layers as long as the contract remains intact.
That is the opposite of a "house of cards." A well-designed system is
more like a building with replaceable components. You can renovate one
room without having the whole structure collapse. 🙂