13 min read

Opinion on code readability, part 1

Opinion on code readability, part 1
Photo by Tim Mossholder / Unsplash

My first draft of this post was originally about 7000 words. And when I saw it at the end, and with one of the opinions being a length and cognitive load, decided maybe it is better to split some things up. And then it grew even beyond that number. So best of luck.

In most cases it is not a big deal, you will pick a poison of your choice and go forward. And then you send your solution forward, to be reviewed. So as you did your research, did the other members of your review process? And there is a big chance that their interpretation of the same material is not the same as yours. And who's to say that your is the correct one? Or that your colleagues have even read the same thing, they could have just as easily discovered something that conforms with their way of thinking?

One such topic that I encounter every time is around the topic of code readability. As you read and learn along the way you will develop a certain set of preferences and opinions on what that means for you. And so will others. One of the reasons why I am putting mine in writing is to see in the future how my views have changed. Others are to communicate my views and in the future, if I am working with anyone that reads it, we can reflect on it. And talking about the written word is much easier, at least for me, than talking about abstract subjects that have more opinions than ever. I will be addressing how I view solutions on all layers of it. From solution level, directory structure, module/project, classes/files, naming, etc. I strongly believe that all these layers, when put together contribute to readability and understanding of the written word. This is all still being worked on, I am putting in context everything that for me contributes to the code readability.

πŸ’‘
Readability refers to the ease with which a reader can understand written text.

The directory structure

One of the first things that comes to my mind, when I open a new solution that will give me insights into the project, will be the directory structures within. This can mean more than what your framework of choice expects, as not all of them are equal and work in the same way. So I am not here talking about solution structure, only, as that is a part of the equation. One of the possible examples out there is shown below:

solution
	.gitignore
	README.md
	/src
		/module_one
		/module_two
	/tests
		/unit
		/integration
		/e2e
	/documentation
		/design
		/api
	/scripts
	/deployment

For me, this already paints a picture of what I can expect when opening the code. It already gives me some points about where to start and what to look at. Or at least, from where my possible questions may start. I would check the documentation, how to run the project, how it is deployed, etc. I also get a notion of how the code is organized, what architecture or naming convention is following, what kind of tests are implemented, and more. Before asking any questions, first I would try following the documentation and seeing if that works. It is not always the case, I know I am sometimes to be blamed as well for forgetting to update things like these. And if I can see things that don't work as expected because documentation is not updated, that would be my first PR on the project. Get it up to the snuff. Leave it in a better state than you found it, I think it goes.

πŸ’‘
There are some nice projects out there, that you can install and run from your CLI. Most of the systems come with something even, like a tree script. Running it will give an output similar to above and then you can start from default and keep increasing levels to see how deep the roots go. Every OS I have used has something or can be installed as a package and the name is pretty much consistent across all of them, with slight differences in parameters.

Documentation

There are different layers of documentation out there. Functional, non-functional, technical, etc. After glancing over what is in there, the next place I would visit is the README.md or its equivalent in the project. Something that would be a starting place for diving deeper to understand how to run the project, how to deploy it, what I need to set up, and so on. It should be an entry point to find everything else related to the project.

I often trick myself into thinking that is not needed. It is self-explanatory. In most cases, it is not. Yeah, just because I think it is clear, doesn't mean it will be for the next person in line. Yeah, it is a dotnet project, you have docker-compose.yaml in there and you are good to go. Nope. What about any integrations, keys, dependencies that you can't run locally, databases, etc? It is not just simple as that. I worked on many projects and I am also to be blamed for similar situations. And then being annoyed by people asking legitimate questions. Yes, it is not the most fun part of the work but it pays in dividends later on.

So what do I think this part of the context should have? It is a good question and I don't have a good answer. This would be my starting point:

# Project Name

## Introduction

Briefly describe the purpose and functionality of your project. Explain what problem it solves or what it aims to achieve.

## Technology Stack

List the technologies, languages, and frameworks used in the project. For example:

- Backend: Node.js, Express.js
- Frontend: React, Redux
- Database: PostgreSQL
- Others: Docker, AWS

## Dependencies

Detail any external dependencies your project requires, and provide instructions or links on how to install them. Provide a high level diagram of dependencies and integrations. 

## Getting Started

### Prerequisites

List any prerequisites required to run your project, like Node.js, npm/yarn, Docker, etc. Provide links/guide where to find any secrets needed to run project locally and warnings in case of possible impact on environmenets. 

