Upload
guilherme-blanco
View
530
Download
7
Embed Size (px)
Citation preview
PHP 4 ADULTSOBJECT CALISTHENICS AND CLEAN CODE
GUILHERMEBLANCO
GUILHERMEBLANCO
Guilherme Blanco
MOTIVATION
▸ Readability
▸ Maintainability
▸ Reusability
▸ Testability
SUMMING UP…
CLEAN CODE
S T U P I D
SINGLETON TIGHT COUPLING UNTESTABILITY PREMATURE OPTIMIZATION INDESCRIPTIVE NAMING DUPLICATION
S O L I D
SINGLE RESPONSIBILITY OPEN/CLOSED PRINCIPLE LISKOV SUBSTITUTION PRINCIPLE INTERFACE SEGREGATION DEPENDENCY INVERSION
LISKOV SUBSTITUTION PRINCIPLE
interface Bird { public function setLocation($longitude, $latitude);
public function setHeight($height);
public function draw(); }
class Penguin implements Bird { public function setHeight($height) { // Do nothing }
}
interface Bird { public function setLocation($longitude, $latitude);
public function draw();}
interface FlightfulBird extends Bird { public function setHeight($height);
}
DEPENDENCY INVERSION PRINCIPLE
namespace Dating\UserBundle\Entity { class User { /** @var \Dating\UserBundle\Entity\Image */ protected $avatar; } }
namespace Dating\MediaBundle\Entity { class Image { /** @var \Dating\UserBundle\Entity\User */ protected $owner; } }
namespace Dating\UserBundle\Entity { class User { /** @var AvatarInterface */ protected $avatar; }
interface AvatarInterface { // ... }}
namespace Dating\MediaBundle\Entity { use Dating\UserBundle\Entity\AvatarInterface;
class Image implements AvatarInterface { /** @var \Dating\UserBundle\Entity\User */ protected $owner; } }
OBJECT CALISTHENICS
RULE #1ONLY ONE INDENTATION LEVEL PER METHOD
public function validateForm($filters='', $validators='', $options='') { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if ($input->hasInvalid() || $input->hasMissing()) { foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } }
return $input;}
12
34
public function validateForm($filters='', $validators='', $options='') { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if ($input->hasInvalid() || $input->hasMissing()) { foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } } }
return $input;}
Class prototype
EARLY RETURNS
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } }
return $input;}
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } }
return $input;}
12
3
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { if (strpos($message, "empty")) { throw new Tss_FormException( "The field {$field} cannot be empty!", 3, "javascript:history.back();" ); } else { throw new Tss_FormException( "{$message}", 3, "javascript:history.back();" ); } } }
return $input;}
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message;
throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } }
return $input;}
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message;
throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } }
return $input;}
12
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { foreach ($messageList as $message) { $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message;
throw new Tss_FormException( $errorMessage, 3, "javascript:history.back();" ); } }
return $input;}
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message;
throw new Tss_FormException($errorMessage, 3, $jsAction); }
return $input;}
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST;
$input = new Zend_Filter_Input($filters, $validators, $data, $options); $input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? "The field {$field} cannot be empty!" : $message;
throw new Tss_FormException($errorMessage, 3, $jsAction); }
return $input;}
Logical groups
Variable interpolation
public function validateForm($filters=array(), $validators=array(), $options=array()) { $data = $_POST; $input = new Zend_Filter_Input($filters, $validators, $data, $options);
$input->setDefaultEscapeFilter(new Zend_Filter_StringTrim());
if (! ($input->hasInvalid() || $input->hasMissing())) { return $input; }
foreach ($input->getMessages() as $field => $messageList) { $message = array_shift($messageList); $jsAction = "javascript:history.back();"; $errorMessage = (strpos($message, "empty") === false) ? sprintf("The field %s cannot be empty!", $field) : $message;
throw new Tss_FormException($errorMessage, 3, $jsAction); }
return $input;}
BENEFITS
▸ Single Responsibility Principle ("S" in SOLID)
▸ Reusability
RULE #2NO "ELSE" KEYWORD
public function createPost($request) { $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()){ $repository = $this->getRepository('MyBundle:Post'); if (!$repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
public function createPost($request) { $entity = new Post(); $form = new MyForm($entity); $form->bind($request); if ($form->isValid()){ $repository = $this->getRepository('MyBundle:Post'); if (!$repository->exists($entity)) { $repository->save($entity); return $this->redirect('create_ok'); } else { $error = "Post Title already exists"; return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields"; return array('form' => $form, 'error' => $error); } }
Type-casting
Coding standards
Separate into logical groups.
Consider as paragraphs!
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
UMLNORMAL VS. ALTERNATIVE FLOWS
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if ($form->isValid()) { if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } } else { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if (! $form->isValid()) { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); }
if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if (! $form->isValid()) { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); }
if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if (! $form->isValid()) { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); }
if (! $repository->exists($entity)) { $repository->save($entity);
return $this->redirect('create_ok'); } else { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); } }
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if (! $form->isValid()) { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); }
if ($repository->exists($entity)) { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); }
$repository->save($entity);
return $this->redirect('create_ok');}
public function createPost(Request $request) { $repository = $this->getRepository(‘MyBundle:Post'); $entity = new Post(); $form = new MyForm($entity);
$form->bind($request);
if (! $form->isValid()) { $error = "Invalid fields";
return array('form' => $form, 'error' => $error); }
if ($repository->exists($entity)) { $error = "Post Title already exists";
return array('form' => $form, 'error' => $error); }
$repository->save($entity);
return $this->redirect('create_ok');}
BENEFITS
▸ Prevents code duplication
▸ Increases legibility
▸ Reduce cyclomatic complexity
RULE #3ENCAPSULATE ALL PRIMITIVE TYPES AND STRINGS
RULE #3ENCAPSULATE ALL PRIMITIVE TYPES AND STRINGS
IF THEY HAVE BEHAVIOR
BUT… WHY?
EXCESSIVE USAGE OF OBJECTS IN PHP (IF PHP <7!) DRASTICALLY INCREASES MEMORY FOOTPRINT!
Guilherme Blanco
class Item { final public static function find($id) { if (is_string($id) && trim($id) != '') { // do find ... }
throw new \InvalidArgumentException('$id must be a non-empty string'); }
final public static function create($id, array $data) { if ( ! is_string($id)) { throw new \InvalidArgumentException('$id must be a string'); }
if (empty(trim($id))) { throw new \InvalidArgumentException('$id must be a non-empty string'); }
// do create ... }}
class Item { final public static function find($id) { if (! is_string($id) || trim($id) === '') { throw new \InvalidArgumentException('$id must be a non-empty string'); }
// do find ... }
final public static function create($id, array $data) { if (! is_string($id) || trim($id) === '') { throw new \InvalidArgumentException('$id must be a non-empty string'); }
// do create ... }}
class Item { final public static function find($id) { if (! is_string($id) || trim($id) === '') { throw new \InvalidArgumentException('$id must be a non-empty string'); }
// do find ... }
final public static function create($id, array $data) { if (! is_string($id) || trim($id) === '') { throw new \InvalidArgumentException('$id must be a non-empty string'); }
// do create ... }}
final class Id { /** @var string */ public $value;
public function __construct($value) { if (! is_string($id) || trim($id) === '') { $message = sprintf('%s must be a non-empty string', $value);
throw new \InvalidArgumentException($message); }
$this->value = $value; }
public function getValue() { return $this->value; }}
class Item { final public static function find(Id $id) { // do find ... }
final public static function create(Id $id, array $data) { // do create ... }}
BENEFITS
▸ Type hinting
▸ Encapsulation
▸ Prevents code duplication
RULE #4ONE OBJECT OPERATOR (->) PER LINE
$this->manager->getConfig()->getSegment()->setName("foo");
Properties are hard to mock What if previous call returned NULL?
JUST USE A NULL OBJECT!
Someone watching this talk, one day
final class NullObject { public function __get($property) { return new self; }
public function __set($property, $value) { return new self; }
public function __call($method, array $arguments) { return new self; }
public function __callStatic($method, array $arguments) { return new self; }
public function__toString() { return 'null'; } }
WHY IS IT BAD?
▸ Hide encapsulation problem
▸ Hard to debug and handle exceptions
▸ Codebase must be structured to use NullObject
▸ Hard to read and understand
EXCEPTION TO RULEFLUENT INTERFACE UNDER SAME METHOD
$filterChain ->addFilter(new Zend_Filter_Alpha()) ->addFilter(new Zend_Filter_StringToLower());
BENEFITS
▸ Law of Demeter
▸ Readability
▸ Increases testability (easier to mock)
▸ Simplifies debugging
RULE #5DO NOT ABBREVIATE
THERE ARE 2 HARD PROBLEMS IN COMPUTER SCIENCE: CACHE INVALIDATION, NAMING THINGS AND OFF BY 1 ERRORS.
Tim Bray (mentioning Phil Karlton)
WHY DO YOU ABBREVIATE?
CODE DUPLICATION PROBLEM!WRITE SAME NAME REPEATEDLY
MULTIPLE RESPONSIBILITY PROBLEM!
LONG NAMES
public function getPage($data) { ... }
"Get" from where?
public function startProcess() { ... }
$trx->process('site.login');
How?
WTF is that?
renderHomePage
forkIntoChildProcess
extendedTranslator
BENEFITS
▸ Readability
▸ Better exposing method’s intent
▸ Improved maintainability
▸ Good indicator of code duplication and encapsulation
RULE #6KEEP YOUR CLASSES SMALL
OBJECTIVE
▸ Maximum 200 lines per class(including docblock/documentation)
▸ 10 methods per class
▸ Up to 20 lines per method
▸ 15 classes/interfaces/traits per namespace
BENEFITS
▸ Single Responsibility Principle
▸ Clear and objective methods
▸ Better code segregation
▸ Cleaner namespaces
RULE #7LIMIT CLASS INSTANCE VARIABLES IN A CLASS (BETWEEN 2 TO 5)
class MyRegistrationService { protected $userService; protected $passwordService;
protected $logger;
protected $translator;
protected $entityManager;
protected $imageCropper;
// ... }
Database interactions should be on UserService
Rely on an Event system and move this to a listener
Cross-cutting concerns. Should be auto-injected by your DI through an interface hint
class MyRegistrationService implements LoggerAwareInterface, TranslatorAwareInterface{ use LoggerAwareTrait; use TranslatorAwareTrait;
protected $userService; protected $passwordService;
protected $eventDispatcher;
// ... }
BENEFITS
▸ Single Responsibility Principle
▸ Loose coupling
▸ Better encapsulation
▸ Testability
RULE #8USE FIRST CLASS COLLECTIONS
OR IN OTHER TERMS…
ANY CLASS THAT CONTAINS AN ARRAY MUST NOT HAVE ANY OTHER PROPERTY.
Guilherme Blanco
TEXT
class User { private $name; // ... private $albumList = array();
public function getPublicAlbumList() { $filteredAlbumList = array();
foreach ($this->albumList as $album) { if ($album->getPrivacy() === AlbumPrivacy::PUBLIC) { $filteredAlbumList[] = $album; } } return $filteredAlbumList; }
// ... }
$publicAlbumList = $user->getPublicAlbumList();
class AlbumList extends Collection{ public function getPublic() { $filteredAlbumList = array();
foreach ($this->value as $album) { if ($album->getPrivacy() === AlbumPrivacy::PUBLIC) { $filteredAlbumList[] = $album; } } return $filteredAlbumList; } }
class User{ private $name; private $albumList = new AlbumList(); // ... }
$publicAlbumList = $user->getAlbumList()->getPublic();
class AlbumList extends Collection{ public function getPublic() { return new ArrayCollection( array_filter( $this->value, function (Album $album) { return $album->isPublic(); } ) ); } }
class User{ private $name; private $albumList = new AlbumList(); // ... }
$publicAlbumList = $user->getAlbumList()->getPublic();
BENEFITS
▸ Single Responsibility Principle
▸ Collection operations implemented inside of Collection
▸ Usage of SPL classes
▸ Easy to group collections without concerns over their members’ behavior
▸ Filtering, ordering, mapping, combining are good example methods
RULE #9USE GETTERS AND SETTERS
class BankAccount { public $balance = 0;
public function deposit($amount) { $this->balance += $amount; }
public function withdraw($amount) { $this->balance -= $amount; }}
// Example: $account = new BankAccount(); $account->deposit(100.00); // ... $account->balance = 0; // ... $account->withdraw(10.00);
Balance can be modified without class being notified, leading to unexpected errors.
BENEFITS
▸ Operations injection
▸ Transformations encapsulation
▸ Promotes Open/Closed Principle ("O" in SOLID)
QUESTIONS?
THANKS! =) GUILHERMEBLANCO
GUILHERMEBLANCO