code smells in php

47
Dagfinn Reiersøl, ABC Startsiden 1 Code smells in PHP

Upload: dagfinnr

Post on 11-May-2015

10.713 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 1

Code smells in PHP

Page 2: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 2

Who am I?

• Dagfinn Reiersøl

[email protected]

– Twitter: @dagfinnr

– Blog: agilephp.com

• Mostly PHP since 1999

• Wrote PHP in Action

• Code quality / agile development enthusiast

Page 3: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 3

What is a code smell?

Train your nose to tell you...

– ...when to refactor

– ...what to refactor

– ...how to refactor

Page 4: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 4

It's more like a diagnosis

• A disease has a treatment, a code smell has a refactoring

• (or several)

• Code smell distinctons are important

• For each smell, there is one or more refactorings

• http://industriallogic.com/papers/smellstorefactorings.pdf

Page 5: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 5

What is refactoring?

• “Improving the design of existing code”

• Maintain behavior

• Change the structure

• Make it more readable, eliminate duplication

• Proceed by small steps

• Keep code working always

Page 6: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 6

The “bible” of refactoring

• “Martin Fowler...is not Jesus Christ, and his books are not the Bible.”

• Except this one really is the bible.

• Java examples, but mostly PHP-relevant

Page 7: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 7

Refactoring is a specific, learnable skill

• Learn to apply specific, named refactorings.

• Refactorings have specific instructions

• Learn to go in baby steps

• Test between each step

• Undo if you get lost

• Weird intermediate results are OK

Page 8: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 8

Refactoring in PHP

• Very little tool support

• This is both a bad thing and a good thing

• Extract Method is particularly crucial, but unsupported by tools

Page 9: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 9

Why refactor?

• Make code easier to read (saves time)

• Make it easier to find bugs

• Learn design principles

• Discover new abstractions

• Clean, maintainable code

Page 10: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 10

How much is enough?

• Make it as clean as you possibly can, if circumstances allow

• Boy Scout Rule

• When you change code, you're likely to change it again soon

• Better code needs less refactoring

• Better code is easier to refactor

Page 11: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 11

Why duplication is so bad

• Harder to maintain

• Harder to debug

• Incomplete bug fixes

Original code

DebugFirst copy

Second copy

Original code

First copy

Second copy

Page 12: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 12

Automated test coverage is essential

• Unit tests primarily

• Tests make it easy to fix when you break something

• Acceptance tests helpful sometimes

• Manual testing only in special, desperate circumstances

• Legacy code paradox

Page 13: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 13

Another bible: Clean Code

• Lots of smells and heuristics

Page 14: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 14

Don't take examples personally

• I'm using somewhat real open-source examples

• No personal criticism implied

• Refactoring examples must be somewhere in the middle (not awful, not perfect)

• Awful is too hard to refactor (=advanced material)

• Perfect doesn't exist

• Just pretend I wrote all of it ;-)

Page 15: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 15

Duplicated Code

// even if we are interacting between a table defined in a// class and a/ table via extension, ensure to persist the

// definition

if (($tableDefinition = $this->_table->getDefinition()) !== null

&& ($dependentTable->getDefinition() == null)) {

$dependentTable->setOptions(

array(Table::DEFINITION => $tableDefinition));

}

...

// even if we are interacting between a table defined in a

// class and a/ table via extension, ensure to persist the

// definition

if (($tableDefinition = $this->_table->getDefinition()) !== null

&& ($parentTable->getDefinition() == null)) {

$parentTable->setOptions(

array(Table::DEFINITION => $tableDefinition));

}

...plus two more

Page 16: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 16

Long Method

• Long methods are evil

• Hard to read (time-consuming)

• Hard to test

• Tend to have duplicate logic

• Hard to override specific behaviors

Page 17: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 17

How long?

• “The first rule of functions is that they should be small”

• “The second rule of functions is that they should be smaller than that”. - Robert C. Martin, Clean Code

• My experience: the cleanest code has mostly 2-5 line methods

• But don't do it if it doesn't make sense

• Do One Thing

• One level of abstraction only

Page 18: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 18

Refactoring a Long Method

• Split method into smaller methods

• Extract Method is the most important refactoring// Execute cascading updates against dependent tables.// Do this only if primary key value(s) were changed.if (count($pkDiffData) > 0) { $depTables = $this->_getTable()->getDependentTables(); if (!empty($depTables)) { $pkNew = $this->_getPrimaryKey(true); $pkOld = $this->_getPrimaryKey(false); foreach ($depTables as $tableClass) { $t = $this->_getTableFromString($tableClass); $t->_cascadeUpdate($this->getTableClass(), $pkOld, $pkNew); } }

Page 19: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 19

Extract Method: mechanics

1.Copy the code into a new method

2.Find all temporary variables

3.Return all of them from the methodIn PHP, unlike Java, we can return multiple variabless

