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. 🙂

Reply via email to