Complex classes like State often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use State, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
44 | class State { |
||
45 | |||
46 | /** |
||
47 | * state name if it is unknown (not configured) |
||
48 | * @var string |
||
49 | */ |
||
50 | const STATE_UNKNOWN = 'unknown'; |
||
51 | |||
52 | /** |
||
53 | * default name for the first/only initial state (but you can specify whatever you want for your initial state) |
||
54 | * @var string |
||
55 | */ |
||
56 | const STATE_NEW = 'new'; |
||
57 | |||
58 | /** |
||
59 | * default name for a normal final state |
||
60 | * @var string |
||
61 | */ |
||
62 | const STATE_DONE = 'done'; |
||
63 | |||
64 | /** |
||
65 | * default exit/entry command |
||
66 | * @var string |
||
67 | */ |
||
68 | const COMMAND_NULL = '\izzum\command\NullCommand'; |
||
69 | |||
70 | /** |
||
71 | * default exit/entry command for constructor |
||
72 | * @var string |
||
73 | */ |
||
74 | const COMMAND_EMPTY = ''; |
||
75 | const CALLABLE_NULL = null; |
||
76 | const REGEX_PREFIX = 'regex:'; |
||
77 | const REGEX_PREFIX_NEGATED = 'not-regex:'; |
||
78 | |||
79 | const CALLABLE_ENTRY = 'state entry'; |
||
80 | const CALLABLE_EXIT = 'state exit'; |
||
81 | |||
82 | /** |
||
83 | * the state types: |
||
84 | * - 'initial': a statemachine has exactly 1 initial type, this is always the only |
||
85 | * entrance into the statemachine. |
||
86 | * - 'normal': a statemachine can have 0-n normal types. |
||
87 | * - 'done': a statemachine should have at least 1 final type where it has no |
||
88 | * further transitions. |
||
89 | * - 'regex': a statemachine configuration could have regex states, which serve a purpose to create transitions |
||
90 | * from or to multiple other states |
||
91 | * |
||
92 | * @var string |
||
93 | */ |
||
94 | const TYPE_INITIAL = 'initial', TYPE_NORMAL = 'normal', TYPE_FINAL = 'final', TYPE_REGEX = 'regex'; |
||
95 | |||
96 | /** |
||
97 | * The state type: |
||
98 | * - State::TYPE_INITIAL |
||
99 | * - State::TYPE_NORMAL |
||
100 | * - State::TYPE_FINAL |
||
101 | * - State::TYPE_REGEX |
||
102 | * @var string |
||
103 | */ |
||
104 | protected $type; |
||
105 | |||
106 | /** |
||
107 | * an array of transitions that are outgoing for this state. |
||
108 | * These will be set by Transition objects (they provide the association) |
||
109 | * |
||
110 | * this is not a hashmap, so the order of Transitions *might* be important. |
||
111 | * whenever a State is asked for it's transitions, the first transition |
||
112 | * might |
||
113 | * be tried first. this might have performance and configuration benefits |
||
114 | * |
||
115 | * @var Transition[] |
||
116 | */ |
||
117 | protected $transitions; |
||
118 | |||
119 | /** |
||
120 | * The name of the state |
||
121 | * |
||
122 | * @var string |
||
123 | */ |
||
124 | protected $name; |
||
125 | |||
126 | /** |
||
127 | * fully qualified command name for the command to be executed |
||
128 | * when entering a state as part of a transition. |
||
129 | * This can actually be a ',' seperated string of multiple commands that |
||
130 | * will be executed as a composite. |
||
131 | * |
||
132 | * @var string |
||
133 | */ |
||
134 | protected $command_entry_name; |
||
135 | |||
136 | /** |
||
137 | * fully qualified command name for the command to be executed |
||
138 | * when exiting a state as part of a transition. |
||
139 | * This can actually be a ',' seperated string of multiple commands that |
||
140 | * will be executed as a composite. |
||
141 | * |
||
142 | * @var string |
||
143 | */ |
||
144 | protected $command_exit_name; |
||
145 | |||
146 | /** |
||
147 | * the entry callable method |
||
148 | * @var callable |
||
149 | */ |
||
150 | protected $callable_entry; |
||
151 | |||
152 | /** |
||
153 | * the exit callable method |
||
154 | * @var callable |
||
155 | */ |
||
156 | protected $callable_exit; |
||
157 | |||
158 | /** |
||
159 | * a description for the state |
||
160 | * |
||
161 | * @var string |
||
162 | */ |
||
163 | protected $description; |
||
164 | |||
165 | /** |
||
166 | * |
||
167 | * @param string $name |
||
168 | * the name of the state (can also be a regex in format: [not-]regex:/<regex-specification-here>/) |
||
169 | * @param string $type |
||
170 | * the type of the state (on of self::TYPE_<*>) |
||
171 | * @param $command_entry_name optional: |
||
172 | * a command to be executed when a transition enters this state |
||
173 | * One or more fully qualified command (sub)class name(s) to |
||
174 | * execute when entering this state. |
||
175 | * This can actually be a ',' seperated string of multiple |
||
176 | * commands that will be executed as a composite. |
||
177 | * @param $command_exit_name optional: |
||
178 | * a command to be executed when a transition leaves this state |
||
179 | * One or more fully qualified command (sub)class name(s) to |
||
180 | * execute when exiting this state. |
||
181 | * This can actually be a ',' seperated string of multiple |
||
182 | * commands that will be executed as a composite. |
||
183 | * @param callable $callable_entry |
||
184 | * optional: a php callable to call. eg: "function(){echo 'closure called';};" |
||
185 | * @param callable $callable_exit |
||
186 | * optional: a php callable to call. eg: "izzum\MyClass::myStaticMethod" |
||
187 | */ |
||
188 | 79 | public function __construct($name, $type = self::TYPE_NORMAL, $command_entry_name = self::COMMAND_EMPTY, $command_exit_name = self::COMMAND_EMPTY, $callable_entry = self::CALLABLE_NULL, $callable_exit = self::CALLABLE_NULL) |
|
198 | |||
199 | /** |
||
200 | * get the entry callable, the callable to be called when entering this state |
||
201 | * @return callable |
||
202 | */ |
||
203 | 25 | public function getEntryCallable() |
|
207 | |||
208 | /** |
||
209 | * set the entry callable, the callable to be called when entering this state |
||
210 | * @param callable $callable |
||
211 | */ |
||
212 | 79 | public function setEntryCallable($callable) |
|
217 | |||
218 | /** |
||
219 | * get the exit callable, the callable to be called when exiting this state |
||
220 | * @return callable |
||
221 | */ |
||
222 | 25 | public function getExitCallable() |
|
227 | |||
228 | /** |
||
229 | * set the exit callable, the callable to be called when exiting this state |
||
230 | * @param callable $callable |
||
231 | */ |
||
232 | 79 | public function setExitCallable($callable) |
|
237 | |||
238 | /** |
||
239 | * is it an initial state |
||
240 | * |
||
241 | * @return boolean |
||
242 | */ |
||
243 | 25 | public function isInitial() |
|
247 | |||
248 | /** |
||
249 | * is it a normal state |
||
250 | * |
||
251 | * @return boolean |
||
252 | */ |
||
253 | 5 | public function isNormal() |
|
257 | |||
258 | /** |
||
259 | * is it a final state |
||
260 | * |
||
261 | * @return boolean |
||
262 | */ |
||
263 | 68 | public function isFinal() |
|
267 | |||
268 | /** |
||
269 | * is this state a regex type of state? |
||
270 | * formats: |
||
271 | * "regex:<regular-expression-here>" |
||
272 | * "not-regex:<regular-expression-here>" |
||
273 | * |
||
274 | * @return boolean |
||
275 | * @link https://php.net/manual/en/function.preg-match.php |
||
276 | * @link http://regexr.com/ for trying out regular expressions |
||
277 | */ |
||
278 | 79 | public function isRegex() |
|
283 | |||
284 | /** |
||
285 | * is this state a normal regex type of state? |
||
286 | * "regex:<regular-expression-here>" |
||
287 | * |
||
288 | * @return boolean |
||
289 | */ |
||
290 | 79 | public function isNormalRegex() |
|
294 | |||
295 | /** |
||
296 | * is this state a negated regex type of state? |
||
297 | * "not-regex:<regular-expression-here>" |
||
298 | * |
||
299 | * @return boolean |
||
300 | */ |
||
301 | 79 | public function isNegatedRegex() |
|
305 | |||
306 | /** |
||
307 | * get the state type |
||
308 | * |
||
309 | * @return string |
||
310 | */ |
||
311 | 2 | public function getType() |
|
317 | |||
318 | /** |
||
319 | * set the state type |
||
320 | * |
||
321 | * @param string $type |
||
322 | */ |
||
323 | 79 | protected function setType($type) |
|
333 | |||
334 | /** |
||
335 | * add an outgoing transition from this state. |
||
336 | * |
||
337 | * TRICKY: this method should be package visibility only, |
||
338 | * so don't use directly. it is used to set the bidirectional association |
||
339 | * for State and Transition from a Transition instance on the state the transition will be allowed to |
||
340 | * run from ('state from'). |
||
341 | * |
||
342 | * @param Transition $transition |
||
343 | * @return boolan yes in case the transition was not on the State already or in case of an invalid transition |
||
344 | */ |
||
345 | 67 | public function addTransition(Transition $transition) |
|
359 | |||
360 | /** |
||
361 | * get all outgoing transitions |
||
362 | * |
||
363 | * @return Transition[] an array of transitions |
||
364 | */ |
||
365 | 20 | public function getTransitions() |
|
370 | |||
371 | /** |
||
372 | * gets the name of this state |
||
373 | */ |
||
374 | 79 | public function getName() |
|
378 | |||
379 | /** |
||
380 | * sets the name of this state |
||
381 | * @param string $name |
||
382 | */ |
||
383 | 79 | protected function setName($name) |
|
388 | |||
389 | /** |
||
390 | * |
||
391 | * @return string |
||
392 | */ |
||
393 | 79 | public function __toString() |
|
397 | |||
398 | /** |
||
399 | * Do we have a transition from this state with a certain name? |
||
400 | * |
||
401 | * @param string $transition_name |
||
402 | * @return boolean |
||
403 | */ |
||
404 | 67 | public function hasTransition($transition_name) |
|
415 | |||
416 | /** |
||
417 | * An action executed every time a state is entered. |
||
418 | * An entry action will not be executed for an 'initial' state. |
||
419 | * |
||
420 | * @param Context $context |
||
421 | * @throws Exception |
||
422 | */ |
||
423 | 22 | public function entryAction(Context $context) |
|
429 | |||
430 | /** |
||
431 | * calls a $callable if it exists, with the arguments $context->getEntity() |
||
432 | * @param callable $callable |
||
433 | * @param Context $context |
||
434 | * @param string $type the type of callable (self::CALLABLE_ENTRY | self::CALLABLE_EXIT) |
||
435 | */ |
||
436 | 22 | protected function callCallable($callable, Context $context, $type = 'n/a') |
|
443 | |||
444 | /** |
||
445 | * An action executed every time a state is exited. |
||
446 | * An exit action will not be executed for a 'final' state since a machine |
||
447 | * will not leave a 'final' state. |
||
448 | * |
||
449 | * @param Context $context |
||
450 | * @throws Exception |
||
451 | */ |
||
452 | 22 | public function exitAction(Context $context) |
|
458 | |||
459 | /** |
||
460 | * helper method |
||
461 | * |
||
462 | * @param ICommand $command |
||
463 | * @throws Exception |
||
464 | */ |
||
465 | 22 | protected function execute(ICommand $command) |
|
475 | |||
476 | /** |
||
477 | * returns the associated Command for the entry/exit action. |
||
478 | * the Command will be configured with the domain model via dependency injection |
||
479 | * |
||
480 | * @param string $command_name |
||
481 | * entry or exit command name |
||
482 | * @param Context $context |
||
483 | * @return ICommand |
||
484 | * @throws Exception |
||
485 | */ |
||
486 | 22 | protected function getCommand($command_name, Context $context) |
|
490 | |||
491 | /** |
||
492 | * get the transition for this state that can be triggered by an event code. |
||
493 | * |
||
494 | * @param string $event |
||
495 | * the event code that can trigger a transition (mealy machine) |
||
496 | * @return Transition[] |
||
497 | */ |
||
498 | 11 | public function getTransitionsTriggeredByEvent($event) |
|
508 | |||
509 | /** |
||
510 | * get the fully qualified command name for entry of the state |
||
511 | * |
||
512 | * @return string |
||
513 | */ |
||
514 | 24 | public function getEntryCommandName() |
|
518 | |||
519 | /** |
||
520 | * get the fully qualified command name for exit of the state |
||
521 | * |
||
522 | * @return string |
||
523 | */ |
||
524 | 24 | public function getExitCommandName() |
|
528 | |||
529 | /** |
||
530 | * set the exit command name |
||
531 | * @param string $name a fully qualified command name |
||
532 | */ |
||
533 | 79 | public function setExitCommandName($name) |
|
538 | |||
539 | /** |
||
540 | * set the entry command name |
||
541 | * @param string $name a fully qualified command name |
||
542 | */ |
||
543 | 79 | public function setEntryCommandName($name) |
|
548 | |||
549 | /** |
||
550 | * set the description of the state (for uml generation for example) |
||
551 | * |
||
552 | * @param string $description |
||
553 | */ |
||
554 | 15 | public function setDescription($description) |
|
559 | |||
560 | /** |
||
561 | * get the description for this state (if any) |
||
562 | * |
||
563 | * @return string |
||
564 | */ |
||
565 | 3 | public function getDescription() |
|
569 | } |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.