Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / Agile

Modularization Isn’t Just About Code

4.75/5 (5 votes)
25 May 2017CPOL9 min read 12.1K  
Let’s take a look at the forces you need to consider when modularizing your system.

Click here to download IncrediBuild for free and accelerate your C++ development.

Breaking a large project into more maintainable modules is more than splitting code across files. It involves componentization, distribution, developer considerations, ease of debugging, the effectiveness of testing, performance concerns, scripts, tools, and more. Then, after all that, you can consider the structure of the code itself. Let’s take a look at the forces you need to consider when modularizing your system.

Classes != Files

Let’s begin with an important point in large-scale software development: good object-oriented design doesn’t equate to ideal code modularization. In a nutshell, this means it’s okay to break down a single C++ class into multiple source files. You can even break classes out across directories (or packages in other languages). In fact, this is one advantage of C++ over Java; it allows you to have different logical and physical class representations when it comes to the filesystem. Let’s look at some strategies to achieve this.

File length considerations

Breaking out code across multiple files can be based simply on file length. There are different philosophies here: no more than a screenful or two of code, to as many lines as it takes to define a discrete class or pattern. The best answer is a guideline somewhere in between. Other approaches to modularization are based on system logical and physical breakdown, such as componentization, which we’ll look at next.

Consider Components and Subsystems

Componentization involves breaking systems into logical pieces that do something, where these pieces may include multiple source code files. For example, you can use interfaces (via IDL or abstract base classes) and concrete classes to divide contract from implementation, or user interface widgets that make up a single visual component but are made up of multiple code modules. Both examples can implement a one-to-many relationship in terms of modularization (see Figure 1).

Image 1

Figure 1 - Single components (i.e. a class or UI component) can have a one-to-many relationship when it comes to files.

Embrace Packages/Namespaces/Hierarchy

Packages aren’t the same as components, and they certainly aren’t source files. Packages fall somewhere in between. However, developers often put little consideration into them because they simply equate a package with a directory. But that’s the point. Organization through an abstraction is what code is all about, and packages should be a bigger part of your strategy.

For languages that support it directly, start with package definition first, not source files. For languages that don’t explicitly support packages, such as C++, enforce them through namespaces, directories, or some sort of hierarchy such as inheritance or containment. The code in Listing 1, for example, uses namespaces to help modularize classes foo and bar.

namespace my_module { 
    class foo { };
    class bar { };
}
Listing 1 - Using a C++ namespace to modularize code via an enforced hierarchy.

Along with this, you can place the modularized classes in a directory that matches the name of the namespace. Going further, this pattern is useful to help segregate legacy from new, enhanced code as shown in Listing 2.

namespace MyServer {
    void doThis();
    void doThat();
}

namespace NewServer {
    MyServer oldServer;

    void doSomethingNew();
    void doSsomethingElseNew();
}
Listing 2 - C++ namespaces help modularize legacy from new code.

Again, using a modular directory structure that matches the namespace helps further separate new from old C++ code. The same directory approach can be applied to C++ inheritance trees.

JavaScript is not HTML

Avoid putting code in a location simply because it’s convenient. Although we’re focused on C++ code here, let’s look at a JavaScript example to make a point—For instance, embedding JavaScript within a web page that uses it is a common practice based on convenience, but this also limits the code’s reusability and findability, and it can hinder collaboration.

Breaking code into proper modules allows sets of developers to work on them separately, increases the chance of reuse, and may even lead to new sources of revenue, as the code in question may have value beyond the system you’re building. This is a common practice for companies employing a service-based architecture, as well as in API design.

Code isn’t Just Code

When it comes to modularization, don’t forget other system components such as build scripts (i.e. make, ant, maven, gradle), automated tests, deployment scripts, documentation and code- generation tools (i.e. UML and IDL), and so on.

Declaration and Definition

Other modularization guidelines that work well include:

  • Separate when you can: you don’t need to include a class’s definition code in the same module, component, or file where it’s declared. Fortunately, C++ helps here, as you usually have separate header and source files, but there are cases where smaller classes are defined in place, or declared as inline. Don’t sacrifice modular correctness and readability for nominal performance gains.
  • Combine when it makes sense: on the flip side, groups of smaller, related classes can be defined within a single header file, or have their common methods placed within a single source file, if it makes sense to do so. The general guideline here is: feel free to combine if it doesn’t violate the object-oriented design, file length considerations, and componentization rules described earlier.
  • Avoid compile-time coupling: in general, any change to a concrete class that requires client code (i.e. code that uses the class) to be recompiled indicates tight coupling. Using some form of dynamic class loading, such as the use of abstract base classes (see Figure 1 above), and factory classes helps tremendously when it comes to modularization. This also allows for future refactoring when further modularization is needed due to system growth.

