Following the previous post on object design, I covered service objects and some useful principles that could help fellow developers in designing objects in their object-oriented programs. In this blog post, I will be talking about other objects apart from service objects which could be available in any object-oriented programs.
Introduction
Service objects are known to either perform a task or return a piece of information. Also, there are other kinds of objects that might hold data, and optionally expose some behavior for manipulating or retrieving that data. These other objects can be divided into more specific types, namely; Entities, Value Object, and Data Transfer Objects.
Entities, Value Objects and Data Transfer Objects
By definition, entities are at the core of objects and they represent important concept from a business domain like a reservation, ordering product, customer, etc. As a developer, entities help you to model business domains in your application. For instance, you can model hotel reservation with a reservation entity. In all, an entity holds the relevant data, it may provide ways to manipulate and expose some useful information to other objects. Data about entities are likely to change over time hence you should not be mistaken to have those changes occur in a different object. This means as an entity may change over time, the same objects should be the one that undergoes the changes, not a different object. In this case, entities need to be identifiable. For instance SalesInvoice
entity returns the same entity on creating it and therefore, changes are recorded in the same entity.
<?php
final class SalesInvoice
{
private SalesInvoiceId $salesInvoiceId;
public static function create(SalesInvoiceId $salesInvoiceId): SalesInvoice {
$object = new self();
$object->salesInvoiceId = $salesInvoiceId;
return $object;
}
}
Given the fact that the state of entities changes over time, entities are mutable objects. They come with specific rules for their implementation. The method that changes an entity's state should be of a void return type. These methods should protect the entity against ending up in an invalid state. Also, they shouldn't expose all their internals just for testing what's going on inside. Instead, an entity should keep a log and expose that so objects can find out what changed about it and why;
final class SalesInvoice
{
/**
* @var object[]
*/
private array $events = [];
// You can finalize it
public function finalize(): void
{
$this->finalized = true;
$this->events[] = new SalesInvoiceFinalized(/* ... */);
}
/**
* @return object[]
*/
public function recordedEvents(): array
{
return $this->events;
}
}
// In a test scenario:
$salesInvoice = SalesInvoice::create(/* ... */);
$salesInvoice->finalize();
assertEquals(
[
new SalesInvoiceFinalized(/* ... */)
],
$salesInvoice->recordedEvents()
);
Value Objects are completely different objects. They are often much smaller with one or two properties. They represent domain concepts and in that case, they represent part of an entity. For example, in the SalesInvoice entity, we need value object for the ID of the sales invoice, the date on which the invoice was created and the quantity and ID fo the product on each line;
<?php
final class SalesInvoiceId
{
}
final class Date
{
}
final class ProductId
{
public static function fromInt(int $productId): ProductId
{
// ...
}
}
final class Quantity
{
public static function fromInt(
int $quantity,
int $precision
): Quantity {
// ...
}
}
final class SalesInvoice
{
public static function create(
SalesInvoiceId $salesInvoiceId,
Date $invoiceDate
): SalesInvoice{
}
public function addLine(
ProductId $productId,
Quantity $quantity
): void {
$this->lines[] = Line::create(
$productId,
$quantity
);
}
}
Value objects wrap one or more primitive type values. We don't need a value object to be identifiable, we don't care about the exact instance we are working with since we don't need to track the changes that happen to the value. If we transform the value to some other value, we should just instantiate a new copy which represents the modified value. As an example when adding two quantities, instead of changing the internal value of the original Quantity, we return a new Quantity object the represent their sum;
<?php
final class Quantity
{
private $quantity;
private $precision;
private function __construct(int $quantity, int $precision){
$this->quantity = $quantity;
$this->precision = $precision;
}
public static function fromInt(int $quantity, int $precision): Quantity{
return new self($quantity, $precision);
}
public function add(Quantity $other): Quantity
{
// Assertion::same($this->precision, $other->precision);
return new self(
$this->quantity + $other->quantity,
$this->precision
);
}
}
// A quantity of 1500 with a precision of 2 represents 15.00
$originalQuantity = Quantity::fromInt(1500, 2);
// The modified quantity represents 15.00 + 5.00 = 20.00
$newQuantity = $originalQuantity->add(Quantity::fromInt(500, 2));
Data Transfer Objects are the types of object that you will find at the edge of applications, where data coming from the world outside are converted into a structure that the application can work with. It does not have to protect its state and exposes all of its properties. There is no need for getters and setters which means it is quite sufficient to use public properties for them. They are often used as a command object, matching ht user's intention and containing all the data needed to fulfill their wish. An example of such a command object is the following ScheduleMeetup command which represents the user's wish to schedule a meetup with the given title and date;
<?php
final class ScheduleMeetup
{
public $title;
public $date;
}
final class MeetupController
{
public function scheduleMeetupAction(Request $request)
{
// Extract the form data from the request body:
$formData = /* ... */
// Create the command object using this data:
$scheduleMeetup = new scheduleMeetup();
$scheduleMeetup->title = $formData['title'];
$scheduleMeetup->date = $formData['date'];
$this->scheduleMeetupService->__invoke($scheduleMeetup);
}
}
In the case of a DTO, this isnβt a problem at all, because a DTO doesnβt protect its internals anyway. So, if it makes
sense, you can add a property filler method to a DTO. For example, to copy form data or JSON request data directly into the command object. Since filling the properties is the first thing that should happen to a DTO, it makes sense to implement the property filler as a named constructor:
<?php
final class ScheduleMeetup
{
public $title;
public $date;
public static function fromFormData(array $formData): ScheduleMeetup{
$scheduleMeetup = new Self();
$scheduleMeetup->title = $fromData['title'];
$scheduleMeetup->date = $fromData['date'];
return $scheduleMeetup;
}
}
final class MeetupController
{
public function scheduleMeetupAction(Request $request)
{
// Extract the form data from the request body:
$formData = /* ... */
$scheduleMeetup = ScheduleMeetup::fromFormData($formData);
$this->scheduleMeetupService->__invoke($scheduleMeetup);
}
}
Conclusion
Here, we continued to look at other objects apart from service objects that could be available in an objected-oriented application, named entities, value objects, and data transfer objects.
I hope you enjoyed it. Next, we would look at how to create and use these objects based on guiding principles.
Further Reading
Matthias Noback's description is one of the best in-depth discussion of Style Guide for Object Design from a practical design principle perspective.
Keep In Touch
Let's keep in touch as we continue to learn and explore more about software engineering. Don't forget to connect with me on Twitter or LinkedIn
Top comments (0)