DEV Community

Cover image for 🔧 Mastering Modularization: A Beginner's Guide to Organizing Complex Software Systems
🧑‍💻 Kamil Bączek
🧑‍💻 Kamil Bączek

Posted on • Updated on • Originally published at artofsoftwaredesign.net

🔧 Mastering Modularization: A Beginner's Guide to Organizing Complex Software Systems

 ⚡ Tl;dr

  • Modularization is a method of dividing complex systems into smaller, manageable parts for better management and understanding.

  • It improves the efficiency, reliability, and maintainability of software projects by organizing code into modules.
    It reduces cognitive load for developers by decreasing the amount of information they need to process at one time, making complex systems easier to understand and preventing burnout.

  • Modules in software development can be thought of as building blocks, like lego pieces.
    Each module has a unique set of public interfaces, data structures or messages that act as contracts for other developers to use.

  • When working with modules, it's important to treat them as black boxes and only interact with them through their defined public interfaces to avoid coupling and improve the modularity of the system.

  • In .NET, a module is a self-contained piece of code that serves a specific purpose or function and can be imported and used in other pieces of code.
    Assemblies are used to group code in .NET because they allow for a higher level of encapsulation (using internal access), which allows developers to control the level of access other code has to the members of a type and helps to protect the implementation details of a type or member.

  • To make implementation visible for test, you can use attribute in the csproj file and specify the test project assembly name.

🧩 Why Developers Neglect Modularization?

One reason developers may not prioritize modularization is a lack of knowledge on the subject. Developers often learn languages and frameworks from documentation, which may not emphasize best practices for organizing code and creating modules.

🤷‍♂️ Another factor may be a lack of understanding about business processes and system domain, which can lead to unrelated pieces of code being closely tied together.

🤔 Additionally, developers may focus too much on technologies and frameworks that help them build a project quickly, rather than considering business processes and connections between them.

😟 Poor design and a lack of modularization can lead to the need for a project to be completely rewritten, while using certain frameworks or technologies may not necessarily have the same impact.

🧠 Why is modularization considered so valuable in software development?

Modularization 🧩 is a method of dividing complex systems into smaller, manageable parts for better management and understanding. It is based on the principle of "divide and conquer" and improves the efficiency, reliability, and maintainability 🔧 of software projects by organizing code into modules.

It also reduces cognitive load 🧠 for developers by decreasing the amount of information they need to process at one time, making complex systems easier to understand and preventing burnout 💀.

🧱The Lego Principle: Use Modules as the Building Blocks of Your Software Systems

Modules in software development can be thought of as building blocks, like lego pieces. Each module has a unique set of public interfaces, data structures or messages that act as contracts for other developers to use. When working with modules, it's important to treat them as black boxes and only interact with them through their defined public interfaces. This helps to avoid coupling and improve the modularity of the system.

One common mistake I've seen in projects is when all classes are made public, which means modularization doesn't exist. Everything is touchable and can be coupled with any other element, leading to a fast path to a "big ball of mud" 💩💩 that we want to avoid 🙅‍♂️.

big ball of mud examples
High coupling module example. Avoid it !!!

To prevent this, it's key to use the internal access modifier 🔒 and only expose a clean, well-defined API using public interfaces and data structures that are easy to consume. Test-driven development (TDD) 🧪 can also help with this, as the first consumer of your module will be yourself. By following these practices, you can create a modular system that is easy to understand, maintain, and extend.

🧱 Module Example in .NET

A module is a self-contained piece of code that serves a specific purpose or function, and can be imported and used in other pieces of code. It helps to organize and make code more reusable, and is often used in software development to break up large projects into smaller, more manageable pieces.

🤖 Module in .NET

Module is single Assembly or set of assemblies grouped by Solution Folder

Modules view

Divstack Estimation Tool - Emails module view

Divstack.Company.Estimation.Tool.Emails.Core is example of sub module of Emails Module. This module gives technical capability to sending Emails. Purpose is to be reusable.

Why assemblies are used to group code? Why not dictionary?

The Divstack.Company.Estimation.Tool.Emails.Core assembly is an example of a submodule of the Emails module that provides the technical capability for sending emails. It is designed to be reusable.

One reason for using assemblies to group code is that they allow for a higher level of encapsulation. Access modifiers such as internal and public allow developers to control the level of access other code has to the members of a type, which helps to protect the implementation details of a type or member.

In contrast, using a dictionary approach would not allow for the same level of encapsulation, as it would not support access modifiers and would always provide access to the class.

💪🏼 Let’s go to example

csharp module view

In this module there is only two public classes.



public interface IEmailSender
{
    void Send(string email, string subject, string text);
}


