In the previous post, I introduced call graphs as a way to view the structure of software. Today, I want to talk about the aspect of software that isn’t structure: behavior.
The behavior of a piece of code is simply what it does, as far as anyone outside it can tell. Behavior and structure are somewhat independent. We can change structure without changing behavior.
For example, these two JavaScript functions have the same behavior (adding an array of numbers) but very different structures:
function addV1(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
function addV2(numbers) {
const plus = (a, b) => a + b;
return numbers.reduce(plus, 0);
}
The fact that the functions do different things on the inside (addV1
defines a variable sum
and addV2
defines a function plus
) doesn’t matter as far as their behavior is concerned, because these implementation details aren’t visible from the outside. From the perspective of someone who wants to add a list of numbers, these functions are essentially interchangeable. Any function with the same behavior will do. If it walks like a duck and quacks like a duck… it might as well be a duck.
The behavior of a piece of code often has subtle details that don’t show up in an informal summary like “it adds a list of numbers.” For instance, the addV3
function below looks very similar to addV2
, but has a subtle bug due to the fact that we’re no longer passing 0
to reduce
. Let me know in the comments if you can figure out what’s different about this function’s behavior:
function addV3(numbers) {
const plus = (a, b) => a + b;
return numbers.reduce(plus);
}
In a future post, I’ll give a more formal definition of “behavior” that will let us precisely capture details like this.
Let’s look at another example: here’s a function that sends an email to a specified recipient
:
import sendmail from "sendmail";
function sendEmail(recipient) {
sendmail()({
from: "me@example.com",
to: recipient,
subject: "Hello!",
html: "This is just an example.",
});
}
The behavior of this function is simple to state informally: it just… sends an email, with the subject and HTML body you see there.
One more example—perhaps a more practical one. Here’s a function that takes a restaurant order and adds its price to the bill total:
let bill = 0;
function order(item) {
bill += item.price;
}
Here, the externally visible behavior of order
is that bill
gets updated. Other functions might be able to see the value of bill
(in fact, if they couldn’t, there would be no point in assigning to it!) Since stuff outside the order
function cares about whether bill
gets the correct total, the assignment to bill
is part of order
’s behavior.
What’s the point?
We can use the concept of behavior to describe various code-changing techniques.
Two examples:
Refactoring is the technique of making small, reversible changes to code that alter its structure without affecting its behavior. As long as all our little refactorings are behavior-preserving, we can be confident we’re not introducing bugs.
When we write tests, we want our assertions to be about the code’s behavior—because that’s what the users of the code care about.
I plan to write (much) more about these techniques in future posts. If you’d like to be notified when they’re published, you can add yourself as a recipient
below.