The true value of objects

The true value of objects

Although the latest PHP versions provide us with enough types and keywords to enable object oriented development, the language itself is not truly object oriented. In pure object oriented languages, like Java, almost everything is an object. Even primitives have their object equivalent. Concepts that first look like simple values, can in fact be modelled as objects. This enables us to add a lot of behavior to them. These so called Value Objects make our code more readable, elegant, maintainable and dry. We will explore the possibilities and advantages of these Value Objects together, guided by some real world code samples.

0cab45c75f109eb044564efbd28af603?s=128

Stijn Vannieuwenhuyse

January 28, 2017
Tweet

Transcript

  1. The true value of objects

  2. Stijn Vannieuwenhuyse @stijnvnh

  3. Teamleader CRM - invoicing - project management head of engineering

  4. The true value of objects

  5. what are Objects?

  6. represent domain concepts encapsulate data expose behaviour

  7. SOLID

  8. Domain Driven Design

  9. Service Objects

  10. Entities

  11. Value Objects

  12. class Money { private $amount; private $currency; public function __construct($amount,

    $currency) { $this->amount = $amount; $this->currency = $currency; } public function getAmount() { return $this->amount; } public function getCurrency() { return $this->currency; } }
  13. $a = new Money(5.00, "EUR"); $b = new Money(5.00, "EUR");

    $c = new Money(4.23, "EUR"); $d = new Money(5.00, "USD"); $a === $b; //false $a == $b; //true $a == $c; //false $a == $d; //false
  14. but ... Why?

  15. Model domain concepts

  16. class InvoiceItem { private $invoiceId; private $productId; private $amount; private

    $price; private $vatrate; public function __construct($invoiceId, $productId, $amount, $price, $vatrate) { //... } public function getLineTotalExclusiveVat() { return $this->amount * $this->price; } public function getVatTotal() { return $this->getLineTotalExclusiveVat() * $this->vatrate; } public function getLineTotalInclusiveVat() { return $this->getLineTotalExclusiveVat() + $this->getVatAmount(); } //other getters } $invoiceItem = new InvoiceItem(1, 2, 100, 1000.00, 21.0);
  17. class PriceExclusiveVat { private $money; private $vat; public function __construct(

    $money, $vat ) { $this->money = $money; $this->vat = $vat; } public function getVatAmount() { return $this->money * $this->vat; } //other getters }
  18. class InvoiceItem { private $invoiceId; private $productId; private $amount; private

    $price; private $vatrate; public function __construct($invoiceId, $productId, $amount, PriceExclusiveVat $price) { //... } public function getLineTotalExclusiveVat() { return $this->amount * $this->price->getAmountExclusiveVat(); } public function getVatTotal() { return $this->amount * $this->price->getVatAmount(); } public function getLineTotalInclusiveVat() { return $this->amount * $this->price->getAmountInclusiveVat(); } //other getters } $invoiceItem = new InvoiceItem( 1, 2, 100, new PriceExclusiveVat(new Money(1000.00, "EUR"), 21.0) );
  19. Avoid bugs

  20. $invoiceItem = new InvoiceItem( 1, 2, 100, new PriceExclusiveVat(new Money(1000.00,

    "EUR"), 21.0) ); /// public function getVatAmount() { return $this->money * $this->vat }
  21. $invoiceItem = new InvoiceItem( 1, 2, 100, new PriceExclusiveVat(new Money(1000.00,

    "EUR"), new Percentage(21.0)) ); class Percentage { private $percentage; public function __construct( $percentage ) { $this->percentage = $percentage } public function asFloat() { return $this->percentage / 100; } //other getters }
  22. Achieve single responsibility

  23. class HexColor { private $hexColor; public function __construct($hexColor) { if(!preg_match('/^#?[a-f0-9]{6}$/i',

    $hexColor)) { throw new \InvalidArgumentException(); } $this->hexColor = $hexColor; } }
  24. class HexColor { private $hexColor; public function __construct($hexColor) { //...

    } public function blend(HexColor $otherHexColor) { $newHexColor = // color blending...; return new HexColor($newHexColor); } }
  25. PHP BUILT IN

  26. $a = new DateTime("2017-01-28"); $b = $a->modify("+1 day"); $a ==

    new DateTime("2017-01-28"); //false
  27. class User { /** * @var DateTime */ private $registrationDate;

    public function __construct(DateTime $registrationDate) { $this->registrationDate = $registrationDate; } public function getRegistrationDate() { return $this->registrationDate; } } $user = new User(new DateTime("2017-01-28")); if($user->getRegistrationDate()->modify("+1 year") == new DateTime("today")) { //send anniversary gift } $user->getRegistrationDate() == new DateTime("2017-01-28"); //false $user->getRegistrationDate() == new DateTime("2018-01-28"); //true
  28. CAUTION

  29. Value objects should be immutable

  30. class User { /** * @var DateTimeImmutable */ private $registrationDate;

    public function __construct(DateTimeImmutable $registrationDate) { $this->registrationDate = $registrationDate; } public function getRegistrationDate() { return $this->registrationDate; } } $user = new User(new DateTimeImmutable("2017-01-28")); if($user->getRegistrationDate()->modify("+1 year") == new DateTimeImmutable("today")) { //send anniversary gift } $user->getRegistrationDate() == new DateTimeImmutable("2017-01-28"); //true
  31. class Money { //fields //constructor //getters public function add(Money $other)

    { if($this->currency != $other->currency) { throw new CurrenciesDontMatch(); } return new Money($this->amount + $other->amount, $this->currency); } }
  32. ENDLESS POSSIBILITIES

  33. class DateRange { private $start; private $end; public function __construct(

    DateTimeImmutable $start, DateTimeImmutable $end ) { if($start > $end) { throw new InvalidArgumentException(); } $this->start = $start; $this->end = $end; } //getters } $a = new DateRange(new DateTimeImmutable("today"), new DateTimeImmutable("tomorrow")); $b = new DateRange(new DateTimeImmutable("today"), new DateTimeImmutable("tomorrow")); $c = new DateRange(new DateTimeImmutable("yesterday"), new DateTimeImmutable("today")); $a == $b; //still true $a < $c; //will it work?
  34. class DateRange { private $start; private $end; //constructor //getters /**

    * @return bool */ public function isLessThan(DateRange $other) { return $this->start < $other->start; } } $a = new DateRange(new DateTimeImmutable("today"), new DateTimeImmutable("tomorrow")); $b = new DateRange(new DateTimeImmutable("today"), new DateTimeImmutable("tomorrow")); $c = new DateRange(new DateTimeImmutable("yesterday"), new DateTimeImmutable("today")); $a->isLessThan($b); //false $a->isLessThan($c); //true
  35. class DateRange { private $start; private $end; //constructor //getters //comparation

    public function __toString() { return $this->start->format("U") . " " . $this->end->format("U"); } public static function fromString($string) { if(!preg_match("/^(\S+) (\S+)$/", string, $matches)) { throw new InvalidArgumentException(); } return new DateRange( new DateTimeImmutable($matches[1]), new DateTimeImmutable($matches[2]) ) } }
  36. class DateRange { //... /** * @return DateRange[] */ public

    static function summarize(array $dateRanges) { //some magic to combine overlapping dateranges return $dateRanges; } }
  37. class PersonName { private $firstName; private $nickName; private $lastName; public

    function __construct($firstName, $lastName, $nickName = '') { $this->firstName = $firstName; $this->lastName = $lastName; $this->nickName = $nickName; } //getters public function getDisplayShortName() { return $this->nickName ?: $this->firstName; } public function getDisplayFullName() { return $this->getDisplayShortName() . " " . $this->lastName; } }
  38. $string = " FooBar "; trim($string); //FooBar substr($string, 0, 2);

    //Fo strlen($string); // 2 strtolower($string); //fo //...
  39. class String { public function __construct($string) { /*...*/ } public

    function length() { /*...*/ } public function substring($start, $length) { /*...*/ } public function toLowercase() { /*...*/ } public function trim() { /*...*/ } } $string = new String("foobar"); $string ->trim() ->substring(0, 2) ->toLowercase(); //fo
  40. $workingHours = new WeeklyTimeSlot( WeekDay::friday(), new TimeSlot( new Time(08, 30)

    new Time(17, 00) ) ); /** @var duration Duration */ $duration = $weeklyTimeSlot->getDuration();
  41. $workingHours = new WeeklyTimeSlot( WeekDay::friday(), new TimeSlot( new Time(10, 30)

    new Time(20, 00) ) ); /** @var duration Duration */ $duration = $weeklyTimeSlot->getDuration();
  42. class SchoolYear { private $startYear; public function __construct($startYear) { if(!is_int($startYear))

    { throw new InvalidArgumentException(); } $this->startYear = $startYear; } public function getStartYear() { /* ... */ } public function getEndYear() { /* ... */ } public function __toString() { /* ... */ } //2014-2015 }
  43. class SchoolYear { private $startYear; public function __construct($startYear) { /*

    ... */ } public static function current() { /* ... */ } public static function containingDate(DateTimeImmutable $date) { /* ... */ } public static function fromString($string) { /* ... */ } public function next() { /* ... */ } public function previous() { /* ... */ } public function getStartYear() { /* ... */ } public function getEndYear() { /* ... */ } public function __toString() { /* ... */ } public function equals(SchoolYear $schoolYear) { /* ... */ } public function getStartDate() { /* ... */ } public function getEndDate() { /* ... */ } public function getDateRange() { /* ... */ } public function getCalendarWeeks() { /* ... */ } public function startsInWeekend() { /* ... */ } public function isActive() { /* ... */ } public function isPassed() { /* ... */ } public function containsCalendarWeek(CalendarWeek $calendarWeek) { /* ... */ } public function containsDate(DateTimeImmutable $date) { /* ... */ } public function containsDateRange(DateRange $date) { /* ... */ } }
  44. interface PriceExclusiveVat { public function getInclusiveAmount(); public function getExclusiveAmount(); public

    function getVatAmount(); public function toPriceInclusiveVat(); } interface PriceInclusiveVat { public function getInclusiveAmount(); public function getExclusiveAmount(); public function getVatAmount(); public function toPriceExclusiveVat(); }
  45. interface Invoice { public function getInclusiveAmount(); public function getTotalVat(); public

    function getAppliedVat(); //AppliedVat[] } interface AppliedVat { public function getVatRate(); public function getExclusiveAmount(); public function getVatAmount(); public function getInclusiveAmount(); }
  46. But ...

  47. what about database storage?

  48. class Talk { //... /** * @ORM\Column(type="datetime") */ private $startTime;

    /** * @ORM\Column(type="datetime") */ private $endTime; public function __construct(/*...*/, TimeSlot $slot) { $this->startTime = $slot->getStartTime(); $this->endTime = $slot->getEndTime(); } /** * @return TimeSlot */ public function getTimeSlot() { return new TimeSlot($this->startTime, $this->endTime); } }
  49. /** * @ORM\Embeddable */ class TimeSlot { /** * @ORM\Column(type="datetime")

    */ private $startTime; /** * @ORM\Column(type="datetime") */ private $endTime; //... }
  50. class Talk { //... /** * @ORM\Embedded(class="TimeSlot") */ private $timeSlot

    public function __construct(/*...*/, TimeSlot $slot) { $this->timeSlot = $timeSlot; } /** * @return TimeSlot */ public function getTimeSlot() { return $this->timeSlot; } //... }
  51. what about memory usage?

  52. $cars = []; for($i = 0; $i <= 1000000; $i++)

    { $cars[] = new Car("Audi A6", "VIN-NR-2015" . $i, new HexColor("FF0000")); }
  53. $cars = []; for($i = 0; $i <= 1000000; $i++)

    { $cars[] = new Car("Audi A6", "VIN-NR-2015" . $i, HexColor::define("FF0000")); } class HexColor { private $hexColor; private static $hexColors = []; private function __construct($hexColor) { /*...*/ } public static function define($hexColor) { if(!array_key_exists($hexColor, self::$hexColors)) { self::$hexColors[$hexColor] = new self($hexColor); } return self::$hexColors[$hexColor]; } }
  54. Value object or entity?

  55. User

  56. Username

  57. Product-id

  58. Date

  59. Money

  60. Blog Post

  61. Blog Post Category

  62. VALUE OBJECTS represent domain values encapsulate data expose behaviour

  63. VALUE OBJECTS dry readability immutable

  64. QUESTIONS?

  65. Thank you Stijn Vannieuwenhuyse - @stijnvnh https://joind.in/talk/a0ccb