### Installation

Step-by-step instructions to install your project. For example:

1. Clone the repo:
	git clone https://github.com/yourusername/yourprojectname.git

2. Getting it ready
	cd yourprojectname
	npm install

### Running the project
Explain how to run project, how to configure neccessary environemnt variables and so on. For example:
	set API_KEY=some-secret-value
	npm run start

Ensure that someonew new can't mess things up with clear configuration and warnings. If they don't know, you can't blame them if someone drops your production database. 

## Deployment

Provide detailed instructions on how to deploy the project, possibly covering different environments like staging and production. Also a reading material to understand the technologies used in this part of the process. 

## Additional resources

- Project documentation
- API documentation
- Wiki or similar
- Issue tracker

The README shouldn't be a book. It should be a clear set of guidelines on how to get you up and running. Describe the bones of your project and how it sits in the rest of the system. For anything more, there should be links to your more detailed documentation in Additional resources the section. Where more details about the domain and business will be described, hopefully, in detail. Or maybe a directory within your solution, whatever works for you. Functional and non-functional requirements. And anything that provides more context about implementation later on.

With the increased complexity that we're working in, when it comes to documentation the balance is hard to find. What should be documented and how? Who is the intended audience, etc. But for the technical aspect of the project, I find that getting me up and running fast, with a clear set of points on what and how just puts you in a better mood. And then you don't need to bounce around, asking questions and whatnot. There are metrics for everything out there, so I assume there is one for this as well, but I would say that getting you up and running and being able to run the project you will be working on should take hours. Not days or weeks. Naturally, you won't understand everything but you will have something to play around and learn as you go. Instead of being stuck reading documentation and meetings. Meetings should be there to answer any questions you may have or to present you with a context of what you will be working on.

Functional documentation

Functional documentation provides direction, acting as a guide that outlines what the software is designed to do. What is the problem you're solving? It bridges the gap between technical and non-technical stakeholders, ensuring that everyone involved has a clear picture of the project objectives and requirements. This alignment is vital for steering the project towards its intended goals and avoiding costly misunderstandings or misalignments.

It is a valuable reference that aids in the development. It offers a clear framework and specifications, reducing the likelihood of deviations that could lead to feature creep or misinterpretation of requirements. This not only streamlines development but also simplifies maintenance and future enhancements, as the documentation serves as a reliable blueprint of the software's intended functionality.

Moreover, functional documentation is indispensable for quality assurance. It provides a basis for creating test cases and scenarios, ensuring that all aspects of the software are thoroughly tested against the defined requirements.

It may not be the fun part of the work, but it does save a lot of time. The clearer you can make your documentation, the better. The first thing I would expect, before the first meeting with stakeholders, is there is something I can read and understand. I will have questions that will improve my understanding and documentation as well. It is a living thing, so should be updated with new findings and understanding.

Non-functional documentation

The bigger the team or company, the more layers in decision-making you will have. This will come with guidelines on how development teams work, what should be thought of when implementing a feature, and so on. The non-functional documentation focuses on the 'how' rather than the 'what,' detailing the operational aspects of a software system. It encompasses requirements related to performance, security, scalability, reliability, and maintainability - elements that define the quality and effectiveness of the final product.

It guides developers in following the standards, ensuring that the software not only meets its functional objectives but also adheres to critical quality standards. This is crucial for the software's performance optimization and robustness. In terms of maintenance and scalability, non-functional documentation is important. It provides a set of chosen standards for evaluating the system's current performance and guides future enhancements, ensuring that the software can continue to evolve without compromising on quality.

Moreover, in today’s environment where security and data privacy are getting challenged daily, non-functional documentation offers clear guidelines and standards for safeguarding user data, thus fulfilling legal and ethical responsibilities.

Architectural Decision Records

One of the concepts I try having in most companies I worked in, is a way to document decisions and architecture choices that may have "contrasting" opinions. When you first look at them. I find that the concept of ADRs is a good way that document those. And when the question comes in again, I forward the link to the topic in question. There I would like to find the context around the topic, design, and consequences.

Sometimes there is just X vs. Y. And if you ever want to deliver something, decisions need to be made. And then just make one. Document the reasons and points behind it and go forward. And if turns out not to be the best one, you can reflect and learn from it. Improve. But don't be stuck in endless debates about what is the right way, because I am certain whatever path you choose, you will find something. That will have the opposing party go: I told you so.

