DEV Community

yifang dong
yifang dong

Posted on

How to read code

Reading code is essential for a developer. We need to understand the current code to add new features, find bugs, or do refactoring. Most of the time we are working and reworking on some familiar modules, so we only need to focus on a quite small scope of code. But what if we need to work on an unknown module. How to start and get the 80% most important information quickly. From the code reading workshop, we got the following tips:

Start from the functional description

When we get a new module, the first thing is not to dive into the code, but to understand functionally speaking what’s the module used for. If it’s a UI module, manipulate a bit with the screen it creates. If it’s a domain module, understand the basic business logic it represents. If it’s a module for a certain process, comprehend the main steps of the process. We need to know what is the module used for before studying how the code achieves it.

Find the entry point class

The entry point class defines the boundary of the code. The entry point seems transparent when we work on a known project because usually we are not supposed to modify the entrance frequently. But to work on a project which we don’t have any knowledge, it’s always good to firstly find the entry point class. So that we can focus on code after and ignore all the code before the entry point class. Starting from that at least we know where to put our first debugger.

Ask the question to ourselves: How the code decomposes

For the first analysis, there’s only one aim: to answer the question of how the code decomposes. Without a goal, we are easily attracted by some implementations and lost in the code jungle. The initial analysis should avoid going too deeply. Usually, the larger the codebase is, the shallower we should dig in it.

Here are several approaches to discover the decomposition:

Glance through the packages to have a general image of the module

The package organization gives us a high-level decomposition clue. When we see a UI module that has packages named models, views, controllers, we can guess the module uses an MVC pattern. When we see a module that has packages named services, sessions, repositories, databases, we can find a layered relationship between them. Some packages use their domain languages as a name like queues, aggregates, filters, if we don’t know about the domain, it’s hard for us to figure out what it stands for. When we see some packages like tools, helpers, or utilities, we know we can skip them in the first analysis. However, sometimes we may find nothing senseful because of the ambiguity or evolution of the module.

Imports are helpful to discover the dependency

By looking at the imports of each class, we can quickly build a dependency graph. From imports, we can analyze what are the internal and external dependencies. In a well-designed module, if a class has lots of external dependencies, it is normally a boundary class that should have little logic in it. From those classes, we can build the skeleton of the module. UML generated by IntelliJ can be an assistant to discover the dependency but if we are not careful about the direction to explore the dependency graph, it goes quickly to be very complex to read with classes and their arrows in all directions.

Get more information from interfaces, attributes, and methods

We go through interfaces to understand the main function. From attributes, we can see what is encapsulated. We can separate them by which is used for reading which is used for writing or is used for both. Or we can separate them by in which step it is used. Some times when the module is not well designed or it’s quite simple and small, the decomposition may be based on method rather than class. In this case, we need to dive into the method to analyze.

Recognize pattern can accelerate analysis

When we find some patterns in some parts of the code, we can suppose what the rest of the code will do. In this situation, we are not reading the code, we just verify our hypothesis. We know where to go and what to look at, so we can analyze the code much quicker. This also means if we gathered lots of patterns in our head, we would read the code quicker.

Check the commit history if we find something strange

Commit history gives us an idea about the code is new or old, it was modified frequently or it has rarely been modified since it was created. If we find some code is inconsistent with the context, we can check the commit history. If it’s in the same commit, maybe there is a hidden reason that we don’t know. If not, then the inconsistency may come from the developer thought differently with the previous one. We can also check the story description of this commit to know why the code was added like that if we want to go further.

Tests are useful when we want to understand a specific part of the code.

Sometimes tests are useful also. Unit tests can help us to understand a class, integration tests can help us to understand a whole business logic. Some tests are using the Given-When-Then style. They are executable documentation that gives us concrete examples of what is the expected behavior.

Top comments (0)