4.Find the ones that are initialized in the method

5.Pass all of those into the method

6.The result is ugly, but a step forward

Page 20: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 20

Extract Method: result

private function executeCascadingUpdates($pkDiffData) { if (count($pkDiffData) > 0) { $depTables = $this->_getTable()->getDependentTables(); if (!empty($depTables)) { $pkNew = $this->_getPrimaryKey(true); $pkOld = $this->_getPrimaryKey(false); foreach ($depTables as $tableClass) { $t = $this->_getTableFromString($tableClass); $t->_cascadeUpdate( $this->getTableClass(), $pkOld, $pkNew); } } } return array($pkDiffData,$tableClass, $depTables,$pkNew,$pkOld,$t);}

Page 21: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 21

Validation Overcrowding

function setTable(Table $table){ $tableClass = get_class($table); if (! $table instanceof $this->_tableClass) { require_once 'My_Exception.php'; throw new My_exception("blah blah"); } $this->_table = $table; $this->_tableClass = $tableClass; $info = $this->_table->info(); if ($info['cols'] != array_keys($this->_data)) { require_once 'My_Exception.php'; throw new My_exception("blah blah"); } if (!array_intersect((array)$this->_primary, info['primary']) == (array) $this->_primary) { require_once 'My_Exception.php'; throw new My_exception("blah blah"); } $this->_connected = true;

Page 22: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 22

Extracting Validation

• Don't waste time reading validation code

• Extract validation (logging, error handling) into separate method(s)

function setTable(Table $table) { $this-validateTable($table); $this->_table = $table; $this->_tableClass = get_class($table); $this->_connected = true;}

Page 23: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 23

Large Class

• As methods get smaller, there will be more of them

• Hard to keep track of all the methods

• Class has multiple responsibilities

• A class should have only one reason to change

• Duplication is likely

Page 24: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 24

Refactoring a Large Class

• Primarily Extract Class

• Look for patterns in method namesfunction __construct($url = null, $useBrackets = true)...function initialize()...function getURL()...function addQueryString($name, $value, $preencoded = false)...function removeQueryString($name)...function addRawQueryString($querystring)...function getQueryString()...function _parseRawQuerystring($querystring)...function resolvePath($path)...function getStandardPort($scheme)...function setProtocol($protocol, $port = null)...function setOption($optionName, $value)...function getOption($optionName)...

Page 25: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 25

Refactoring a Large Class

• Look for

– Patterns in method names (see previous)

– Subset of data and methods that go together

– Subset of data that change together

• Mechanics in short

– Create a new class

– Copy variables and methods into it

– Change methods one by one to delegate to the new class

• You must have automated tests

Page 26: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 26

Primitive Obsession

“People new to objects usually are reluctant to use small objects for small tasks, such as money classes that combine number and currency, ranges with an upper and lower, and special strings such as telphone numbers and ZIP codes.”

- Martin Fowler, Refactoring

Page 27: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 27

Primitive obsession: non-OO dates

• Will this work?strftime($arrivaltime);

• Plain PHP date handling is ambiguous, obscure and error-prone

• Use objects instead$datetime = new DateTime('2008-08-03 14:52:10');echo $datetime->format('jS, F Y') . "\n";

Page 28: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 28

Primitive obsession example

• The primary key is an array (when?) or a scalar (when?)

• Are these names or values?if (is_array($primaryKey)) { $newPrimaryKey = $primaryKey;} else { $tempPrimaryKey = (array) $this->_primary; $newPrimaryKey = array( current($tempPrimaryKey) => $primaryKey);}return $newPrimaryKey;

Page 29: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 29

Primitive obsession example

• Huh? /** * Were any of the changed columns part of the primary key? */$pkDiffData = array_intersect_key( $diffData, array_flip((array)$this->_primary));}

• It's clever, obscure, therefore error-prone

• I think I prefer this:foreach ((array)$this->_primary as $pkName) { if (array_key_exists($pkName,$diffData)) $pkDiffData[$pkName] = $diffData[$pkName];}

Page 30: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 30

Primary key class

class PrimaryKey {

public function __construct($primitive) { $this->primitive = $primitive; } public function isCompoundKey() { return is_array($this->primitive); } public function getSequenceColumn() { return array_shift($this->asArray()); } public function asArray() { return (array) $this->primitive; } public function filter($data) { return array_intersect_key( $data, array_flip($this->asArray())); }}

Page 31: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 31

Benefits of the PrimaryKey class

• More expressive client code

• Details can be found in one place

• Less duplication

• Easier to add features

• Much easier to test

Page 32: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 32

More expressive code and tests

• What this comment is telling us.../** * [The class] assumes that if you have a compound primary key * and one of the columns in the key uses a sequence, * it's the _first_ column in the compound key. */

• ...can be expressed as a test./** @test */

