# Beautiful Software Matthew Weier O'Phinney ZendCon 2012 .fx: titleslide --- # Who is that guy? * Matthew Weier O'Phinney * Project Lead, Zend Framework * [https://github.com/weierophinney](https://github.com/weierophinney) * [http://mwop.net/](http://mwop.net/) * @weierophinney --- # Analogies # Presenter Notes - Haven't always been a developer; worked in construction and design both at different times. - Both provide lessons for us to learn from. --- # Construction .fx: imageslideheadingbottom whiteheading # Presenter Notes - Construction has a few basic tools and methods. --- # .fx: imageslide # Presenter Notes - Hammers are the raw materials for assembly. --- # .fx: imageslide # Presenter Notes - Saws allow us to make materials fit. --- # .fx: imageslide # Presenter Notes - Drills are used to make roads for pipes and wires, and to attach things to the building's frame. - Carpenters can do a lot with these tools, without investing much time in mastering them. But then you end up with houses like this: ---- # .fx: imageslide # Presenter Notes - Dull, unimaginative, and likely full of flaws both big and small that will drive you crazy over time. --- # .fx: imageslide # Presenter Notes - Things like bathrooms that crowd you in so much, you can't move. --- # .fx: imageslide # Presenter Notes - or kitchens so poorly laid out that you have nowhere to do the essential work of cooking. - A good carpenter takes the time to learn his trade. --- # .fx: imageslide # Presenter Notes - She uses a finishing hammer instead of a framing hammer in order to do fine detail that doesn't leave marks. --- # .fx: imageslide # Presenter Notes - A drywall hanger, knowing he has to get the stone up quickly, uses an impact driver so he can screw it into the frame quickly. --- # .fx: imageslide # Presenter Notes - A finisher will have a mud mixer so he can get the joint compound to the right consistency. --- # .fx: imageslide # Presenter Notes - Manchester Cathedral - Basically, a craftsman aims to build things that last. --- # .fx: imageslide # Presenter Notes - I also worked as a graphics technician --- # .fx: imageslide # Presenter Notes - I drew maps used in guidebooks --- # .fx: imageslide # Presenter Notes - and did catalog and book layout. - I didn't design them, though. That was the job of the designer. --- # .fx: imageslide # Presenter Notes - A designer considers things like typography -- what font communicates best for the content, how the space between letters, words, and paragraphs will convey a message. --- # .fx: imageslide # Presenter Notes - Color and gradients are chosen to help lead the eye around the page, guiding a viewer to the message. --- # .fx: imageslide # Presenter Notes - Essentially, a good designer is thinking about how things relate semantically. - "Kate Ray interviews leading lights in the semantic web: see video at vimeo. Wordle made by Jim Stauffer." --- # .fx: imageslide # Presenter Notes - You *could* build something or design something out of duct tape -- but would it have lasting value? - Yes, it might last, it's duct tape, after all. --- # PHP # Presenter Notes - But what does this have to do with PHP? - When I started programming again, I started with Perl, classic ASP, and Perl. They all excelled at letting you get immediate results. --- # .fx: imageslide # Presenter Notes - Ball of nails analogy - PHP gives you all the tools you need to get things done... --- # .fx: imageslide # Presenter Notes - If you need to connect to a database, you can. If you need to process a form, PHP was built for that. - Over time, I added more tools to my belt... --- # <?xml # Presenter Notes - OOP, DOM manipulation, and more. - These made me more efficient, but not necessarily *better*. - My point: do you want to crank out code, or *craft code*? --- # Beautiful Software:
Software Craftmanship # Presenter Notes - Just like a carpenter or a designer, you consider the small details, and how they relate to the whole. - How do objects relate, how do the communicate with each other, how do you subsitute implementations, and more. --- # Responsibilities * Maintainability * Extensibility * Substitution # Presenter Notes - How easy is it to fix an issue, or add a feature? - Can the behavior be extended or modified? - Can you create alternate implementations easily? --- # Principles **Don't Repeat Yourself** (DRY),
and avoid
**Not Invented Here** (NIH) syndrome. --- # Principles **You Ain't Gonna Need It** (YAGNI) --- # Principles Favor **Composition** over inheritance - Use value objects - Move shared logic into traits or helpers # Presenter Notes - Essentially, the "Open Closed Principle" --- # Principles Create **Contracts** for your objects - Define interfaces # Presenter Notes * Interface Segregation Principle, and related to Liskov Substitution Principle and the Dependency Inversion principle. --- # Principles Objects should **do one thing**, well. - Methods should also be short - Use Facades to simplify and organize # Presenter Notes - Single Responsibility Principle --- # Practical Example --- # Pastebin # Presenter Notes - Pastebins are for sharing code. --- # Pastebin # Presenter Notes - They can be listed, unless private --- # Pastebin # Presenter Notes - Each paste may be viewed via unique hash URL --- # Novice approach !php $hash = $_GET['hash']; mysql_connect(); $res = mysql_query( "SELECT * FROM pastes WHERE hash = '$hash'" ); $row = mysql_fetch_assoc($res); # Presenter Notes - Is it good? - security issues - untestable - cannot alter behavior without altering code --- # A Manageable Approach # Presenter Notes - We'll look at decisions and details I'd consider when developing this functionality. --- # Test all the things # Presenter Notes - Let's you play with the system before committing to it - Experiment with object interactions - Don't like something? fix it. --- # Pastebin Requirements - We need to be able to create new "pastes". - We need to be able to view a given "paste". - We need to be able to list non-private "pastes", in reverse order from when they were created. # Presenter Notes * Lead in to next slide: First things first, create a value object representing a paste. --- # Value object / Entity Why? - Arrays are not typed - Arrays require testing key existence - Arrays require manual reference handling --- # Why arrays suck !php echo (array_key_exists('content', $this->paste) ? $this->paste['content'] : ''); echo (array_key_exists('language', $this->paste) ? $this->paste['language'] : ''); # Presenter Notes * Consider it from a view script: messy. * Unless you're willing to clog up your error logs. * These are important details. --- # Why value objects work !php echo $this->paste->content; echo $this->paste->language; # Presenter Notes * We know what's available based on type * Our code becomes simpler --- # Paste !php class Paste { public $hash; public $language = 'txt'; // programming language of content public $content = ''; public $timestamp; public $private = false; } # Presenter Notes - Now we know what properties and/or methods are available - Now that we have our object, let's start considering behavior. We need to think about identity of each paste. --- # Identity !php public function testCreateReturnsAHashIdentifier() { $paste = new Paste(); $this->service->create($paste); $this->assertRegex( '/^[a-f0-9]{8}$/', $paste->hash ); } # Presenter Notes - I've made decisions about architecture: - A service layer will encapsulate business logic - I've specified what a hash looks like (16^8, or > 4 billion, possible identities - Now let's look at fetching pastes --- # Fetch a paste !php public function testCanFetchAPasteByHash() { $paste = new Paste(); $this->service->create($paste); $test = $this->service->fetch($paste->hash); $this->assertInstanceOf('Paste', $test); $this->assertEquals($paste, $test); } # Presenter Notes - Compare that two objects are equal -- but not necessarily identical. --- # Fetch a list of pastes !php public function testCanFetchCollectionOfPastes() { // seed the service first $count = $this->seedService($this->service); $collection = $this->service->fetchAll(); $this->assertInstanceOf( 'Zend\Paginator\Paginator'); $this->assertEquals($count, count($collection)); } # Presenter Notes - List in reverse chronological order - TDD often criticized for being rushed and not resulting in design or architecture. Rubbish. We make design decisions. - Choosing to return Paginator; who wants to pull 4 billion results? --- # Iterate !php public function testPastesAreInReverseChronoOrder() { // seed the service first $this->seedService($this->service); $collection = $this->service->fetchAll(); $previous = false; foreach ($collection as $paste) { $this->assertInstanceOf('Paste', $paste); if ($previous) { $this->assertLessThan($previous, $paste->timestamp); } $previous = $paste->timestamp; } } # Presenter Notes - Testing that as we iterate, we get Paste objects, and that each is older than the previous - Another design decision: using timestamps, which allows for comparison. --- # Public only !php public function testCollectionContainsNoPrivates() { // seed the service first $this->seedService($this->service); $collection = $this->service->fetchAll(); foreach ($collection as $paste) { $this->assertInstanceOf('Paste', $paste); $this->assertFalse($paste->private); } } # Presenter Notes - Assert both type, and value of property --- # What about the code? # Presenter Notes - I've showed tests, and the Paste object only. What about the service? - Notice that I've not shown anything about how pastes are persisted. - Jr. dev would start with schema. But what if RDBMS is the wrong tool for the job? What if I want to cache? --- # Service interface !php interface PasteServiceInterface { /** * @return Paste */ public function create(Paste $paste); /** * @return Paste|null */ public function fetch($hash); /** * @return \Zend\Paginator\Paginator */ public function fetchAll(); /** * @return bool */ public function exists($hash); } # Presenter Notes - PHP doesn't allow defining return values; thus annotations - What is the "exists" method for? We'll cover that later. --- # Implementation !php class MemoryPasteService implements PasteServiceInterface { protected $pastes = array(); protected $public; public function create(Paste $paste) { $hash = $this->createHash($paste); $paste->hash = $hash; $this->pastes[$hash] = $paste; if (!$this->pastes->private) { $this->public->insert($paste); } return $paste; } } # Presenter Notes - Simple implementation, in-memory - What is "createHash()"? --- # Create a hash !php trait CreateHash { public function createHash(Paste $paste) { $hashSeed = sprintf( '%d:%s:%s', microtime(true), $paste->language, hash('sha1', $paste->content) ); do { $hashSeed .= ':' . uniqid(); $hash = hash('sha256', $hashSeed); $hash = substr($hash, 0, 8); } while ($this->exists($hash)); return $hash; } abstract public function exists($hash); } # Presenter Notes - We need to create a unique hash. Collisions force generation of a new one - exists() method allows us implementations to define it themselves. - Defined in both interface and trait == must implement!!! - "But we don't have 5.4 on our servers yet!" --- # Create a hash, redux !php abstract class CreateHash { public static function createHash( Paste $paste, PasteServiceInterface $service ) { // all is the same except that the "while" // condition becomes: // while ($service->exists($hash)) } } # Presenter Notes - This will work in any version. - Abstract means we cannot instantiate. - Important thing to consider is that this doesn't require inheritance, but rather sending messages between objects. --- # Existence !php class MemoryPasteService implements PasteServiceInterface { use CreateHash; /* ... previous code ... */ public function exists($hash) { return array_key_exists($hash, $this->pastes); } } # Presenter Notes - Using traits, because I can - Very simple test. --- # Fetch a paste !php class MemoryPasteService implements PasteServiceInterface { /* ... previous code ... */ public function fetch($hash) { if (!$this->exists($hash)) { return null; } return $this->pastes[$hash]; } } # Presenter Notes - How you handle errors is up to you - Do you consider inability to fetch recoverable? --- # Fetch many !php class MemoryPasteService implements PasteServiceInterface { /* ... previous code ... */ public function fetchAll() { $adapter = new IteratorPaginator( $this->public); $paginator = new Paginator($adapter); return $paginator; } } # Presenter Notes - Our tests said we needed to return a paginator - Only fetch public pastes - but what is the "public" property? --- # Filtering !php class SortedPastes extends SplMaxHeap { public function insert($value) { if (!$value instanceof Paste) { throw new \InvalidArgumentException(); } parent::insert($value); } public function compare($a, $b) { if ($a->timestamp === $b->timestamp) { return 0; } if ($a->timestamp > $b->timestamp) { return 1; } return -1; } } # Presenter Notes - Know your tools, especially the SPL - Max heap sorts larger values to the top. - Our public property will be a heap --- # Internal implementations !php class MemoryPasteService implements PasteServiceInterface { /* ... previous code ... */ public function __construct() { $this->public = new SortedPastes; } } # Presenter Notes - Internal implementation details may not need composition - Tests now pass! --- # Isn't it overkill? # Presenter Notes - Aren't we making a simple idea complex? - Do we really want to store in memory? - The point: we've made something replaceable - The trait encapsulates shared logic - The interface defines the expected operations --- # Alterate implementation !php class MongoPasteService implements PasteServiceInterface { use CreateHash; protected $collection; public function __construct( MongoCollection $collection ) { $this->collection = $collection; } public function create(Paste $paste) { $hash = $this->createHash($paste); $paste->hash = $hash; $data = (array) $paste; $this->collection->insert($data); return $paste; } } # Presenter Notes - Requires some work to get cursors to return Paste objects - But the point is that I consume it exactly the same --- # Decorator !php class CachingService implements PasteServiceInterface { protected $cache; protected $service; public function __construct( Cache $cache, PasteServiceInterface $service ) { $this->cache = $cache; $this->service = $service; } public function create(Paste $paste) { $paste = $this->service->create($paste); $this->cache->store($paste->hash, $paste); $this->cache->invalidate('list'); } } # Presenter Notes - No inheritance -- compose, and implement the interface. - Allows us to write code once, and consume any implementation - Knowledge of _design patterns_, coupled with good composition, allows us to accomplish non-trivial patterns simply. --- # # Presenter Notes - Interactions are clearly defined - Substitution is easily possible - Changing _implementation_ details does not affect the consumer --- # Summary The goals of software craftsmanship are: - Maintainability. - Extensibility. - Substitution. --- # Tools - **Don't Repeat Yourself** (DRY), and avoid **Not Invented Here** (NIH) syndrome. - **You Ain't Gonna Need It** (YAGNI) principle. - Favor **composition** over inheritance. - Create **contracts** for your objects. - Have objects (and methods!) **do one thing**, and one thing well. # Presenter Notes - Making these goals and principles your focus allows you to achieve beauty in software --- # Practice Software Craftsmanship --- # Thank You