And hence a need for something to get you up and running. And prevent every meeting from starting on the same note: Why?

Architecture, paradigms, patterns, principles...

Now we come to the first "big" part that will have, in my opinion, an impact on code readability. Directly. That is: What architecture, paradigms, patterns, and principles are we following in this solution?

With a list getting larger and larger, this one is a fun discussion to have. And then when you start mixing and matching things, it is a great way to put yourself into the shoes of an artist. That is making a masterpiece that in the end he can't even explain. At one point will just try to add meaning to a mix of paint, colors, and shapes.

Thus, choices here matter and should be well documented and clear. Monolithic, SOA, microservices, n-tier, event-driven... Just talking about architecture is a day of discussion sometimes. Depending on your "architect" went to some conference recently and saw on a "hello world" example how to use a hammer to fix a window. I am not saying this is not an important topic to decide on. I am saying try understanding your problem and see what you wish to evaluate. And if there is a place to evolve instead of being stuck with, potentially, wrong choices when you started.

So how does this impact your code and its readability? All of the aforementioned have some styles and naming conventions. To code organization, naming, and so on. So that answers it, as they will influence your implementation details, at least on the organizational structure if not even on the implementation level. And I do expect, when opening a code base, to see this being in there. Diverging from these things adds a cognitive load and me asking: Ok, it says that we're doing X but I can't map this to the code?

So if you're saying that you're doing N-tier, I do expect to find something like this in the code and its organization:

MyApplication/
β”‚
β”œβ”€β”€ PresentationLayer/
β”‚   β”œβ”€β”€ Views/
β”‚   β”‚   β”œβ”€β”€ HomeView.html
β”‚   β”‚   β”œβ”€β”€ LoginView.html
β”‚   β”‚   └── ...
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   β”œβ”€β”€ HomeController.cs
β”‚   β”‚   β”œβ”€β”€ AccountController.cs
β”‚   β”‚   └── ...
β”‚   └── Static/
β”‚       β”œβ”€β”€ css/
β”‚       β”œβ”€β”€ js/
β”‚       └── images/
β”‚
β”œβ”€β”€ BusinessLogicLayer/
β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”œβ”€β”€ UserService.cs
β”‚   β”‚   β”œβ”€β”€ ProductService.cs
β”‚   β”‚   └── ...
β”‚   └── Interfaces/
β”‚       β”œβ”€β”€ IUserService.cs
β”‚       β”œβ”€β”€ IProductService.cs
β”‚       └── ...
β”‚
β”œβ”€β”€ DataAccessLayer/
β”‚   β”œβ”€β”€ Repositories/
β”‚   β”‚   β”œβ”€β”€ UserRepository.cs
β”‚   β”‚   β”œβ”€β”€ ProductRepository.cs
β”‚   β”‚   └── ...
β”‚   β”œβ”€β”€ Context/
β”‚   β”‚   β”œβ”€β”€ DatabaseContext.cs
β”‚   β”‚   └── ...
β”‚   └── Interfaces/
β”‚       β”œβ”€β”€ IUserRepository.cs
β”‚       β”œβ”€β”€ IProductRepository.cs
β”‚       └── ...
β”‚
β”œβ”€β”€ DataLayer/
β”‚   β”œβ”€β”€ Models/
β”‚   β”‚   β”œβ”€β”€ User.cs
β”‚   β”‚   β”œβ”€β”€ Product.cs
β”‚   β”‚   └── ...
β”‚   └── Enums/
β”‚       β”œβ”€β”€ UserType.cs
β”‚       β”œβ”€β”€ OrderStatus.cs
β”‚       └── ...
β”‚
β”œβ”€β”€ Common/
β”‚   β”œβ”€β”€ Utilities/
β”‚   β”œβ”€β”€ Constants/
β”‚   └── Exceptions/
β”‚
└── Tests/
    β”œβ”€β”€ UnitTests/
    β”œβ”€β”€ IntegrationTests/
    └── MockData/

Any deviation from this should be documented with clear reasons why. I am willing to even accept: Originally it was a good idea, but we grew and never had time to refactor. This is on the backlog to be improved and refactored in the future.

