Hi folks, DonCallisto here.
In this article we’ll see why I personally don’t like status flags and how to handle them in a more elegant way. We’ll find out code written without flags which is semantically better and convey business rules way better. Curious? Keep reading!
Before start, I would like to be sure you understand what status flag are. Since I’m not aware of a literature definition for the concept, I come up with one (if someone finds it, please leave a comment and I’ll be happy to edit with an accurate quote). When I talk about status flag I mean a class member which decree the current status of an object (class instance). For example think of a kanban board with several columns like “confirmed”, “assigned”, “in progress”, “done” and “released”. A task inside the kanban, in a given moment, can be pinned in one and only one of these columns. It follows that a task has a status, alternatively confirmed, assigned (to someone) and so on. That’s what I mean when I use “status flag” locution. You can also think them in terms of enums. Having said that, I would not include binary flags in this definition as only two possible values can be assigned, like true or false, on or off, open or closed, and so on and so forth; further on should be clear why.
Going straight to the point, consider a piece of software responsible for blog’s articles management. An article has a status depending on the point in time you look at it. In this example possible status are: draft, ready for review, reviewed and published. There’s also a business rule we need to take into account and states that a status transition can happen only in a finite-state machine like fashion. An article is created in a draft status, then it can be moved to ready for a review, then when reviewed it acquire a reviewed status and only when reviewed it can be published. So we got a pretty straightforward state machine, where transitions happens in a one way direction from a state to the other as depicted below
To ease the understanding of the concept, Article was modeled as a simplified version of a real world scenario so we can focus on what matters the most leaving out the irrelevant details. For example I decided to avoid transitions like the one back from “ready for review” to “draft”, I also kept out some status like “need edit” when a review is done but not accepted and – you’ll see in no time – articles doesn’t have a title or other attributes one expects to find in a real world scenario. These are only some of the simplification you can find reading on this post.
So far so good, context is set and we’re now ready to go on. As Linus Torvalds said once… “Talk is cheap, show me the code!”
Consider the following PHP 8.1 classes (feel free to check out code directly in GitHub; please notice the link point to a specific commit and not to the whole repo, further on, when needed, I’ll link other commits)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php declare(strict_types=1); namespace Domain\Article; enum Status { case DRAFT; case REVIEW; case REVIEWED; case PUBLISHED; public function isDraft(): bool { return $this == self::DRAFT; } public function isReview(): bool { return $this == self::REVIEW; } public function isReviewed(): bool { return $this == self::REVIEWED; } public function isPublished(): bool { return $this == self::PUBLISHED; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\Article\Exception\ArticleException; use Domain\User\User; class Article { private Status $status; private ?User $reviewer; private ?\DateTimeImmutable $publicationDate; public function __construct(private User $author, private string $content) { $this->status = Status::DRAFT; $this->reviewer = null; $this->publicationDate = null; } public function isInDraft(): bool { return $this->status->isDraft(); } /** * @throws ArticleException */ public function readyForReview(): void { if ($this->isReadyForReview()) { return; } if (!$this->isInDraft()) { throw ArticleException::cantBeReadyForReview(); } $this->status = Status::REVIEW; } public function isReadyForReview(): bool { return $this->status->isReview(); } /** * @throws ArticleException */ public function reviewed(User $reviewer): void { if ($this->isReviewed()) { return; } if (!$this->isReadyForReview()) { throw ArticleException::cantBeReviewed(); } if ($reviewer === $this->author) { throw ArticleException::reviewedByAuthor(); } $this->status = Status::REVIEWED; $this->reviewer = $reviewer; } public function isReviewed(): bool { return $this->status->isReviewed(); } /** * @throws ArticleException */ public function publish(\DateTimeInterface $publicationDate): void { if ($this->isPublished()) { return; } if (!$this->isReviewed()) { throw ArticleException::cantBePublished(); } $this->status = Status::PUBLISHED; $this->publicationDate = \DateTimeImmutable::createFromInterface($publicationDate); } public function isPublished(): bool { return $this->status->isPublished(); } public function getAuthor(): User { return $this->author; } public function getContent(): string { return $this->content; } /** * @param string $newContent * * @throws ArticleException */ public function changeContentTo(string $newContent): void { if (!$this->isInDraft()) { throw ArticleException::contentCantBeChanged(); } $this->content = $newContent; } // We can also throw an exception... public function getReviewer(): ?User { return $this->reviewer; } // We can also throw an exception... public function getPublicationDate(): ?\DateTimeImmutable { return $this->publicationDate; } } |
User
and ArticleException
were left out; you can find whole code on GitHub repo linked above.
Status
is an enum, a new PHP feature you can take advantage of from PHP 8.1. Status
is our status flag, the one you’ve read during introduction. Looking at Article
, it’s easy to find how I used Status
and I’m pretty sure you, in your developer career, did wrote code in this fashion. No shame in that. Probably you’re wondering what’s wrong with Article
class as it seems to model every business rule described before. The point is I’m using a single class to represent and model four different status, so four different behaviors, all in one class. You may argue that there’s only one domain concept – other than Status
that you can see pretty much as a value object as it’s immutable and its identity is verified not by its id but by its values – and it is Article
, but from my point of view, as long as Article
should behave differently depending on its status, it should be modeled as different objects. To support latter statement, consider what follows.
publicationDate
attribute must be nullable as it has meaning only for a certainStatus
. This is a smell for little cohesion. Moreover aspublicationDate
is nullable,getPublicationDate
must be able to returnnull
or, alternatively, must throw an exception ifpublicationDate
isnull
. Either chances are bad as the first forces the client to explicitly check fornull
values, whereas the second forces the client to catch possible exceptions and can be fuzzy if seen from outside (why on the worldpublicationDate
retrieval can lead to an exception?). In addition, and as a side effect,publicationDate
concept has no meaning in certain situation. To prove that, go and ask your domain experts something like “what’s the publication date of a draft/ready for review/reviewed article?”. No surprise if they’ll be puzzled. The same goes forreviewer
.- There’s a lot handling for public API. For instance, look at
readyForReview
public method (the same goes for all transitional method). Internally we are forced to put an extra effort to convey and guarantee a domain rule forStatus
changes. - Public API is not telling anything about the business. Leaving out is-ser methods in
Article
and looking at the class from the outside through its API we havereadyForReview
,reviewed
,publish
,changeContentTo
,getReviewer
andgetPublicationDate
. For a client it might seem possible to changeArticle
status to any of the possible status regardless of its current one. Anybody which knows well domain rules is forced to look intoArticle
code to find what can go possibly wrong. Even if domain invariants are respected, here we’re hiding them and communicating something wrong aboutArticle
nature. Talking aboutgetReviewer
andgetPublicationDate
, I’ve already pointed out what’s the issue. - Think of a statistics service (a class) designed to provide daily, weekly, monthly and yearly unique readers for a (published) article. Can you imagine its unique public method? It would be something like
public function getReaders(Article $article): int
. Again looking at this method some questions could be raised; one above all is what to return when an article is not published. Should we return 0 that’s equivocal with the concept of “no readers”, or should we returnnull
, meaning statistics cannot be retrieved? Should we raise an exception if article is not published? From the point of view of a newcomer, whatever path you take, is it clear the concept behind the return value? Is it clear onlyStatus::PUBLISHED
articles will be manageables? - Think about entity retrieval. Let’s pretend we wish to retrieve only
Status::DRAFT
articles. You need to add an extraWHERE
condition when querying. Yes of course it’s not dramatic from performance perspective, but this is true as long as you store few records. Reading this article you may find that your queries could have nearly 60% of overhead (when millions records stored). - Think about a new attribute which has meaning only for some or just one status but not for the whole object (like
publicationDate
). Whoever modifyArticle
must put attention when introducing this kind of change as a forgetfulness could lead to inconsistent API and data (see it like adding a method or an attribute to a class where it does not belongs. Yes, it won’t happen when dealing with unrelated classes but could happen in this specific case).
These are some points against status flags. Maybe there could be more, or maybe you won’t consider any of these problematic. As usual your mileage may vary.
An alternative
Taking up one of previous statements
“The point is I’m using a single class to represent and model four different status, so four different behaviors, all in one class.”
Let’s see how code can be changed to express better domain concepts (GitHub code here)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\User\User; interface ArticleInterface { public function getContent(): string; public function getAuthor(): User; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\User\User; class DraftArticle implements ArticleInterface { public function __construct(private User $author, private string $content) { } public function getAuthor(): User { return $this->author; } public function getContent(): string { return $this->content; } public function changeContentTo(string $newContent): void { $this->content = $newContent; } public function readyForReview(): ReadyForReviewArticle { return new ReadyForReviewArticle($this); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\User\User; class ReadyForReviewArticle implements ArticleInterface { private User $author; private string $content; public function __construct(DraftArticle $draft) { $this->author = $draft->getAuthor(); $this->content = $draft->getContent(); } public function getAuthor(): User { return $this->author; } public function getContent(): string { return $this->content; } public function reviewed(User $reviewer): ReviewedArticle { return new ReviewedArticle($this, $reviewer); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\User\User; class ReviewedArticle implements ArticleInterface { private User $author; private string $content; public function __construct(ReadyForReviewArticle $readyForReview, private User $reviewer) { $this->author = $readyForReview->getAuthor(); $this->content = $readyForReview->getContent(); } public function getAuthor(): User { return $this->author; } public function getReviewer(): User { return $this->reviewer; } public function getContent(): string { return $this->content; } public function publish(\DateTimeInterface $publicationDate): PublishedArticle { return new PublishedArticle($this, $publicationDate); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?php declare(strict_types=1); namespace Domain\Article; use Domain\User\User; class PublishedArticle implements ArticleInterface { private User $author; private string $content; private \DateTimeImmutable $publicationDate; public function __construct(ReviewedArticle $reviewed, \DateTimeInterface $publicationDate) { $this->author = $reviewed->getAuthor(); $this->content = $reviewed->getContent(); $this->publicationDate = \DateTimeImmutable::createFromInterface($publicationDate); } public function getAuthor(): User { return $this->author; } public function getContent(): string { return $this->content; } public function getPublicationDate(): \DateTimeImmutable { return $this->publicationDate; } } |
Previous code was split into four classes and an interface. The interface is helpful for circumstances when an article should be handled regardless its Status
. Moreover we got rid of Status
object and exception object as Status
is incapsulated directly into object types. New classes has narrower public APIs and domain concepts are better defined and communicated to the outside. New developers can easily understand what domain rules are when talking about transitions, no exceptions or nullable values could be raised or returned, narrow services API (you can choose to accept only an article in a specific status, if needed), and so on and so forth. Finally, if a new attribute and/or API method should be added it’s easier not to mess up other classes with senseless concepts.
Of the critical points showed above, only questionable one is the retrieval. If it is true that a WHERE
condition could lead to worse performance, it’s also true that retrieving all articles (in any state) could require up to four queries (unless you use some kind of technique like single table inheritance or similar which can bring out some other kind of issues).
A practical example
As I didn’t want to left the reader with only a “conceptual” solution where these objects weren’t shown “in action”, I’ve decided to develop a nearly real world scenario. Let’s say we would like to export the whole list of articles in different formats. When using a single object it is child’s play as all methods reside in the same class and only one class is involved, but how we can do this when working with four different objects, considering that every class has its own methods and type?
See the code
An interface is defined for articles (not entities one, but a sort of DTO as I would not wanted to bloat domain class with application concepts) to accept a Visitor
, following the Visitor Pattern
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace Application\Exporter\Article\Visitor; use Application\Exporter\Model\Article\ExportableDraftArticle; use Application\Exporter\Model\Article\ExportablePublishedArticle; use Application\Exporter\Model\Article\ExportableReadyForReviewArticle; use Application\Exporter\Model\Article\ExportableReviewedArticle; interface ArticleVisitor { public function visitDraft(ExportableDraftArticle $draftArticle): void; public function visitReadyForReview(ExportableReadyForReviewArticle $readyForReviewArticle): void; public function visitReviewed(ExportableReviewedArticle $reviewedArticle): void; public function visitPublished(ExportablePublishedArticle $publishedArticle): void; public function visitResult(): mixed; } |
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php declare(strict_types=1); namespace Application\Exporter\Article; use Application\Exporter\Article\Visitor\ArticleVisitor; interface ExportableArticle { public function accept(ArticleVisitor $visitor): void; } |
Then two types of visitors are defined: one for array format and the other for json format, both under ArticleVisitor
interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
<?php declare(strict_types=1); namespace Application\Exporter\Article\Visitor; use Application\Exporter\Model\Article\ExportableDraftArticle; use Application\Exporter\Model\Article\ExportablePublishedArticle; use Application\Exporter\Model\Article\ExportableReadyForReviewArticle; use Application\Exporter\Model\Article\ExportableReviewedArticle; use JetBrains\PhpStorm\ArrayShape; class ArticleArrayVisitor implements ArticleVisitor { #[ArrayShape([ 'author' => [ 'username' => 'string', 'email' => 'string', ], '?reviewer' => [ 'username' => 'string', 'email' => 'string', ], 'content' => 'string', '?publicationDate' => \DateTimeImmutable::class, 'status' => 'string' ])] private array $visitResult = []; public function visitDraft(ExportableDraftArticle $draftArticle): void { $this->visitResult = [ 'author' => [ 'username' => $draftArticle->getAuthor()->getUsername(), 'email' => $draftArticle->getAuthor()->getEmail(), ], 'content' => $draftArticle->getContent(), 'status' => $draftArticle->getStatus()->value, ]; } public function visitReadyForReview(ExportableReadyForReviewArticle $readyForReviewArticle): void { $this->visitResult = [ 'author' => [ 'username' => $readyForReviewArticle->getAuthor()->getUsername(), 'email' => $readyForReviewArticle->getAuthor()->getEmail(), ], 'content' => $readyForReviewArticle->getContent(), 'status' => $readyForReviewArticle->getStatus()->value, ]; } public function visitReviewed(ExportableReviewedArticle $reviewedArticle): void { $this->visitResult = [ 'author' => [ 'username' => $reviewedArticle->getAuthor()->getUsername(), 'email' => $reviewedArticle->getAuthor()->getEmail(), ], 'reviewer' => [ 'username' => $reviewedArticle->getReviewer()->getUsername(), 'email' => $reviewedArticle->getReviewer()->getEmail(), ], 'content' => $reviewedArticle->getContent(), 'status' => $reviewedArticle->getStatus()->value, ]; } public function visitPublished(ExportablePublishedArticle $publishedArticle): void { $this->visitResult = [ 'author' => [ 'username' => $publishedArticle->getAuthor()->getUsername(), 'email' => $publishedArticle->getAuthor()->getEmail(), ], 'content' => $publishedArticle->getContent(), 'publicationDate' => $publishedArticle->getPublicationDate(), 'status' => $publishedArticle->getStatus()->value, ]; } public function visitResult(): array { return $this->visitResult; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<?php declare(strict_types=1); namespace Application\Exporter\Article\Visitor; use Application\Exporter\Model\Article\ExportableDraftArticle; use Application\Exporter\Model\Article\ExportablePublishedArticle; use Application\Exporter\Model\Article\ExportableReadyForReviewArticle; use Application\Exporter\Model\Article\ExportableReviewedArticle; class ArticleJsonVisitor implements ArticleVisitor { private string $visitResult; public function __construct(private ArticleArrayVisitor $visitor) { } public function visitDraft(ExportableDraftArticle $draftArticle): void { $this->visitor->visitDraft($draftArticle); $this->visitResult = json_encode($this->visitor->visitResult(), JSON_THROW_ON_ERROR); } public function visitReadyForReview(ExportableReadyForReviewArticle $readyForReviewArticle): void { $this->visitor->visitReadyForReview($readyForReviewArticle); $this->visitResult = json_encode($this->visitor->visitResult(), JSON_THROW_ON_ERROR); } public function visitReviewed(ExportableReviewedArticle $reviewedArticle): void { $this->visitor->visitReviewed($reviewedArticle); $this->visitResult = json_encode($this->visitor->visitResult(), JSON_THROW_ON_ERROR); } public function visitPublished(ExportablePublishedArticle $publishedArticle): void { $this->visitor->visitPublished($publishedArticle); $this->visitResult = json_encode($this->visitor->visitResult(), JSON_THROW_ON_ERROR); } public function visitResult(): string { return $this->visitResult; } } |
ArticleJsonVisitor
is defined as a decorator for ArticleArrayVisitor
.
I would not report ExportableArticle
implementations as you can find whole code in GitHub repo.
Lastly here’s the exporter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?php declare(strict_types=1); namespace Application\Exporter; use Application\Exporter\Article\ExportableArticle; use Application\Exporter\Article\Visitor\Factory\VisitorAbstractFactory; class Exporter { public function export(VisitorAbstractFactory $visitorAbstractFactory, ExportableArticle ...$exportableArticles) { $visitor = $visitorAbstractFactory->createVisitor(); $results = []; foreach ($exportableArticles as $exportableArticle) { $exportableArticle->accept($visitor); $results[] = $visitor->visitResult(); } $aggregator = $visitorAbstractFactory->createAggregator(); return $aggregator->aggregate($results); } } |
where VisitorAbstractFactory
is a factory used to get both visitor and aggregator (find it, again, on GitHub repo) in an Abstract Factory pattern fashion. As you can see we’re using ExportableArticle
that’s the corresponding (more or less) for domain’s ArticleInterface
; just to show how easy is to export all kind of objects as it would have been with a single Article
class.
Lastly, a working example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
<?php declare(strict_types=1); include('vendor/autoload.php'); use Application\Exporter\Article\Visitor\Factory\ArrayAuthorAggregatedVisitorFactory; use Application\Exporter\Article\Visitor\Factory\ArrayVisitorFactory; use Application\Exporter\Article\Visitor\Factory\JsonAuthorAggregatedVisitorFactory; use Application\Exporter\Article\Visitor\Factory\JsonVisitorFactory; use Application\Exporter\Exporter; use Application\Exporter\Model\Article\ExportableDraftArticle; use Application\Exporter\Model\Article\ExportablePublishedArticle; use Application\Exporter\Model\Article\ExportableReadyForReviewArticle; use Application\Exporter\Model\Article\ExportableReviewedArticle; use Domain\Article\DraftArticle; use Domain\Article\PublishedArticle; use Domain\Article\ReadyForReviewArticle; use Domain\Article\ReviewedArticle; use Domain\User\User; $donCallisto = new User('DonCallisto', 'samuele.lilli@gmail.com'); $johnDoe = new User('JohnDoe', 'john.doe@gmail.com'); $otsillacNod = new User('OtsillacNod', 'samuele.lilli@madisoft.it'); $a1 = new ExportableDraftArticle( new DraftArticle($donCallisto, 'foo') ); $a2 = new ExportableReadyForReviewArticle( new ReadyForReviewArticle( new DraftArticle($donCallisto, 'foo') ) ); $a3 = new ExportableReviewedArticle( new ReviewedArticle( new ReadyForReviewArticle( new DraftArticle($donCallisto, 'foo'), ), $otsillacNod ) ); $a4 = new ExportablePublishedArticle( new PublishedArticle( new ReviewedArticle( new ReadyForReviewArticle( new DraftArticle($donCallisto, 'foo'), ), $otsillacNod ), new \DateTime() ) ); $a5 = new ExportableDraftArticle( new DraftArticle($johnDoe, 'foo') ); $a6 = new ExportableReadyForReviewArticle( new ReadyForReviewArticle( new DraftArticle($johnDoe, 'foo') ) ); echo 'Start array exporting...' . PHP_EOL; $exporter = new Exporter(); $result = $exporter->export(new ArrayVisitorFactory(), $a1, $a2, $a3, $a4, $a5, $a6); echo 'Results: ' . PHP_EOL; echo print_r($result, true) . PHP_EOL; echo 'Finish array exporting' . PHP_EOL; echo 'Start json exporting...' . PHP_EOL; $exporter = new Exporter(); $result = $exporter->export(new JsonVisitorFactory(), $a1, $a2, $a3, $a5, $a6); echo 'Results: ' . PHP_EOL; echo print_r($result, true) . PHP_EOL; echo 'Finish array exporting' . PHP_EOL; echo 'Start array (author aggregated) exporting...' . PHP_EOL; $exporter = new Exporter(); $result = $exporter->export(new ArrayAuthorAggregatedVisitorFactory(), $a1, $a2, $a3, $a4, $a5, $a6); echo 'Results: ' . PHP_EOL; echo print_r($result, true) . PHP_EOL; echo 'Finish array exporting' . PHP_EOL; echo 'Start json (author aggregated) exporting...' . PHP_EOL; $exporter = new Exporter(); $result = $exporter->export(new JsonAuthorAggregatedVisitorFactory(), $a1, $a2, $a3, $a5, $a6); echo 'Results: ' . PHP_EOL; echo print_r($result, true) . PHP_EOL; echo 'Finish array exporting' . PHP_EOL; |
Here’s the output
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
Start array exporting... Results: Array ( [0] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [status] => draft ) [1] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [status] => review ) [2] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [reviewer] => Array ( [username] => OtsillacNod [email] => samuele.lilli@madisoft.it ) [content] => foo [status] => reviewed ) [3] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [publicationDate] => DateTimeImmutable Object ( [date] => 2021-11-12 17:52:17.254673 [timezone_type] => 3 [timezone] => UTC ) [status] => published ) [4] => Array ( [author] => Array ( [username] => JohnDoe [email] => john.doe@gmail.com ) [content] => foo [status] => draft ) [5] => Array ( [author] => Array ( [username] => JohnDoe [email] => john.doe@gmail.com ) [content] => foo [status] => review ) ) Finish array exporting Start json exporting... Results: [{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"content":"foo","status":"draft"},{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"content":"foo","status":"review"},{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"reviewer":{"username":"OtsillacNod","email":"samuele.lilli@madisoft.it"},"content":"foo","status":"reviewed"},{"author":{"username":"JohnDoe","email":"john.doe@gmail.com"},"content":"foo","status":"draft"},{"author":{"username":"JohnDoe","email":"john.doe@gmail.com"},"content":"foo","status":"review"}] Finish json exporting Start array (author aggregated) exporting... Results: Array ( [DonCallisto] => Array ( [articles] => Array ( [0] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [status] => draft ) [1] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [status] => review ) [2] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [reviewer] => Array ( [username] => OtsillacNod [email] => samuele.lilli@madisoft.it ) [content] => foo [status] => reviewed ) [3] => Array ( [author] => Array ( [username] => DonCallisto [email] => samuele.lilli@gmail.com ) [content] => foo [publicationDate] => DateTimeImmutable Object ( [date] => 2021-11-12 17:52:17.254673 [timezone_type] => 3 [timezone] => UTC ) [status] => published ) ) ) [JohnDoe] => Array ( [articles] => Array ( [0] => Array ( [author] => Array ( [username] => JohnDoe [email] => john.doe@gmail.com ) [content] => foo [status] => draft ) [1] => Array ( [author] => Array ( [username] => JohnDoe [email] => john.doe@gmail.com ) [content] => foo [status] => review ) ) ) ) Finish array exporting Start json (author aggregated) exporting... Results: {"DonCallisto":{"articles":[{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"content":"foo","status":"draft"},{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"content":"foo","status":"review"},{"author":{"username":"DonCallisto","email":"samuele.lilli@gmail.com"},"reviewer":{"username":"OtsillacNod","email":"samuele.lilli@madisoft.it"},"content":"foo","status":"reviewed"}]},"JohnDoe":{"articles":[{"author":{"username":"JohnDoe","email":"john.doe@gmail.com"},"content":"foo","status":"draft"},{"author":{"username":"JohnDoe","email":"john.doe@gmail.com"},"content":"foo","status":"review"}]}} Finish json exporting |
Looking at this example is easy to understand that splitting Article
into separated and more focused classes does not lead to harder handling. Of course there are more classes but that’s the price for flexibility and pattern usage: the more you’re abstract, flexible and have “quick to change” code, the more classes and abstraction you have in your codebase.
What’s next?
Following this solution could force a change int the way you code (and rightfully so, sometimes). One of the possible scenarios left out is about persistence. When there’s only an Article
class persistence is just straightforward, but as soon as you model domain this way some decisions must be taken. For instance, how to handle the ids? As a transition from one state to the other happens to create a new entity, should the new entity take the old id or should it have a brand new one? If old id is used, who have to take care of persistence order (as id must be unique)? If new id is used, how to manage something like permalink or caching strategy or any kind of comparable issues? I would not provide specific answers as you might apply different choices based on different situations but, to give some pointers, if old ids are chosen, so if the id will be assigned from starting entity to new one, a solution could be to wrap persistence operations in a transaction, perform a delete first then add new entity. If new ids are chosen is it possible to use “surrogate identity” (or something similar) and provide to the outside world an immutable id. The key concept here is don’t let the application or infrastructure fool you when modeling your domain: if you choose not to split things because of persistence tools like ORMs you can fall into worst issues. Moreover data is your ultimate source of truth as application and code may vary along the time but data must adhere to business/domain rules and having a unique Article
entity could lead to bugs and consequentially corrupted data.
Conclusions
Status flags, sometimes, are a smell for a unique class which incapsulates much more than a single concept. When internal data is meaningful for a subset or worst for a single status you’ll obtain a not so cohesive class. This kind of situation calls for a split. Also when clients behave differently depending on flags this is true. You’ve seen a split in action and what it entails. You’re now also aware that more “uncommon” decision must be taken when dealing with splitted code (when not operating into RAD logics that’s pretty much always true) and more abstract code could be part of the codebase. Be also aware that persistence have to be managed “by hand”. However what you get in return is priceless: more expressive and flexible code, no null checks due to “polymorphic” nature of the class, no fuzzy exceptions, less buggy code and data.
What do you think about this solution? Is it something you’ve already adopted? Will you try to embrace it in your next or current project? Let us know and happy coding!