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 IncrementalCommand 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 IncrementalCommand, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
19 | class IncrementalCommand extends AbstractMagentoCommand |
||
20 | { |
||
21 | const TYPE_MIGRATION_STRUCTURE = 'structure'; |
||
22 | const TYPE_MIGRATION_DATA = 'data'; |
||
23 | |||
24 | /** |
||
25 | * @var OutputInterface |
||
26 | */ |
||
27 | protected $_output; |
||
28 | |||
29 | /** |
||
30 | * @var InputInterface |
||
31 | */ |
||
32 | protected $_input; |
||
33 | |||
34 | /** |
||
35 | * Holds our copy of the global config. |
||
36 | * |
||
37 | * Loaded to avoid grabbing the cached version, and so |
||
38 | * we still have all our original information when we |
||
39 | * destroy the real configuration |
||
40 | * |
||
41 | * @var mixed $_secondConfig |
||
42 | */ |
||
43 | protected $_secondConfig; |
||
44 | |||
45 | protected $_eventStash; |
||
46 | |||
47 | /** |
||
48 | * @var array |
||
49 | */ |
||
50 | protected $_config; |
||
51 | |||
52 | protected function configure() |
||
63 | |||
64 | /** |
||
65 | * @param InputInterface $input |
||
66 | * @param OutputInterface $output |
||
67 | * |
||
68 | * @return int|null|void |
||
69 | */ |
||
70 | protected function execute(InputInterface $input, OutputInterface $output) |
||
71 | { |
||
72 | $this->_config = $this->getCommandConfig(); |
||
73 | |||
74 | //sets output so we can access it from all methods |
||
75 | $this->_setOutput($output); |
||
76 | $this->_setInput($input); |
||
77 | if (false === $this->_init()) { |
||
78 | return; |
||
79 | } |
||
80 | $needsUpdate = $this->_analyzeSetupResourceClasses(); |
||
81 | |||
82 | if (count($needsUpdate) == 0) { |
||
83 | return; |
||
84 | } |
||
85 | $this->_listDetailedUpdateInformation($needsUpdate); |
||
86 | $this->_runAllStructureUpdates($needsUpdate); |
||
87 | $output->writeln('We have run all the setup resource scripts.'); |
||
88 | } |
||
89 | |||
90 | protected function _loadSecondConfig() |
||
91 | { |
||
92 | $config = new \Mage_Core_Model_Config; |
||
93 | $config->loadBase(); //get app/etc |
||
94 | $this->_secondConfig = \Mage::getConfig()->loadModulesConfiguration('config.xml', $config); |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @return array |
||
99 | */ |
||
100 | protected function _getAllSetupResourceObjects() |
||
101 | { |
||
102 | $config = $this->_secondConfig; |
||
103 | $resources = $config->getNode('global/resources')->children(); |
||
104 | $setupResources = array(); |
||
105 | foreach ($resources as $name => $resource) { |
||
106 | if (!$resource->setup) { |
||
107 | continue; |
||
108 | } |
||
109 | $className = 'Mage_Core_Model_Resource_Setup'; |
||
110 | if (isset($resource->setup->class)) { |
||
111 | $className = $resource->setup->getClassName(); |
||
112 | } |
||
113 | |||
114 | $setupResources[$name] = new $className($name); |
||
115 | } |
||
116 | |||
117 | return $setupResources; |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * @return \Mage_Core_Model_Resource |
||
122 | */ |
||
123 | protected function _getResource() |
||
124 | { |
||
125 | return \Mage::getResourceSingleton('core/resource'); |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * @param \Mage_Core_Model_Resource_Setup $setupResource |
||
130 | * @param array $args |
||
131 | * |
||
132 | * @return array|mixed |
||
133 | */ |
||
134 | View Code Duplication | protected function _getAvaiableDbFilesFromResource($setupResource, $args = array()) |
|
|
|||
135 | { |
||
136 | $result = $this->_callProtectedMethodFromObject('_getAvailableDbFiles', $setupResource, $args); |
||
137 | |||
138 | //an install runs the install script first, then any upgrades |
||
139 | if ($args[0] == \Mage_Core_Model_Resource_Setup::TYPE_DB_INSTALL) { |
||
140 | $args[0] = \Mage_Core_Model_Resource_Setup::TYPE_DB_UPGRADE; |
||
141 | $args[1] = $result[0]['toVersion']; |
||
142 | $result = array_merge( |
||
143 | $result, |
||
144 | $this->_callProtectedMethodFromObject('_getAvailableDbFiles', $setupResource, $args) |
||
145 | ); |
||
146 | } |
||
147 | |||
148 | return $result; |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @param \Mage_Core_Model_Resource_Setup $setupResource |
||
153 | * @param array $args |
||
154 | * |
||
155 | * @return array|mixed |
||
156 | */ |
||
157 | View Code Duplication | protected function _getAvaiableDataFilesFromResource($setupResource, $args = array()) |
|
158 | { |
||
159 | $result = $this->_callProtectedMethodFromObject('_getAvailableDataFiles', $setupResource, $args); |
||
160 | if ($args[0] == \Mage_Core_Model_Resource_Setup::TYPE_DATA_INSTALL) { |
||
161 | $args[0] = \Mage_Core_Model_Resource_Setup::TYPE_DATA_UPGRADE; |
||
162 | $args[1] = $result[0]['toVersion']; |
||
163 | $result = array_merge( |
||
164 | $result, |
||
165 | $this->_callProtectedMethodFromObject('_getAvailableDbFiles', $setupResource, $args) |
||
166 | ); |
||
167 | } |
||
168 | |||
169 | return $result; |
||
170 | } |
||
171 | |||
172 | /** |
||
173 | * @param string $method |
||
174 | * @param object $object |
||
175 | * @param array $args |
||
176 | * |
||
177 | * @return mixed |
||
178 | */ |
||
179 | protected function _callProtectedMethodFromObject($method, $object, $args = array()) |
||
180 | { |
||
181 | $r = new ReflectionClass($object); |
||
182 | $m = $r->getMethod($method); |
||
183 | $m->setAccessible(true); |
||
184 | |||
185 | return $m->invokeArgs($object, $args); |
||
186 | } |
||
187 | |||
188 | /** |
||
189 | * @param string $property |
||
190 | * @param object $object |
||
191 | * @param mixed $value |
||
192 | */ |
||
193 | View Code Duplication | protected function _setProtectedPropertyFromObjectToValue($property, $object, $value) |
|
194 | { |
||
195 | $r = new ReflectionClass($object); |
||
196 | $p = $r->getProperty($property); |
||
197 | $p->setAccessible(true); |
||
198 | $p->setValue($object, $value); |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * @param string $property |
||
203 | * @param object $object |
||
204 | * |
||
205 | * @return mixed |
||
206 | */ |
||
207 | View Code Duplication | protected function _getProtectedPropertyFromObject($property, $object) |
|
208 | { |
||
209 | $r = new ReflectionClass($object); |
||
210 | $p = $r->getProperty($property); |
||
211 | $p->setAccessible(true); |
||
212 | |||
213 | return $p->getValue($object); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * @param string $name |
||
218 | * |
||
219 | * @return string |
||
220 | */ |
||
221 | protected function _getDbVersionFromName($name) |
||
222 | { |
||
223 | return $this->_getResource()->getDbVersion($name); |
||
224 | } |
||
225 | |||
226 | /** |
||
227 | * @param string $name |
||
228 | * |
||
229 | * @return string |
||
230 | */ |
||
231 | protected function _getDbDataVersionFromName($name) |
||
232 | { |
||
233 | return $this->_getResource()->getDataVersion($name); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * @param Object $object |
||
238 | * |
||
239 | * @return mixed |
||
240 | */ |
||
241 | protected function _getConfiguredVersionFromResourceObject($object) |
||
242 | { |
||
243 | $moduleConfig = $this->_getProtectedPropertyFromObject('_moduleConfig', $object); |
||
244 | |||
245 | return $moduleConfig->version; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * @param bool|array $setupResources |
||
250 | * |
||
251 | * @return array |
||
252 | */ |
||
253 | protected function _getAllSetupResourceObjectThatNeedUpdates($setupResources = false) |
||
254 | { |
||
255 | $setupResources = $setupResources ? $setupResources : $this->_getAllSetupResourceObjects(); |
||
256 | $needsUpdate = array(); |
||
257 | foreach ($setupResources as $name => $setupResource) { |
||
258 | $db_ver = $this->_getDbVersionFromName($name); |
||
259 | $db_data_ver = $this->_getDbDataVersionFromName($name); |
||
260 | $config_ver = $this->_getConfiguredVersionFromResourceObject($setupResource); |
||
261 | |||
262 | if ( |
||
263 | (string) $config_ver == (string) $db_ver && //structure |
||
264 | (string) $config_ver == (string) $db_data_ver //data |
||
265 | ) { |
||
266 | continue; |
||
267 | } |
||
268 | $needsUpdate[$name] = $setupResource; |
||
269 | } |
||
270 | |||
271 | return $needsUpdate; |
||
272 | } |
||
273 | |||
274 | /** |
||
275 | * @param string $message |
||
276 | */ |
||
277 | protected function _log($message) |
||
278 | { |
||
279 | $this->_output->writeln($message); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * @param OutputInterface $output |
||
284 | */ |
||
285 | protected function _setOutput(OutputInterface $output) |
||
286 | { |
||
287 | $this->_output = $output; |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * @param InputInterface $input |
||
292 | */ |
||
293 | protected function _setInput(InputInterface $input) |
||
294 | { |
||
295 | $this->_input = $input; |
||
296 | } |
||
297 | |||
298 | /** |
||
299 | * @param array $needsUpdate |
||
300 | */ |
||
301 | protected function _outputUpdateInformation(array $needsUpdate) |
||
302 | { |
||
303 | $output = $this->_output; |
||
304 | foreach ($needsUpdate as $name => $setupResource) { |
||
305 | $dbVersion = $this->_getDbVersionFromName($name); |
||
306 | $dbDataVersion = $this->_getDbDataVersionFromName($name); |
||
307 | $configVersion = $this->_getConfiguredVersionFromResourceObject($setupResource); |
||
308 | |||
309 | $moduleConfig = $this->_getProtectedPropertyFromObject('_moduleConfig', $setupResource); |
||
310 | $output->writeln( |
||
311 | array( |
||
312 | '+--------------------------------------------------+', |
||
313 | 'Resource Name: ' . $name, |
||
314 | 'For Module: ' . $moduleConfig->getName(), |
||
315 | 'Class: ' . get_class($setupResource), |
||
316 | 'Current Structure Version: ' . $dbVersion, |
||
317 | 'Current Data Version: ' . $dbDataVersion, |
||
318 | 'Configured Version: ' . $configVersion, |
||
319 | ) |
||
320 | ); |
||
321 | |||
322 | $args = array( |
||
323 | '', |
||
324 | (string) $dbVersion, |
||
325 | (string) $configVersion, |
||
326 | ); |
||
327 | |||
328 | $args[0] = $dbVersion |
||
329 | ? \Mage_Core_Model_Resource_Setup::TYPE_DB_UPGRADE |
||
330 | : \Mage_Core_Model_Resource_Setup::TYPE_DB_INSTALL; |
||
331 | $output->writeln('Structure Files to Run: '); |
||
332 | $filesStructure = $this->_getAvaiableDbFilesFromResource($setupResource, $args); |
||
333 | $this->_outputFileArray($filesStructure, $output); |
||
334 | $output->writeln(""); |
||
335 | |||
336 | $args[0] = $dbVersion |
||
337 | ? \Mage_Core_Model_Resource_Setup::TYPE_DATA_UPGRADE |
||
338 | : \Mage_Core_Model_Resource_Setup::TYPE_DATA_INSTALL; |
||
339 | $output->writeln('Data Files to Run: '); |
||
340 | $filesData = $this->_getAvaiableDataFilesFromResource($setupResource, $args); |
||
341 | $this->_outputFileArray($filesData, $output); |
||
342 | $output->writeln('+--------------------------------------------------+'); |
||
343 | $output->writeln(''); |
||
344 | } |
||
345 | } |
||
346 | |||
347 | /** |
||
348 | * @param array $files |
||
349 | */ |
||
350 | protected function _outputFileArray($files) |
||
351 | { |
||
352 | $output = $this->_output; |
||
353 | if (count($files) == 0) { |
||
354 | $output->writeln('No files found'); |
||
355 | |||
356 | return; |
||
357 | } |
||
358 | foreach ($files as $file) { |
||
359 | $output->writeln(str_replace(\Mage::getBaseDir() . '/', '', $file['fileName'])); |
||
360 | } |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Runs a single named setup resource |
||
365 | * |
||
366 | * This method nukes the global/resources node in the global config |
||
367 | * and then repopulates it with **only** the $name resource. Then it |
||
368 | * calls the standard Magento `applyAllUpdates` method. |
||
369 | * |
||
370 | * The benefit of this approach is we don't need to recreate the entire |
||
371 | * setup resource running logic ourselves. Yay for code reuse |
||
372 | * |
||
373 | * The downside is we should probably exit quickly, as anything else that |
||
374 | * uses the global/resources node is going to behave weird. |
||
375 | * |
||
376 | * @todo Repopulate global config after running? Non trivial since setNode escapes strings |
||
377 | * |
||
378 | * @param string $name |
||
379 | * @param array $needsUpdate |
||
380 | * @param string $type |
||
381 | * |
||
382 | * @throws RuntimeException |
||
383 | * @internal param $string |
||
384 | */ |
||
385 | protected function _runNamedSetupResource($name, array $needsUpdate, $type) |
||
386 | { |
||
387 | $output = $this->_output; |
||
388 | if (!in_array($type, array(self::TYPE_MIGRATION_STRUCTURE, self::TYPE_MIGRATION_DATA))) { |
||
389 | throw new RuntimeException('Invalid Type [' . $type . ']: structure, data is valid'); |
||
390 | } |
||
391 | |||
392 | if (!array_key_Exists($name, $needsUpdate)) { |
||
393 | $output->writeln('<error>No updates to run for ' . $name . ', skipping </error>'); |
||
394 | |||
395 | return; |
||
396 | } |
||
397 | |||
398 | //remove all other setup resources from configuration |
||
399 | //(in memory, do not persist this to cache) |
||
400 | $realConfig = \Mage::getConfig(); |
||
401 | $resources = $realConfig->getNode('global/resources'); |
||
402 | foreach ($resources->children() as $resource) { |
||
403 | if (!$resource->setup) { |
||
404 | continue; |
||
405 | } |
||
406 | unset($resource->setup); |
||
407 | } |
||
408 | //recreate our specific node in <global><resources></resource></global> |
||
409 | //allows for theoretical multiple runs |
||
410 | $setupResourceConfig = $this->_secondConfig->getNode('global/resources/' . $name); |
||
411 | $moduleName = $setupResourceConfig->setup->module; |
||
412 | $className = $setupResourceConfig->setup->class; |
||
413 | |||
414 | $specificResource = $realConfig->getNode('global/resources/' . $name); |
||
415 | $setup = $specificResource->addChild('setup'); |
||
416 | if ($moduleName) { |
||
417 | $setup->addChild('module', $moduleName); |
||
418 | } else { |
||
419 | $output->writeln( |
||
420 | '<error>No module node configured for ' . $name . ', possible configuration error </error>' |
||
421 | ); |
||
422 | } |
||
423 | |||
424 | if ($className) { |
||
425 | $setup->addChild('class', $className); |
||
426 | } |
||
427 | |||
428 | //and finally, RUN THE UPDATES |
||
429 | try { |
||
430 | ob_start(); |
||
431 | if ($type == self::TYPE_MIGRATION_STRUCTURE) { |
||
432 | $this->_stashEventContext(); |
||
433 | \Mage_Core_Model_Resource_Setup::applyAllUpdates(); |
||
434 | $this->_restoreEventContext(); |
||
435 | } else { |
||
436 | if ($type == self::TYPE_MIGRATION_DATA) { |
||
437 | \Mage_Core_Model_Resource_Setup::applyAllDataUpdates(); |
||
438 | } |
||
439 | } |
||
440 | $exceptionOutput = ob_get_clean(); |
||
441 | $this->_output->writeln($exceptionOutput); |
||
442 | } catch (Exception $e) { |
||
443 | $exceptionOutput = ob_get_clean(); |
||
444 | $this->_processExceptionDuringUpdate($e, $name, $exceptionOutput); |
||
445 | if ($this->_input->getOption('stop-on-error')) { |
||
446 | throw new RuntimeException('Setup stopped with errors'); |
||
447 | } |
||
448 | } |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * @param Exception $e |
||
453 | * @param string $name |
||
454 | * @param string $magentoExceptionOutput |
||
455 | */ |
||
456 | protected function _processExceptionDuringUpdate( |
||
457 | Exception $e, |
||
458 | $name, |
||
459 | $magentoExceptionOutput |
||
460 | ) { |
||
461 | $output = $this->_output; |
||
462 | $output->writeln(array( |
||
463 | "<error>Magento encountered an error while running the following setup resource.</error>", |
||
464 | "", |
||
465 | " $name ", |
||
466 | "", |
||
467 | "<error>The Good News:</error> You know the error happened, and the database", |
||
468 | "information below will help you fix this error!", |
||
469 | "", |
||
470 | "<error>The Bad News:</error> Because Magento/MySQL can't run setup resources", |
||
471 | "transactionally your database is now in an half upgraded, invalid", |
||
472 | "state. Even if you fix the error, new errors may occur due to", |
||
473 | "this half upgraded, invalid state.", |
||
474 | '', |
||
475 | "What to Do: ", |
||
476 | "1. Figure out why the error happened, and manually fix your", |
||
477 | " database and/or system so it won't happen again.", |
||
478 | "2. Restore your database from backup.", |
||
479 | "3. Re-run the scripts.", |
||
480 | "", |
||
481 | "Exception Message:", |
||
482 | $e->getMessage(), |
||
483 | "", |
||
484 | )); |
||
485 | |||
486 | if ($magentoExceptionOutput) { |
||
487 | $this->getHelper('dialog')->askAndValidate( |
||
488 | $output, |
||
489 | '<question>Press Enter to view raw Magento error text:</question> ' |
||
490 | ); |
||
491 | $output->writeln("Magento Exception Error Text:"); |
||
492 | echo $magentoExceptionOutput, "\n"; //echoing (vs. writeln) to avoid seg fault |
||
493 | } |
||
494 | } |
||
495 | |||
496 | /** |
||
497 | * @return bool |
||
498 | */ |
||
499 | protected function _checkCacheSettings() |
||
500 | { |
||
501 | $output = $this->_output; |
||
502 | $allTypes = \Mage::app()->useCache(); |
||
503 | if ($allTypes['config'] !== '1') { |
||
504 | $output->writeln('<error>ERROR: Config Cache is Disabled</error>'); |
||
505 | $output->writeln('This command will not run with the configuration cache disabled.'); |
||
506 | $output->writeln('Please change your Magento settings at System -> Cache Management'); |
||
507 | $output->writeln(''); |
||
508 | |||
509 | return false; |
||
510 | } |
||
511 | |||
512 | return true; |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * @param string $toUpdate |
||
517 | * @param array $needsUpdate |
||
518 | * @param string $type |
||
519 | */ |
||
520 | protected function _runStructureOrDataScripts($toUpdate, array $needsUpdate, $type) |
||
536 | |||
537 | /** |
||
538 | * @return array |
||
539 | */ |
||
540 | protected function _getTestedVersions() |
||
541 | { |
||
542 | return $this->_config['tested-versions']; |
||
543 | } |
||
544 | |||
545 | protected function _restoreEventContext() |
||
546 | { |
||
547 | $app = \Mage::app(); |
||
548 | $this->_setProtectedPropertyFromObjectToValue('_events', $app, $this->_eventStash); |
||
549 | } |
||
550 | |||
551 | protected function _stashEventContext() |
||
552 | { |
||
553 | $app = \Mage::app(); |
||
554 | $events = $this->_getProtectedPropertyFromObject('_events', $app); |
||
555 | $this->_eventStash = $events; |
||
556 | $this->_setProtectedPropertyFromObjectToValue('_events', $app, array()); |
||
557 | } |
||
558 | |||
559 | /** |
||
560 | * @return bool |
||
561 | */ |
||
562 | protected function _init() |
||
563 | { |
||
564 | //bootstrap magento |
||
565 | $this->detectMagento($this->_output); |
||
566 | if (!$this->initMagento()) { |
||
567 | return false; |
||
568 | } |
||
569 | |||
570 | //don't run if cache is off. If cache is off that means |
||
571 | //setup resource will run automagically |
||
572 | if (!$this->_checkCacheSettings()) { |
||
573 | return false; |
||
574 | } |
||
575 | |||
576 | //load a second, not cached, config.xml tree |
||
577 | $this->_loadSecondConfig(); |
||
578 | |||
579 | return true; |
||
580 | } |
||
581 | |||
582 | /** |
||
583 | * @return array |
||
584 | */ |
||
585 | protected function _analyzeSetupResourceClasses() |
||
586 | { |
||
587 | $output = $this->_output; |
||
588 | $this->writeSection($output, 'Analyzing Setup Resource Classes'); |
||
589 | $setupResources = $this->_getAllSetupResourceObjects(); |
||
590 | $needsUpdate = $this->_getAllSetupResourceObjectThatNeedUpdates($setupResources); |
||
591 | |||
592 | $output->writeln( |
||
593 | 'Found <info>' . count($setupResources) . '</info> configured setup resource(s)</info>' |
||
594 | ); |
||
595 | $output->writeln( |
||
596 | 'Found <info>' . count($needsUpdate) . '</info> setup resource(s) which need an update</info>' |
||
597 | ); |
||
598 | |||
599 | return $needsUpdate; |
||
600 | } |
||
601 | |||
602 | /** |
||
603 | * @param array $needsUpdate |
||
604 | */ |
||
605 | protected function _listDetailedUpdateInformation(array $needsUpdate) |
||
616 | |||
617 | /** |
||
618 | * @param array $needsUpdate |
||
619 | */ |
||
620 | protected function _runAllStructureUpdates(array $needsUpdate) |
||
621 | { |
||
622 | $output = $this->_output; |
||
623 | $this->writeSection($output, "Run Structure Updates"); |
||
624 | $output->writeln('All structure updates run before data updates.'); |
||
625 | $output->writeln(''); |
||
626 | |||
627 | $c = 1; |
||
628 | $total = count($needsUpdate); |
||
629 | View Code Duplication | foreach ($needsUpdate as $key => $value) { |
|
630 | $toUpdate = $key; |
||
631 | $this->_runStructureOrDataScripts($toUpdate, $needsUpdate, self::TYPE_MIGRATION_STRUCTURE); |
||
632 | $output->writeln("($c of $total)"); |
||
633 | $output->writeln(''); |
||
634 | $c++; |
||
635 | } |
||
636 | |||
637 | $this->writeSection($output, "Run Data Updates"); |
||
638 | $c = 1; |
||
639 | $total = count($needsUpdate); |
||
640 | View Code Duplication | foreach ($needsUpdate as $key => $value) { |
|
641 | $toUpdate = $key; |
||
642 | $this->_runStructureOrDataScripts($toUpdate, $needsUpdate, self::TYPE_MIGRATION_DATA); |
||
648 | } |
||
649 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.