In a well-maintained system, it might take a while to figure out how to make a change, but once you do, the change is usually easy and you feel much more comfortable with the system.

In a legacy system, it can take a long time to figure out what to do, and the change is difficult also. You might also feel like you haven’t learned much beyond the narrow understanding you had to acquire to make the change.

In the worst cases, it seems like no amount of time will be enough to understand everything you need to do to make a change, and you have to walk blindly into the code and start, hoping that you’ll be able to tackle all the problems that you encounter. Here are some of the patterns to deal with legacy code from the classic Working Effectively with Legacy Code.

I don't understand legacy code
First step is to face the legacy code. Stepping into unfamiliar code, especially legacy can be scary. If you dwell on it before you start coding, that makes it worse. You never know whether a change is going to be simple or a weeklong hair-pulling exercise that leaves you cursing the system, previous developers. and nearly everything around you.
  • Sketching things out often helps us see things in a different way.
  • When reading through code gets confusing, start drawing pictures and making notes. 
  • Write down the name of the last important thing you saw, and write the name of the next one. Draw a line, if there is a relationship.  
  • Scratch refactor in a separate branch. Extract method, move varialbes, refactor it whatever way you want to get a better understanding of the code. Then throw away the code once you have learned a lot. 
My codebase has no structure
Long lived applications tend to get bloated. They might begin with a well thought out archtecture and design, but over the years, under tight deadlines they get to a point at which nobody really understands the complete structure. The codebase can be so complex that it takes al long time to get the big picture, or there could be no big picture. 
    
There are some serious consequences to this. In some cases, programmers run up against a wall. It's difficult to add new features, and that brings the whole company into crisis mode. In others, the system carries along slowly. It takes way longer to add new features. Nobody knows how much better it could be or how much money is lost because of poor structure. The team gets into a reactive mode, dealing with emergency after emergency so much that they can't add any value. 

This class is too big 
When we keep adding code to existing classes, we end up with long methods and large classes. As time goes on, it takes more time to understand how to add new features or even just understand how old features work. Big classes cause confusion. It's hard to understand what you need to change and how it will affect anything else. 

To fix bloated classes, we can do the extract class refactoring. When you need to add a feature to a system and it can be formulated completely as new code, write the code in a new method or create a new class. Call it from the places where the new functionality needs to be. Sometimes you can factor out duplicated code in new classes. If we end up with yet more code that needs to work with them, we can introduce a class and shift all of those new methods over to it. The net effect is that we end up keeping this method small and we end up with shorter, easier-to-understand methods overall.

The key to refactoring big classes is to identify the different responsibilities and then figure out a way to incrementally move toward more focused responsibilities. Some techniques to separate responsibilities in a class include:
  1. Group methods by similar names, and try to find ones that seem to go together. 
  2. Extracting private methods from a class to a new class and make them public. Make the new class a private instance on the original class.  
  3. Look for decisions that can change. 
  4. Try to describe the primary responsibility of the class in a single sentence. The other responsibilities should probably be factored out into other classes. 
  5. Use the Interface Segregation Principle to extract interfaces from a large class and have the clients deal with them, instead of the concrete class. 
Legacy code is a better candidate to improve your design chops than a greenfield project. It offers far more possibilities for applying design skills than new features do. You have all the code in front of you, it's easier to see whether a particular design is appropriate in a given context because the context is real and right in front of you.