Software Modularity — Part 1

Prathamesh Deshmukh
Technogise
Published in
6 min readMay 6, 2023

We will cover different types of measures of modularity in this multi-part blog series

Modularity

Definition

Modularity is an important concept in software development, but what do we mean when we refer to a “module”? The dictionary definition of module is “each of a set of standardized parts or independent units that can be used to construct a more complex structure, such as an item of furniture or a building.” I think of a module as a lego piece that can be used to build complex structures.

At its core, modularity is grouping related code into logical units, which can be a collection of classes in object-oriented languages or functions in functional languages. This logical grouping is independent of physical separation and provides a way to organize code into manageable and reusable pieces.

Most languages have some modularity mechanism which provides the namespaces as well to organize things, like variable, functions/methods, classes. Language like Java requires that its package structure reflects the directory structure of the physical files.

Measuring modularity

Now that we have established what modularity means to us in the software context, we need ways to measure the modularity of our code as well. Researchers have created some language-agnostic metrics to help developers understand modularity.

Let’s focus on three concepts here, cohesion, coupling and connascence.

Cohesion

Cohesion refers to the measure of how related the parts are to one another. Ideally, a cohesive module cannot be divided further which will result in increased coupling.

Following are the range of cohesion measures from best to worst,

Functional cohesion

Every part of the module is related to the other, and the module contains everything essential to function.

Example:

public class Calculator {
private int result;

public int add(int a, int b) {
result = a + b;
return result;
}

public int subtract(int a, int b) {
result = a - b;
return result;
}

public int multiply(int a, int b) {
result = a * b;
return result;
}

public int divide(int a, int b) {
result = a / b;
return result;
}
}

In this example, we have a class called Calculator that contains four methods: add, subtract, multiply, and divide. Each of these methods performs a specific mathematical operation on two input integers and returns the result.

This class demonstrates functional cohesion because every method is related to the others and together they contain everything essential to function as a calculator. Each method has a specific purpose and performs a specific task that is necessary for the overall functionality of the calculator.

Sequential cohesion

Two modules interact, where one outputs data that becomes the input for the other.

Example:

public class DataProcessor {
private FileReader fileReader;
private DataParser dataParser;
private DataWriter dataWriter;

public DataProcessor(FileReader fileReader, DataParser dataParser, DataWriter dataWriter) {
this.fileReader = fileReader;
this.dataParser = dataParser;
this.dataWriter = dataWriter;
}

public void processData() {
String rawData = fileReader.readFile();
Data parsedData = dataParser.parseData(rawData);
dataWriter.writeData(parsedData);
}
}

In this example, we have a class called DataProcessor that processes data by reading it from a file, parsing it, and then writing it to another location. This class demonstrates sequential cohesion because the three modules (FileReader, DataParser, and DataWriter) interact with each other in a specific order, where the output of one module becomes the input of the next module.

The processData() method first reads raw data from a file using the FileReader module, then passes that data to the DataParser module to parse it into a Data object, and finally writes the parsed data to another location using the DataWriter module. Each of these modules performs a specific task in sequence, with the output of one module becoming the input of the next module.

Communicational cohesion

Two modules form a communication chain, where each operates on information and/or contributes to some output.

Example:

public class UserAccount {
private UserDatabase userDatabase;
private UserNotifier userNotifier;

public UserAccount(UserDatabase userDatabase, UserNotifier userNotifier) {
this.userDatabase = userDatabase;
this.userNotifier = userNotifier;
}

public void createUser(String name, String email, String password) {
User user = new User(name, email, password);
userDatabase.saveUser(user);
userNotifier.sendWelcomeEmail(user);
}

public void deleteUser(String email) {
User user = userDatabase.getUserByEmail(email);
userDatabase.deleteUser(user);
userNotifier.sendGoodbyeEmail(user);
}
}

In this example, we have a class called UserAccount that manages user accounts by interacting with two modules: UserDatabase and UserNotifier. This class demonstrates communicational cohesion because the two modules form a communication chain, where each operates on information and/or contributes to some output.

