⚡ 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 🙅♂️.
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
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
In this module there is only two public
classes.
public interface IEmailSender
{
void Send(string email, string subject, string text);
}
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;
}
}
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);
}
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;
}
}
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>
🔖 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
- Modular project example - Estimation Tool
- A lot of information about modularization with project example
- Great talk about hermetization and architecture that inspired me to start designing systems in modular way
- Courses and materials about modularization DevMentors
- My talk about modular monolith
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)
great article, thank you