At our agency, a client project typically lasts a couple of months. From the first client contact and the design phase to the implementation and the initial launch, a project roughly takes half a year. But sometimes we develop and maintain a particular software over the course of several years.
For example, we started GED VIZ for the Bertelsmann Foundation in 2012, released it in 2013 and added new features and data every few years. In 2016 we turned the core visualization into a reusable library, refactoring it significantly. The flow data visualization engine is still used today by the European Central Bank (ECB). Another long-lived project is the OECD Data Portal front-end: We started the implementation in 2014, and we are still expanding the codebase.
After the main development phase, we apply fixes and add new features. Typically there is no budget for a major refactoring or even a rewrite. Therefore, in some projects I am stuck with the code I wrote 4-6 years ago and the library stack that was in vogue back then.
Small improvements instead of big rewrites
Migrating to a new set of libraries and tools is a substantial investment that may pay off soon. It may ease the maintenance. It may reduce the cost of change. It allows to iterate faster and to implement new features more quickly. It may reduce errors, improve the robustness and the performance. Eventually, such an investment may reduce the total cost of ownership.
But when a client cannot make this investment, we look for ways how to gradually improve the existing codebase.
Learning from long-term projects
For some web developers, being stuck with an existing codebase is a nightmare. They use the word “legacy” in a derogatory way for code that they have not written recently.
For me, the opposite is true. Maintaining a project’s code over a couple of years taught me more about software development than multiple short-lived, fire-and-forget projects.
Most importantly, it confronts me with code that I have written years ago. Decisions I have made years ago have consequences on the whole system today. Decisions I make today determine the fate of the system in the long term.
Often I am wondering: what would I do different today? What needs to be improved? Like every developer, I sometimes have the urge to destroy everything and build it from scratch.
Avoid complex structures
By “complex” I do not simply mean large. Every non-trivial project has lots of logic in it. Lots of cases to consider and to test. Different data to process.
Complexity comes from interweaving different concerns. One cannot avoid that entirely, but I have learned to separate the concerns first and then to bring them back in a controlled way.
The next complex structure is an object. In its simplest form, an object maps strings to arbitrary values, devoid of logic. But it can also contain logic: Functions become methods when attached to an object.
Classes are appropriate if they have one defined purpose. I have learned to avoid adding more concerns to a class. For example, stateful React components are typically declared as classes. This makes sense for the particular problem domain. They have one clear purpose: Grouping the props, the state and a couple of functions that operate on both. In the center of the class lies the
I stopped enriching these classes with more, loosely related logic. It is worth noting that the React team is slowly moving away from classes to stateful functional components.
Likewise, component classes in Angular are an intersection of several concerns: Metadata fields applied using the
@Component() decorator. Constructor-based dependency injection. State as instance properties (inputs, outputs as well as custom public and private properties). Such classes are not simple or single-purpose at all. They are manageable as long as they only contain the required Angular-specific logic.
Over the years, I have come to these guidelines:
- Use the most straightforward, most flexible and versatile structure: a function. If possible, let it be a pure function.
- Avoid mixing data and logic in an object if possible.
- Avoid using classes if possible. If you use them, let them do one thing.
That does not mean that one needs to stick to these structures to model the business logic. Better put this logic into functions and separate them from the UI framework. This allows to evolve the framework code and the business logic separately.
Modules, plenty of them
My use of modules has changed over time. I used to create pretty large files with multiple exports. Alternatively, the single export was a giant object of grouping a bunch of constants and functions. Today I try to create small, flat modules with one export or just a few exports. This results in one file per function, one file per class and so on. A file
foo.js would look like this:
If you prefer named exports over default exports:
This makes individual functions easier to reference and easier to reuse. In my experience, lots of small files do not come with a significant cost. They allow to navigate in the code more easily. Also the dependencies of a particular piece of code are declared more efficiently.
Avoid creating untyped objects
In a small codebase, creating objects on the fly is a productivity feature. In a large codebase though, object literals become a liability. In my opinion, objects with arbitrary properties should not exist in such projects.
The problem is not the object literal itself. The problem are objects that do not adhere to a central type definition. They are often the source of runtime errors: Properties may exist or not, may have a certain type or not. The object may have all required properties, but also more. By reading the code, you cannot tell which properties an object will have at runtime.
Likewise, a function can check at runtime whether an argument is usable. It may check the type explicitly using
Number.isNaN etc. or implicitly using duck typing.
Thanks to Susanne Nähler, designer at 9elements, for creating the teaser illustration.
Learned something? Share this article with others or
feel free to join our newsletter.