Have you ever worked on a state of the art programming project? And then a new requirement arrives, it's implemented and suddenly you're working on a legacy project?
How it usually starts it's like this: the product owner comes and says that they need a project to do a task, be it simple or complex. The architect and the team think about the design, think about how data should be stored, processed and displayed. The project starts, latest and best tools and frameworks are used. This would be a great project. All the team ever hoped for was a new, greenfield project. Everyone is motivated. This time it would be different.
The first version is great. The UI looks very posh, it's latest fashion, the database has the perfect balance between the 3rd normal form and the performance requirements. You even asked the stakeholders about what are the nonfunctional requirements and did some automated tests around that. You may have some small performance problems, in some strange cases, but after you'll see how the application is used, you will find the best solution for that in v2.
People are seeing the application and they are charmed and want more out of it. This is where things get tricky. You built a product for one purpose. And having that and all the best practices in mind you made the design. Now, as the things evolve, the purpose changes. It happens almost every time, and numerous books where written in order to helping us, developers, protect our project from this. Robert C. Martin is just the most famous one to having built a writer and speaker carrier on this topic. The Open/Closed Principle is the best example of a good code design practice for minimizing the risk of breaking the project in v2+. But nobody tells you, the developer, how to approach the new requirements, how to try to understand the core need and model it in order to come with a solution that is the best compromise between the business and the technical sides.
Let's say for example you need to display all the orders created in a store. First, you'll have a table with columns for the number of the order, the date, the state, the value, and the possibility to see the details. This is great, clear and straight forward. Then the users realize that they not only want to filter the table, but want to see in separate tabs new, processing and closed orders. Because it would be easier to just have a tab right there, click on it. Of course, the product owner still wants to see everything aggregated, someone must have gotten used to that. After seeing them separately new functional ideas come to mind. The processing orders should have a new column with a new status, like "ordered from supplier/in transit from supplier/in stock/sent for delivery". These do not make sense for new or closed orders. So the "All" tab will be inconsistent in some way. Either you have the new column populated just for processing orders, or you take this column out, or you will populate it with closed or new for these two statuses. Either way, your model will not be 100% compatible with your view. And this is where a well written code should either signal a problem in the specs, and you, as a professional can either challenge as much as you can the requirements, or take it as it is, listen to the product owner, admit that it's just one new column, shouldn't be that much work, and end up, without realizing in a legacy project.
There is a trap in trying to align the magnitude of the business requirements with the magnitude of the changes needed in code. It's "just" a new column, it's "just" a new view like the old one with minor changes. What we as developers should do is detect those moments when the functionality evolves even a little bit away from the old one. In that moment we should either make a point that the purpose of the application is no longer the same, or that we need to make structural changes, probably even in the requirements.
We lack the concept of "functional refactoring". Wouldn't it be nice from time to time to analyse the capabilities of our application and reorganize or delete the legacy ones? An application that lived more than a few years and had grown during that time it's usually a monster, functionally speaking: users can do stuff nobody remembers it's possible, old functionalities are still there even if nobody uses them, users sometimes discover flows no one thought about and are using them day by day. It looks a little bit like Istanbul: it used to be magnificent, it's still magnificent here and there, but it's growth is out of control and it looks like an unpredictable amazing monster these days.
So that's the problem. What's the solution?
1. Listen to your code. If you think you must add a forced conditional, whether it's an if, of some kind of polymorphism, or dynamically composed screens, no matter how cool it is from the technical point of view, take a closer look. Is it a natural evolution of the code and functional behavior? If not you should challenge it.
2. Know your business domain. You should know what every stakeholder is gaining from the product. You should know how the users are behaving inside the app, how the roadmap for the product development is aligned to the current requirements, why and when are they needed. Try to find out all these things. Log flows in the app to see what the users are doing, ask around the organization if people are familiar with the app, ask the decision makers what they think about it, what it's missing, what do they think are it's strengths. Listen carefully to the boring organizational status meetings if products are presented. Look to your competitors. These all are sources of information that we developers are constantly ignoring because we think it would to be too much to fit in our heads. But by knowing these things, and paying attention to these details we can add real value because this is where you learn how the product should evolve. Everyone should be able to write code these days; internet is helping, IDEs are helping, there are a lot of good books and talks about good programming practices. What a great developer should be able to do is identify the real needs from his project and make them happen.
3. Always ask the stakeholders what is the basic need. When you are required to make some modifications, ask why if it's not obvious. Maybe the need can be satisfied better. Maybe, going back to our example, a new screen would be better than adding a new column. The user might need afterwards to be able to also update the processing orders in ways that would not apply to new and closed orders.
4. Challenge requirements. The idea of adding a new column on the tab with all orders is a bad one. Not uncommon, just bad. The users will have mixed information, will need to do classifications in their own mind in order to understand the presented information. You should ask the product owner why that tab is needed. If it's because aggregating all the information is found in every app, that is a bad answer. If it's because the user is accustomed to seeing this as a first screen, it's also bad. If it's because the aggregated data provides important insights, like how many orders are received in a day, then you have a better answer. But then the display of all information available for all order types it's useless. Users will, most probably, change tabs for the detailed and filtered information. In my experience, inconsistencies in the requirements, which of course are reflected in the code, are the most fragile and volatile parts of an application. If you sense that your design is unnaturally modified in order to implement a shaky solution, raise the problem. Try to find a better solution for the basic need.
5. Do not be afraid to provide out of the box solutions. Your most important advantage is that you see things from a different perspective. The PO can be stuck on one solution and the user can be stuck on one face of the problem. You, with the knowledge of the code, and without the pressure of being expected to come up with the perfect solution are in the best position to be able to provide a new perspective. A new way to approach the problem, a new way to solve it.
6. Do not over reuse UI code. I am not providing technical solutions here, in this article. On the contrary, I'm saying that for once we should trust our code when it screams that the functionality is bad, and help it stay clean by changing the requirements. That being said... The UI will change often, it's the most volatile part of the project. And I'm not talking about CSS or overall style, which of course should be uniform and shared. I'm taking about the two screens that seem almost identical in v1, and became totally different in v2. It's usually stated like this: "We have these two screens, which should look the same, but the second one will have some small additions, like being able to edit, delete, listen for updates, nothing much". Never, and I mean it, never reuse the UI code between these two because it will come and haunt you, and this is what you will be talking about in the project's lessons learnt meeting.
7. Do not forget that "Thinking is always a best practice". All our processes, all the measurements, all the paradigms, all the patterns, are made to made our work easier, i.e. to help us increase productivity without increasing brain effort. It's easy then to follow the good or bad practices blindly, because everybody does it. I dare you to stop for a moment and think if what you do every day makes sense to you, your project, your organization. If not, challenge it.
In my experience the worst code decisions come from the worst business decisions. In those moments when it just didn't feel right what was done in the code, a wrong feature was implemented. Almost always in these cases, after releasing, there was another stakeholder that was under the impression that a feature should look different, or behave different. And it is normal. For a good user experience a product should be very clear in its intentions. Tweaking functionalities in order to add abilities in unorthodox places destroys this clarity, and the first place where you can see that happening is in the code. This is why it is the responsibility of every developer to challenge their tasks when those don't seem right. Remember that at the end of the day, your work's purpose is to have an excellent product, and not to be able to say: I was able to do what I was told to.