Enter fullscreen mode Exit fullscreen mode


public static class EmailCoreModule
{
    public static IServiceCollection AddCore(this IServiceCollection services)
    {
        services.AddScoped<IEmailSender, EmailSender>();
        services.AddSingleton<IMailConfiguration, MailConfiguration>();
        services.AddScoped<IMailTemplateReader, MailTemplateReader>();

        return services;
    }
}


Enter fullscreen mode Exit fullscreen mode

Other classes are implementation details

A module is like a black box for other developers. They only have access to the interface provided by the module, and don't need to worry about how it is implemented. This can be either a service like SendGrid or a standard SMTP server. The advantage of this approach is that it doesn't matter how the module is implemented, as long as it provides the expected functionality through the interface.

📦 The Power of the Black Box Approach

A public API should be stable, meaning that it should not change frequently or unexpectedly. It is important for the developer to carefully design the interface of the API to make it easy to understand and use 🧑‍💼. This is similar to the importance of designing a pure REST API. One way to ensure that the API is developer-friendly is to use unit testing 🧪, as tests can serve as the first "client" of the code 💻. That is why it is often recommended to use a technique called Test-Driven Development (TDD) 🧑‍💻, in which tests are written before the implementation code. By writing tests first 🧑‍🔬, the developer can focus on designing a clean and easy-to-use interface for the API.

🔧 How to test encapsulated module implementation?

A common problem with understanding unit tests is understanding the word 'Unit'. In our modules, we will have a public interfaces. In our unit tests, we will be testing the implementation of the interface whose has public access modifier.



public interface IEmailSender
{
    void Send(string email, string subject, string text);
}


Enter fullscreen mode Exit fullscreen mode


internal sealed class EmailSender : IEmailSender
{
    private const string EmailType = "text/html";
    private readonly IBackgroundProcessQueue _backgroundProcessQueue;
    private readonly IMailConfiguration _mailConfiguration;

    public EmailSender(IMailConfiguration mailConfiguration, IBackgroundProcessQueue backgroundProcessQueue)
    {
        _mailConfiguration = mailConfiguration;
        _backgroundProcessQueue = backgroundProcessQueue;
    }

    public void Send(string email, string subject, string text)
    {
        if (!IsNullOrEmpty(_mailConfiguration.MailFrom))
        {
            _backgroundProcessQueue.Enqueue(() => SendMessageAsync(email, subject, text));
        }
    }

    public async Task SendMessageAsync(string email, string subject, string text)
    {
        var client = new SendGridClient(_mailConfiguration.ApiKey);

        var message = BuildMessage(email, subject, text);

        await client.SendEmailAsync(message);
    }

    private SendGridMessage BuildMessage(string email, string subject, string text)
    {
        var message = new SendGridMessage();
        message.AddTo(email);
        message.AddContent(EmailType, text);

        var fromEmailAddress = new EmailAddress(_mailConfiguration.MailFrom);
        message.SetFrom(fromEmailAddress);
        message.SetSubject(subject);

        return message;
    }
}


Enter fullscreen mode Exit fullscreen mode

To make it visible for test project we have to add this code to Divstack.Company.Estimation.Tool.Emails.Core.csproj file.



<ItemGroup>
<InternalsVisibleTo Include="Divstack.Company.Estimation.Tool.Emails.Core.UnitTests" />
</ItemGroup>

Enter fullscreen mode Exit fullscreen mode




🔖 Summary

It is important to note that modularization is a powerful technique that can help improve the overall quality of your code and make it more maintainable and testable. With the proper use of the internal access modifier and following best practices, developers can create a modular system that is easy to understand, maintain, and extend.

This was a basic introduction to modularization in .NET. In the future, more information will be provided on topics such as:

  • Module extraction
  • Module definition heuristics
  • Modular monolith architecture
  • Feature slices

This series will be continued..

🔗 References

Follow me

🚀 If you're looking for ways to improve your software development skills and stay up to date with the latest trends and best practices, be sure to follow me on dev.to!📚 I regularly share valuable insights and tips on software development, as well as updates on my latest projects.

Speaking of projects, I'd like to invite you to check out my modular monolith project on GitHub💻, called Estimation Tool. It's a great example of how you can use modularization to build a more scalable and maintainable system. The project is open-source and well-documented, so you can learn by example and see how I applied the concepts of modularization in practice.

https://github.com/kamilbaczek/Estimation-Tool 🔗

So if you're looking to level up your development skills, be sure to follow me on dev.to and check out my modular monolith project on GitHub. Happy coding!🎉

Top comments (1)

Collapse
 
alirezamgh profile image
alireza

great article, thank you