While we’d all love to be working on new technologies, nice clean code and have a generally easy life all of the time — this can’t always be the case.
At some point in our careers we have to lift the veil of a legacy software system and gaze at what is beneath…
“Oh wow… Struts 1, SOAP and ANT with no dependency management… Beautiful… 🤮”
What can we do to make this somewhat undesirable situation a little more tolerable?
Docker is a tool designed to make it easier to create, deploy, and run applications by using containers. Containers allow developers to package applications with all of the parts it needs, such as libraries and other dependencies, and ship it all out as one package.
Incorporating Docker into a project should become like a reflex and I’d recommend is the first thing you do — before you even write any actual code.
An Example: You need to maintain an application which runs on an outdated application server, has no tests and no dependency management. You need to share this software environment with other’s in the team but installing the dependencies is a considerable effort.
A Solution: Write a Dockerfile
for deploying the application and its dependencies. Create a Docker container / image for anything else pertinent to running the environment (e.g. a development database) and use something like Docker-Compose for orchestrating the containers in your multi-container development environment.
Benefits:
Dockerfile
in case it needs to be reproduced elsewhere.Here’s a toy example from a project I’ve worked on in the past. The project used GlassFish as an application server, Ant to build the project and had no dependency management or unit tests:
Dockerfile:
FROM glassfish:latest
LABEL maintainer="andymacdroo"
COPY dist/App.war /App.war
COPY lib/mysql-connector*.jar /usr/local/glassfish4/glassfish/domains/domain1/lib/ext/COPY scripts/configure-glassfish-datasource.sh /configure-glassfish-datasource.shCOPY scripts/deploy-war.sh /deploy-war.shRUN /configure-glassfish-datasource.sh
EXPOSE 4848 8080 8181
# Start asadmin console and the domainCMD ["/deploy-war.sh"]
And to build, simply run in the project’s directory:docker build -t andymacdroo/app .
deploy-war.sh:
#!/bin/sh
asadmin start-domainasadmin -u admin deploy /App.warasadmin stop-domainasadmin start-domain -v
configure-glassfish-datasource.sh:
#!/bin/sh
asadmin start-domainasadmin create-jdbc-connection-pool --user admin --restype javax.sql.DataSource --datasourceclassname com.mysql.jdbc.jdbc2.optional.MysqlDataSource --property "user=root:password=root:url=jdbc\\:mysql\\://mysql\\:3306/myDB" myDB;asadmin create-jdbc-resource --user admin --connectionpoolid myDB jdbc/myDBasadmin stop-domain
docker-compose.yml
version: '3'
services:mysql:image: mariadb:10.3container_name: mariadbvolumes:- container-volume:/var/lib/mysql- ./data/dump.sql:/docker-entrypoint-initdb.d/dump.sqlenvironment:MYSQL_ROOT_PASSWORD: rootMYSQL_DATABASE: myDBports:- "3306:3306"myApp:image: andymacdroo/app:latestcontainer_name: appports:- "4848:4848"- "8082:8080"- "8083:8081"links:- mysql
volumes:container-volume:
With this setup, the environment can be easily ported to others in the team and is completely disposable (you also don’t need to dirty your own development machine with obsolete software 🤢_)._
Do you actually need to write any code within the legacy project? Can you avoid doing so altogether?
It could be helpful to consider the current legacy code or application as a 3rd party that you can’t control or change. What could you do?
In many situations you can leverage the adapter design pattern — taking legacy software output and converting it into a form that can be consumed by a new application or a system that you’re migrating to.
An Example: You need to migrate a legacy daemon process which takes data from a queue and makes API calls, to make those API calls to another 3rd party API.
A Solution: Write an adapter function to slot in between the legacy daemon process and the new 3rd party API — the job of this adapter is to convert the current request body that gets generated into a form the 3rd party can consume and understand and to forward it to the 3rd party.
You then just need to point the legacy process at the new adapter function and it takes care of the rest.
It can be really tempting to take a legacy project and begin changing absolutely everything.
But without good tests (as most legacy code seems to be without), what’s to protect you from introducing a defect or changing functionality in a major way?
Write tests that seem to be missing or broken, but if this is impractical, only change what must be changed.
Imagine being a brain surgeon, delicately performing micro-surgery on an extremely complicated organ that no-one fully understands.
You won’t truly understand the pain and anxiety of having to debug a legacy application — after you’ve casually implemented a massive sweeping change and there’s been a critical incident in production, until it’s happened to you.
Trust me, it isn’t a fun experience… 😢
And, if you are writing additional tests…
Refactor your code (now care about code quality again!); re-run your tests to ensure your code still passes_._
TDD: Paint by Numbers Programming_Map out the tests you need to write, implement the code and move on to the next test. Eventually you get a work of art._medium.com
Newly qualified medical doctors typically abide by the pledge / oath:
“Primum non nocere” — “First, do no harm”
For developer’s we should go one step further and follow the Boy Scout Rule:
Boy Scouts believe in leaving the campground cleaner than it was found, developer’s should embrace this with the code they work on
This is for one simple reason: “Broken Window Theory”:
One broken window, if left unrepaired for a substantial amount of time, instills a sense of abandonment. So another window gets broken. People start littering. Graffiti appears. Serious structural damage begins. In a relatively short time, the building becomes damaged beyond the owner’s desire to fix it, and the sense of abandonment becomes reality — James Q. Wilson and George Kelling (1982)
The same situation can happen to any code-base if we cut too many corners and don’t make the effort to maintain projects: avoiding repaying technical debt when opportunities present themselves to do so.
Do you want this to eventually happen to your new projects?
All legacy code and software started life as a new and shiny project at some point.
Treat legacy projects with the same level of TLC as any new and shiny project. Be pragmatic, sometimes we have to be, but don’t make things worse.
Thank you for reading. I hope this article has given you a handful of tips to help you navigate the difficulties of working on legacy applications. 😀