4

How Do You Handle Legacy Code When Starting a New Project?

 1 year ago
source link: https://dev.to/codenewbieteam/how-do-you-handle-legacy-code-when-starting-a-new-project-2ml3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Top comments (19)

CollapseExpand

What I'd like to do with legacy code

giphy

What usually happens:

Image description

Comment button Reply

CollapseExpand

I would love to take a flame-thrower to the legacy code on my project. However, that would only leave ashes of a project and it has only been running for 2-3 years with a team of 6 developers! I think I might be the next thing fired.

Comment button Reply

CollapseExpand

100% accurate. There is some legacy code that isn't garbage that I'd like to burn down but that's doesn't happen too often.

Comment button Reply

CollapseExpand

Start by convincing yourself not to add any features until the architecture is ready. Aim to refactor only, without changing functionality.

Find or write the test cases that establish whether the deployment target has the necessary functionality and connectivity, (platform tests) [sidenote: use a different test framework to the one baked into the application so as to avoid that dependency e.g. github.com/keithy/okay-php ]. These will allow you to try deploying to different places, upgraded languages, libraries and servers, or local development environments. Attempting to set up a fresh local/upgraded/alternative development environment will produce failures that you can turn into more detailed platform tests. These test-cases give you some agility, the ability to move with confidence, and will pay you back handsomely when it comes to re-deployment or when "heading for the clouds".

Find or write test cases that establish whether the overall functionality is as expected. These need to be at a level that envelops the whole project, and covers most significant features, the goal here is have a sanity check, to be able to refactor without a successful refactoring being the cause of a test failing. A failing test needs to communicate whether or not a refactoring step has been successful in changing structure without breaking functionality. Therefore these tests need to be at a level that does not break due to changing internal structure. It is safe to assume that this 'whole-application' testing will not be the fastest, so if possible spin off a separate project to parallelise running these tests spinning up test instances in the cloud.

The first refactoring efforts need to be to enhance testability. It is usual to begin by looking at how the application is configured, and to begin by looking at the patterns used to introduce configurable values into the code. It goes without saying that any hard-coded config needs to be moved into configuration files. Look at adding feature-flag type functionality into the configuration. The testability improves as it becomes possible to run the test scenarios with different configurations.

If databases are involved, setting up test data, and or faked data, and being able to run against an in-memory database, will allow for faster testing without resorting to mocks. Create the ability to snapshot and trim a database so that it is able to provide test fixtures. Use/introduce a database migrations framework so that the schema is managed under source control. Have the test configuration be able to specify the data source, so that a test run can target an appropriate test fixture.

Now that the application is testable as a whole, it is possible to start looking at its internals. Start by looking at class instantiation, and introduce the factory pattern, so that class names are no longer hard coded, but go via a factory. Factories are chosen or configured from the configuration file, either directly or via feature flags. This allows classes to be replaced with improved implementation, without removing the existing class, which can remain for reference.

Then we look at some services that are used across the whole application, since we are interested in testing and instrumentation prior to refactoring, the most likely the first candidate will be logging. Introducing a modern logging framework can be your first real chunk of new functionality. Using the factory pattern existing logging classes can be switched out for the new ones, and a few strategic classes can copied sideways and have instrumentation added; keep the existing implementation in case the logging itself adversely effects the functionality. There is usually a fair bit of tidying that can be done to organise decent logging levels. Try and add logging that has zero-impact when it is not enabled. You can also freely add logged-assertions anywhere in the codebase. There may be language/runtime support (e.g. PHP) for assertions, that allow them to have zero-impact when disabled. Also add verbose logging levels to all external interfaces, especially the database.

Once the application is instrumented, a separate project can look at log analysis to provide some overall performance metrics. A detailed log-level can be added to the factories to tell you which classes are being instantiated, and then even the most obtuse app will reveal intimate relationships between classes.

At this point you are equipped to start thinking about modularity, and re-architecting the application. We are looking to improve the structure of the application, introducing or enforcing a layered architecture and improving modularity. The specific goal here is to clarify and tidy up the dependencies so that they face in a singular and correct direction. In a layered architecture, your building depends upon the foundation, and your roof upon the building. If the roof depends upon the foundation, or your foundation is tied to the roof, you are unable to treat any component as pluggable.

For web-applications and REST APIs it should be noted that the web is an I/O device, the application business logic should not have any code that is specific to any UI framework. The web or UI, the database(s), the logger, can all be plugins to the core functionality.

If you identify a module of functionality that can be extracted into a separate package, then that package can be published independently, and effectively listed as an external dependency. Easy targets include data importers and exporters. The platform tests we began with can now include a requirement upon this "external" package as a pre-requisite. We should already be thinking in terms of managing our project and its releases though a modern packaging tool that understands dependencies.

Having all the pieces in place for configurability, instrumentation and testing, and having pared down to the core functionality by extracting any optional modular components, we have set the stage for bigger changes.

Before doing so however, at this point it ought to be possible to release the product once more, even though no actual new features will have been added. However the new release, and operations environments can already take advantage of the better logging and instrumentation. Not to mention that existing bugs will be illuminated with more information.

While your 2.0 release is out in the wild, you should anticipate supporting this for a few point releases, and incrementally improving that parts you have worked upon thus far, while you branch off and work on version 3.0.

Now you should have earned yourself some breathing space. You have the technologies in place and some time in which to branch off in a more radical direction. You can now refactor-radically in the case that a major architectural shake up is needed, and refactor-mercilessly in the case that the system is not tidy, or sufficiently readable. You can even experiment with several different projects trying different architectural ideas.

Now we are ready to start introducing new features, we look at how to add these as additional plugins. If possible we will refactor the application to have a pluggable architecture.

I would find it difficult to resist attempting to break free of class-oriented programming and to adopt an actual object-oriented architecture, namely Data-Context-Interaction (DCI). [sidenote: If it is Java you likely will be dissapointed] Whether your language of choice can support DCI efficiently, is a question that your new performance metrics will be able to shed light upon.

Good luck.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK