Designing Interfaces for Reuse
A subroutine is a procedural abstraction whose interface can be used without knowing how the procedure is implemented.
Using Code You Never Open
Suppose you need a list of numbers sorted, the text pulled out of a web page, or a date converted into a different format. You can often accomplish this by calling a subroutine that someone else wrote, and you may never look at the code inside it. You only need to know what to give the subroutine and what it will hand back to you.
This article is about why that works, and about the idea that makes it safe: the details of how a task is carried out can stay hidden from the people who merely use the result. Keep one question in mind as you read. If you were handed a subroutine with no access to its code, what would you still need to know in order to use it correctly? By the end you should be able to answer that precisely, and explain why the answer is so small.
Predicting the Effect of a Rewrite
Before reading on, picture a rounding subroutine that a program calls. Its author rewrites the body so that it uses a completely different method. The new version still returns the same results the documentation promises. Would the calling program need to be edited?
What do you think happens?
If the body of a subroutine is rewritten with a different method but still returns the promised results, must the calling program be edited?
Reveal answer
No. The calling program does not need to be edited.
The caller depends on the interface, namely how to invoke the subroutine and what it produces. As long as that interface and its promised results stay the same, the internal method can change without any change to the caller. The rest of this article explains why.
Caller, Interface, and Implementation
A subroutine is described as a procedural abstraction. The word abstraction here means that the subroutine stands in for a procedure, which is a sequence of steps that accomplishes some job. The caller does not work with those steps directly. It works with the subroutine's interface, and the interface is the abstraction you actually use. The interface tells you how to invoke the procedure and what it will produce. It does not require you to know how the procedure is carried out.
The same relationship has three parts, and it helps to name them separately. The caller is the code that wants the job done. The interface is the agreed way of asking and answering. The implementation is the actual sequence of steps that runs when the call happens. The caller and the interface are what the rest of a program sees. The implementation is what the interface stands in front of.
The diagram shows the chain of responsibility. The caller depends on the interface. The interface, in turn, is backed by an implementation that the caller never has to inspect. From the outside, you reason about what goes in and what comes out, and the middle is someone else's concern.
The Black Box Principle in Use
The source describes the black box principle as another name for the idea just discussed: using a subroutine or an abstraction without knowing its internal implementation. The phrase is informal, but the picture is useful. Think of a black box as something with an input slot and an output slot. You feed it what it needs, you receive what it produces, and the mechanism between those slots is not part of your reasoning.
Consider a small subroutine that averages a list of scores. The example below is an original illustration, not taken from the source. Notice that the caller needs only the name, the parameter, and the returned value to use it correctly.
def average_of(numbers):
total = 0
for n in numbers:
total += n
return total / len(numbers)
score = average_of([80, 90, 70])
print(score)- 1The function is defined with one parameter called numbers. This is part of the interface.
- 2Inside the body, a running total is built up with a loop. A caller does not need to know this.
- 3The return statement hands back the average. This is the output slot of the black box.
- 4The caller passes a list and stores the result in score. It never inspects total or the loop.
- 5Printing score shows 80.0, the average of 80, 90, and 70.
- The def line is the interface's name and parameter
- The loop and total variable are internal details the caller never sees
- The return line is the value handed back through the interface
80.0Tracing a Call Through the Boundary
Follow one call step by step. The caller supplies an input through the interface. The interface passes that input to the hidden implementation. The implementation carries out its steps and produces a result, which travels back out through the interface to the caller. At no point does the caller need to know which steps ran in between. The caller's entire view is a name, some inputs, and a returned result. The implementation sits behind the interface and is reachable only through it.
A caller who never opens the implementation can still write a correct program. That is precisely what the black box principle promises.
Information Hiding and Changing the Inside
An important aspect of abstraction in general is information hiding. An abstraction hides information about what lies behind it, in its implementation. Information hiding is what allows a subroutine's inside to be changed without affecting the programs that use it.
The state change works like this. The body moves from one version to another, while the interface that callers rely on is not part of the change. Because the callers were written against the interface alone, their code does not need to be edited. The diagram below marks what changes and what stays the same.
def average_of(numbers):
total = 0
for n in numbers:
total += n
return total / len(numbers)The body builds a running total with an explicit loop.
def average_of(numbers):
return sum(numbers) / len(numbers)The body uses a built-in sum. The name, parameter, and return value are unchanged, so score = average_of([80, 90, 70]) still prints 80.0 in both versions.
The source also gives two concrete illustrations of this idea. Java control structures can be used on different types of CPU with different underlying machine-language implementations. The inside of a subroutine can be changed without affecting the programs in which it is used.
What a Caller Depends On
The interface is a contract. Changes to what callers depend on still matter, even when the inside changes freely. The caller relies on the signature, which is the subroutine's name and how it is invoked. It relies on what each parameter means and on the return type. It also relies on the documented behavior, meaning the results the documentation promises. The caller does not rely on internal variables, the specific algorithm, or the data structures used inside the body.
| Detail | Does the caller depend on it? | Example from average_of |
|---|---|---|
| Subroutine name | Yes | average_of must be spelled exactly as defined |
| Parameter meaning | Yes | numbers is a list of scores to average |
| Return type and value | Yes | The caller receives the average, such as 80.0 |
| Documented behavior | Yes | The promised result is the mean of the values |
| Loop or built-in sum | No | How the total is computed |
| Local variable total | No | A name used only inside the body |
The caller's dependencies are the interface. The body's choices are invisible to the caller.
Mistakes That Break the Boundary
Common mistake Reading the body to decide how to call the subroutine
Why it failsThe interface already tells you how to invoke the procedure and what it produces. Relying on the body makes your code depend on details the caller is not meant to use.
Do this insteadRead the name, parameters, return value, and documentation. Use the subroutine according to its interface alone.
Common mistake Assuming any change inside the subroutine is harmless
Why it failsInformation hiding protects callers only when the promised results stay the same. The interface is a contract, so changes to what callers depend on still matter.
Do this insteadReplace the internals freely, but keep the signature, parameter meanings, return type, and documented behavior fixed.
Common mistake Treating information hiding as security
Why it failsThe source describes information hiding as a discipline for separating parts that evolve freely from parts other code depends on, not as protection against attackers.
Do this insteadUse hiding to keep callers independent of implementation changes, not as a way to protect secrets.
Common mistake Changing the parameter order while keeping the name, which silently breaks every caller
Practice: Spot the Dependency
A teammate replaces the body of average_of with a version that sorts the list first and then averages it. The name, parameter, and documented result are unchanged. Explain whether the calling code needs editing, and identify one detail the caller would depend on if the parameter were renamed to scores with a different meaning.
Hints
- Ask which parts of the interface the caller actually relies on.
- Sorting is an internal algorithm choice, the same kind of detail as a loop versus a built-in sum.
- A different meaning for the parameter changes a part of the contract, not just the body.
Key Takeaways
- A subroutine is a procedural abstraction: callers use its interface without knowing how the procedure is implemented.
- The caller, interface, and implementation form a chain. The caller depends on the interface, which is backed by a hidden implementation.
- The black box principle means reasoning about inputs and outputs while leaving the internal mechanism out of your reasoning.
- Information hiding lets the inside of a subroutine change without affecting callers, as long as the interface and its promised results stay the same.
- A caller depends on the signature, parameter meanings, return type, and documented behavior, but not on internal variables, algorithms, or data structures.