raulo5

Working Effectively with Legacy Code

Quotes and notes

by Michael Feathers

https://www.oreilly.com/library/view/working-effectively-with/0131177052/https://www.oreilly.com/library/view/working-effectively-with/0131177052/

What I liked about this book: There is a lot of focus on how to enforce test harness on legacy code (so you have a safety net to start making changes on it). It offers some specific approaches to look for “parts of the legacy code that may pay more the effort of making it testable”, but there is also the warning about the trade offs: If the legacy code is not testable, the effort needed to put the coverage in place may add some risk and be time consuming too.

Changing Software

Reasons to change software

  • Adding a feature

  • Fixing bug

  • Improving the design

  • Optimising resource usage

Improving design

  • We want to keep behaviour intact

  • We make small structural modifications, supported by tests to make the code easier to understand, follow and change

Risk change

  • We don’t know how much behaviour is at risk when we make changes

  • How to validate our changes makes the right thing? / doesn’t break any existing behaviour? Enter automated testing

  • Being conservative (less changes) generates fear of future changes

Working with feedback

Covering with tests as a Safety Net

Covering software means covering it with tests. When we have a good set of tests around a piece of code, we can make changes and find out very quickly whether the effects were good or bad.

  • Unit tests gives feedback as you develop so you are able to refactor with much more safety.

  • By breaking dependencies we can make deeper changes.

Legacy Code Change Algorithm

Made with http://mural.co/Made with http://mural.co/

Why Break Dependencies?

Sensing and Separation

  • Sensing: When we need to access a computed value to test.

  • Separation: When we cannot put code into a test harness, here we can use Fake Collaborators (Fake Objects, Mock Objects,Stubs, etc.)

The Seam Model

  • It’s just a point in code where you can switch the behaviour.

  • You decide which behaviour to use (the Seam Enabling Point)

About Refactoring

Remember, code is your house, and you have to live in it.

Sprout Class/Method: It’s just a class or method you can use in instead of the original Class/Method, when you want to update or add a feature.

Wrap Class/Method: Renaming Class or Method so you can update or add a feature.

Dependency Inversion: Another tool to update or add a feature, it helps decoupling modules or collaborators.

Once we have tests in place, we are in a better position to add new features. We have a solid foundation.

Made with http://mural.co/Made with http://mural.co/

  • TDD: Good to focus on une thing at a time.

  • Programming by Difference: Inherit classes to fix / change behaviour. May help in some cases, but it breaks Liskov Substitution Principle.

Issues when trying to Test Harness a class

  • Objects of the class not easy to create

  • Dependencies makes the Test Harness difficult to build (global dependencies makes it even worse)

  • The constructor (for the class to test) have side effects and/or suffer from “Construction Blob” (lots of complex parameters).

  • Need to validate effects of a change in a class being tested: Enter *Effect Sketches: *Similar to Component Interaction diagrams, but using methods (inside the same class). This way you can clearly see how a change in a method affects other methods.

Made with http://mural.co/Made with http://mural.co/

  • Global or Static data can also make test harder

  • **Interception Point: **Place in code where you can detect effects of a particular change. Useful when writing tests.

The tests document the actual current behaviour of the system.

Storytelling to Explain the Architecture

https://www.pexels.com/https://www.pexels.com/

  • The rough idea is to explain the 2 or 3 main ideas if the Architecture as a first approach.

  • You explain what are the pieces of the design and how they interact

  • You first approach will be vague on purpose, you won’t be lying, you’ll be just hiding some complexity to be able to explain the higher level

  • Then you can “drill down” explaining the concepts in depth

We start out by making a brief description. When we simplify and rip away detail to describe a system, we are really abstracting. Often when we force ourselves to communicate a very simple view of a system, we can find new abstractions. The story gives us guidance.

Problems with big classes

  • Task Scheduling issues: The more responsibilities a class has, the more you may need to change it.

  • Low coupling and high cohesion always matter, even more on a big class.

The best approach to breaking down big classes is to identify the responsibilities, make sure that everyone else on the team understands them, and then break down the class on an as-needed basis. When you do that, you spread out the risk of the changes and can get other work done as you go.

Addressing duplicated code

It can be frustrating discovering the same duplicated code all over. Removing this duplicated code should be not difficult, but there is always need of test coverage as a contention.

  • Removing duplicated code needs tests coverage

  • It is also a way of distilling the design

  • Open/Closed Principle: Code should be open for extension, but closed to modification.

Some other helpful practices

  • Replace Function with Function Pointer

This way you can decouple an implementation, making it easier to test / extend / replace

  • Push down a dependency

This way (by pushing a dependency down in the hierarchy) you make the original class abstract (in relation to that dependency).

  • Subclass and Override Method

Makes possible to change / extend original behaviour, and you are even able to invoke the original implementation.

  • Pull up feature

By doing this, you can focus on the specific pulled up feature (again, to test it or modify / extend it).