DEV Community

Cover image for Carving the Path to Modularity: A Lobzik Tool Case Study on the ProtonMail Android App
Mikhail Levchenko
Mikhail Levchenko

Posted on

Carving the Path to Modularity: A Lobzik Tool Case Study on the ProtonMail Android App

Premature and overdue modularisation problems

When you begin building your project, there may be a strong temptation to modularise it right from the start in order to save costs down the line. However, I believe that premature modularisation not only drains your resources but can also hinder the long-term success of your project. At the early stages, your product vision may not be fully formed, and it can undergo significant changes. The module boundaries that you establish initially can quickly become outdated as the project evolves through numerous iterations and achieves success.

However, as your team grows and the number of features starts to accumulate, the tech debt of modularisation can send a chill down your spine. You begin to notice an increase in git conflicts and heisenbugs caused by a lack of clear separation of concerns within your application. Eventually, you reach a point where you declare "enough is enough" and decide to modularise your monolith. But the question remains: where do you begin unraveling this tangled mess? You don't want to stop developers from pumping out new features for your project, so you need to pinpoint the most impactful areas that can be extracted with minimal effort. But how can you achieve this without spending a lot of time delving into the void your codebase had become?

Introducing Lobzik: The Modularisation Toolkit

Having been tasked with modularising the codebase of my work project, with a whopping 200kloc monolith, I embarked on a quest to find a way to reason about modularising this chonky boy. Being an enthusiast of graphs, I was interested in the network of dependencies within the monolith. Soon enough, I discovered that this network could serve as a good place for for community detection algorithms, which could reveal structures that looked like modules. After weeks of experimentation, I successfully devised a way to extract the dependency graph, carefully selected the most suitable community detection methods, and came up with the tricks to yield optimal results.

These insightful findings led to the birth of my pet-project: the Lobzik Gradle Plugin. Rather than relying on complex GUI graph toolkits like Gephi or spinning up Jupyter Notebooks filled with NetworkX Python code, you can effortlessly integrate my tool into your build pipeline. Lobzik provides guidance, pointing you towards the optimal path for modularising your project. However, this tool needs some knowledge to operate, so let this article serve as your guide to use this tool correctly.

Applying Lobzik to the ProtonMail Android App

For the reference project, I've chosen the ProtonMail Android App, which is one of the largest open-source Android apps that has not been modularised yet. With over 50kloc in the main module, it truly represents a monolith that is worth modularising.

cloc app/src/main --include-lang=Kotlin,Java

