00:00:00

Beautiful Software

Matthew Weier O'Phinney

ZendCon 2012

Notes

Who is that guy?

Notes

Analogies

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

Notes

  • Construction has a few basic tools and methods.

Notes

  • Hammers are the raw materials for assembly.

Notes

  • Saws allow us to make materials fit.

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:

Notes

  • Dull, unimaginative, and likely full of flaws both big and small that will drive you crazy over time.

Notes

  • Things like bathrooms that crowd you in so much, you can't move.

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.

Notes

  • She uses a finishing hammer instead of a framing hammer in order to do fine detail that doesn't leave marks.

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.

Notes

  • A finisher will have a mud mixer so he can get the joint compound to the right consistency.

Notes

  • Manchester Cathedral
  • Basically, a craftsman aims to build things that last.

Notes

  • I also worked as a graphics technician

Notes

  • I drew maps used in guidebooks

Notes

  • and did catalog and book layout.
  • I didn't design them, though. That was the job of the designer.

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.

Notes

  • Color and gradients are chosen to help lead the eye around the page, guiding a viewer to the message.

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."

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

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.

Notes

  • Ball of nails analogy
  • PHP gives you all the tools you need to get things done...

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

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

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

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.

Notes

Principles

You Ain't Gonna Need It (YAGNI)

Notes

Principles

Favor Composition over inheritance

  • Use value objects
  • Move shared logic into traits or helpers

Notes

  • Essentially, the "Open Closed Principle"

Principles

Create Contracts for your objects

  • Define interfaces

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

Notes

  • Single Responsibility Principle

Practical Example

Notes

Pastebin

Notes

  • Pastebins are for sharing code.

Pastebin

Notes

  • They can be listed, unless private

Pastebin

Notes

  • Each paste may be viewed via unique hash URL

Novice approach

1 $hash = $_GET['hash'];
2 mysql_connect();
3 $res = mysql_query(
4     "SELECT * FROM pastes WHERE hash = '$hash'"
5 );
6 $row = mysql_fetch_assoc($res);

Notes

  • Is it good?
  • security issues
  • untestable
  • cannot alter behavior without altering code

A Manageable Approach

Notes

  • We'll look at decisions and details I'd consider when developing this functionality.

Test all the things

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.

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

Notes

Why arrays suck

1 echo (array_key_exists('content', $this->paste) 
2     ? $this->paste['content'] 
3     : '');
4 echo (array_key_exists('language', $this->paste) 
5     ? $this->paste['language'] 
6     : '');

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

1 echo $this->paste->content;
2 echo $this->paste->language;

Notes

  • We know what's available based on type
  • Our code becomes simpler

Paste

1 class Paste
2 {
3     public $hash;
4     public $language = 'txt'; // programming language of content
5     public $content = '';
6     public $timestamp;
7     public $private = false;
8 }

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

1 public function testCreateReturnsAHashIdentifier()
2 {
3     $paste = new Paste();
4     $this->service->create($paste);
5     $this->assertRegex(
6         '/^[a-f0-9]{8}$/', $paste->hash
7     );
8 }

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

1 public function testCanFetchAPasteByHash()
2 {
3     $paste = new Paste();
4     $this->service->create($paste);
5     $test = $this->service->fetch($paste->hash);
6     $this->assertInstanceOf('Paste', $test);
7     $this->assertEquals($paste, $test);
8 }

Notes

  • Compare that two objects are equal -- but not necessarily identical.

Fetch a list of pastes

1 public function testCanFetchCollectionOfPastes()
2 {
3     // seed the service first
4     $count = $this->seedService($this->service); 
5     $collection = $this->service->fetchAll();
6     $this->assertInstanceOf(
7         'Zend\Paginator\Paginator');
8     $this->assertEquals($count, count($collection));
9 }

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

 1 public function testPastesAreInReverseChronoOrder()
 2 {
 3     // seed the service first
 4     $this->seedService($this->service); 
 5     $collection = $this->service->fetchAll();
 6     $previous   = false;
 7     foreach ($collection as $paste) {
 8         $this->assertInstanceOf('Paste', $paste);
 9         if ($previous) {
10             $this->assertLessThan($previous, 
11                 $paste->timestamp);
12         }
13         $previous = $paste->timestamp;
14     }
15 }

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

 1 public function testCollectionContainsNoPrivates()
 2 {
 3     // seed the service first
 4     $this->seedService($this->service); 
 5     $collection = $this->service->fetchAll();
 6     foreach ($collection as $paste) {
 7         $this->assertInstanceOf('Paste', $paste);
 8         $this->assertFalse($paste->private);
 9     }
10 }

Notes

  • Assert both type, and value of property

What about the code?

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

 1 interface PasteServiceInterface {
 2     /**
 3      * @return Paste
 4      */
 5     public function create(Paste $paste);
 6     /**
 7      * @return Paste|null
 8      */
 9     public function fetch($hash);
10     /**
11      * @return \Zend\Paginator\Paginator
12      */
13     public function fetchAll();
14     /**
15      * @return bool
16      */
17     public function exists($hash);
18 }

