Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Refactor legacy code through pure functions

152 views

Published on

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.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Refactor legacy code through pure functions

  1. 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. 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
  3. 3. The Problem Of Legacy Code
  4. 4. Legacy Code is everywhere Depiction of legacy code
  5. 5. Legacy Code erodes the will of programmers
  6. 6. What is Legacy Code? Michael Feathers: “Any piece of code that doesn’t have automated tests”
  7. 7. Generalized Definition Alex Bolboaca: “Any piece of code that you are afraid to change”
  8. 8. Legacy Code leads to the Dark Side Yoda: “Fear leads to anger. Anger leads to hate. Hate leads to suffering.”
  9. 9. Existing Methods
  10. 10. “Change and pray” method If it works for you, great. Is it repeatable and transferrable though?
  11. 11. “No more changes” method Freeze code, nobody touches it. Works with strangler pattern
  12. 12. Strangler Pattern Strangle the existing code with a new implementation until the new implementation works fine.
  13. 13. Michael Feathers’ Refactoring method Identify seams -> small, safe refactorings -> write characterization tests -> refactor code to desired design
  14. 14. Mikado method
  15. 15. Disadvantages of existing methods
  16. 16. Legacy Code is Messy Often multiple methods required, and time consuming
  17. 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
  18. 18. Learning the methods takes long Months, maybe years of practice
  19. 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. 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)
  21. 21. Step 1: To pure functions
  22. 22. What is a pure function? • Returns same output values for the same input values • Does not depend on state
  23. 23. Example of Pure Functions int add(const int first, const int second){ return first + second; }
  24. 24. Example of Impure Function int addToFirst(int& first, const int second){ first+=second; return first; }
  25. 25. Example of “Tolerated” Pure Function int increment(int first){ return first; //state change, local to the function }
  26. 26. Why pure functions? When you turn your switch on, the water doesn’t start on your sink
  27. 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. 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. 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. 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)); } }
  31. 31. 1. Pick a code block ostringstream oss(ostringstream ::out); oss << ”Pop Question ” << i; popQuestions.push_back(oss.str());
  32. 32. 2. Extract method void Game ::doSomething(int i) { ostringstream oss(ostringstream ::out); oss << ”Pop Question ” << i; popQuestions.push_back(oss.str()); }
  33. 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. 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()); }
  35. 35. Revert change void Game ::doSomething(const int i) { ostringstream oss(ostringstream ::out); oss << ”Pop Question ” << i; popQuestions.push_back(oss.str()); }
  36. 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. 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. 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. 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. 40. Some simplification string Game ::createPopQuestion(const int i) const { ostringstream oss(ostringstream ::out); oss << ”Pop Question ” << i; return oss.str(); }
  41. 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. 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. 43. Let’s stop and evaluate Refactorings: extract method, inline, change signature, turn to lambda
  44. 44. The compiler helps us The compiler tells us the function dependencies
  45. 45. Mechanical process - or engineering procedure Mechanical process
  46. 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. 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. 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; }
  49. 49. Extract method void Game ::addPlayerNameToPlayersList(const string &playerName) { players.push_back(playerName); }
  50. 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. 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; }
  52. 52. Inline computation void Game ::addPlayerNameToPlayersList(const string &playerName) { players = addPlayer_Pure(playerName); }
  53. 53. Inline method bool Game ::add(string playerName) { players = addPlayer_Pure(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; }
  54. 54. Make static vector<string> Game ::addPlayer_Pure(const string &playerName) { // Compilation error: players unavailable vector<string> initialPlayers(players); initialPlayers.push_back(playerName); return initialPlayers; }
  55. 55. Undo static & Introduce parameter vector<string> Game ::addPlayer_Pure( const string &playerName, const vector<string> &thePlayers) { vector<string> initialPlayers(thePlayers); initialPlayers.push_back(playerName); return initialPlayers; }
  56. 56. Move to lambda auto addPlayerNameToCollection_Lambda = [](const string &playerName, const vector<string> &thePlayers) -> vector<string> { vector<string> initialPlayers(thePlayers); initialPlayers.push_back(playerName); return initialPlayers; }; vector<string> Game ::addPlayer_Pure(const string &playerName, const vector<string> &thePlayers) { addPlayerNameToCollection_Lambda(playerName, thePlayers); }
  57. 57. Inline bool Game ::add(string playerName) { players = addPlayerNameToCollection_Lambda(playerName, players); places[howManyPlayers()] = 0; purses[howManyPlayers()] = 0; inPenaltyBox[howManyPlayers()] = false; cout << playerName << ” was added” << endl; cout << ”They are player number ” << players.size() << endl; return true; }
  58. 58. List of mechanics (work in progress) • extract method -> make const -> make static -> introduce parameter -> turn to lambda • state change -> newValue = computeNewValue(oldValue) • if( ...){ ...}else{ ...} -> result = decide( ...) • if( ...){ ...} -> std ::optional • I/O -> void outputSomething( ...) or int readSomething( ...)
  59. 59. Step 2: Test pure functions
  60. 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. 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. 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. 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; };
  64. 64. Step 3: Pure functions to classes
  65. 65. Equivalence A class is a set of partially applied, cohesive pure functions
  66. 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. 67. Equivalent with auto printName = [string name]() -> void { cout << name << endl; } auto printName = [](string name) -> void { cout << name<< endl; }
  68. 68. Identify cohesive pure functions // Similarity in names: Player auto printPlayerName = [](string playerName) -> void { ... } auto computePlayerScore = []( ...) -> { ... }
  69. 69. Identify cohesive pure functions // Similarity in parameter list auto doSomethingWithPlayerNameAndScore = [](string playerName, int playerScore) -> { ... } auto doSomethingElseWithPlayerNameAndScore = [](string playerName, int playerScore) -> { ... }
  70. 70. Refactoring steps • Create class • Move functions into class • Common parameters -> data members • Add constructor • Remember: you have tests now!
  71. 71. Final Thoughts
  72. 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
  73. 73. Careful • Introducing unexpected side effects • Temporal coupling • Global state
  74. 74. Caveats • discipline & practice is required • does not solve all the problems • the resulting design doesn’t follow “Tell, don’t ask”
  75. 75. Is it safe? More testing required
  76. 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
  77. 77. Questions Win a book (more at “ask the speakers”)
  78. 78. Photo Attribution https://unsplash.com/@alexagorn?utm_source=unsplash&utm_medium=referral&utm_content=creditCop https://fthmb.tqn.com/5Jqt2NdU5fjtcwhy6FwNg26yVRg=/1500x1167/filters:fill(auto,1)/yoda- 56a8f97a3df78cf772a263b4.jpg https://aleteiaen.files.wordpress.com/2017/09/web3-praying-hands-work-computer-desk-office- prayer-shutterstock.jpg?quality=100&strip=all https://docs.microsoft.com/en-us/azure/architecture/patterns/_images/strangler.png https://images.unsplash.com/photo-1526814642650-e274fe637fe7?ixlib=rb- 1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80 https://images.unsplash.com/photo-1536158525388-65ad2110afc8?ixlib=rb- 1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80 https://cdn.pixabay.com/photo/2019/11/25/21/24/switch-4653056_960_720.jpg

×