Fundamentals
How to change legacy code?
Tools
Techniques
"Others" code?
Code difficult to understand?
Difficult code to maintain?
…
Legacy code is code without tests.
(2004) M. Feather
Add a feature
Fix a bug
Improve the design
Optimize the use of resources
Adding a Feature | Fixing a Bug | Refactoring | Optimizing | ||
Structure | Changes | Changes | Changes | — | |
Functionality | Changes | Changes | — | — | |
Resource Usage | — | — | — | Changes |
What changes do we have to make?
How will we know that the changes were made correctly?
How will we know that we have not broken anything?
Industry-standard?
Tests
When we change the code, we should have tests in place. For testing, we often have to change the code.
Identify points of change.
Find test points.
Break dependencies.
Write tests.
Make changes and refactor.
IDEs
Unit Test Frameworks
public class TransactionGate {
public void postEntries(List entries) {
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
entry.postDate();
}
transactionBundle.getListManager().add(entries);
}
//...
}Verify new entries are not previously in transactionBundle
public class TransactionGate {
public void postEntries(List entries) {
List entriesToAdd = new LinkedList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry) {
entry.postDate();
entriesToAdd.add(entry);
}
}
transactionBundle.getListManager().add(entriesToAdd);
}
//...
}public class TransactionGate {
//...
List uniqueEntries(List entries) {
List result = new ArrayList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry) {
result.add(entry);
}
}
return result;
}
//...
}public class TransactionGate{
//...
public void postEntries(List entries) {
List entriesToAdd = uniqueEntries(entries);
for (Iterator it = entriesToAdd.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
entry.postDate();
}
transactionBundle.getListManager().add(entriesToAdd);
}
//...
}Identify
Add the reference in the code
Inputs and outputs as arguments
Identify return values
Implementation of the method via TDD
Delete comment and enable the call.
Clear separation between the old code and the new one.
The affected variables can be identified.
The new method includes tests.
You can leave the original method in a weird state (invocations to external functions).
You can leave the code in limbo (The new method may not be associated with the responsibility of the original code)
std::string QuarterlyReportGenerator::generate() {
std::vector<Result> results = database.queryResults(
beginDate, endDate);
std::string pageText;
pageText += "<html><head><title>"
"Quarterly Report"
"</title></head><body><table>";
if (results.size() != 0) {
for (std::vector<Result>::iterator it = results.begin();it != results.end();++it) {
pageText += "<tr>";
pageText += "<td>" + it->department + "</td>";
pageText += "<td>" + it->manager + "</td>";
char buffer [128];
sprintf(buffer, "<td>$%d</td>", it->netProfit / 100);
pageText += std::string(buffer);
sprintf(buffer, "<td>$%d</td>", it->operatingExpense / 100);
pageText += std::string(buffer);
pageText += "</tr>";
}
} else {
pageText += "No results for this period";
}
pageText += "</table>";
pageText += "</body>";
pageText += "</html>";
return pageText;
}Add a header to the type list
<tr><td>Department</td><td>Manager</td><td>Profit</td><td>Expenses</td></tr>using namespace std;
class QuarterlyReportTableHeaderProducer{
public:
string makeHeader();
};
string QuarterlyReportTableProducer::makeHeader(){
return "<tr><td>Department</td><td>Manager</td>"
"<td>Profit</td><td>Expenses</td>";
}QuarterlyReportGenerator::generate():
//...
QuarterlyReportTableHeaderProducer producer;
pageText += producer.makeHeader();
//...class QuarterlyReportTableHeaderGenerator{
public:
string generate();
};class HTMLGenerator{
public:
virtual ~HTMLGenerator() = 0;
virtual string generate() = 0;
};
class QuarterlyReportTableHeaderGenerator : public HTMLGenerator {
public:
//...
virtual string generate();
//...
};
class QuarterlyReportGenerator : public HTMLGenerator {
public:
//...
virtual string generate();
//...
};Public Class ManagerProdFin
{
Public Sub BuscarAltas(ByVal fechadesde As Date, ByVal fechahasta As Date, ByVal solovigentes As Boolean)
{
'...
Dim listaapoderadosaltas = From apo As Apoderados In ListaApoderados Where
Convert.ToInt32(apo.aponrocuenta) = Convert.ToInt32(obj.NroCliente)
AndAlso Not apo.apotipodocumento.Equals("UBO")
AndAlso (Not solovigentes OrElse
Not apo.apoFechaVencimiento.HasValue()
OrElse apo.apoFechaVencimiento.Value.CompareTo(fechahasta) > 0) Select apo
'...
}
}Public Class ManagerProdFin
{
Public Sub BuscarAltas(ByVal fechadesde As Date, ByVal fechahasta As Date, ByVal solovigentes As Boolean)
{
'...
Dim listatitularesaltas As List(Of Titulares) = New List(Of Titulares)
listatitularesaltas = AuxManager.titularesvinculados(obj, ListaTitulares)
'...
}
}Public Class ManagerAuxiliar
Public Sub New()
End Sub
Public Function titularesvinculados(ByVal ObjCustomer As Customer_Account,
ByVal archivotitulares As List(Of Titulares)) As List(Of Titulares)
Dim archivofiltrado = New List(Of Titulares)
Dim listafiltrada = From titu As Titulares In archivotitulares
Where Convert.ToInt32(titu.titubase_no) = ObjCustomer.NroClienteAsInt() Select titu
For Each T In listafiltrada
archivofiltrado.Add(T)
Next
Return archivofiltrado
End Function
End ClassImports NUnit.Framework
<TestFixture()>
Public Class ManagerAuxiliarUnitTest
<Test()>
Public Sub GivenObjCustandtitulares_WhenTvinculados_ThenArchfiltrado()
Dim AuxiliaryM As ManagerAuxiliar = New ManagerAuxiliar()
Dim titularesSet As List(Of Titulares) = New List(Of Titulares)
titularesSet.Add(New Titulares With {.titubase_no = "0000123"})
titularesSet.Add(New Titulares With {.titubase_no = "0000124"})
titularesSet.Add(New Titulares With {.titubase_no = "0000125"})
Dim Customer As Customer_Account = New Customer_Account()
Customer.campocustacc = "000124"
Dim result As List(Of Titulares) = AuxiliaryM.titularesvinculados(Customer, titularesSet)
Assert.That(result, Has.Count().EqualTo(1))
End Sub
End ClassIdentify where you need to change your code.
Can it be grouped in a method? Think of a class name and invoke the method. Comment on the invocation.
Variables and arguments.
Return values.
Develop the sprout class using TDD.
Uncomment and invoke.
Allows you to advance in development with more confidence than if you make invasive changes.
Conceptual complexity.
public class Employee{
//...
public void pay() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
}Update a file every time a payment is run to employees
public class Employee {
private void dispatchPayment() { (1)
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
public void pay() { (2)
logPayment(); (3)
dispatchPayment(); (4)
}
private void logPayment() {
//...
}
}
public class Employee {
public void makeLoggedPayment() {
logPayment();
pay();
}
public void pay() {
//...
}
private void logPayment() {
//...
}
}Identify the method you need to change.
If the change is to be made as a separate method, rename the method and create a new method with the same name and signature as the previous method.
It is important to keep the signature **.
Invoke the old method from the new method.
Develop the new functionality using tests, then call from within the new method.
Identify the method you need to change.
If the change can be formulated as a single sequence of statements in one place, develop a new method using tests.
Create another method that calls the new method and the old method.
Unlike the sprout methods, the original methods do not change. With the sprout technique, at least one line is added.
The new functionality is independent of the existing functionality.
Difficulty in naming the new methods.
There are only two hard things in Computer Science: cache invalidation and naming things. - Phil Karlton.
class LoggingEmployee extends Employee {
public LoggingEmployee(Employee e) {
employee = e;
}
public void pay() {
logPayment();
employee.pay();
}
private void logPayment() {
//...
}
//...
}
class LoggingPayDispatcher {
private Employee e;
public LoggingPayDispatcher(Employee e) {
this.e = e;
}
public void pay() {
employee.pay();
logPayment();
}
private void logPayment() {
//...
}
//...
}
Identify a method where you need to make a change.
If the change as a separate method, create a class that accepts the class to be modified as a constructor argument.
Create a method in that class, using tests, and implement the new functionality. Write another method that calls the new method and the old method in the Wrap class.
Create an instance of the Wrap class in the place where the new functionality needs to be enabled.
The behavior I want to add is completely independent, and I don’t want to pollute the existing class with low-level or unrelated behavior.
The class has grown so much that I really can’t bear to make it worse. In a case like this, the wrap is created only as a base for later changes.
Catalog of refactorings: https://refactoring.com/catalog/
Working with legacy code is difficult
Tests are necessary
When the disease cannot be prevented, it has to be treated
The application of these techniques depends on the context
The idea of these techniques is to support them with unit tests, without applying test only add more legacy code
Sources:
Working Effectively with Legacy Code, 1st. Michael Feathers (2004)
http://xurxodev.com/trabajando-con-codigo-legado-sprout-method-y-sprout-class/
https://www.yegor256.com/2016/01/26/defensive-programming.html
I help developers to deliver good quality software following the best practices and applying clean code principles - Christian - Husband
Twitter: @JuanMorenoDev
Presentation link: https://bit.ly/embu-refactoring
Video link: https://www.youtube.com/watch?v=ae2tNgmh3Nc