Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like CompletionHandler 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 CompletionHandler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
16 | class CompletionHandler |
||
17 | { |
||
18 | /** |
||
19 | * Application to complete for |
||
20 | * |
||
21 | * @var \Symfony\Component\Console\Application |
||
22 | */ |
||
23 | protected $application; |
||
24 | |||
25 | /** |
||
26 | * @var Command |
||
27 | */ |
||
28 | protected $command; |
||
29 | |||
30 | /** |
||
31 | * @var CompletionContext |
||
32 | */ |
||
33 | protected $context; |
||
34 | |||
35 | /** |
||
36 | * Array of completion helpers. |
||
37 | * |
||
38 | * @var CompletionInterface[] |
||
39 | */ |
||
40 | protected $helpers = array(); |
||
41 | |||
42 | /** |
||
43 | * Index the command name was detected at |
||
44 | * |
||
45 | * @var int |
||
46 | */ |
||
47 | private $commandWordIndex; |
||
48 | |||
49 | public function __construct(Application $application, CompletionContext $context = null) |
||
73 | |||
74 | public function setContext(CompletionContext $context) |
||
78 | |||
79 | /** |
||
80 | * @return CompletionContext |
||
81 | */ |
||
82 | public function getContext() |
||
86 | |||
87 | /** |
||
88 | * @param CompletionInterface[] $array |
||
89 | */ |
||
90 | public function addHandlers(array $array) |
||
94 | |||
95 | /** |
||
96 | * @param CompletionInterface $helper |
||
97 | */ |
||
98 | public function addHandler(CompletionInterface $helper) |
||
102 | |||
103 | /** |
||
104 | * Do the actual completion, returning an array of strings to provide to the parent shell's completion system |
||
105 | * |
||
106 | * @return CompletionResultInterface |
||
107 | * @throws \RuntimeException |
||
108 | */ |
||
109 | public function runCompletion() |
||
140 | |||
141 | /** |
||
142 | * Attempt to complete the current word as a long-form option (--my-option) |
||
143 | * |
||
144 | * @return array|false |
||
145 | */ |
||
146 | protected function completeForOptions() |
||
162 | |||
163 | /** |
||
164 | * Attempt to complete the current word as an option shortcut. |
||
165 | * |
||
166 | * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion. |
||
167 | * |
||
168 | * @return array|false |
||
169 | */ |
||
170 | protected function completeForOptionShortcuts() |
||
184 | |||
185 | /** |
||
186 | * Attempt to complete the current word as the value of an option shortcut |
||
187 | * |
||
188 | * @return array|false |
||
189 | */ |
||
190 | View Code Duplication | protected function completeForOptionShortcutValues() |
|
215 | |||
216 | /** |
||
217 | * Attemp to complete the current word as the value of a long-form option |
||
218 | * |
||
219 | * @return array|false |
||
220 | */ |
||
221 | View Code Duplication | protected function completeForOptionValues() |
|
245 | |||
246 | /** |
||
247 | * Attempt to complete the current word as a command name |
||
248 | * |
||
249 | * @return \Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionResultInterface|false |
||
250 | */ |
||
251 | protected function completeForCommandName() |
||
259 | |||
260 | /** |
||
261 | * Attempt to complete the current word as a command argument value |
||
262 | * |
||
263 | * @return CompletionResultInterface|array|false |
||
264 | * @see Symfony\Component\Console\Input\InputArgument |
||
265 | */ |
||
266 | protected function completeForCommandArguments() |
||
294 | |||
295 | /** |
||
296 | * Find a CompletionInterface that matches the current command, target name, and target type |
||
297 | * |
||
298 | * @param string $name |
||
299 | * @param string $type |
||
300 | * @return CompletionInterface|null |
||
301 | */ |
||
302 | protected function getCompletionHelper($name, $type) |
||
318 | |||
319 | /** |
||
320 | * Complete the value for the given option if a value completion is availble |
||
321 | * |
||
322 | * @param InputOption $option |
||
323 | * @return array|false |
||
324 | */ |
||
325 | protected function completeOption(InputOption $option) |
||
337 | |||
338 | /** |
||
339 | * Step through the command line to determine which word positions represent which argument values |
||
340 | * |
||
341 | * The word indexes of argument values are found by eliminating words that are known to not be arguments (options, |
||
342 | * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument |
||
343 | * value, |
||
344 | * |
||
345 | * @param InputArgument[] $argumentDefinitions |
||
346 | * @return array as [argument name => word index on command line] |
||
347 | */ |
||
348 | protected function mapArgumentsToWords($argumentDefinitions) |
||
380 | |||
381 | /** |
||
382 | * Build a list of option words/flags that will have a value after them |
||
383 | * Options are returned in the format they appear as on the command line. |
||
384 | * |
||
385 | * @return string[] - eg. ['--myoption', '-m', ... ] |
||
386 | */ |
||
387 | protected function getOptionWordsWithValues() |
||
403 | |||
404 | /** |
||
405 | * Filter out results that don't match the current word on the command line |
||
406 | * |
||
407 | * @param CompletionResultInterface $result |
||
408 | * @return CompletionResultInterface |
||
409 | */ |
||
410 | protected function filterResult($result) |
||
423 | |||
424 | /** |
||
425 | * Get the combined options of the application and entered command |
||
426 | * |
||
427 | * @return InputOption[] |
||
428 | */ |
||
429 | protected function getAllOptions() |
||
440 | |||
441 | /** |
||
442 | * Get command names available for completion |
||
443 | * |
||
444 | * Filters out hidden commands where supported. |
||
445 | * |
||
446 | * @return string[] |
||
447 | */ |
||
448 | protected function getCommands() |
||
473 | |||
474 | /** |
||
475 | * Find the current command name in the command-line |
||
476 | * |
||
477 | * Note this only cares about flag-type options. Options with values cannot |
||
478 | * appear before a command name in Symfony Console application. |
||
479 | * |
||
480 | * @return Command|null |
||
481 | */ |
||
482 | private function detectCommand() |
||
513 | } |
||
514 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.