/usr/portage

Polite Exceptions – Fixing the stepchild of API design 2

Every API has an visible, an invisibile and a hidden part. The visible part is obvious: public methods and properties but also constants and parameter values. That’s the most visible part to any client (read: user) of your API. The invisible part is everything private, you can’t really see it and – more important – you can’t use it (except if you resort to reflection). The hidden part consists of all the protected symbols, as you can’t really see them until you extend a class. The other hidden part are Exceptions. You can’t really see them and there is no common expectation what methods throw what kind of exception. Yes, throws@-docblocks help, but that’s mostly all we have.

Exceptions handling: the problem

The usability of hidden parts of an API is all about expectations: people love languages like Ruby because once you learned a certain set of API (e.g. the string API), you can instinctively infer a large part of other APIs. This is good and keeps learning costs down. PHP, on the other hand, with its historically grown standard extension is on the opposite site of the fence: various parameter order the naming scheme is unreliable at best.
The future is multi-lingual, you need to know more than one programming language and speed of learning matters. Like, a lot. Because “X programmers”, for any value of X, are weak players. What type of exception a class might throw should be defined by clear expectations for the general case. If you use a preconceived HTTP client httpFoo, call method request() and want to handle exception cases, what exactly do you catch?

Talent borrows, genius steals

Zend Framework 2 has a lot of problems but there are two things they did particularly well: naming of abstract classes and interfaces and how they treat exceptions. Every component (see, component is a lie here, as they aren’t really stand alone components but I digress) has its own exception subpackage which has extension specific exceptions. Those exception all implement a single marker interface called ExceptionInterface. If you use Zend\Something and want to handle all exceptions, just catch Zend\Something\Exception\ExceptionInterface.

Programming transaction costs

Time to relevant data is the new time to market. We no longer optimize for feature-complete products shipping on a certain date but relevant changes generating relevant data as soon as possible. Therefore programmer round-trips matter. I consider everything that is not core domain or core UI a round trip:

These steps aren’t worthless, they are worth less from a business perspective as they don’t generate revenue very soon. However they are needed to keep revenue over time. So let’s make those things cheaper.

When dealing with Exceptions in Symfony 2 projects, two steps are particularly expensive:

Especially the latter can be simplified quite dramatically.

Simplifying

To simplify Exception handling, we just open sourced a bundle we developed at InterNations. Let’s create a few custom exceptions:

php app/console exception:generate app/src/MyVendor/MyBundle "MyVendor\MyBundle" \
 ExceptionInterface RuntimeException DomainException RuntimeException:SpecificRuntimeException
Create directory app/src/MyVendor/MyBundle/Exception
Writing app/src/MyVendor/MyBundle/Exception/ExceptionInterface.php
Writing app/src/MyVendor/MyBundle/Exception/RuntimeException.php
Writing app/src/MyVendor/MyBundle/Exception/SpecificRuntimeException.php
Writing app/src/MyVendor/MyBundle/Exception/DomainException.php

Let’s rewrite an existing bundle to use custom exceptions:

php app/console exception:rewrite app/src/MyVendor/MyBundle "MyVendor\MyBundle"
Found bundle specific exception class BadFunctionCallException
Found bundle specific exception class BadMethodCallException
Found bundle specific exception class DomainException
Found bundle specific exception class InvalidArgumentException
Found bundle specific exception class LengthException
Found bundle specific exception class LogicException
Found bundle specific exception class OutOfBoundsException
Found bundle specific exception class OutOfRangeException
Found bundle specific exception class OverflowException
Found bundle specific exception class RangeException
Found bundle specific exception class RuntimeException
Found bundle specific exception class UnderflowException
Found bundle specific exception class UnexpectedValueException
...............
------------------------------------------------------------
------------------------------------------------------------
SUMMARY
------------------------------------------------------------
------------------------------------------------------------
Files analyzed:               15
Files changed:                1
------------------------------------------------------------
"throw" statements found:     2
"throw" statements rewritten: 1
------------------------------------------------------------
"use" statements found:       1
"use" statements rewritten:   1
"use" statements added:       1
------------------------------------------------------------
"catch" statements found:     0

You’ll find the ExceptionBundle over at github. It uses PHP Parser to rewrite code which proves again to be a wonderful project.

Filed on 20-12-2012, 10:10 under , , , , & two comments & no trackbacks

Trackbacks

Trackback specific URI for this entry

No Trackbacks

Comments

  1. cordoval reckons:
    published on January 7th 2013, 10:36:11 am *

    thanks for the bundle to generate/scan exceptions, will give it a try

    what you did not explain on this post was the catching and using of the exceptions

    please let me know best practices for how to use all the rewritten exceptions

    Reply

  2. Lars Strojny supposes:
    published on April 29th 2013, 08:02:50 am *

    That depends very much on what you want to do. If you only want to catch a specific exception, do that. What helps doing layered exception handling is, that you can catch "all exception from bundle ABC" ignoring what the details are. E.g. you might have an infrastructure bundle providing Redis integration. This infrastructure bundle only throws exception implementing the Redis bundle specific Exception interface. Another bundle implementing a service layer using Redis would only handle the Exception interface, not detailed exceptions of the bundle. This allows those two bundle evolve more independently from each other without breaking each others exception handling.
    Hope that answered your question, let me know if you have any more questions (and sorry for the very late response, your comment must have slipped through).

    Reply

Add a Comment & let me know what you think