professional-grade software design
DESCRIPTION
Talk given at MidwestPHP 2014 about guidelines for SOLID object-oriented designTRANSCRIPT
Professional-grade software design
whoami
Brian Fenton
@brianfenton
www.brianfenton.us
Mashery, an Intel Company
Overview
Testing
SOLID
Object calisthenics
Smells
Patterns
Resources
Tests!
If you don’t have tests, you’re REWRITING, not refactoring
Tests first
Testing after you write the class won’t improve your design
More likely to just test the implementation (low value, brittle tests)
Unit testing forces you to feel the pain of bad
design up front
Some smells exposed by tests
Lots of dependenciesClass/method does too much
Requires lots of setup to do anythingClass is too coupled to its environment
Lots of protected/private methodsLikely another class worth of behavior hidden inside
SOLID
S – Single Responsibility
O – Open/Closed
L – Liskov Substitution
I – Interface Segregation
D – Dependency Inversion
Because no one can pronounce SRPOCPLSPISPDIP
Single Responsibility Principle
<?phpclass Person extends Model { public $name; public $birthDate; protected $preferences;
public function getPreferences() {}
public function save() {}}
<?phpclass Person extends Model { public $name; public $birthDate; protected $preferences;
public function getPreferences() {}}
class DataStore public function save(Model $model) {}}
Open/Closed Principle
Open for extension
Closed for modification
The OCP litmus test
Can you add/change a feature by only adding new classes?
Also allowed to updateControllers
Configuration
Templates
Liskov Substitution Principle
“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
abstract class Shape{ public function getHeight();
public function setHeight($height);
public function getLength();
public function setLength($length);}
class Square extends Shape{ protected $size;
public function getHeight() { return $this->size; }
public function setHeight($height) { $this->size = $height; }
public function getLength() { return $this->size; }
public function setLength($length) { $this->size = $length; }}
class Rectangle extends Shape{ protected $height; protected $length;
public function getHeight() { return $this->height; }
public function setHeight($height) { $this->height= $height; }
public function getLength() { return $this->length; }
public function setLength($length) { $this->length= $length; }}
Interface Segregation Principle
Dependency Inversion Principle
“Higher level modules should not depend on lower level modules”
TL;DR
mysqli_query() BAD
DataStore->query() GOOD
Naming
Care about your names (class/method/variable)
If it’s hard to name something, it means you can’t describe it succinctly
If you can’t describe what it does, it does too much or you don’t understand what it does
Good Naming
Don’t abbreviate
Don’t be afraid to be verbose
Suspect Names
ClassesManagerHandler
MethodsProcess
“And”
Comments
Use inline comments sparingly
Do use docblocks though
TODOs
Tend to rot and never get fixed
If you use a TODO, add a ticket number
A well-named method that communicates intent is far
more valuable than a comment
Methods
You can (almost) always make a method smaller
Pay attention to your execution path
Check your CRAP index with phpmd or codesniffer
You can (almost) always add more methods
Cognitive load
Declare variables as close to when they will be used as possible
If you can pass data from method to method directly, no need for a variable
Source order
Declare methods in the order they’re called
Public to private
Avoid “magic” values
DRY
Single source of truth
Self-documenting
Which of these is easier to understand?
json_last_error() == 5;
json_last_error() == JSON_ERROR_UTF8;
$length > 1024
$length > self::MAX_LENGTH
$limit = 0
$limit = self::RATE_UNLIMITED
Not what I mean!
unsigned three = 1;
unsigned five = 5;
unsigned seven = 7;
https://github.com/torvalds/linux/blob/d158fc7f36a25e19791d25a55da5623399a2644f/fs/ext4/resize.c#L698-700
“2 is a code smell”- Alex Miller
Dependency Injection
Pass external dependencies into objectsConstructor injection
Setter injection
Potential smell: too many dependencies
Ask for things, don’t look for them
Object Calisthenics(briefly)
No more than one level of indentation per method
public function processData($data) { $newData = array(); $count = 1; foreach ($data as $row) { if (!$row) { continue; } if ($count === 1) { $newData[] = implode(',', array_keys($row)); } else { $newData[] = implode(',', $row); } $count++; } return $newData;}
…foreach ($data as $row) { // skip empty rows if (!$row) { continue; } if ($count === 1) { $newData[] = implode(',', array_keys($row)); } else { $newData[] = implode(',', $row); } $count++;}…
public function processData($data) { $newData = array(); $count = 1; $data = $this->filterEmptyRows($data); foreach ($data as $row) { if ($count === 1) { $newData[] = implode(',', array_keys($row)); } else { $newData[] = implode(',', $row); } $count++; } return $newData;}
public function filterEmptyRows($rows) { return array_filter($rows);}
public function processData($data) { $newData = array(); $count = 1; $data = $this->filterEmptyRows($data); foreach ($data as $row) { $newData[] = $this->processRow($row, $count); $count++; } return $newData;}
public function processRow($row, $count) { if ($count === 1) { return implode(',', array_keys($row)); } else { return implode(',', $row); }}
public function processData($data) { $newData = array(); $headersOnly = true; $data = $this->filterEmptyRows($data); foreach ($data as $row) { $newData[] = $this->processRow($row, $headersOnly); $headersOnly = false; } return $newData;}
public function processRow($row, $headersOnly) { if ($headersOnly === true) { return implode(',', array_keys($row)); } else { return implode(',', $row); }}
Interlude… which is clearer?
$this->setActive(true);
$this->setActive(false);
OR
$this->activate();
$this->deactivate();
public function processRow($row, $headersOnly) { if ($headersOnly === true) { return $this->getHeaderRow($row); } else { return implode(',', $row); }}
public function getHeaderRow($row) { return implode(',', array_keys($row));}
public function processRow($row, $headersOnly) { if ($headersOnly === true) { return $this->getHeaderRow($row); } else { return $this->toCsv($row); }}
public function toCsv($row) { return implode(',', $row);}
public function processData($data) { $newData = array(); $data = $this->filterEmptyRows($data); $firstRow= array_pop($data); $newData[] = $this->getHeaderRow($firstRow); foreach ($data as $row) { $newData[] = $this->toCsv($row); } return $newData;}
public function transformToCsv($data) { $data = $this->filterEmptyRows($data);
$firstRow= array_pop($data); $csv = array(); $csv[] = $this->getHeaderRow($firstRow);
foreach ($data as $row) { $csv[] = $this->toCsv($row); } return $csv;}
public function processData($data) { $newData = array(); $count = 1; foreach ($data as $row) { if (!$row) { continue; } if ($count === 1) { $newData[] = implode(',', array_keys($row)); } else { $newData[] = implode(',', $row); } $count++; } return $newData;}
Don’t use else
public function addThreeInts($first, $second, $third) { if (is_int($first)) { if (is_int($second)) { if (is_int($third)) { $sum = $first + $second + $third; } else { return null; } } else { return null; } } else { return null; } return $sum;}
public function addThreeInts($first, $second, $third) { if (!is_int($first)) { return null; }
if (!is_int($second)) { return null; }
if (!is_int($third)) { return null; }
return $first + $second + $third;}
Command-Query Separation
Complete separation between questions and commands
“Asking a question shouldn’t change the answer”
public function getUser($id) { $user = $this->dataStore->fetchUser($id); if (!$user) { $user = new User(array($id)); }
return $user;}
public function getUser($id) { return ($this->dataStore->fetchUser($id) ?: null;}
The final secret…
OOP is all about message passing and behaviors
It’s not about inheritance
It’s not about code reuse
Favor composition over inheritance
Treat objects like APIs
So decouple! Such
architecture
So amaze
Wow
Much message
Summary
Write small objects
Write tiny methods
Strive for good names
Seek loose coupling
Focus on message passing
Treat objects like APIs
Write tests (first)
Refactor w/discipline
Limit nesting/no else
Use guard clauses
Avoid magic (anything)
Use CQS
Avoid in-line comments
Reduce cognitive load
Resources - Presentations
The Clean Code Talks -- Inheritance, Polymorphism, & Testing
Object Calisthenics
Resources – Books
Code Complete: A Practical Handbook of Software Construction, Second Edition
Growing Object-Oriented Software, Guided by Tests
Refactoring: Improving the Design of Existing Code