Code is read far more often than it’s written. Programming is two jobs at once, telling the machine what to do and making that clear to the next person who opens the file. You can tell when someone wrote code for that second reader. The names mean something, the boundaries sit in the right places, and it’s a pleasure to read.
Reading a piece of code takes more than knowing the language. You need the business domain, the problem, and the way the code intends to solve it. That last part is the programmer’s job, and it’s where most code fails. The challenge is to explain a solution through abstractions and how they interact. In other words, to express the intent.
The tool I use for that is conceptual compression, which means reducing complexity through abstraction. Take several parts of a concept, fold them into one name, and reuse the name without explaining it again. Like the word Quidditch in Harry Potter. One word, and every reader knows the whole game.
Compress the right pieces of code into concepts, make those concepts interact at one level of abstraction, and the code gets easy to understand. Three language constructs do the compressing.
Variables
A variable is the cheapest abstraction there is. This conditional builds an important business rule out of implementation details:
const user = /* ... */;
if (project.ownerId === user.userId || user.role === 'MANAGE_PROJECTS') { /* ... */ }
The same rule, with the details named:
const currentUser = /* ... */;
const canManageProjects = currentUser.role === 'MANAGE_PROJECTS';
const isProjectOwner = project.ownerId === currentUser.userId;
if (isProjectOwner || canManageProjects) { /* ... */ }
The compressed concepts make the conditional read almost like plain English.
Functions
When a concept has to cross a function boundary, a function carries it. Say the same check now lives inside an endpoint:
app.put('/project/:projectId', async ({ request, response }) => {
const user = /* ... */;
const project = /* ... */;
const canManageProjects = user.role === 'MANAGE_PROJECTS';
const isProjectOwner = project.ownerId === user.userId;
if (isProjectOwner || canManageProjects) { /* ... */ }
});
The names help, but the function mixes two levels. It pulls the user and the project out of the request, and it decides who may update a project.
function getCurrentUser(request) {
/* ... */
}
function canManageProjects(user) {
return user.role === 'MANAGE_PROJECTS';
}
function isProjectOwner(user, project) {
return project.ownerId === user.userId;
}
app.put('/project/:projectId', async ({ request, response }) => {
const user = getCurrentUser(request);
const project = /* ... */;
if (isProjectOwner(user, project) || canManageProjects(user)) {
/* ... */
}
});
The endpoint is simpler, and it says what it does without the details.
Modules
Splitting concepts across files is a harder problem. Files connect to each other with different strengths, which is a topic for another post. The need is the same either way. Decide what goes together and what doesn’t. Packages, classes, modules, the construct differs by language, so I’ll call them modules.
The same use case, split into a project module:
// src/projects/policies/can-update-project.policy.js
function canManageProjects(actor) {
return actor.role === 'MANAGE_PROJECTS';
}
function isProjectOwner(actor, project) {
return project.ownerId === actor.userId;
}
export function canUpdateProjectPolicy({ actor, project }) {
return isProjectOwner(actor, project) || canManageProjects(actor);
}
// src/projects/use-cases/update-project.use-case.js
import { canUpdateProjectPolicy } from '../policies/can-update-project.policy';
import ProjectManager from '../project-manager';
export async function updateProjectUseCase({ projectId, payload, actor }) {
const project = await ProjectManager.find(projectId);
if (!canUpdateProjectPolicy({ actor, project })) {
/* ... */
}
return ProjectManager.update(projectId, payload);
}
// src/projects/web/index.js
import { updateProjectUseCase } from '../use-cases/update-project.use-case';
import { getCurrentUser } from '../../shared/http/helpers';
app.put('/project/:projectId', async ({ request, response }) => {
const updatedProject = await updateProjectUseCase({
projectId: request.params.projectId,
payload: request.body,
actor: getCurrentUser(request),
});
/* ... */
});
The project module holds everything about projects and splits into smaller ones, policies, use cases, web, each with room for more files. At this level there are many layers of abstraction to keep in balance, and that balance is the new challenge. Take can-update-project.policy. Its one job is to say when a user may update a project. If it also had to run SQL, it would be harder to read, and the concept would leak.
Compressing concepts doesn’t remove the hard decisions about what belongs together and how tightly. Those decisions were always there. Compression makes them explicit.