Modernising Legacy
      Code
     Sam Harwood
What is Legacy Code?
Old


Fragile


Incomprehensible


Not under test
Why Modernise Legacy Code?
New development


Bug fixes


Change requests


To reduce maintenance costs
When to Refactor Legacy Code
There's no smoke...


'It's not safe to refactor'


Key element of application


'My boss told me not to'
An Iterative Approach
Start small


Identify target areas


Deploy, deploy, deploy!
Process Plan
Break dependencies with minimal changes;
 apply tests


Make small changes


Clean up tests


Refactor to clean code
Untestable Behaviour
DB connectivity
Web services
Global vars
exit();
File work
System calls
echo(), print(), var_dump(), …
Memcache etc.
How to Break Dependencies
Partial mockery


Dependency injection


Out-of-context testing
Option 1: Partial Mockery
public function __construct() {
    $dbh = new DbConnection('localhost', 'db', 'user', 'pass',
        3306);
    if ($dbh->connection_fail) {exit;}
    if ($GLOBALS['debug mode']) {
        print 'Constructing!';
    }
    $this->doOtherThings();
}
Partial Mocks cont.
public function __construct() {
    $dbh = $this->_getDbConnection();
    //...
}

/**
  * @return DbConnection
  */
protected function _getDbConnection() {
     return new DbConnection('localhost', 'db', 'user', 'pass',
         3306);
}
Partial Mocks cont.
public function setUp() {
    $this->_objectUnderTest = $this->getMock('MyClass',
        array('_getDbConnection', '_doExit', '_sendHttpQuery'),
        array(), '', '', true);

    $this->_mockDbConnection = $this->getMock('DbConnection');

    $this->_objectUnderTest->expects($this->any())
                           ->method('_getDbConnection')
                           ->will($this->returnValue($this->
                                 _mockDbConnection));
}
Option 2: Dependency Injection
public function __construct() {
    $dbh = $this->_yadifContainer->getComponent('DbConnection');
    //...
}
Option 3: Out-of-Context Testing
 Fake objects


 set_include_path('');


 runkit.internal_override &
  runkit_function_redefine()
Next Steps
Provide satisfactory test coverage, and
 refactor...


Add tests for constructor
Comment code to help plan tests
Cover other code addressed in this iteration
Add Comments...
public function massiveMethod($param = 'hello world') {

   // Check if DB knows how awesome we are
   $dbConnection = $this->_getDbConnection();
   $resultSet = $dbConnection->executeSql(
       'SELECT group_of_developers FROM london WHERE everyone_is
       LIKE "%we're the best php%" GROUP BY miles');
   $dbRes = count($resultSet) > 0;

   // Calculate something else
   $aString = $param;
   $aString .= 'more string';
   $aString .= 'more string';
   $aString .= 'more string';
   $aString .= 'more string';
...Remove Comments
public function massiveMethod($param = 'hello world') {


   $dbRes = $this->_checkIfDbKnowsHowAwesomeWeAre();




   $aString   = $this->_generateSomeString($param);
Things to Bear in Mind When
Breaking Up Large Functions

Method name reflects comment


Test section before moving it


Parameter count
Looking Inside Large Methods
Sensing variables
  if () {
      foreach () {
          if () {
              $this->_privateAttrib = 'something';
              $this->sensingVariable = true;
          }
      }
  }


Indecent exposure


Partially mock most of class
Cleaning Up Production Code
Minimise public API


Remove unnecessary comments


Roll out dependency injection


Be sure to remove methods for partial
 mocking
Refactoring Tests
Inject mock objects via DI


Stop testing partial mock


Remove 'out-of-context' setup


Remove single-line constructor tests,
 sensing variable tests etc.
Summary
Break dependencies


Apply tests


Refactor production code


Refactor tests
samthephpdev@gmail.com

     @AgileTillIDie

Modernising Legacy Code

  • 1.
    Modernising Legacy Code Sam Harwood
  • 2.
    What is LegacyCode? Old Fragile Incomprehensible Not under test
  • 3.
    Why Modernise LegacyCode? New development Bug fixes Change requests To reduce maintenance costs
  • 4.
    When to RefactorLegacy Code There's no smoke... 'It's not safe to refactor' Key element of application 'My boss told me not to'
  • 5.
    An Iterative Approach Startsmall Identify target areas Deploy, deploy, deploy!
  • 6.
    Process Plan Break dependencieswith minimal changes; apply tests Make small changes Clean up tests Refactor to clean code
  • 7.
    Untestable Behaviour DB connectivity Webservices Global vars exit(); File work System calls echo(), print(), var_dump(), … Memcache etc.
  • 8.
    How to BreakDependencies Partial mockery Dependency injection Out-of-context testing
  • 9.
    Option 1: PartialMockery public function __construct() { $dbh = new DbConnection('localhost', 'db', 'user', 'pass', 3306); if ($dbh->connection_fail) {exit;} if ($GLOBALS['debug mode']) { print 'Constructing!'; } $this->doOtherThings(); }
  • 10.
    Partial Mocks cont. publicfunction __construct() { $dbh = $this->_getDbConnection(); //... } /** * @return DbConnection */ protected function _getDbConnection() { return new DbConnection('localhost', 'db', 'user', 'pass', 3306); }
  • 11.
    Partial Mocks cont. publicfunction setUp() { $this->_objectUnderTest = $this->getMock('MyClass', array('_getDbConnection', '_doExit', '_sendHttpQuery'), array(), '', '', true); $this->_mockDbConnection = $this->getMock('DbConnection'); $this->_objectUnderTest->expects($this->any()) ->method('_getDbConnection') ->will($this->returnValue($this-> _mockDbConnection)); }
  • 12.
    Option 2: DependencyInjection public function __construct() { $dbh = $this->_yadifContainer->getComponent('DbConnection'); //... }
  • 13.
    Option 3: Out-of-ContextTesting Fake objects set_include_path(''); runkit.internal_override & runkit_function_redefine()
  • 14.
    Next Steps Provide satisfactorytest coverage, and refactor... Add tests for constructor Comment code to help plan tests Cover other code addressed in this iteration
  • 15.
    Add Comments... public functionmassiveMethod($param = 'hello world') { // Check if DB knows how awesome we are $dbConnection = $this->_getDbConnection(); $resultSet = $dbConnection->executeSql( 'SELECT group_of_developers FROM london WHERE everyone_is LIKE "%we're the best php%" GROUP BY miles'); $dbRes = count($resultSet) > 0; // Calculate something else $aString = $param; $aString .= 'more string'; $aString .= 'more string'; $aString .= 'more string'; $aString .= 'more string';
  • 16.
    ...Remove Comments public functionmassiveMethod($param = 'hello world') { $dbRes = $this->_checkIfDbKnowsHowAwesomeWeAre(); $aString = $this->_generateSomeString($param);
  • 17.
    Things to Bearin Mind When Breaking Up Large Functions Method name reflects comment Test section before moving it Parameter count
  • 18.
    Looking Inside LargeMethods Sensing variables if () { foreach () { if () { $this->_privateAttrib = 'something'; $this->sensingVariable = true; } } } Indecent exposure Partially mock most of class
  • 19.
    Cleaning Up ProductionCode Minimise public API Remove unnecessary comments Roll out dependency injection Be sure to remove methods for partial mocking
  • 20.
    Refactoring Tests Inject mockobjects via DI Stop testing partial mock Remove 'out-of-context' setup Remove single-line constructor tests, sensing variable tests etc.
  • 21.
  • 22.