Readability through Conceptual Compression

ยท

3 min read

Improving code readability is an ongoing journey for both, my future self and colleagues who will maintain code Iโ€™ve written. Programming should be viewed as a two-fold task: giving machines instructions and making those instructions clear to people. Well-named variables and organized code can make the difference between a collaborative codebase and a complex labyrinth.

What makes code readable

  • Strong foundational knowledge

  • Proficiency in the programming language

  • Grasping the business domain

  • Understanding the problem at hand

  • Effective communication of intent

Mastering programming basics and the language used sets the stage, but the key lies in how well your code bridges the business domain and the problem solution and how you put that in the hands of the reader. A real challenge to explain a solution via abstractions. Your code needs to speak, and as a real novel, it should be a consistent, balanced, and focused story.

Conceptual compression: A Primer

Conceptual compression is about reducing complexity through abstraction. It allows for multiple elements of a concept to be encapsulated in a more manageable form so they can be orchestrated to tell the story, reusing the novel analogy.

How can it be achieved with different language constructs?

Variable naming: A quick win

One way to do it:

const u = getCurrentUser();
if (project.ownerId === req.session.userId || userPermission.name === 'MANAGE_PROJECTS') { /* ... */ }

A better way:

const currentUser = getCurrentUser();
const hasPermission = userPermission.name === 'MANAGE_PROJECTS';
const isProjectOwner = project.ownerId === req.session.userId;
if (hasPermission || isProjectOwner) { /* ... */ }

Functions for clarity

Organized function structures encapsulating small units of logic:

function canUpdateProject ({ project, requesterId }) {
  const isOwner = project.ownerId === requesterId

  return isOwner
    ? true
    : hasUpdatePermission({ id: project.id, userId: requesterId })
}

//----------------

async function ensurePermissions ({ project, requesterId }) {
  const canUpdateProject = await canUpdateProject({ project, requesterId })

  if (!canUpdateProject) throw new AuthorizationError()
}

//----------------

async function updateProject (id, requesterId, params) {
  const project = Projects.find(id)

  await ensurePermissions({ project, requesterId })

  return Projects.update(id, params)
}

//-----------------

app.put('/project/:projectId', async ({ request, response }) => {
  const { projectId } = request.params
  const { name, description, category } = request.projectParams

  const currentUserId = currentUserId(request)

  const updatedProject = await updateProject(projectId, currentUserId, {
    name,
    description,
    category
  })

  const presentedProject = presentProject(updatedProject)
  response.json(presentedProject)
})

Modules for scalability

Modules offer an elegant way to manage complexity by encapsulating common functionalities. They present a clean, straightforward interface while hiding internal complexities, an important step in isolating domains of knowledge.

Here's an example reimagining the updateProject endpoint with modules:

// src/projects/commands/index.js
import ensurePermissions from 'not-relevant-now';
import Projects from 'not-relevant-now';

const updateProject = async function (id, requesterId, params) {
  const project = Projects.find(id);
  await ensurePermissions({ project, requesterId });
  return Projects.update(id, params);
};

export default {
  updateProject
};

// src/projects/web/index.js
import commands from 'src/projects/commands';

app.put('/project/:projectId', async ({ request, response }) => {
  const currentUserId = currentUserId(request);
  const updatedProject = await commands.updateProject(request.params.projectId, currentUserId, request.projectParams);
  response.json(presentProject(updatedProject));
});

By relocating updateProject to a commands module, we encapsulate actions, making the primary endpoint cleaner, more understandable, and easier to debug. Modules allow internal functionalities to evolve independently, making future updates less disruptive.

Closing thoughts

Navigating the complexities of business logic often leads to intricate coding challenges. Conceptual compression serves as a powerful tool in this regard, helping to simplify how programs are structured by creating effective abstractions. This practice not only enhances code readability but also improves both maintainability and scalability.

In upcoming posts, I plan to delve into how these principles align with Domain-Driven Design and Hexagonal Architecture. For the time being, I remain committed to leveraging the concept of Conceptual Compression, as I consider it not only an important part of solving problems with code but also the ultimate act of kindness and empathy towards future developers.

ย