github.com/AlDanial/cloc v 1.96  T=0.39 s (2388.4 files/s, 249735.1 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Kotlin                         766           8867          16675          52105
Java                           156           2371           3381          13006
-------------------------------------------------------------------------------
SUM:                           922          11238          20056          65111
-------------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

Setting up Lobzik

To start using Lobzik, we need to apply the xyz.mishkun.lobzik plugin in the root build.gradle.kts file:

plugins {
   // ...
   id("xyz.mishkun.lobzik") version "0.6.0"
}
Enter fullscreen mode Exit fullscreen mode

Then, we can set up the basic configuration in the same build.gradle.kts file as shown below:

lobzik {
    monolithModule.set(":app")
    packagePrefix.set("ch.protonmail.android")
    variantName.set("betaDebug")
}
Enter fullscreen mode Exit fullscreen mode

Here, we set the name of our monolith module (notice the ":" in the module name!), the name of the variant we will be analyzing, and the package prefix of our classes. With this configuration, only the code in packages starting with ch.protonmail.android inside the :app module will be checked, using the betaDebug variant. This is crucial for our tool to work, because we don't want to deal with all of the library dependencies and standard kotlin library messing our dependency graph.

Running Lobzik for the first time

Now that we are all set up, we can run Lobzik for the first time using the command:

./gradlew lobzikReport
Enter fullscreen mode Exit fullscreen mode

If everything was set up correctly, you will find build/reports/lobzik/analysis/report.html file in your project root. Now let's take a closer look at how to interpret this report.

Interpreting the Lobzik Report

Lobzik report consists of four sections:

  • Core Candidates
  • Monolith Modules Table
  • Module Graphs
  • Whole Graph

Monolith Modules Table

The first thing that catches our eye is the Monolith Modules Table. It lists all of the modules detected by Lobzik. They can be sorted by several metrics: coductance, cut and monolithCut.

Monolith Modules Table screenshot

The conductance score is the core metric of this part of the report, as it indicates the benefit-to-effort ratio of extracting modules. A lower score is preferable, with a score of 0 indicating that extracting the module requires virtually no effort since it has no dependencies on other modules.

The cut and monolithCut scores show us how many dependencies should be broken to successfully extract the module. It helps to refine the estimates on how much effort we need to extract this module.

The names of the modules are automatically generated from their classes using the TF-IDF method. Clicking on a module name will take us to the detailed report in the Module Graphs section.

Module Graphs

Module Graphs section screenshot with module dependency graph and list of its classes

This section contains per-module detailed reports, each presenting three subsections:

  • Dependency graph of the module and its neighbourhood
  • List of all of classes belonging to this module
  • List of dependencies that need to be broken to extract this module

Whole Graph

This section at the bottom of the report represents the module dependency graph, which can help identify modules that are relatively easier to extract due to their fewer dependencies on the rest of the project.

The "Star" problem

A careful reader may notice that I have omitted the first section of the report, called Core Candidates. This section is collapsed under a spoiler, but it plays a crucial role in enhancing the report's effectiveness. To fully comprehend its value, let's explore what I refer to as the "star" problem.

Graph of dependencies with many connections to the ListUtil.kt node

Let's consider a scenario where we have a class called ListUtil.kt that contains various list utilities. This class is heavily used throughout our codebase, resulting in numerous connections to other nodes in the network. Due to the high degree of connections, our community detection algorithm of choice, the Louvain method, may mistakenly identify this class as the core of a large community. It's important to note that community detection algorithms were initially designed for social networks, where such hubs represent a significant community led by an outstanding individual.

Same graph as above but with ListUtil.kt removed to reveal modularised structure

However, for a codebase modularisation problem this class should be extracted to the core modules. By doing so, we can reveal a better modularisation path for the rest of the code, as depicted in the image above. To assist in visualizing the benefits of extracting such classes, Lobzik offers the ignoredClasses configuration parameter which accepts a list of regexes of class names that should be excluded from the analysis.

lobzik {
    // ...
    ignoredClasses.addAll("^ListUtils$")
}
Enter fullscreen mode Exit fullscreen mode

But how we identify such classes, you may ask? These classes are commonly found in sections responsible for Dependency Injection (DI) and Navigation, as they serve as the glue that connects otherwise loosely coupled features code. It is a good choice to ignore your Application class and well-known utility classes too. But can we automatically identify more core classes if we have already eliminated the obvious ones? This is where the Core Candidates section of the report becomes valuable.

Core Candidates

Core Candidates section screenshot

The Core Candidates section presents a table that consists of the top 95 percentile of classes based on Degree or Authority metrics. Thoroughly reviewing this list can help identify the classes that should be excluded from the report. In the case of ProtonMail, the following classes might be considered for elimination:

lobzik {
    // ...
    ignoredClasses.addAll(
        ".*UserManager$",
        ".*Constants$",
        ".*ProtonMailApiManager$",
        ".*Util.*",
        ".*ProtonMailApplication$",
        ".*ResponseBody$",
        "Base.*",
        ".*Module",
        "^Message$",
        "^User$",
        "^ProtonMailApi$"
    )
}
Enter fullscreen mode Exit fullscreen mode

By eliminating these classes from the report, we can improve our algorithm's performance, measured by the modularity score, going from 0.586 to 0.684. A great improvement! Now we can use Lobzik report to start extracting each of detected 24 modules one by one.

Conclusion

You can find a fork of ProtonMail client with integrated Lobzik on my github. I hope you will enjoy using Lobzik for modularising you codebase. I encourage you to try it and don't hesitate to submit any issues to the project's github

Top comments (0)