The same goes for any patterns you may need in your code. It may be clear to you as the original implementor that there was a reason for using something like a Chain Of Responsibility. Or whatever other one you need. With clear documentation how to implement and add a new implementation of it. Especially in the case of some "generic" libraries you have done. Show me how to add and test it, making sure things will continue to work, and so on.

Then on to the principles and paradigms. The coding standards and guidelines should reflect the set of choices your team stands behind. And follows in their day-to-day work. There is no need to document them, as they have their description provided at the time of creation. Explain what poison of choice you made and what works for you and point towards the original documentation. No fuss, no need to explain different takes on it. Otherwise, it is not the insert name of whatever here. It is some "spin" on it and opens a door to discussions every time. If you need to adapt it to your views and opinions, it should be documented. Where the original piece is a reference material and base for your own.

I also have a write-up on this subject in one of my previous posts: Architecture, design patterns, why and why not?

Tech stack

Your choice of languages and stack, impacted by choices in the previous section, will also have an impact on your code base. The ecosystem comes with its paradigms, frameworks, and libraries, with their own opinions.

Different programming languages offer varied levels of readability. For instance, Python is regarded for its clean and intuitive syntax, making it a preferred choice for projects where readability is a priority. On the other hand, languages like C++ or Rust, while powerful, often require more complex syntax and a deeper understanding of programming paradigms, impacting the immediate readability for newcomers or less experienced developers. Example of it in Python:

import requests

def fetch_data_from_api(url):
    try:
        response = requests.get(url)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as errh:
        print(f"Http Error: {errh}")
    except requests.exceptions.ConnectionError as errc:
        print(f"Error Connecting: {errc}")
    except requests.exceptions.Timeout as errt:
        print(f"Timeout Error: {errt}")
    except requests.exceptions.RequestException as err:
        print(f"Error: {err}")

# Example usage
url = "https://api.example.com/data"
data = fetch_data_from_api(url)
print(data)

vs. Rust:

use reqwest;
use std::error::Error;

async fn fetch_data_from_api(url: &str) -> Result<reqwest::Response, Box<dyn Error>> {
    let response = reqwest::get(url).await?;
    Ok(response)
}

#[tokio::main]
async fn main() {
    let url = "https://api.example.com/data";
    match fetch_data_from_api(url).await {
        Ok(response) => {
            if response.status().is_success() {
                let data = response.text().await.unwrap();
                println!("{}", data);
            } else {
                println!("Request failed with status: {}", response.status());
            }
        }
        Err(e) => println!("Request failed: {}", e),
    }
}

Depending on your experience both are understandable, but Rust's example comes with a bit "more". It looks simpler, with less code maybe. While on the other hand, it requires some reading if you're not accustomed to it. And depending on the team this can translate into a bit of a head-scratching when looking into it.

The choice of frameworks and libraries significantly influences readability. Frameworks with a steep learning curve or complex configurations can add layers of complexity. Conversely, frameworks that promote convention over configuration, like Ruby on Rails, can enhance readability and speed up development.

Then you add the mix of your dependencies, like caches, databases, infrastructure, etc. Are you using libraries (like ORMs) or writing things yourself (SQL), etc.? This all impacts the readability for me, and how they're implemented in your code. Another example:

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

# Set up the engine and session
engine = create_engine('sqlite:///example.db')
Session = sessionmaker(bind=engine)
session = Session()

# Retrieve a user
user = session.query(User).filter(User.id == 1).first()
if user:
    print(f"User Found: {user.name}, {user.email}")
else:
    print("User not found.")

vs.

import sqlite3

class User:
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return f"<User(name='{self.name}', email='{self.email}')>"

# Connect to the SQLite database
conn = sqlite3.connect('example.db')
cursor = conn.cursor()

# Retrieve a user
user_id = 1
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()

if row:
    user = User(id=row[0], name=row[1], email=row[2])
    print(f"User Found: {user}")
else:
    print("User not found.")

# Close the connection
conn.close()

All of these choices will have a different level of code readability. That is heavily impacted by experience and knowledge. And personal preferences and what you consider a clean code. And the tech stack you work with. Depending on these choices, I am more willing to introduce some abstractions, styles, and so on. In others, I would be inclined to remove some as they're already there with the usage of libraries or frameworks.

Summary

In this part, I tried to detail everything "around" the code that will influence my understanding of it and its implementation. In the coming part 2 of this post, I will be dealing with context as well, but more related to implementation and finally about the code. And then diving deeper into the implementation level and what I am looking in there.

See you there.