O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.

Nikola Poša "Handling exceptional conditions with grace and style"

111 visualizações

Publicada em

Programmers naturally give more attention to a “happy path” - default scenario in application execution in which everything works as expected, therefore neglecting the opposite way things can go. Topics such as dealing with exceptional conditions, use of exceptions, error handling seem to have been insufficiently studied, and it is very difficult to find useful explanations and tutorials online.

This talk is an in-depth study about practices for dealing with exceptional conditions that promote code that is clean, consistent and convenient to work with. Special attention is given to applicable best practices for managing exceptions in a proper way, such as formatting exception messages, component-level exception type, exception wrapping.

To make the story complete, second part of the talk introduces framework-agnostic solution for establishing central error handling system that makes this critical aspect of the software stable, unambiguous and easy to maintain.

At the very end, some attention is given to testing exceptions and ways for keeping test code consistent and readable.

Publicada em: Tecnologia
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

Nikola Poša "Handling exceptional conditions with grace and style"

  1. 1. HANDLING EXCEPTIONAL CONDITIONSHANDLING EXCEPTIONAL CONDITIONS WITH GRACE AND STYLEWITH GRACE AND STYLE Nikola Poša · @nikolaposa
  2. 2. Я радий бути тутЯ радий бути тут
  3. 3. Я радий бути тутЯ радий бути тут ABOUT MEABOUT ME Software Architect specializing in PHP-based applications Lead Architect at Arbor Education Partners PHP Serbia Conference co-organizer  @nikolaposa  blog.nikolaposa.in.rs
  4. 4. AGENDAAGENDA Approaches for dealing with exceptional conditions Set of applicable best practices for managing exceptions in a proper way Solution for establishing central error handling system Few tips for testing exceptions
  5. 5. HAPPY PATHHAPPY PATH a.k.a. Normal Flow Happy path is a default scenario featuring no exceptional or error conditions, and comprises nothing if everything goes as expected. Wikipedia “
  6. 6. $user = $userRepository->get('John'); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } 1 2 3 4 5
  7. 7. $user = $userRepository->get('John'); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } 1 2 3 4 5 if ($user->isSubscribedTo($notification)) { Fatal error: Call to a member function isSubscribedTo() on null $user = $userRepository->get('John');1 2 3 $notifier->notify($user, $notification);4 }5 6 7 8
  8. 8. $user = $userRepository->get('John'); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } 1 2 3 4 5 $user = $userRepository->get('John'); if (null !== $user && $user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } 1 2 3 4 5
  9. 9. Null checks ood if (null !== $user) { if (null !== $todo) { if (null !== $notification) { $user = $userRepository->get('John');1 2 3 $todo = $todoRepository->get('Book flights');4 5 6 $notification = TodoReminder::from($todo, $user);7 8 9 if ($user->isSubscribedTo($notification)) {10 $notifier->notify($user, $notification);11 }12 }13 }14 }15
  10. 10. Vague Interface interface UserRepository { public function get(string $username): ?User; }
  11. 11. Vague Interface interface UserRepository { public function get(string $username): ?User; } interface UserRepository { /** * @param UserId $id * @return User|bool User instance or boolean false if User w */ public function get(string $username); }
  12. 12. DO NOT MESS WITHDO NOT MESS WITH NULLNULL
  13. 13. When we return null, we are essentially creating work for ourselves and foisting problems upon our callers. Robert C. Martin, "Clean Code" “
  14. 14. If you are tempted to return null from a method, consider throwing an exception or returning a Special Case object instead. Robert C. Martin, "Clean Code" “
  15. 15. THROW EXCEPTIONTHROW EXCEPTION interface UserRepository * @throws UserNotFound public function get(string $username): User; 1 {2 /**3 * @param string $username4 5 * @return User6 */7 8 }9 throw new UserNotFound(); final class DbUserRepository implements UserRepository1 {2 public function get(string $username): User3 {4 $userRecord = $this->db->fetchAssoc('SELECT * FROM use5 6 if (false === $userRecord) {7 8 }9 10 return User::fromArray($userRecord);11 }12 }13
  16. 16. interface UserRepository { @throws UserNotFound public function get(string $username): User; }
  17. 17. try { $user = $userRepository->get($username); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } } catch (UserNotFound $ex) { $this->logger->notice('User was not found', ['username' => $u }
  18. 18. try { $user = $userRepository->get($username); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); } } catch (UserNotFound $ex) { $this->logger->notice('User was not found', ['username' => $u } try { $this->notifyUserIfSubscribed($username, $notification); } catch (Throwable $ex) { $this->log($ex); }
  19. 19. SPECIAL CASESPECIAL CASE a.k.a. Null Object A subclass that provides special behavior for particular cases. Martin Fowler, "Patterns of Enterprise Application Architecture" “
  20. 20. class UnknownUser extends User { public function username(): string { return 'unknown'; } public function isSubscribedTo(Notification $notification): b { return false; } } return new UnknownUser(); final class DbUserRepository implements UserRepository1 {2 public function get(string $username): User3 {4 $userRecord = $this->db->fetchAssoc('SELECT * FROM use5 6 if (false === $userRecord) {7 8 }9 10 return User::fromArray($userRecord);11 }12 }13
  21. 21. $user = $userRepository->get('John'); if ($user->isSubscribedTo($notification)) { $notifier->notify($user, $notification); }
  22. 22. Checking for Special Case
  23. 23. Checking for Special Case if ($user instanceof UnknownUser) { //do something }
  24. 24. Checking for Special Case if ($user instanceof UnknownUser) { //do something } if ($user === User::unknown()) { //do something }
  25. 25. Special Case factory class User { public static function unknown(): User { static $unknownUser = null; if (null === $unknownUser) { $unknownUser = new UnknownUser(); } return $unknownUser; } }
  26. 26. Special Case object as private nested class class User { public static function unknown(): User { static $unknownUser = null; if (null === $unknownUser) { $unknownUser = new class extends User { public function username(): string { return 'unknown'; } public function isSubscribedTo(Notification $noti { return false; } }; } return $unknownUser; } }
  27. 27. Returning null from methods is bad, but passing null into methods is worse. Robert C. Martin, "Clean Code" “
  28. 28. class Order { public function __construct( Product $product, Customer $customer, ?Discount $discount ) { $this->product = $product; $this->customer = $customer; $this->discount = $discount; } } final class PremiumDiscount implements Discount { public function apply(float $productPrice): float { return $productPrice * 0.5; } }
  29. 29. ?Discount $discount if (null !== $this->discount) { } class Order1 {2 public function __construct(3 Product $product,4 Customer $customer,5 6 ) {7 $this->product = $product;8 $this->customer = $customer;9 $this->discount = $discount;10 }11 12 public function total(): float13 {14 $price = $this->product->getPrice();15 16 17 $price = $this->discount->apply($price);18 19 20 return $price;21 }22 }23
  30. 30. Discount $discount class Order1 {2 public function __construct(3 Product $product,4 Customer $customer,5 6 ) {7 $this->product = $product;8 $this->customer = $customer;9 $this->discount = $discount;10 }11 }12
  31. 31. Discount $discount class Order1 {2 public function __construct(3 Product $product,4 Customer $customer,5 6 ) {7 $this->product = $product;8 $this->customer = $customer;9 $this->discount = $discount;10 }11 }12 final class NoDiscount implements Discount { public function apply(float $productPrice): float { return $productPrice; } } $order = new Order($product, $customer, new NoDiscount());
  32. 32. EXCEPTION VS SPECIAL CASEEXCEPTION VS SPECIAL CASE Special Case as default strategy instead of optional parameters Exceptions break normal ow to split business logic from error handling Special Case handles exceptional behaviour Exception emphasizes violated business rule
  33. 33. USING EXCEPTIONSUSING EXCEPTIONS
  34. 34. Should be simple as: throw new Exception('User was not found by username: ' . $usernam
  35. 35. Should be simple as: throw new Exception('User was not found by username: ' . $usernam
  36. 36. CUSTOM EXCEPTION TYPESCUSTOM EXCEPTION TYPES bring semantics to your code emphasise exception type instead of exception message allow caller to act di erently based on Exception type
  37. 37. STRUCTURING EXCEPTIONSSTRUCTURING EXCEPTIONS src/ Todo/ Exception/ Model/ User/ Exception/ Model/
  38. 38. CREATING EXCEPTION CLASSESCREATING EXCEPTION CLASSES src/ User/ Exception/ InvalidUsername.php UsernameAlreadyTaken.php UserNotFound.php final class UserNotFound extends Exception { }
  39. 39. use AppUserExceptionUserNotFoundException; try { throw new UserNotFoundException(); } catch (UserNotFoundException $exception) { }
  40. 40. use AppUserExceptionUserNotFoundException; try { throw new UserNotFoundException(); } catch (UserNotFoundException $exception) { } use AppUserExceptionUserNotFound; try { throw new UserNotFound(); } catch (UserNotFound $exception) { }
  41. 41. COMPONENT LEVEL EXCEPTIONCOMPONENT LEVEL EXCEPTION TYPETYPE
  42. 42. COMPONENT LEVEL EXCEPTIONCOMPONENT LEVEL EXCEPTION TYPETYPE namespace AppUserException; interface ExceptionInterface { } final class InvalidUsername extends Exception implements ExceptionInterface { } final class UserNotFound extends Exception implements ExceptionInterface { }
  43. 43. use GuzzleHttpExceptionClientException; use GuzzleHttpExceptionServerException; use GuzzleHttpExceptionGuzzleException; //marker interface try { //code that can emit exceptions } catch (ClientException $ex) { //... } catch (ServerException $ex) { //... } catch (GuzzleException $ex) { //... }
  44. 44. FORMATTING EXCEPTIONFORMATTING EXCEPTION MESSAGESMESSAGES
  45. 45. FORMATTING EXCEPTIONFORMATTING EXCEPTION MESSAGESMESSAGES throw new UserNotFound(sprintf( 'User was not found by username: %s', $username ));
  46. 46. FORMATTING EXCEPTIONFORMATTING EXCEPTION MESSAGESMESSAGES throw new UserNotFound(sprintf( 'User was not found by username: %s', $username )); throw new InsufficientPermissions(sprintf( 'You do not have permission to %s %s with the id: %s', $privilege, get_class($entity), $entity->getId() ));
  47. 47. Encapsulate formatting into Exception classes final class UserNotFound extends Exception implements ExceptionI { public static function byUsername(string $username): self { return new self(sprintf( 'User was not found by username: %s', $username )); } }
  48. 48. Named Constructors communicate the intent throw UserNotFound::byUsername($username);
  49. 49. Coherent exceptional conditions throw TodoNotOpen::triedToSetDeadline($deadline, $this->status); throw TodoNotOpen::triedToMarkAsCompleted($this->status);
  50. 50. PROVIDE CONTEXTPROVIDE CONTEXT
  51. 51. PROVIDE CONTEXTPROVIDE CONTEXT final class UserNotFound extends Exception implements ExceptionI { private string $username; public static function byUsername(string $username): self { $ex = new self(sprintf('User was not found by username: % $ex->username = $username; return $ex; } public function username(): string { return $this->username; } }
  52. 52. EXCEPTION WRAPPINGEXCEPTION WRAPPING
  53. 53. EXCEPTION WRAPPINGEXCEPTION WRAPPING try { return $this->toResult( $this->httpClient->request('GET', '/users') ); } catch (ConnectException $ex) { throw ApiNotAvailable::reason($ex); } 1 2 3 4 5 6 7
  54. 54. EXCEPTION WRAPPINGEXCEPTION WRAPPING try { return $this->toResult( $this->httpClient->request('GET', '/users') ); } catch (ConnectException $ex) { throw ApiNotAvailable::reason($ex); } 1 2 3 4 5 6 7 final class ApiNotAvailable extends Exception implements Exce { public static function reason(ConnectException $error): se { return new self( 'API is not available', 0, $error //preserve previous error ); } } 1 2 3 4 5 6 7 8 9 10 11
  55. 55. EXCEPTION WRAPPINGEXCEPTION WRAPPING try { return $this->toResult( $this->httpClient->request('GET', '/users') ); } catch (ConnectException $ex) { throw ApiNotAvailable::reason($ex); } 1 2 3 4 5 6 7 final class ApiNotAvailable extends Exception implements Exce { public static function reason(ConnectException $error): se { return new self( 'API is not available', 0, $error //preserve previous error ); } } 1 2 3 4 5 6 7 8 9 10 11 $error //preserve previous error final class ApiNotAvailable extends Exception implements Exce1 {2 public static function reason(ConnectException $error): se3 {4 return new self(5 'API is not available',6 0,7 8 );9 }10 }11
  56. 56. RETROSPECTRETROSPECT 1. create custom, cohesive Exception types 2. introduce component-level exception type 3. use Named Constructors to encapsulate message formatting and express the intent 4. capture & provide the context of the exceptional condition 5. apply exception wrapping to rethrow more informative exception
  57. 57. ERROR HANDLINGERROR HANDLING
  58. 58. WHEN TO CATCH EXCEPTIONS?WHEN TO CATCH EXCEPTIONS?
  59. 59. WHEN TO CATCH EXCEPTIONS?WHEN TO CATCH EXCEPTIONS? Do NOT catch exceptions unless you can handle the problem so that the application continues to work
  60. 60. CENTRAL ERROR HANDLERCENTRAL ERROR HANDLER Wraps the entire system to handle any uncaught exceptions from a single place
  61. 61. CHALLENGESCHALLENGES user experience security logging
  62. 62. CHALLENGESCHALLENGES user experience security logging adaptability
  63. 63. EXISTING SOLUTIONSEXISTING SOLUTIONS
  64. 64. EXISTING SOLUTIONSEXISTING SOLUTIONS - stack-based error handling, pretty error page, handlers for di erent response formats (JSON, XML) Whoops
  65. 65. EXISTING SOLUTIONSEXISTING SOLUTIONS - stack-based error handling, pretty error page, handlers for di erent response formats (JSON, XML) - di erent formatting strategies (HTML, JSON, CLI), logging handler, non- blocking errors Whoops BooBoo
  66. 66. Using Whoops final class ErrorHandlerFactory { public function __invoke(ContainerInterface $container) { $whoops = new WhoopsRun(); if (WhoopsUtilMisc::isAjaxRequest()) { $whoops->pushHandler(new JsonResponseHandler()); } elseif (WhoopsUtilMisc::isCommandLine()) { $whoops->pushHandler(new CommandLineHandler()); } else { $whoops->pushHandler(new PrettyPageHandler()); } $whoops->pushHandler(new SetHttpStatusCodeHandler()); $whoops->pushHandler(new LogHandler($container->get('Logg return $whoops; } }
  67. 67. src/bootstrap.php public/index.php bin/app //... initialize DI container $container->get(WhoopsRun::class)->register(); return $container; $container = require __DIR__ . '/../src/bootstrap.php'; $container->get('AppWeb')->run(); $container = require __DIR__ . '/../src/bootstrap.php'; $container->get('AppConsole')->run();
  68. 68. Logging errors final class LogHandler extends Handler { public function handle() { $error = $this->getException(); if ($error instanceof DontLog) { return self::DONE; } $this->logger->error($error->getMessage(), [ 'exception' => $error, ]); return self::DONE; } }
  69. 69. final class UserNotFound extends Exception implements ExceptionInterface, DontLog { //... }
  70. 70. Setting HTTP status code final class SetHttpStatusCodeHandler extends Handler { public function handle() { $error = $this->getException(); $httpStatusCode = ($error instanceof ProvidesHttpStatusCode) ? $error->getHttpStatusCode() : 500; $this->getRun()->sendHttpCode($httpStatusCode); return self::DONE; } }
  71. 71. interface ProvidesHttpStatusCode { public function getHttpStatusCode(): int; } final class UserNotFound extends Exception implements ExceptionInterface, DontLog, ProvidesHttpStatusCode { //... public function getHttpStatusCode(): int { return 404; } }
  72. 72. The OCP (Open­Closed Principle) is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change. Robert C. Martin, "Clean Architecture" “
  73. 73. TEST EXCEPTIONALTEST EXCEPTIONAL BEHAVIOURBEHAVIOUR a.k.a. Negative Testing
  74. 74. TESTING EXCEPTIONS WITHTESTING EXCEPTIONS WITH PHPUNITPHPUNIT class TodoTest extends TestCase { /** * @test */ public function it_throws_exception_on_reopening_if_incomp { $todo = Todo::from('Book flights', TodoStatus::OPEN()) $this->expectException(CannotReopenTodo::class); $todo->reopen(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  75. 75. TESTING EXCEPTIONS WITHTESTING EXCEPTIONS WITH PHPUNITPHPUNIT class TodoTest extends TestCase { /** * @test */ public function it_throws_exception_on_reopening_if_incomp { $todo = Todo::from('Book flights', TodoStatus::OPEN()) $this->expectException(CannotReopenTodo::class); $todo->reopen(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $this->expectException(CannotReopenTodo::class); $todo->reopen(); class TodoTest extends TestCase1 {2 /**3 * @test4 */5 public function it_throws_exception_on_reopening_if_incomp6 {7 $todo = Todo::from('Book flights', TodoStatus::OPEN())8 9 10 11 12 }13 }14
  76. 76. ARRANGE-ACT-ASSERTARRANGE-ACT-ASSERT 1. initialize SUT/prepare inputs 2. perform action 3. verify outcomes
  77. 77. class TodoTest extends TestCase { /** * @test */ public function it_gets_completed() { $todo = Todo::from('Book flights', TodoStatus::OPEN()); $todo->complete(); $this->assertTrue($todo->isCompleted()); } }
  78. 78. /** * @test */ public function it_throws_exception_on_reopening_if_incomplete() { $todo = Todo::from('Book flights', TodoStatus::OPEN()); try { $todo->reopen(); $this->fail('Exception should have been raised'); } catch (CannotReopenTodo $ex) { $this->assertSame( 'Tried to reopen todo, but it is not completed.', $ex->getMessage() ); } }
  79. 79. Thank youThank you Drop me some feedback and make this presentation better · joind.in/talk/8a8d6 @nikolaposa blog.nikolaposa.in.rs

×