-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Protecting invariants when creating value objects through Entity or Factory #34
Comments
The remaining classes and interfaces: interface DormerTypeInterface
{
public function canHaveGutterInstalled(): bool;
}
abstract class DormerType implements DormerTypeInterface
{}
class FlatRoofDormer extends DormerType
{
public function canHaveGutterInstalled(): bool
{
return true;
}
} |
I would go with Version 1, but define the public function testItShouldAllowToAddGutter()
{
$dormer = Dormer::withType(new AlwaysAllowAddingGutter());// canHaveGutterInstalled() always return true
$dormer->addGutter($this->createMock(Gutter::class));
....
}
public function testItDoNotAllowToAddGutter()
{
$dormer = Dormer::withType(new NeverAllowAddingGutter()); // canHaveGutterInstalled() always return false
$this->expectException(DormerTypeCannotHaveGutterInstalledException::class);
$dormer->addGutter($this->createMock(Gutter::class));
....
} Version 2: You could also use it, but instead of defining the Note: if you want the final class Dormer
{
private $type;
private $gutters = [];
private function __construct(DormerType $type, Gutter ...$gutters) {
$this->type = $type;
$this->gutters = \array_map(
function (Gutter $gutter) {
$this->addGutter($gutter); // this makes sure the check to add gutter is respected
},
$gutters
);
}
public function addGutter(Gutter $gutter): Dormer
{
if (!$this->type->canHaveGutterInstalled()) {
throw new DormerTypeCannotHaveGutterInstalledException();
}
return new self($this->dormerType, \array_merge([$gutter], $this->gutters));
}
public static function withType(DormerType $type): self {
return new self($type);
}
} |
As a side note: It feels like the With my previous comment, the factory would become a handler: final class CalculateDormerHandler
{
private $dormerTypeRepository;
public function __invoke(CalculateDormerCommand $dto)
{
$dormerType = $this->dormerTypeRepository->ofId(DormerTypeId::fromInteger($dto->dormerTypeId));
$dormer = Dormer::withType($dormerType);
$dormer->addGutter(Gutter::fromDto($dto->gutter));
// do we save the type? what state di we changed?
}
} |
Thanks again @yvoyer for this comprehensive feedback. Indeed my factory currently holds more complex logic. It bloated the original Application Handler. That's why I moved it to a "Factory". One thing about creating the "Dormer" VO through the "DormerType" Entity: |
The problem if you pass only the id, is that you lose the option to check whether a gutter can be added, since the As I said, you are not passing an entity to the value object, but an interface If you fear that passing the entity would be bad, create an adapter that wraps the entity while implementing the Having a better view at the transaction's process would be easier to help you. |
I'm fine with passing the DormerType to Dormer. i just wanted to ensure that it is ok to pass it but then let the newly created Object only keep the DormerTypeId instead of the complete DormerType. Since the Dormer Value Object will be persisted inside the DormerCalculation Aggregate Root. |
But you will probably need the type to be stored in the // aggregate
public function calculateDormer(Dormer $dormer): void
{
$this->dormer = $dormer;
$this->dormerType = $dormer->getType(); // DormerTypeId
} Does it make sense? |
Absolutely. BTW I already used an interface but honestly did not recognize how easy it will make mocking when testing until you pointed it out. ;) Maybe @matthiasnoback and @Ocramius also have a little feedback on this topic? :) |
The reason I originally liked the Version 2 was an example of the red book by @VaughnVernon: public Discussion startDiscussionFor(
ForumIdentityService aForumIdentityService,
Author anAuthor,
String aSubject,
String anExclusiveOwner) {
if (this.isClosed()) {
throw new IllegalStateException("Forum is closed.");
}
Discussion discussion =
new Discussion(
this.tenant(),
this.forumId(),
aForumIdentityService.nextDiscussionId(),
anAuthor,
aSubject,
anExclusiveOwner);
return discussion;
} Not being able to create a Discussion for a closed Forum is similar to my use case where you can not at a Gutter to DormerType that does not support it. See Version 2. But any developer could still skip the Forum Entity invariant protection and simply create the Discussion Entity in order to persist it: Discussion discussion =
new Discussion(
aTenant,
aForumId,
aForumIdentityService.nextDiscussionId(),
anAuthor,
aSubject,
anExclusiveOwner); Or am I missing something? I guess my main concern form a developer team point-of-view is:
? |
My original post and poll started here:
https://twitter.com/webdevilopers/status/1100102866583339008
This was the original gist:
Use case:
I need to calculate a
Dormer
based on specificDormerType
(Entity).Business rule: Some
DormerType
s can not have aGutter
attached.Dormer
in the end will just be a value object stored inside anDormerCalculation
Entity. / Aggregate Root that could throw theDormerCalculated
Domain Event.This means that the
DormerTypeId
should only be a reference.I currently think of three approaches.
Version 1:
The full Entity is passed to the value object. This feels ugly. Allthough it is now impossible for a developer to create an inconsistent object. E.g. by attaching a gutter to a dormer with a type that does not allow gutters.
Version 2:
Again the business rule is ensured. The value object is created through the dormer type Entity which sounds logical. But in the end it is not an Aggregate Root that e.g. could throw a "DormerCalculated" event.
In addition the factory method
calculateDormer
and the factory method on the dormer value object would look too identical.Version 3:
All creation logic is inside the factory. It protects the invariants and creates the Dormer only with the
DormerTypeId
.But a developer could create an inconsistend value object by skipping the factory. Ouch!?
After reading a post on @culttt b @philipbrown version 3 still feels like the best way though.
What do you guys think?
The text was updated successfully, but these errors were encountered: