Your developers write comments appropriately; you avoid deeply nested loops; and you make a point of detecting and removing duplicate code. Your team does these things, without hesitation, because everyone knows it makes your application easier to maintain.
But how is your team making your application easier to modernize? Our work with over 25 companies refactoring larger applications has shown how widely the modernization effort can vary from application to application. Like maintainability, modernizability is a state of the application, influenced by the traits of its code.
Highly-maintainable applications don’t necessarily make highly-modernizable ones, however. Here are seven habits and practices your developers can learn to significantly reduce the effort of future modernization.
Tip #1: Wrap framework components
The effort to create your own custom components that extend (or better: wrap) those of the UI framework will pay dividends later, when you decide to modernize your application. The more you wrap and hide the framework from your application code, the less your logic depends directly on it and the less the impact when it changes. Some companies have taken this to such an extreme that they can migrate their whole application by refactoring their custom components alone.
This tip works best for the largest applications, since ROI increases as the application grows. For mid-sized applications, whether it’s worth the trouble depends on the ratio of distinct classes to total classes in use, and how unrelated each one is from the others. If you use a low number of distinct, unrelated classes, but use them a large number of times, it makes more sense; on the other hand, if you have a large number of distinct, related classes that are used infrequently, the business case weakens.
Depending on how unrelated the components are (contrast the unrelated TextField, CheckBox, and Button on the one hand, with the related TextField, a Layout containing the TextField, and a Binder used by the TextField on the other), the rule of thumb can vary from:
- Highly unrelated: Number of distinct classes used times 8 is smaller than the total number of classes used, to
- Highly related: Number of distinct classes used squared is smaller than the total number of classes used.
One company we worked with that was considering migrating a larger Swing application to Vaadin, used 148 distinct framework classes, which they instantiated 25970 times. 148x148 is only 21904, so the case for introducing wrapping here was very solid.
Stick to concrete classes, though - things that users see and can interact with, like a ComboBox. Custom wrappers of abstract classes, like an AbstractTextItem, should be avoided, since the inheritance hierarchies of APIs differ widely across frameworks. The basics of a ComboBox are universal, and the component is normally customizable, but what you will or won’t find in an AbstractTextItem is left to the discretion of the framework’s developers.
Tip #2: Use an architecture that favors portability
Maintainability is one of the benefits software developers gain from using architectural patterns like MVC, MVVM or MVP. These architectures can help to clarify where features in the existing source code can be found, and where new features should be placed. Of these three, only MVP (Model-View-Presenter) explicitly aims to improve modernizability (in addition to maintainability), as it facilitates portability of applications to different UI frameworks.
MVP separates concerns through “presenters” that don’t have any dependencies on the underlying UI framework. When it comes time to refactor to a new framework, presenters should not require changing. The net effect of this can be a huge reduction in the cost of your modernization effort, since you might have as much as a third of your logic in presenters, and your automated tests will be highly reusable.
For many development organizations, the cost of full, formal MVP may seem to outweigh the benefits. Fortunately, many organizations have found ways to customize and simplify the MVP patterns, improving their cost-effectiveness. Some ways they have achieved this are: reducing the number of possible partial updates of a view in favor of full updates, emphasizing unit testing over UI testing, and delegating event handling in the views to a model-aware reflection service.
Tip #3: Loose coupling
Regardless of whether you are aiming for modular monoliths or microservices, you want your modernization process to be phase-able. If your modernization can’t be done in phases, you will be stuck with a big bang, and all of the bad things that go with it, like lengthy code freezes or having to do maintenance on both the application in production and the newly-modernized one, at the same time.
Make your modernization phaseable by managing your dependencies. If package A uses package B, ideally make the dependency dynamic, so you can swap B for a modernized B, without having to change A. This also means ensuring dependencies run without cycling - if package A uses package B, and package B uses package C, make sure package C doesn’t use A or B.
Tip #4: Consistent naming conventions
Keep the architecture and loose coupling of your application working as intended, by strictly adhering to naming conventions that reflect the role of the class. Put modules in separate namespaces or packages that identify them. Also, keep modules physically in separate folders and keep the names of the folders, namespaces and modules in sync.
Names of classes that are Views should have a V- prefix or -View suffix, or some other marker that makes them easily identifiable. If views are composed of parts, or if there is a separation of interface and concrete class, you can help make this role clear by using a name that has “impl” or “part” in it. Focusing on view naming is especially helpful if you're aiming for a phased modernization that progresses view by view.
With regard to the non-business classes of your application that aren’t shaped by use cases (for example, the framework components you wrapped as part of Tip #1) there is much less literature and consensus on how these should best be named. Certainly, simple components can be marked with a common suffix or prefix, but it is key not to name them in the same way as your models, views and presenters. These classes should be getting the brunt of the rewriting in your modernization, in order to keep the models and presenters as unchanged as possible.
Tip #5: Conservative third-party libraries
There are good third-party libraries that can help your modernization by being highly portable and causing no side-effects in your application. Third-party libraries that make your modernization more difficult, on the other hand, do so in three ways:
First, when it comes time to modernize your application, the third-party libraries you depend on will fall into two categories - those that are impacted by the change and those that aren’t. Trouble is, this distinction isn’t always easy to make, since some libraries might be using the framework without you being aware of it, or cause unintended side effects on the new framework. The fewer libraries you use, the fewer surprises you’ll get.
Second, you may be considering a library for a specific way the feature has been implemented, in preference to the default feature implementation in your framework. When choosing a library in order to specialize, be mindful that this change will shift your application to a more specific use case that may be more difficult to cater to in the frameworks you use in the future.
And finally, bear in mind it is easier to start using libraries than it is to stop. We often find companies in the situation where they are using versions of third-party libraries that are no longer supported. Modernization can be the time you’re forced to clean house and remove these old dependencies. Cleaning house takes time as well, however.
So before you start using a new library, ask yourself if this is for a feature you would still use if you had to drop the current framework. If your answer is a clear no, consider acting preventively and give it a pass. If your library has been built with portability to other frameworks in mind, these are the libraries that will speed up your modernization later.
Tip #6: Avoid customizing the framework, but if you absolutely must...
Another defensive developer habit is to keep the coding as standard as possible for the framework you’ve chosen. There are a number of ways that your developers can stray and customize your framework:
- Writing your own classes in the platform’s namespace to gain access to protected features.
- Invoking internals or using reflection to invoke the private methods of the platform API.
- Using custom preprocessors that let your developers use syntactically illegal constructs.
If you have a compelling reason to do these sorts of things anyway, be sure to document why and keep the documentation in your version control. Java in particular has added many new features since the first years of its release, and many early customizations have actually been rendered obsolete by subsequent official extensions of the Java platform. The team doing the modernization might not have access to the original development team and not know that the purpose of the hacky customization was to achieve behavior that’s now standard.
Tip #7: Automated tests
Despite the benefits of continuous integration that automated testing provides, we find companies that actually have automated testing procedures are in the minority. Automated testing takes commitment and investment, and many companies don’t see the benefits outweighing the costs.
When it comes time to modernize the application, these benefits get a game-changing boost. When written well, automated tests can help users accept modernized versions of your application and show how features of the modernized application still perform correctly when compared to the existing version.
In particular, this will help companies that have followed Tip #1 above and have written their own custom wrappers. Having a good set of automated tests will let you cheaply validate any fixes you have made to your own custom wrappers, and validate that a single fix in one wrapper has resolved all occurrences of a related problem in your application.
Thinking about the modernizability of your application involves thinking further ahead than just its maintainability. Long-term thinking this far into the future doesn’t come naturally to most software development projects, however. For smaller, more casually written applications (< 60kloc) the easiest course of action come modernization time may be to just rewrite, since the overhead of a modernization strategy to leverage the existing application may outweigh the benefits.
If your application is written well enough to support the collection of a large number of features, then over time the whole will become more valuable than the sum of its individual features. For these successful applications, planning ahead for modernization is prudent and will help ensure both the features and the whole can be leveraged anew.
Looking to take the first step on your path to modernization? See how a Vaadin Migration Assessment can help.