Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

DPC 2019, Amsterdam: Beyond design patterns and principles - writing good OO code

255 views

Published on

Of course, you should read all you can about SOLID, Design patterns, Patterns of Enterprise Application Architecture, etc. Once you have a basic understanding of these topics you still have to write that code though, and write it well too! What is good code? Are there some guidelines, or rules of thumb, which you can follow while making your everyday coding decisions?

In this talk I’ll cover many of these coding guidelines, which aren’t usually covered by patterns or principles books. They should help you write better code and give you a richer vocabulary for reviewing other people’s code. Some of the subjects that we’ll discuss are: state, mutability, CQS, one-method objects, domain-first, API-driven, functional programming influences, object boundaries, (de)serialization, and many more!

Published in: Software
  • Be the first to comment

  • Be the first to like this

DPC 2019, Amsterdam: Beyond design patterns and principles - writing good OO code

  1. 1. Beyond Design Principles & Patterns Writing good OO code Matthias Noback @matthiasnoback
  2. 2. Writing good OO code Without relying on design principles and patterns Matthias Noback @matthiasnoback
  3. 3. Design patterns ● Abstract factory ● Mediator ● Proxy ● Builder ● Composite ● Chain of responsibility ● Adapter ● Strategy ● Façade ● Bridge ● Observer ● Singleton ● Factory method ● Command
  4. 4. Design principles, and more patterns ● Class principles (SOLID) ● Package design principles ● Tactical DDD patterns ● Testing patterns ● Architectural patterns
  5. 5. All this stuff!
  6. 6. Education ● Refer to books (but nobody reads them) ● In interviews: “Do you know SOLID?” ● Knowing is one thing, applying it is something else.
  7. 7. Back to basics
  8. 8. Any class can serve as the blueprint of an object, but not every object will be a Good Objecttm
  9. 9. Objects introduce meaning By wrapping primitive values Class name = Type
  10. 10. Strings Email addresses
  11. 11. Floats Latitude
  12. 12. If not every string counts as an email address, introduce a dedicated type: EmailAddress. If not every integer counts as an age, introduce a dedicated type: Age. And so on!
  13. 13. Objects keep together what belongs together Latitude and Longitude X and Y Amount and Currency Distance and Unit ...
  14. 14. Cohesion What is together belongs together.
  15. 15. Cohesion What belongs together, gets together. Objects attract related data and behavior.
  16. 16. Objects bring together meaningful behaviors Coordinates.distanceTo(Coordinates other): Distance Money.convertTo(Currency other): Money Map.set(String key, T item): void Map.get(String key): T Array.push(T item): void Array.pop(): T
  17. 17. Objects State & Behavior
  18. 18. final class Coordinates { private Latitude latitude; private Longitude longitude; public function halfwayTo( Coordinates other ): Coordinates { // ... } }
  19. 19. State Behavior Primitive values Entities Value objects Services Anemic domain objects
  20. 20. For all objects Make sure they can't exist in an incoherent or inconsistent state
  21. 21. At construction time ● Provide the right data (values, value objects) ● Provide the right services (collaborating objects)
  22. 22. // no setter injection service = new Service(); service.setLogger(...); // no setter party object = new Object(); object.setFoo(...); object.setBar(...);
  23. 23. At modification time ● Provide the right data (values, value objects)
  24. 24. What's the “right” data? ● Valid (correct types, correct number of things, etc.) ● Meaningful (within an allowed range, pattern, etc.)
  25. 25. What's the “right” data? Only allow transitions that make sense final class Order { public function cancel(...): void { if (wasShipped) { throw new LogicException(...); } } }
  26. 26. When implementing behavior: follow this recipe public function someMethod(int $value) { // check pre-conditions Assertion::greaterThan($value, 0); // fail early if (...) { throw new RuntimeException(...); } // happy path: at "0" indent // check post-conditions return ...; }
  27. 27. Command/Query Separation Every method is either a command or a query method.
  28. 28. public function commandMethod(...): void { /* * Changes observable state of the system * * May have side effects: * - network calls * - filesystem changes * * Returns nothing. * May throw an exception. */ }
  29. 29. public function queryMethod(...): [specific type] { /* * Returns something, doesn't change * anything. * * May throw an exception. */ }
  30. 30. CQS principle Asking for information doesn’t change observable state.
  31. 31. Return single-type values ● Object of specific type, or an exception ● List with values of specific type, or an empty list
  32. 32. public function queryMethod(...): [specific type] { if (...) { throw new RuntimeException(...); } return objectOfSpecificType; }
  33. 33. public function queryMethod(...): List { if (...) { return List::empty(); } return List::of(...); }
  34. 34. At failure time Throw useful and detailed exceptions
  35. 35. Defensive programming Recover from failure? No, usually: scream about it!
  36. 36. Offensive programming Add lots of sanity checks and throw exceptions. Assert::greaterThan(...); Assert::count(...); ...
  37. 37. Offensive programming Run static analysis tools DateTime::createFromFormat() returns bool|DateTime
  38. 38. Offensive programming Introduce strictly typed alternatives to PHP's weakly typed functions. public static function createFromFormat( string $format, string $date ): DateTimeImmutable { $result = DateTimeImmutable::createFromFormat( $format, $date ); if ($result === false) { throw new RuntimeException(...); } return $result; }
  39. 39. State versus behavior, revisited Objects have state and behavior But they hide data and implementation details
  40. 40. State versus behavior, revisited Expose more behavior, less state
  41. 41. Treat your objects as black boxes When writing tests for them
  42. 42. Don't test constructors by calling getters Give the object a good reason to remember something, and describe that reason in a test.
  43. 43. public function it_can_be_constructed(): void { money = new Money(1000, new Currency('EUR')); assertEquals( 1000, money.amount() ); assertEquals( 'EUR', money.currency().asString() ); } Getters just for testing!
  44. 44. public function it_can_be_converted(): void { money = new Money(1000, new Currency('EUR')); rate = new ExchangeRate( new Currency('EUR'), new Currency('USD'), 1.23456 ); converted = money.convert(rate); assertEquals( new Money( 1235, // rounded to the second digit new Currency('USD') ) converted ); } No getters! Nor testing them!
  45. 45. Guiding experiment Only add a getter if something other than a test needs it.
  46. 46. final class Money { public function convert(ExchangeRate rate): Money { Assertion::true(rate.fromCurrency() .equals(this.currency)); return new Money( this.amount * (1 / rate.rate()), rate.toCurrency() ); } } ExchangeRate needs to expose all its data
  47. 47. final class ExchangeRate { public function convert(Money money): Money { Assertion::true(money.currency() .equals(this.fromCurrency)); return new Money( money.amount() * (1 / this.rate), rate.toCurrency ); } } Money needs to expose all its dataExchangeRate has intimate knowledge about creating Moneys
  48. 48. Changing the behavior of an object Always aim to do it without touching the code of its class (let it remain a black box!)
  49. 49. class Game { public function move(): void { steps = this.roll(); } private function roll(): int { return randomInt(1, 6); } } How to use a hard-coded value for testing?
  50. 50. class Game { public function move(): void { steps = this.roll(); } protected function roll(): int { return randomInt(1, 6); } } class GameWithFixedRoll extends Game { protected function roll(): int { return 6; } }
  51. 51. final class Game { private Die die; public function __construct(Die die) { this.die = die; } public function move(): void { steps = this.die.roll(); } } interface Die { public function roll(): int; } Dependency injection! Composition instead of inheritance Final classes!
  52. 52. final RandomDie implements Die { public function roll(): int { return randomInt(1, 6); } } final FixedDie implements Die { public function __construct(value) { this.value = value; } public function roll(): int { return this.value; } } Technically a test double called “stub” Behavior can be modified without touching the code
  53. 53. “Composition over inheritance” Changing the behavior of objects by composing them in different ways (as opposed to using inheritance to override behavior)
  54. 54. Guiding experiment Make all classes final
  55. 55. Create better objects ● Introduce more types. ● Be more strict about them. ● Design objects that only accept valid data. ● Design objects that can only be used in valid ways. ● Use composition instead of inheritance to change an object's behavior.
  56. 56. Well designed objects lead to an application that almost clicks together
  57. 57. Questions? Matthias Noback @matthiasnoback Feedback: https://joind.in/talk/d0880

×