iTranslated by AI
Programming by Intention: Creating Maintainable Code with Proper Abstraction
Update: I've written a new article.
I read the book Beyond Legacy Code. It introduced a practice called "Programming by Intent," so I've summarized it in my own words.
What is Programming by Intent?
When you are about to expose an API, method, or service to the outside world, do not put the implementation inside the method. Delegate the implementation to another method.
Beyond Legacy Code p211
In the parts that interact with the outside world (such as APIs or methods), you should not write implementation-level code. Instead, you should delegate to other methods or classes to keep the abstraction level high.
For example, if there were a public API to draw a single card from a deck, here are some bad and good examples.
Bad Example
function drawTramp () {
// Initialization
const trumps = [1,2,3,4,5,6,7,8,9,10,11,12,13];
// Shuffle
for (let i = trumps.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[trumps[i], trumps[j]] = [trumps[j], trumps[i]];
}
// Take the first card
return trumps[0]
}
Good Example
// Pattern 1: Class delegation
function drawTramp () {
const trump = new Trump();
trump.init();
trump.shuffle();
return trump.drawFirstOne();
}
// Pattern 2: Function delegation
function drawTramp () {
const trumps = init();
const shuffled = shuffle(trumps);
return drawFirstOne(shuffled);
}
The shuffle code was borrowed from here.
Details for the good examples are omitted.
The book also mentions that mixing abstraction and implementation is not ideal.
If you delegate some code while implementing others, you are forced to switch between abstraction and implementation perspectives in your mind. This leads to frequent mental task switching. While it might not seem like much at once, continuous task switching becomes a very taxing process.
Using the previous example as a reference, it might look like this.
Bad Example: Code with mixed abstraction and implementation
function drawTramp () {
// Initialization
const trumps = [1,2,3,4,5,6,7,8,9,10,11,12,13];
// Shuffle
const shuffled = shuffle(trumps)
// Take the first card
return shuffled[0]
}
In this way, the contents of a public API are easier to understand if you list "what to do" rather than "how to do it." Imagine creating a TODO list.
I'm making a hamburger!
∧,,∧
(;`・ω・) 。・゚・⌒) I'm making fried rice!!
/ o━ヽニニフ))
しー-J
Since I was able to convey the general meaning of "Programming by Intent" in the previous section, next, I'd like to think together about what abstraction actually is.
Objectives and means are two sides of the same coin
Objectives and means are inseparable. "What to do" and "how to do it" are also two sides of the same coin. It's simply that the level of abstraction is different. I'd like to explain this point.
In the UX Design Textbook, the concept of the "hierarchy of goals" is explained on page 81.

For example, when you have a goal like "I want to travel smoothly to the sea by car," a means to achieve that is "I want to be able to get directions to the destination." Next, for the goal "I want to be able to get directions to the destination," a means to achieve that is "I want to set the destination in the car navigation system." A means becomes a goal, and vice versa.
There is often criticism that "the means have become the end," but it is natural for a means to become a goal. It only becomes problematic when the higher-level objective cannot be achieved as a result (this is what is meant by putting the cart before the horse).
Now that we understand that objectives and means are two sides of the same coin, next, we will consider the appropriate level of abstraction through the process of creating a hamburger recipe.
Let's think of a hamburger recipe
I looked up a basic hamburger recipe. It probably goes something like this. (I've omitted the specific measurements as they aren't essential here.)
1. Mince the onion.
2. Sauté the onion until it turns golden brown.
3. Add ground meat, the sautéed onion, breadcrumbs, milk, and salt. Mix well, form into palm-sized patties, and shape them while removing air to create the hamburger mixture.
4. Heat oil in a pan over medium heat. Sear one side of the patties for 2-3 minutes until browned.
5. Once one side is browned, flip them over, reduce to low heat, cover with a lid, and steam-cook for about 10 minutes.
6. Mix ketchup, Worcestershire sauce, and soy sauce to make the sauce.
7. Plate the dish.
From this list, try to distinguish between the implementation level and the abstraction level. Since it's a recipe, it's quite implementation-heavy.
Here's how I thought about it. How about you?
Implementation Level
- Mix ketchup, Worcestershire sauce, and soy sauce to make the sauce.
- Add ground meat, sautéed onion, breadcrumbs, milk, and salt. Mix well, form into palm-sized patties, and shape while removing air.
Abstraction Level
- Mince.
- Sauté until golden brown.
- Sear until browned.
- Steam-cook.
- Plate.
Even what I called the "implementation level" is just a lower level of abstraction, so it could be broken down even further.
Aligning abstraction levels
This recipe seems to mix abstraction and implementation levels. Let's try raising the level of abstraction a bit more.
1. Prepare the hamburger patties.
2. Cook the hamburgers.
3. Make the sauce.
4. Plate the dish.
It's much simpler now—though it's a recipe no one would bother following! (lol) In actual programming, however, having this high a level of abstraction is better because it allows you to see what's happening at a glance. Additionally, the granularity of these abstraction levels is consistent (it's a bit frustrating that I can only describe this intuitively).
Appropriate Abstraction Level
In the previous section, we aligned the abstraction levels, so the process of mincing onions and sautéing them until golden brown now sits under the process of "preparing the hamburger patties."
Now, if someone asks you what "mincing" is, how would you answer? Would you explain it based on the concept of "mincing"? If someone didn't know what mincing was and you had to explain the process specifically, it would be quite a task. I've tried it below.
The mincing process
- First, cut the onion in half vertically.
- Leave the root part intact and peel the skin from one half; save the other half if you aren't using it.
- Place the cut surface on the cutting board. At this time, the root part should be at the top.
- While keeping the top part connected, cut vertically at 5mm intervals.
- While keeping the top part connected, cut horizontally to the cut surface at 5–8mm intervals. Rough intervals are fine.
- Cut horizontally at 5mm intervals up to the top part. It will start becoming dice-shaped from where you cut.
- Cut any large or unevenly remaining parts as appropriate.
Done.
Which abstraction level is appropriate depends on the reader's experience and level of knowledge.
The phrase "sauté until golden brown" can be rephrased at the same level of abstraction as "sauté until the Maillard reaction is visually observable." However, even if you wrote this in a recipe, only a few experts would understand it. In that regard, "golden brown" (or "fox-colored" as we often say in Japanese) is an excellent choice of words.
By choosing the right metaphors, you can imagine what is happening without having to look at the details.
Considerations regarding setters
Let's apply what we've learned here to setters in programming.
We have a User class like the one below. The properties are encapsulated as private, and to change the status, you need to call setStatus. This is a common pattern.
type UserStatus = 'TEMPORARY' | 'ACTIVE' | 'PENDING';
class User {
private name: string;
private status: UserStatus;
constructor(args: {name: string; status: UserStatus}){
this.name = args.name;
this.status = args.status;
}
setStatus(status: UserStatus): void {
this.status = status;
}
}
In the next example, it's the same User class, but instead of setStatus, methods called getActivated and getPending are defined.
type UserStatus = 'TEMPORARY' | 'ACTIVE' | 'PENDING';
class User {
private name: string;
private status: UserStatus;
constructor(args: {name: string; status: UserStatus}){
this.name = args.name;
this.status = args.status;
}
getActivated(): void {
this.status = 'ACTIVE';
}
getPending(): void {
this.status = 'PENDING';
}
}
This is a difference in the level of abstraction.

setStatus is closer to the implementation level, while getActivated is at a higher level of abstraction. Since there are many reasons to change a status, we define a method for each one. Because the reason for changing code is determined at a higher level of abstraction (for example, a change to perform some kind of check before user activation), writing code with a high level of abstraction like this makes it easier to maintain.
Discussion