function shouldAssumeFirstColumnIsSequenceColumn() {

$primaryKey = new PrimaryKey('phone','name');

$this->assertEquals(

'phone',

$primaryKey->getSequenceColumn()

);

}

Page 33: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 33

Consider a small class when...

• ...two or more data values occur together repeatedly (ranges, etc)

• ...you keep testing the type of a data value

• ...you keep converting a data value

• ...you keep testing for null

Page 34: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 34

More Primitive Obsession

Error-prone:$info = $table->info();$this->_primary = (array) $info['primary'];

Verbose:$info = $table->info();$this->_primary = (array) $info[SomeClassName::PRIMARY];

Better:$this->_primary = $table->getPrimaryKey();

Page 35: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 35

Don't return null

Alternatives:

• Throw an exception

• Return an empty array

• Return a Null Object (Special Case)

Page 36: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 36

Nested ifs and loops

• Hard to read

• Even harder to testif (isset($this->session)) { //... if (isset($this->session['registered']) && isset($this->session['username']) && $this->session['registered'] == true && $this->session['username'] != '') { //... if ($this->advancedsecurity) { $this->log( 'Advanced Security Mode Enabled.',AUTH_LOG_DEBUG); //...

Page 37: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 37

How to deal with nesting

• Start with the deepest level, extract methods

• How much code to extract?

– Whole expression (foreach (...) {...})

– Code inside expression

– Part of code inside expression

• Adding tests (if none exist)

– Write tests first, then extract?

– Or do careful extraction, then add tests?

• Replace Nested Conditional with Guard Clauses

Page 38: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 38

Conditionals in general

• Often smelly

• Learn how to avoid or simplify them

• http://www.antiifcampaign.com

• Replace Conditional with Polymorphism

• Decompose Conditional// handle single space characterif(($nb==1) AND preg_match("/[\s]/u", $s))

if($this->isSingleSpaceCharacter($s))

Page 39: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 39

Feature envy / Inappropriate Intimacy

• Does the row object need to poke inside the table objects, checking for null?

• No, it's an implementation detail of the Table class

• It's accessed as if it were public, breaking encapsulationabstract class Row...

if (($tableDefinition = $this->_table->getDefinition())

!== null

&& ($dependentTable->getDefinition() == null))

{

$dependentTable->setOptions(

array(Table::DEFINITION

=> $tableDefinition));

Page 40: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 40

Move everything into the “envied” class?

This simple?$dependentTable->copyDefinitionFrom($this->_table);

Misleading method name. How about this?$dependentTable->copyDefinitionIfNeededFrom($this->_table);

Ugh. Let's Separate Query from Modifierif (!$dependentTable->hasDefinition()) //query

$dependentTable->copyDefinitionFrom($this->_table);

Page 41: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 41

Redundant Comment

/** * If the _cleanData array is empty, * this is an INSERT of a new row. * Otherwise it is an UPDATE. */if (empty($this->_cleanData)) { return $this->_doInsert();} else { return $this->_doUpdate();}

Page 42: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 42

(Partly) obsolete comment

The comment has (apparently) not been updated to include all options

* Supported params for $config are:-

* - table...

* - data...

* @param...

public function __construct(array $config = array())...

{

if (isset($config['table'])...

if (isset($config['data']))...

if (isset($config['stored'])...

if (isset($config['readOnly'])...

Page 43: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 43

Functions should descend only one level of abstraction

• _cleanData is at a lower level of abstraction than _doInsert() and _doUpdate()

• This is hard, but importantif (empty($this->_cleanData)) { return $this->_doInsert();} else { return $this->_doUpdate();}

if ($this->isNewObject()) { return $this->_doInsert();} else { return $this->_doUpdate();}

Page 44: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 44

Long Parameter List

• Hard-to-read method calls, especially with nulls and booleans

• Uncle Bob: More than three arguments “require special justification”

• Easy to mix up arguments, causing bugs

• Hard to test all variations$nextContent = $phpcsFile->findNext( array(T_WHITESPACE, T_COMMENT), ($closeBrace + 1), null, true);

Page 45: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 45

One way to shrink the argument list

• Remove unused argumentspublic function quoteInto($text, $value,

$type = null, $count = null)

Page 46: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 46

Replace optional / boolean arguments with methods

Boolean argumentprotected function _getPrimaryKey($useDirty = true)

Split into separate methods insteadprotected function _getPrimaryKeyDirty()

protected function _getPrimaryKeyClean()

Or even objects$this->dirtyData->getPrimaryKey();

$this->cleanData->getPrimaryKey();

Page 47: Code smells in PHP

Dagfinn Reiersøl, ABC Startsiden 47

Introduce Parameter Object

• Encapsulate two or more arguments in a class

• Try to make it more meaningful than “options”public function log($id, $username, $command = 'unknown',

$action,$e)

public function log(LogEvent $event)...