Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Anti-Patterns Found in Your Object-Oriented Codebase

Anti-Patterns Found in Your Object-Oriented Codebase

If you ask some developers, following a functional programming paradigm will solve all your problems. Others will tell you OOP is the way to go. The reality is there are good things found in all paradigms. There are also bad things. This talk will cover examples of the most egregious anti-patterns found in almost every object-oriented codebase ever written. We will talk about what they are, why they are made, and how you can avoid them. Walking out of the room you should be able to spot problems in your own code and the code of your peers that you can fix tomorrow to make your codebase a better place.

Jeff Carouth

October 21, 2015
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

  1. Today we'll look at several patterns found in object- oriented

    codebases which may be considered undesirable.
  2. anti pattern A common solution to a recurring problem which

    might seem like an appropriate response but often brings more consequences than benefits.
  3. <?php $object = null; $object->doSomethingMagical(); Pop Quiz 1. What does

    the following code snippet output? a) An Elephpant b) null c) Parse error: syntax error, unexpected '::' (T_PAAMAYIM_NEKUDOTAYIM) d) Fatal error: Call to a member function doSomethingMagical() on null e) All of the above f) None of the above g) I do not know
  4. ! php null.php Fatal error: Call to a member function

    doSomethingMagical() on null in /Users/jcarouth/null.php on line 3 Call Stack: 0.0006 227176 1. {main}() /Users/jcarouth/null.php:0
  5. Right now you're thinking: but I would never assign null

    to an variable and try to use it like it's an object.
  6. <?php class UserRepository { public function findByEmail($email) { $statement =

    $this->db->prepare( 'SELECT * FROM users WHERE email = :email' ); $statement->execute(['email' => $email]); if ($statement->rowCount() === 0) { return null; } return new User($statement->fetch()); } }
  7. Why does it matter? $userRepository = new UserRepository(new PDO(/*...*/)); $email

    = '[email protected]'; $user = $userRepository->findByEmail($email); if (null === $user) { header("HTTP/1.1 404 Not Found"); exit; } //do something magical with the $user object
  8. Why does it matter? $userRepository $email $user if } //do

    something magical with the $user object if (null === $user) { header("HTTP/1.1 404 Not Found"); exit; }
  9. Why does it matter? $userRepository $email $user if } //do

    something magical with the $user object $email = 'doesnotexist'; $user = $userRepository->findByEmail($email);
  10. Why does it matter? $userRepository $email $user if } //do

    something magical with the $user object $email = 'doesnotexist'; $user = null;
  11. Semantic Method Names class UserRepository { /** * @return User|null

    */ public function findByEmail($email) { /*...*/ } }
  12. Semantic Method Names class UserRepository { /** * @return User|null

    */ public function findByEmailOrNullIfNotFound($email) { /*...*/ } }
  13. Alternative #1: Throw Exceptions Instead of returning a null you

    can use built-in exception handling to properly handle the situation for you. Object-oriented languages have exception handling to get away from the imperative style error handling.
  14. class ResourceNotFoundException extends \Exception { } class UserRepository { public

    function findByEmail($email) { $statement = $this->db->prepare( 'SELECT * FROM users WHERE email = :email' ); $statement->execute(['email' => $email]); if ($statement->rowCount() === 0) { throw new ResourceNotFoundException(); } return new User($statement->fetch()); } }
  15. How does that change the client code? $userRepository = new

    UserRepository(new PDO(/*...*/)); $email = '[email protected]'; $user = $userRepository->findByEmail($email); if (null === $user) { header("HTTP/1.1 404 Not Found"); exit; } //do something magical with the $user object
  16. How does that change the client code? try { $userRepository

    = new UserRepository(new PDO(/*...*/)); $email = '[email protected]'; $user = $userRepository->findByEmail($email); $user->doSomethingMagical(); } catch (ResourceNotFoundException $e) { header("HTTP/1.1 404 Not Found"); exit; }
  17. Using exceptions allows you to let the caller or other

    upstream callers handle the situation appropriately.
  18. How does that change the client code? try { $userRepository

    = new UserRepository(new PDO(/*...*/)); $email = '[email protected]'; $user = $userRepository->findByEmail($email); $user->doSomethingMagical(); } catch (ResourceNotFoundException $e) { header("HTTP/1.1 404 Not Found"); exit; }
  19. Alternative #2: Return a Null Object The Null Object pattern

    is sometimes useful when you can use an object which implements neutral behavior in place of another object.
  20. Using a Null User to Represent Guests class User {

    public function isRegistered() { return true; } } class NullUser { public function isRegistered() { return false; } }
  21. Using a Null User to Represent Guests class UserRepository {

    public function findByEmail($email) { $statement = $this->db->prepare( 'SELECT * FROM users WHERE email = :email' ); $statement->execute(['email' => $email]); if ($statement->rowCount() === 0) { return new NullUser(); } return new User($statement->fetch()); } }
  22. class StatTracker { public function __construct($trackerFile) { $this->trackerFile = $trackerFile;

    } public function trackEvent($event, $value) { $data = json_encode( ['event' => $event, 'value' => $value] ); FileUtils::write($this->trackerFile, $data); } }
  23. class FileUtils { public static function write($file, $content) { $fileHandle

    = fopen($file, 'a'); fwrite($fileHandle, $content); fclose($fileHandle); } }
  24. class FileUtils { public static function write($file, $content) { $fileHandle

    = fopen($file, 'a'); fwrite($fileHandle, $content); fclose($fileHandle); } } $statTracker = new StatTracker('/path/to/stats.txt'); $statTracker->trackEvent('used a util class', 'jcarouth');
  25. What's the problem then? Right now, nothing. Unless you're worried

    about testability. It's a minor inconvenience but you'll have to have a real filesystem because of the static method call.
  26. Alternative: Create an Object to deal with your FileSystem abstraction

    FileUtility::write() becomes (new FileManager())->write()
  27. class FileManager { public function __construct($file) { $this->path = $file;

    } public function write($content) { fwrite($this->handle, $content); } public function open($mode = 'r') { $this->handle = fopen($this->file, $mode); return $this; } public function close($handle) { fclose($this->handle); } }
  28. class StatTracker { public function __construct($trackerFile) { $this->trackerFile = $trackerFile;

    } public function trackEvent($event, $value) { $data = json_encode( ['event' => $event, 'value' => $value] ); $fileManager = new FileManager($this->trackerFile); $fileManager->open('a')->write($data); } }
  29. class FileManager { public function __construct($file) { $this->path = $file;

    } public function write($content) { fwrite($this->handle, $content); } public function open($mode = 'r') { $this->handle = fopen($this->file, $mode); return $this; } public function close($handle) { fclose($this->handle); } }
  30. class TextFile { protected $path; public function __construct($file) { $this->file

    = $file; } public function write($content) { $file = $this->open('a'); fwrite($file, $content); $this->close($file); } protected function open($mode = 'r') { /** snip **/ } protected function close($handle) { /** snip **/ } }
  31. class StatTracker { public function __construct(TextFile $file) { $this->trackerFile =

    $file; } public function trackEvent($event, $value) { $data = json_encode( ['event' => $event, 'value' => $value] ); $this->trackerFile->write($data); } }
  32. class { [ ) } $statTracker = new StatTracker(new TextFile('/path/to/stats.txt'));

    $statTracker->trackEvent('made a real object', 'jcarouth');
  33. The manifestation of this anti pattern is in classes that

    have large complexity due to poor abstraction.
  34. class DataExport { public function __construct($type, $fileName = "report") {

    $this->fileName = $fileName; $this->outputType = strtolower($type); switch ($this->outputType) { case 'csv': case 'txt': $this->contentType = 'application/csv'; break; case 'xls': $this->contentType = 'application/vnd.ms-excel'; break; default: throw new Exception('Unsupported export type'); } } }
  35. class DataExport { public function outputFromArray($data) { $firstRow = current($data);

    $columns = array_keys($firstRow); switch ($this->outputType) { case 'csv': case 'txt': //create text body break; case 'xls': //build xls file break; } } }
  36. class DataExport { public function outputFromArray($data) { $firstRow = current($data);

    $columns = array_keys($firstRow); switch ($this->outputType) { case 'csv': case 'txt': //create text body break; case 'xls': //build xls file break; } } }
  37. interface FileExport { public function outputFromArray($data); } class CsvExportFile implements

    FileExport { public function __construct($name) { $this->name = $name; } public function outputFromArray($data) { //create the text body } } class XlsExportFile implements FileExport {}
  38. class FileExportFactory { public static function create($type, $name) { switch

    (strtolower($type)) { case 'csv': case 'txt': return new CsvExportFile($name); break; case 'xls': return new XlsExportFile($name); break; } } }
  39. class Product { protected $id; protected $price; public function __construct($id,

    Price $price) { $this->id = $id; $this->price = $price; } public function getPrice() { return $this->price; } public function setPrice(Price $price) { $this->price = $price; } }
  40. class Price { public $value; public $currency; public function __construct($value,

    $currency) { $this->value = $value; $this->currency = $currency; } }
  41. How do you reduce the price of a Product? Let's

    say we want to put a particular object on sale for 20% off.
  42. $product = new Product('12345', new Price(9999, 'USD')); $salePrice = new

    Price( intval(round($product->getPrice()->value * 0.80, 0)), $product->getPrice()->currency ); $product->setPrice($salePrice);
  43. Wouldn't it be cool if I could just tell a

    product to reduce its price by 20 percent? Why can't I?
  44. class Product { public function price() { return $this->price; }

    public function reducePriceByPercentage($salePercent) { $multiplier = 1 - ($salePercent / 100.00); $this->price = new Price( intval( round( $product->getPrice()->value * $multiplier, 0 ) ), $this->price->value ); } }
  45. null references can be problematic for client code and in

    an object- oriented world can be mitigated with use of exceptions or null objects
  46. Purely utility objects are sometimes damaging to object-oriented codebases and

    should be extracted to proper abstractions when possible
  47. Do not be afraid to think about places where you

    are adding complexity by avoiding adding classes. Classes are not the enemy of object-oriented code.
  48. Exposing internals of objects can leak details and increase surface

    area of objects. Codifying the behaviors you expect as the interface avoids this situation entirely.