The initial intent behind the base/core classes and modules is often rooted in the desire to prevent duplication and promote reusability. However, in the long term, if we don’t implement them with best practices, it becomes apparent that base classes and modules raise many issues. This article explores the problems arising from the implementation of this concept and introduces alternative approaches.
Overview: Duplication and Base/Core classes and modules
Duplication can lead to problems such as inconsistencies, increased debugging efforts, decreased maintainability and code readability, and difficulties in making changes across multiple instances of duplicated code which increase the development costs in the long term.
The base/core, classes and modules concept involves the creation of classes and modules that encapsulate common logic, preventing duplication throughout the project. For instance, in Android development, teams often utilize a Base Fragment to consolidate shared functionality among multiple Fragments. Similarly, modules like core-ui or base-ui may provide UI helpers and components that can be reused across different application parts. This seems a good idea at a glance, but implementing this idea with bad practices can cause problems which is more important than reusability.
The main challenge arises when we attempt to address all problems with a single solution. Using Base and Core naming for the classes and modules leads us to add any functionality that does not fit elsewhere into these classes or modules. For example, consider the case of BaseFragment, initially designed to encapsulate duplication logic like view binding for all fragments. However, over time, this BaseFragment tends to accumulate additional functionalities, such as helper methods for displaying snack bars, callbacks, or other functionalities as per team conventions. A similar situation may occur with a module with a base/core UI name for holding shared UI components and logic across the project.
Clothes are too dirty for the closet but too clean for the laundry. Welcome to “the chair.”
The Hidden Costs: The Problems Caused by Bases/Cores
Using base classes and modules in our Android projects can lead to several issues that impact the overall development process. These issues include dependency problems, increased couplings between components, violations of the Single Responsibility Principle (SRP) and encapsulation, decreased testability, and difficulties maintaining the codebase.
In the provided diagram, the base-ui module consists of various UI components and helper functionalities. Each feature within the project relies on specific components from this module. Similarly, the core-utils module contains extensions for string and collection operations and general-purpose extensions. Other modules depend on these base and core modules to access these components and utilities to use the small functionality they want. Using this approach could lead to violations of the SRP since these components often encompass multiple responsibilities. Let’s consider the example of a module called core-utils, which contains utilities for strings, DI, encryption, and potentially other functionalities. Core utils assume more than one responsibility by encompassing multiple duties within a single module. Regarding testing, let’s consider the scenario where we have a BaseViewModel that contains certain functionalities for our view models. However, for each specific view model that does not utilize these functionalities, we would still need to mock those unused functionalities during testing. This additional effort introduces complexity, potentially making the test setup more complex and fragile.
Example of Base Fragment and ViewModel which I find in an open-source project on GitHub
When inspecting this
BaseViewModelregardless if it’s logical to have these methods or not as utility/base, It becomes apparent that it contains methods/functionality that some children may or may not require, this implementation violates several principles that we mentioned earlier.
These examples and issues highlight the difficulties in maintaining and scaling the project. In the upcoming sections, we will discuss approaches to overcome these problems.
Alternative Approaches: Breaking More, Embracing Composition and Dependency Injection
While there may not be a single ideal solution for fixing these issues, we can explore alternative approaches to mitigate the challenges posed by base/core modules and classes. Our focus will be on discussing strategies that can effectively reduce the problems associated with their usage, promoting simpler and more maintainable code in our Android projects.
Breaking More and More
To ensure a more organized and maintainable codebase, it is crucial to break down functionalities into smaller, focused components, each with a singular purpose with meaningful abstraction. Abstract names like “core” and “base” can inadvertently lead developers to place unrelated functionalities within these classes and modules. To address this concern, we should adopt a more specific naming convention that clearly communicates the responsibility of each module or class. By doing so, developers will have a clearer understanding of where to place specific functionalities, reducing the risk of mixing unrelated code and promoting a more modular and manageable project structure.
Having thin modules with specific duties can be a practical solution to address problems caused by large base modules. This approach resolves issues such as placing unrelated functionalities in modules with abstract names. For instance, a module named “database” should only contain database-related components, not unrelated remote data sources or UI helpers. While the initial build-time may experience a slight increase due to the granularity of small modules, it results in faster incremental builds. Small modules benefit from caching, making recompilation quicker when changes occur. Overall, this approach proves efficient in managing dependencies and improving the codebase’s maintainability, enhancing the overall development experience. Indeed, the same approach can be applied to classes as well. For instance, rather than embedding the functionality to show a Snackbar or any base functionality directly within your BaseFragment or Activity, it is beneficial to use extension functions or utility classes specifically dedicated to handling such actions. By adhering to this principle, each class and method will serve a singular purpose, making them easier to understand, maintain, test and modify.
As we mentioned, breaking a base class into single-purpose classes offers significant advantages in terms of code organization and maintainability. However, using these single-purpose classes while adhering to Object-Oriented Programming principles requires careful consideration.
By applying composition, you can utilize the functionalities of these single-purpose classes. Instead of relying on inheritance, where subclasses depend heavily on the implementation details of the base class, composition allows you to build complex objects by assembling these smaller classes. This reduces the risk of the Fragile Base Class Problem, where changes in the base class inadvertently affect derived classes.
Moreover, composition enables code extensibility. You can easily add or remove functionalities by combining different components, making the system more adaptable to changes without modifying existing classes.
By employing dependency injection, we can seamlessly inject these specific, single-purpose base functionalities into relevant parts of our codebase. Although the advantages of using DI frameworks are not the main focus of this article, it’s worth noting that DI simplifies dependency creation and offers benefits such as improved modularity, testability, and reduced boilerplate code.
Using base/core classes and modules in Android projects initially aims to prevent duplication and promote reusability. While this approach seems logical, it can lead to numerous issues in the long term if not implemented with best practices. However, by breaking functionalities into smaller, focused components and embracing composition and dependency injection, we can overcome these challenges.
In software development, no approach fits all problems perfectly, and we always have to make trade-offs. The best solution depends on the issues we want to solve, even in the case we discuss. Understanding these trade-offs helps us make informed decisions and create a codebase that is easy to maintain and scale.
This article was previously published on proandroiddev.com