Existing methods for refactoring legacy code are either unsafe, or slow and require a lot of rare skills. I'm proposing a new method composed of three steps: refactor to pure functions, write tests for the pure functions, and refactor the pure functions to classes or something else. I believe that with a few mechanical steps a trained developer can safely refactor legacy code faster using this method.
1. Refactor Legacy Code Through Pure Functions
Alex Bolboaca, CTO, Trainer and Coach at Mozaic Works
alexboly https://mozaicworks.com alex.bolboaca@mozaicworks.com
. . . . . . . . . . . . . . . . . . . .
2. The Problem Of Legacy Code
Existing Methods
Disadvantages of existing methods
Step 1: To pure functions
Step 2: Test pure functions
Step 3: Pure functions to classes
Final Thoughts
16. Legacy Code is Messy
Often multiple methods required, and time consuming
17. Existing methods require multiple rare skills
• How to identify and “abuse” seams
• How to write characterization tests
• How to imagine a good design solution
• How to refactor in small steps
• Lots of imagination
19. 3 Poor Choices
• Live with the code you’re afraid to change (and slow down development)
• Refactor the code unsafely (spoilers: you will probably fail)
• Use a safe method (spoilers: it’s slow)
20. I’m proposing a new method
• Refactor code to pure functions
• Write data-driven or property based tests on the pure functions
• Refactor pure functions to classes (or something else)
27. Why pure functions?
• Pure functions are boring
• Pure functions have no dependencies
• Pure functions are easy to test
• Any program can be written as a combination of: pure functions + I/O functions
• We can take advantage of lambda operations with pure functions: functional composition,
currying, binding
28. Why Pure functions?
The hardest part of writing characterization tests (M. Feathers’ method) is dealing with
dependencies.
Pure functions make dependencies obvious and explicit.
29. Refactor to pure functions
• Pick a code block
• Extract method (refactoring)
• Make it immutable (add consts)
• Make it static and introduce parameters if needed
• Replace with lambda
Remember: this is an intermediary step. We ignore for now design and performance, we just want
to reduce dependencies and increase testability
30. Example
Game ::Game() : currentPlayer(0), places{}, purses{} {
for (int i = 0; i < 50; i ) {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
popQuestions.push_back(oss.str());
char str[255];
sprintf(str, ”Science Question %d”, i);
scienceQuestions.push_back(str);
char str1[255];
sprintf(str1, ”Sports Question %d”, i);
sportsQuestions.push_back(str1);
rockQuestions.push_back(createRockQuestion(i));
}
}
33. 3. Try to make it immutable: const parameter
void Game ::doSomething(const int i) {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
popQuestions.push_back(oss.str());
}
34. 3. Try to make it immutable: const function
void Game ::doSomething(const int i) const {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
//! Compilation Error: state change
popQuestions.push_back(oss.str());
}
36. Separate the pure from impure part
void Game ::doSomething(const int i) {
// Pure function
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
const string &popQuestion = oss.str();
// State change
popQuestions.push_back(popQuestion);
}
37. Extract pure function
void Game ::doSomething(const int i) {
string popQuestion = createPopQuestion(i);
popQuestions.push_back(popQuestion);
}
string Game ::createPopQuestion(const int i) const {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
const string &popQuestion = oss.str();
return popQuestion;
}
38. Inline initial function
Game ::Game() : currentPlayer(0), places{}, purses{} {
for (int i = 0; i < 50; i ) {
const string popQuestion = createPopQuestion(i);
popQuestions.push_back(popQuestion);
...
}
}
39. Back to the pure function
string Game ::createPopQuestion(const int i) const {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
const string &popQuestion = oss.str();
return popQuestion;
}
40. Some simplification
string Game ::createPopQuestion(const int i) const {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
return oss.str();
}
41. Transform to lambda
auto createPopQuestion_Lambda = [](const int i) -> string {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
return oss.str();
};
string Game ::createPopQuestion(const int i) const {
createPopQuestion_Lambda(i);
}
42. Move lambda up and inline method
Game ::Game() : currentPlayer(0), places{}, purses{} {
for (int i = 0; i < 50; i ) {
popQuestions.push_back(createPopQuestion_Lambda(i));
char str[255];
sprintf(str, ”Science Question %d”, i);
scienceQuestions.push_back(str);
...
}
}
43. Let’s stop and evaluate
Refactorings: extract method, inline, change signature, turn to lambda
46. The result
// Pure function
// No dependencies
auto createPopQuestion_Lambda = [](const int i) -> string {
ostringstream oss(ostringstream ::out);
oss << ”Pop Question ” << i;
return oss.str();
};
47. What about state changes?
bool Game ::add(string playerName) {
players.push_back(playerName);
places[howManyPlayers()] = 0;
purses[howManyPlayers()] = 0;
inPenaltyBox[howManyPlayers()] = false;
cout << playerName << ” was added” << endl;
cout << ”They are player number ” << players.size() << endl;
return true;
}
48. Pick code that makes the state change
bool Game ::add(string playerName) {
// State change
players.push_back(playerName);
// State change
places[howManyPlayers()] = 0;
// State change
purses[howManyPlayers()] = 0;
// State change
inPenaltyBox[howManyPlayers()] = false;
// I/O
cout << playerName << ” was added” << endl;
cout << ”They are player number ” << players.size() << endl;
return true;
}
50. Extract initial value
void Game ::addPlayerNameToPlayersList(const string &playerName) {
// Copy initial value of players
vector<string> &initialPlayers(players);
// Modify the copy
initialPlayers.push_back(playerName);
// Set the data member to the new value
players = initialPlayers;
}
51. Separate pure from impure
void Game ::addPlayerNameToPlayersList(const string &playerName) {
vector<string> initialPlayers = addPlayer_Pure(playerName);
players = initialPlayers;
}
vector<string> Game ::addPlayer_Pure(const string &playerName) const {
vector<string> initialPlayers(players);
initialPlayers.push_back(playerName);
return initialPlayers;
}
60. Data-driven tests
// Groovy and Spock
class MathSpec extends Specification {
def ”maximum of two numbers”(int a, int b, int c) {
expect:
Math.max(a, b) c
where:
a | b | c
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
}
}
61. C++ with doctest
// chapter 11, ”Hands-on Functional Programming with C ”
TEST_CASE(”1 raised to a power is 1”){
int exponent;
SUBCASE(”0”){
exponent = 0;
}
SUBCASE(”1”){
exponent = 1;
}
...
CAPTURE(exponent);
CHECK_EQ(1, power(1, exponent));
}
62. Property-based tests
// chapter 11, ”Hands-on Functional Programming with C ”
TEST_CASE(”Properties”){
cout << ”Property: 0 to power 0 is 1” << endl;
CHECK(property_0_to_power_0_is_1);
...
cout << ”Property: any int to power 1 is the value” << endl;
check_property(
generate_ints_greater_than_0,
prop_any_int_to_power_1_is_the_value, ”generate ints”
);
}
63. Property
// chapter 11, ”Hands-on Functional Programming with C ”
auto prop_any_int_to_power_1_is_the_value = [](const int base){
return power(base, 1) base;
};
66. Example
// Pseudocode
class Player{
string name;
int score;
Player(string name, int score) : name(name), score(score){}
void printName() const {
cout << name << endl;
}
}
67. Equivalent with
auto printName = [string name]() -> void {
cout << name << endl;
}
auto printName = [](string name) -> void {
cout << name<< endl;
}
68. Identify cohesive pure functions
// Similarity in names: Player
auto printPlayerName = [](string playerName) -> void {
...
}
auto computePlayerScore = []( ...) -> {
...
}
69. Identify cohesive pure functions
// Similarity in parameter list
auto doSomethingWithPlayerNameAndScore =
[](string playerName, int playerScore) -> {
...
}
auto doSomethingElseWithPlayerNameAndScore =
[](string playerName, int playerScore) -> {
...
}
70. Refactoring steps
• Create class
• Move functions into class
• Common parameters -> data members
• Add constructor
• Remember: you have tests now!
72. Evaluation
I believe it can be:
• faster to learn: around 10 - 20 mechanics to practice
• faster to refactor: eliminate dependencies while moving to pure functions
• easier to write tests: fundamentally data tables
• safer to refactor to classes
76. More Information
“Think. Design. Work Smart.” YouTube Channel
https://www.youtube.com/channel/UCSEkgmzFb4PnaGAVXtK8dGA/
Codecast ep. 1: https://youtu.be/FyZ_Tcuujx8
Video “Pure functions as nominal form for software design” https://youtu.be/l9GOtbhYaJ8