Skip to content
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

[Workflow] Added Definition builder #20451

Closed
wants to merge 14 commits into from

Conversation

Nyholm
Copy link
Member

@Nyholm Nyholm commented Nov 8, 2016

Q A
Branch? "master"
Bug fix? no
New feature? yes
BC breaks? yes (in un-released branch)
Deprecations? no
Tests pass? yes
Fixed tickets Related to #20441
License MIT
Doc PR n/a

Make the Definition immutable and add a DefinitionBuilder.

I basically broke up the Definition class into an immutable class and one builder.

*/
public function __construct(array $places = array(), array $transitions = array())
public function __construct(array $places = array(), array $transitions = array(), $initialPlace)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that the default values for $places and $transitions can be removed, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are correct

*/
class Definition
final class Definition
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we okey with making this final?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please next time don't ever do this without providing abstraction that final class implements.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because then there is neither a way how to switch nor extend implementation.

https://ocramius.github.io/blog/when-to-declare-classes-final/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an answer. Sometimes you don't need to "switch" or extend implementation.

Copy link
Contributor

@ostrolucky ostrolucky Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no such need, it means there is no need to make it final.

Edit: To not seem snarky: Marking the class as final is used to force user of the class to use composition over inheritance. If you don't provide abstraction (interface), he can't do that. If it's obvious there is never a need to switch/extend the implementation, keyword final doesn't change the thing. But if it turns out you are wrong and there is single case in world where it makes sense, poor sob who needs it can't do it and needs to wait for upstream to change this, which can take a while.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A: Hey, guys, this class is self-sufficient and we don't see sense in other implementations. Also, we want to encourage to use composition over inheritance. Let's mark it final.
B: Don't use final.
A: Why?
B: Because it makes impossible to extend this class nor replace implementation.
A: ... That's the point. You shouldn't do it.

Our dialog looks like above as long as you don't provide real use case when you need to inherit this class. It's perfectly fine to have the only implementation sometimes, especially for VOs.

Because then there is neither a way how to switch nor extend implementation.

It's like saying that poison shouldn't be marked with "don't eat it" label, because you want to eat it. Well, that's not the purpose of the object. Don't eat extend it.

Copy link
Contributor

@unkind unkind Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's obvious there is never a need to switch/extend the implementation, keyword final doesn't change the thing

If someone needs to replace/extend implementation, it doesn't mean he is right and extending is correct. He should find better ways to solve his issue. You are as a vendor know how to use your tool and you are allowed to introduce some impediments to avoid incorrect usage of your product.

@Nyholm
Copy link
Member Author

Nyholm commented Nov 8, 2016

Im done with this implementation now.

@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

LGTM

@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

Actually, I have one question: I don't really like the idea to define the Definition as a service. What about adding Workflow::getDefinition() and to remove it from the DIC?

@Nyholm
Copy link
Member Author

Nyholm commented Nov 9, 2016

The definition is not a service... or not a public service. (https://github.com/symfony/symfony/pull/20451/files#diff-0e793081ceb720201745c982a568903fR426)

I agree with your arguments before saying that the definition is an value object.

What about adding Workflow::getDefinition() and to remove it from the DIC?

I agree. I did not add it here because I thought that would be a separate PR. But I add it now.

*/
public function build()
{
return new Definition($this->places, $this->transitions, $this->initialPlace);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about reseting $this->places, $this->transitions, $this->initialPlace in order to be able to re-use the Builder to build another Definition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not sure about that. One might want to have definition A and B where A is a subset of B.
If we automatically reset the builder one have to add places and transaction twice.

How do you feel about adding DefinitionBuilder::reset()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok for reset ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. I've updated the PR.

Should I change my doc PR to use the builder?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if you put it in a new PR. The current doc PR is already pretty big and I would like to merge it as quick as possible.

throw new InvalidArgumentException(sprintf('The place "%s" contains invalid characters.', $place));
}

if (!count($this->places)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is always array this can be just:

if (!$this->places) {}

@@ -59,30 +59,30 @@ public function provideWorkflowDefinitionWithoutMarking()

public function createprovideComplexWorkflowDefinition()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provideComplexWorkflowDefinition

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I've updated the PR to comply with your suggestions

@@ -29,18 +30,28 @@ class Definition
*
* @param string[] $places
* @param Transition[] $transitions
* @param string $initialPlace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string|null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you

@unkind
Copy link
Contributor

unkind commented Nov 9, 2016

From design point of view, the idea of DI-service DefinitionBuilder is dangerous.

If you really want to have a builder, why not just instantiate new builder every time you want to build Definition:

$builder = new DefinitionBuilder();
$definition = $builder->...->build();

This logic is static in fact (that's why it perfectly can be moved to the Definition class itself).

I call this idea dangerous, because it makes mutable service.
Just imagine case, when you need to make very complex building like that:

$this->builder->reset();
$this->builder->addState('foo')->addState($this->stateProvider->getBar())->build();

What if stateProvider uses for some reason builder inside and resets it? It introduces a bug.
Moreover, user can simply forget to call reset somewhere (but he has to do it every time).

After all, what's the benefit of this complexity?

@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

Hello @unkind

I'm not sure to understand your comment, and what you propose.

After all, what's the benefit of this complexity?

Which complexity are you talking about?

@Nyholm
Copy link
Member Author

Nyholm commented Nov 9, 2016

The builder is not a public service so you can not use it like in your examples.

@unkind
Copy link
Contributor

unkind commented Nov 9, 2016

The builder is not a public service so you can not use it like in your examples.

Ah, you mean it is inlined? My bad.

Which complexity are you talking about?

I'm talking about introducing new entity rather than moving this logic to the Definition class itself:

$definition = Definition::withInitialState(..., 'bar');
$definition = $definition->withNewState('foo');

@stof
Copy link
Member

stof commented Nov 9, 2016

@unkind using methods returning a new mutated instance rather than mutating the existing one makes things harder for the DIC registration, as we still need to have a way to build the whole object without having to replace it by a different object in the meantime (unless we want to build a chain of private services being the factory for the next one, which becomes a nightmare)

@unkind
Copy link
Contributor

unkind commented Nov 9, 2016

as we still need to have a way to build the whole object without having to replace it by a different object in the meantime

You still can have a factory just for DIC:

public function createDefinition()
{
    $definition = ...;

    foreach ($this->transitions as ...) {
        $definition = ...;    
    }    

    return $definition;
}

@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

@unkind I really prefer the build way here. And as @stof said, it's much easier with the DIC.

@Nyholm Sorry, but I just merge #20460 so you have to rebase and fix WorkflowDumpCommand.

@Nyholm
Copy link
Member Author

Nyholm commented Nov 9, 2016

I missed that PR. Good!

I've rebased my PR now.

Copy link
Member

@lyrixx lyrixx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one Little things to remove, another one to try to remove and then It will be good.

foreach ($input->getArgument('marking') as $place) {
$marking->mark($place);
}

$output->writeln($dumper->dump($definition, $marking));
$output->writeln($dumper->dump($workflow->getDefinition(), $marking));
}

private function getProperty($object, $property)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot to remove this method.

$container->setDefinition($workflowId, $workflowDefinition);
$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to store it with an id, right? (ie, juste remove just line, everything should be OK)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this line or else we can grab it from the container in the compiler pass.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I Though the service was in-lined later ;)

@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

👍

@fabpot
Copy link
Member

fabpot commented Nov 9, 2016

👍

@javiereguiluz javiereguiluz added this to the 3.2 milestone Nov 9, 2016
@lyrixx
Copy link
Member

lyrixx commented Nov 9, 2016

Thanks Tobias for working on this feature, this is much appreciated.

@lyrixx lyrixx closed this in 61b1334 Nov 9, 2016
@Nyholm Nyholm deleted the definition-builder branch November 9, 2016 15:21
@Nyholm
Copy link
Member Author

Nyholm commented Nov 9, 2016

Every time I submit a PR to Symfony I get great response no matter if my PR is good or not.

Thank you!

$reflectionProperty->setAccessible(true);

return $reflectionProperty->getValue($object);
$output->writeln($dumper->dump($workflow->getDefinition(), $marking));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice :)

$definitionDefinition = new Definition(Workflow\Definition::class);
$definitionDefinition->addMethodCall('addPlaces', array($workflow['places']));
// Create a DefinitionBuilder
$definitionBuilderDefinition = new Definition(Workflow\DefinitionBuilder::class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we make use of the DefinitionBuilder in the container extension? While I agree that the builder is useful in userland code, I do not see the added value here. On the downside, it just adds another level of indirection that is not really needed, is it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no real reason. I can try to remove it.

lyrixx added a commit that referenced this pull request Nov 10, 2016
This PR was squashed before being merged into the 3.2-dev branch (closes #20478).

Discussion
----------

[Workflow] Removed definition builder

| Q             | A
| ------------- | ---
| Branch?       | "master"
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

As of @xabbuh comment in #20451 (comment)

We do not really need the definition builder here.

Commits
-------

caa3d6f [Workflow] Removed definition builder
@fabpot fabpot mentioned this pull request Nov 17, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.