The createUser() method creates a new user object with the provided name, email, and password, saves it to the UserDatabase module, and then sends a welcome email to the user using the UserNotifier module. Similarly, the deleteUser() method retrieves a user object by email from the UserDatabase module, deletes it, and then sends a goodbye email to the user using the UserNotifier module.

In this example, the two modules form a communication chain where the UserAccount class acts as a coordinator between them.

Procedural cohesion

Two modules must execute code in a particular order.

Example:

public class OrderProcessor {
private InventoryManager inventoryManager;
private PaymentProcessor paymentProcessor;
private ShippingManager shippingManager;

public OrderProcessor(InventoryManager inventoryManager, PaymentProcessor paymentProcessor, ShippingManager shippingManager) {
this.inventoryManager = inventoryManager;
this.paymentProcessor = paymentProcessor;
this.shippingManager = shippingManager;
}

public void processOrder(Order order) {
inventoryManager.reserveStock(order);
paymentProcessor.processPayment(order);
shippingManager.shipOrder(order);
}
}

In this example, we have a class called OrderProcessor that processes orders by interacting with three modules: InventoryManager, PaymentProcessor, and ShippingManager. This class demonstrates procedural cohesion because the two modules must execute code in a particular order.

The processOrder() method first reserves stock for the order using the InventoryManager module, then processes the payment using the PaymentProcessor module, and finally ships the order using the ShippingManager module. Each module performs a specific task in sequence, with the output of one module becoming the input of the next module.

Temporal cohesion

Modules are related based on timing dependencies.

Example:

public class SystemInitializer {
private DatabaseInitializer databaseInitializer;
private LoggerInitializer loggerInitializer;
private CacheInitializer cacheInitializer;

public SystemInitializer(DatabaseInitializer databaseInitializer, LoggerInitializer loggerInitializer, CacheInitializer cacheInitializer) {
this.databaseInitializer = databaseInitializer;
this.loggerInitializer = loggerInitializer;
this.cacheInitializer = cacheInitializer;
}

public void initializeSystem() {
databaseInitializer.initializeDatabase();
loggerInitializer.initializeLogger();
cacheInitializer.initializeCache();
}
}

In this example, we have a class called SystemInitializer that initializes different components of the system by interacting with three modules: DatabaseInitializer, LoggerInitializer, and CacheInitializer. Many systems have a list of seemingly unrelated things that must be initialized at system startup, these are temporally cohesive.

Logical cohesion

The data within modules is related logically but not functionally.

Example:

public class StringUtils {
public static boolean isNullOrEmpty(String string) {
return string == null || string.trim().isEmpty();
}

public static String toTitleCase(String string) {
// return title case
}

public static boolean containsIgnoreCase(String string, String substring) {
// return if substring contains
}
}

In this example, we have a class called StringUtils that contains several static methods for operating on strings. This class demonstrates logical cohesion because the methods are related logically but not functionally.

The isNullOrEmpty() method checks whether a given string is null or empty, the toTitleCase() method converts a given string to title case, and the containsIgnoreCase() method checks whether a given string contains a given substring in a case-insensitive manner. Each method operates on strings, but the functions are quite different.

Concidental cohesion

Elements in a module are not related other than being in the same source file; this represents the most negative form of cohesion.

Example:

public class Utils {
public void calculateArea(int length, int breadth) {
// Some code here to calculate area of a rectangle
}

public void validateEmail(String email) {
// Some code here to validate email format
}

public void generateRandomNumber() {
// Some code here to generate a random number
}

}

The class has methods such as calculateArea(), validateEmail(), and generateRandomNumber(), but there is no obvious relationship or shared purpose between them. Each method performs a completely different task, and there is no overall purpose or relationship between them.

This is Part — 1 of the modularity series, I will be covering further concepts like Coupling and Connascence in the coming partial series.

--

--