So far we’ve looked at code and overall software design factors for modularization. Let’s take a step back and look at other factors next.

Agile Considerations

The Agile development methodology, with sprint-based cycles, feedback loops, and hyper-collaboration has taken hold in the development community. This approach to development can have an impact on your overall system modularization.

Quick and Iterative

Agile demands a more rapid and iterative approach to development and deployment. The structure of your system can affect how nimble and agile your organization will become. Structuring your code with sub-system deployment efficiency and support will help in the long run. For example, ensure that your code breakdown encourages as much parallel and group-based effort as possible.

When there’s contention at the source, component, subsystem, and even server level bottlenecks are created, and developers are blocked from making progress. Use the same iterative approach to modularization as you do software design—Make small but continuous changes and optimizations based on experience and feedback as you move along from sprint to sprint.

Continuous Development

The use of Agile and the related DevOps often benefit from a continuous development system. This means you may need to better modularize and structure your code to work with modern build tools and environments. The benefits include efficient, reliable, and automated builds and deployments.

Collaboration and Pair Programming

Proper modularization helps promote Agile through additional collaboration, the use of remote programmers working from home, or programmers working in pairs. Some companies take this to an extreme to achieve 24-hour development cycles based on programmer shifts and the geographical distribution of resources. All of this demands proper design, modularization, and tools.

Structure for Testing

Writing code is only part of the software development lifecycle. You need to design and modularize your system so that it can be properly tested as well. Here are some points to consider when doing so:

  • Mind your dependencies: Building components that are directly dependant on one another results in coupling. In terms of testing, this can broaden the effort, as a change in one will mean regression-testing the others. Through separation of concerns and other design patterns, you can modularize to avoid this.
  • Enable black-box testing: componentization through services and API-driven design can help you test discrete components individually and prove them to be stable before integration testing.
  • Consider ease of debugging: Does your system modularization hinder or help the use of debugging tools? Can you easily follow and describe code execution flow? How easily does it help you identify and contain errors when they occur? With the right structure, you can enable each of these, and help new developers learn your system more quickly to boot.

In general, avoid patterns of componentization that make it difficult to debug and test your overall system. This includes the overuse of C++ macros, platform-specific component models, dependency injection, and so on. Again, there are tradeoffs here in terms of overall design versus modularization that need to be considered.

Performance Considerations

So far we’ve looked at reasons and ways to break a system down, and modularize in a way that makes sense. However, don’t forget to consider performance in each decision you make. For example, good software design uses abstraction to properly break down complexity, but some of the techniques and modularization that result can also impact performance. These include:

  • Too much data copying: Too much modularization and the overuse of componentization can result in excessive distribution and marshaling (copying) of data. This can greatly impact performance, especially if translation and security implications enter the mix.
  • Too many caches: Related to the previous point, where modularization and abstraction are taken to the extreme, extra layers of data caching can result. Adverse effects include large memory usage, negative impact on garbage collection for applicable languages, and more.
  • Ability to integrate managed code: Looking beyond C++, other popular managed code platforms (i.e. Java, Ruby, Python, and so on) can be easily and efficiently integrated only if your C++ system code is modularized the right way.

Structure for Future Growth

Up to now, we’ve looked at code, development methodology, testing, and performance factors to consider when modularizing a system. Another big factor that affects your modularization strategy includes the consideration of future growth. The most important factors include:

  • Modularization for reuse: imposing structure on the relatively small code base of a new or growing system might seem like overkill, but the benefits will be paid back soon enough.
  • Monolithic versus libraries: Using a “building block” approach to large software system composition via the development of internal libraries is rarely used, but leads to a very readable and very maintainable system.
  • Future distribution: Even if all parts of your system are running together today, plan for how they might be split across servers and even data centers in the future.
  • Platform insulation: Consider future OS porting, even language porting (i.e. C++ -> Java, or Java -> C#). It’s rare to see modern systems built in a single language or platform. Consider full-stack development in your modularization efforts.
  • Cloud and mobile: How easily might your system (or parts of it) be moved to the cloud? Can you make the code run as part of a mobile application? Consider what can be done now to enable both, while insulating yourself from total disruption of future technology shifts.

To sum up, it’s never too soon nor too late to consider modularization or refactoring efforts. Always consider structure and modularization for many factors, including collaboration, performance, testing, the future restructuring of system components, and the ability to distribute components in the cloud and on mobile devices.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)