Modular programming originated back in the 1960s when developers began breaking up larger software programs into smaller parts. Although this concept is around 6 decades old, it’s still extremely relevant and useful for today’s software developer, and one of the key programming principles we follow here at Tiny.
By the way, this blog is the first in a series on how we use programming principles at Tiny, so keep your eye out for future articles.
Let’s start with some definitions.
What is modular programming?
Modular programming (also referred to as modular architecture) is a general programming concept. It involves separating a program’s functions into independent pieces or building blocks, each containing all the parts needed to execute a single aspect of the functionality. Together, the modules make up the executable application program.
While most people talk about modularity at a file/folder/repository level, I think about modularity on multiple levels:
- Functions within files
- Files within repositories/libraries
- Libraries/repositories within projects
Modularity is about making building blocks, and even building blocks made up of smaller building blocks. Each is separate, strong, and testable, and can be stacked together at the end to create your application. Thinking about modularity in this way also means that you can build it into your code style and architecture. You can create rules and guidelines around what modularity means at each level and how you handle certain things, like how each part is defined and located within files, folders, and libraries.
Modules and APIs
One very useful way modules can interact is through interfaces or APIs. With an API, you only expose what bits the developers need to use the module, while hiding the guts of the code.
In a way, this means your API can act like a contract, defining what the module/library does and how it can be used by outside code. For example, the API might say “this module has this function that does this and returns this”.
With APIs, you can be confident about what parts of the modules should or shouldn’t change without warning. That way, even when you need to change or fix things under the hood in a library, other things can keep using the API and trust that nothing will unexpectedly change out from under them. Plus, it’s also much easier to glance at a module and know what it can be used for (I’ll talk more about this in a bit).
Modular programming at Tiny
At Tiny, we are very fond of modularity. For example, the TinyMCE monorepo contains several modules that each contain code relating to specific functionality. For example:
...and many more.
Each “module” then contains files and folders that follow specific guidelines. Katamari is a good example with its API and Util folders that contain specific kinds of code, as per our standards and guidelines. The API folder then contains a file for each concept that is exposed via Katamari’s API, and they’re all collected in the Main.ts file. This way, if we want to know what methods Katamari has for arrays, we can just glance at the Main.ts to see what Katamari exposes and, from there, go to any of the API files - such as Arr.ts which contains methods for dealing with arrays. This makes it quick and easy to find the code we’re looking for.
As a result of this kind of architecture, TinyMCE itself consists of a big dependency tree of libraries of helper functions (built on more libraries of helper functions!) that are all pulled together at the end to make the editor. It might sound complex, but we find that the benefits are very much worth it.
Why do modular programming?
The purpose of modular programming is to make it easier to develop and maintain large software programs, by breaking them up into smaller parts. It comes with a number of benefits:
Code is easier to read
Modular programming usually makes your code easier to read because it means separating it into functions that each only deal with one aspect of the overall functionality. It can make your files a lot smaller and easier to understand compared to monolithic code. For example, most of our files don’t get over a couple of hundred lines of code, whereas I’ve seen other apps where they’ll have thousands of lines of code in each file, which can make it rather hard to follow and to find specific pieces.
Modular programming can get a bit messy if you split things into too many tiny functions, or if you’re passing data or functions around between too many files. But if you’ve split your modules up sensibly, it works really well - much better than a function that’s hundreds of lines long!
In fact, I think it’s one reason why my team can get away with fewer comments on our code. A small function with a good, descriptive name can help you understand a block of code without needing a comment for it.
Code is easier to test
Software that’s split into distinct modules is also ideal for testing. That’s because tests can be much stricter and more detailed when you test small functions that do less, compared to big functions that do a number of things. This is especially true if you can only test the outputs of a function, not the steps it follows.
Plus, easier testing may mean fewer big, in-depth comments are needed, since tests can act as examples of how code works. If you don’t understand a block of code, checking out the tests can be an easy way to get a good idea of it.
Easily find things later
Modularity involves grouping similar types of functions into their own files and libraries, and splitting out related helper functions into their own files (instead of leaving them mixed in with the core logic code). For example, if you pop over to GitHub, you’ll see that TinyMCE has specific files containing:
- Unicode helper functions
- Helper methods for dealing with Objects
- Helper methods for Arrays
… and the list goes on. Separating functions in this way can make it much quicker and easier to find what you’re looking for later.
With modular programming, you can make finding specific code even easier by creating conventions for file names and locations. For example, the code for each community plugin in TinyMCE follows a specific folder structure that groups files relating to API, UI, and core functionality. If you can guess where a file might be and what the file or function might be called, it’s a lot easier to look for and find code.
Reusability without bloat
A lot of the time, you’ll need to use the same code or function in multiple places. Instead of copying and pasting the code, modularity allows you to pull it from a single source by calling it from whatever module or library it’s in. For example, our Katamari and Sugar libraries get used in most of our other projects, so it’s handy to be able to just add them as dependencies, and be able to handle data structures and DOM manipulation the same way in all our projects.
This reduces bloat and size because we don’t have multiple copies of each bit of code that performs a specific function.
Single source for faster fixes
With each module providing a single source of truth for your specific functions, it minimizes the number of places bugs can occur, and makes it faster to fix when you do get bugs. This reduces the risk of problems caused by two pieces of code relying on slightly different implementations of the same functionality. And if there’s a bug in the code or you need to update the specific function, you only have to fix it in the one module, and everything that uses it will get updated right away. Whereas, if you copied and pasted the code into different places, you could easily miss updating one or two instances.
Note: Occasionally you will need to copy code for a specific reason. When we do this, we follow rules around the name and location of the code to make the code easy to find in the future.
Easier, lower risk updates
With modular programming, every library has a defined API layer, which protects things that use it from changes inside the library. As long as you don’t change the API, there’s a much lower risk of unintentionally breaking code somewhere that relies on whatever you changed. Of course, you still have to be careful, but APIs certainly help to make it clearer when you’re about to change a public function. For example, if you didn’t have explicit APIs and someone changed a function they thought was only used within that same library (but it was actually used elsewhere), they could accidentally break something.
With modular programming, refactoring can be easier. This is due to a number of reasons, but to give you an example, both APIs and following a strict file/folder structure can help if you want to script or automate refactoring.
In late 2017, we converted TinyMCE to TypeScript. As part of this conversion, we also restructured TinyMCE to be more modular, by splitting more of the code out into plugins and libraries. We were then able to leverage this increased modularity and particularly the API structure we introduced when automating parts of the TypeScript conversion. It also helped with manual follow-up work, since we could work in sections, plugin by plugin and library by library. And years later it is still helping us with onboarding new developers, finding and fixing bugs, adding new features, and many other things.
Easier to collaborate
Modular programming is essential where multiple teams need to work on different components of a program. When you have multiple developers working on the same piece of code, it usually results in Git conflicts and various other issues which can be annoying and can slow down the team. If the code is split between more functions, files, and/or repositories, you can decrease the chances of this happening.
You can also assign ownership to specific modules of code, ensuring teams are responsible for their part of the software, and enabling them to break the work down into smaller tasks. The teams (and modules) come together with clearly defined interfaces and public APIs, ensuring everything fits together at deployment.
Are there any downsides?
There’s got to be a reason why some people don’t do modular programming, right? Well, yes.
The alternative to modular programming is usually to create a monolithic application of code, where all your code is (more or less) dumped into a single location. This can result in “spaghetti code” as it’s twisted and tangled, like a big bowl of spaghetti 🍝
There are actually a few reasons why some people still write spaghetti code:
- Code size - Modularity can increase code size and impact performance if you can’t tree shake your dependencies
- Complexity - Sometimes complex file systems aren’t necessary and can add unnecessary overhead
- Security - Monolithic code can make it harder for people to mess with code that the original developer doesn’t want changed, hacked, or pirated
And let’s be real - some people just like to live on the edge. It’s kind of thrilling when you’re not sure what (in thousands of lines of code) is broken 😆
If you’re working on a personal or small project, where the size and complexity is small and there aren’t too many people collaborating on it, you also might not benefit from a modular approach as much.
There are also plenty of older projects that have been around for years which may have become a spaghetti mess over time, especially if they’ve suffered from people tacking code onto the original project without fully understanding the best way to add it. These types of projects sometimes even reach the point where they need a structural overhaul, as they become too difficult to manage.
So, even if your project starts out small, consider whether taking a modular approach from the beginning could help future-proof your code. Certainly, if you expect your project to have a long life or to grow a lot, it might be good to consider modularity from the start, because structural overhauls can be very difficult, time consuming, and hard to fit in when there are features to be written.
The future of modular programming
Modular programming isn’t a new concept, but it’s still very popular. I think it’s important for developers to learn what it is and why it is useful. After all, the way that a number of today’s libraries, frameworks, and package managers are set up for code sharing and dependency management makes modular programming the natural choice.
For example, there are many NPM libraries that only do one thing. And more and more developers are using frameworks that only do specific things, like Bulmer, which is a CSS framework that is fairly simple compared to some of the other popular CSS frameworks. Smaller libraries may not sound as impressive due to having less functionality, but there’s also less learning needed before you can use them, and they are usually smaller, so there’s less code bloat than larger libraries.
Use TinyMCE with a module bundler
Already implementing modular programming principles for your projects, or want to start playing around with the concept?
You can use TinyMCE in your project using a module loader like Webpack or Browserify. Check out details on how in our documentation. We also have specific documentation on how to enable the PowerPaste plugin with a module loader.
Do you do modular programming? Why or why not?
Let’s continue the conversation on Twitter @joinTiny - let us know whether you or your team use modular programming and why. And let us know what your favorite tool is that helps you make your code modular!
Make sure you subscribe to our newsletter so you don’t miss the rest of our blogs in this series!