Notes

  • PHP doesn't allow defining return values; thus annotations
  • What is the "exists" method for? We'll cover that later.

Implementation

 1 class MemoryPasteService implements 
 2     PasteServiceInterface
 3 {
 4     protected $pastes = array();
 5     protected $public;
 6     public function create(Paste $paste)
 7     {
 8         $hash = $this->createHash($paste);
 9         $paste->hash = $hash;
10         $this->pastes[$hash] = $paste;
11         if (!$this->pastes->private) {
12             $this->public->insert($paste);
13         }
14         return $paste;
15     }
16 }

Notes

  • Simple implementation, in-memory
  • What is "createHash()"?

Create a hash

 1 trait CreateHash {
 2     public function createHash(Paste $paste) {
 3         $hashSeed = sprintf(
 4             '%d:%s:%s', 
 5             microtime(true), 
 6             $paste->language,
 7             hash('sha1', $paste->content)
 8         );
 9         do {
10             $hashSeed .= ':' . uniqid();
11             $hash      = hash('sha256', $hashSeed);
12             $hash      = substr($hash, 0, 8);
13         } while ($this->exists($hash));
14         return $hash;
15     }
16     abstract public function exists($hash);
17 }

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

 1 abstract class CreateHash
 2 {
 3     public static function createHash(
 4         Paste $paste, 
 5         PasteServiceInterface $service
 6     ) {
 7         // all is the same except that the "while" 
 8         // condition becomes:
 9         //     while ($service->exists($hash))
10     }
11 }

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

 1 class MemoryPasteService implements 
 2     PasteServiceInterface
 3 {
 4     use CreateHash;
 5 
 6     /* ... previous code ... */
 7 
 8     public function exists($hash)
 9     {
10         return array_key_exists($hash, 
11             $this->pastes);
12     }
13 }

Notes

  • Using traits, because I can
  • Very simple test.

Fetch a paste

 1 class MemoryPasteService implements 
 2     PasteServiceInterface
 3 {
 4     /* ... previous code ... */
 5     public function fetch($hash)
 6     {
 7         if (!$this->exists($hash)) {
 8             return null;
 9         }
10         return $this->pastes[$hash];
11     }
12 }

Notes

  • How you handle errors is up to you
  • Do you consider inability to fetch recoverable?

Fetch many

 1 class MemoryPasteService implements 
 2     PasteServiceInterface
 3 {
 4     /* ... previous code ... */
 5 
 6     public function fetchAll()
 7     {
 8         $adapter   = new IteratorPaginator(
 9             $this->public);
10         $paginator = new Paginator($adapter);
11         return $paginator;
12     }
13 }

Notes

  • Our tests said we needed to return a paginator
  • Only fetch public pastes
  • but what is the "public" property?

Filtering

 1 class SortedPastes extends SplMaxHeap {
 2     public function insert($value) {
 3         if (!$value instanceof Paste) {
 4             throw new \InvalidArgumentException();
 5         }
 6         parent::insert($value);
 7     }
 8     public function compare($a, $b) {
 9         if ($a->timestamp === $b->timestamp) {
10             return 0;
11         }
12         if ($a->timestamp > $b->timestamp) {
13             return 1;
14         }
15         return -1;
16     }
17 }

Notes

  • Know your tools, especially the SPL
  • Max heap sorts larger values to the top.
  • Our public property will be a heap

Internal implementations

 1 class MemoryPasteService implements 
 2     PasteServiceInterface
 3 {
 4     /* ... previous code ... */
 5 
 6     public function __construct()
 7     {
 8         $this->public = new SortedPastes;
 9     }
10 }

Notes

  • Internal implementation details may not need composition
  • Tests now pass!

Isn't it overkill?

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

 1 class MongoPasteService implements 
 2     PasteServiceInterface {
 3     use CreateHash;
 4     protected $collection;
 5     public function __construct(
 6         MongoCollection $collection
 7     ) {
 8         $this->collection = $collection;
 9     }
10     public function create(Paste $paste)
11     {
12         $hash = $this->createHash($paste);
13         $paste->hash = $hash;
14         $data = (array) $paste;
15         $this->collection->insert($data);
16         return $paste;
17     }
18 }

Notes

  • Requires some work to get cursors to return Paste objects
  • But the point is that I consume it exactly the same

Decorator

 1 class CachingService implements 
 2     PasteServiceInterface
 3 {
 4     protected $cache;
 5     protected $service;
 6     public function __construct(
 7         Cache $cache, PasteServiceInterface $service
 8     ) {
 9         $this->cache = $cache;
10         $this->service = $service;
11     }
12     public function create(Paste $paste)
13     {
14         $paste = $this->service->create($paste);
15         $this->cache->store($paste->hash, $paste);
16         $this->cache->invalidate('list');
17     }
18 }

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.

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.

Notes

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.

Notes

  • Making these goals and principles your focus allows you to achieve beauty in software

Practice Software Craftsmanship

Notes

Thank You

Notes