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 HierarchicalTrait 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 HierarchicalTrait, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | trait HierarchicalTrait |
||
15 | { |
||
16 | /** |
||
17 | * The object's parent, if any, in the hierarchy. |
||
18 | * |
||
19 | * @var HierarchicalInterface|null |
||
20 | */ |
||
21 | protected $master; |
||
22 | |||
23 | /** |
||
24 | * Store a copy of the object's ancestry. |
||
25 | * |
||
26 | * @var HierarchicalInterface[]|null |
||
27 | */ |
||
28 | private $hierarchy = null; |
||
29 | |||
30 | /** |
||
31 | * Store a copy of the object's descendants. |
||
32 | * |
||
33 | * @var HierarchicalInterface[]|null |
||
34 | */ |
||
35 | private $children; |
||
36 | |||
37 | /** |
||
38 | * Store a copy of the object's siblings. |
||
39 | * |
||
40 | * @var HierarchicalInterface[]|null |
||
41 | */ |
||
42 | private $siblings; |
||
43 | |||
44 | /** |
||
45 | * A store of cached objects. |
||
46 | * |
||
47 | * @var ModelInterface[] $objectCache |
||
48 | */ |
||
49 | public static $objectCache = []; |
||
50 | |||
51 | /** |
||
52 | * Reset this object's hierarchy. |
||
53 | * |
||
54 | * The object's hierarchy can be rebuilt with {@see self::hierarchy()}. |
||
55 | * |
||
56 | * @return HierarchicalInterface Chainable |
||
57 | */ |
||
58 | public function resetHierarchy() |
||
59 | { |
||
60 | $this->hierarchy = null; |
||
61 | |||
62 | return $this; |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * Set this object's immediate parent. |
||
67 | * |
||
68 | * @param mixed $master The object's parent (or master). |
||
69 | * @throws UnexpectedValueException The current object cannot be its own parent. |
||
70 | * @return HierarchicalInterface Chainable |
||
71 | */ |
||
72 | View Code Duplication | public function setMaster($master) |
|
|
|||
73 | { |
||
74 | $master = $this->objFromIdent($master); |
||
75 | |||
76 | if ($master instanceof ModelInterface) { |
||
77 | if ($master->id() === $this->id()) { |
||
78 | throw new UnexpectedValueException(sprintf( |
||
79 | 'Can not be ones own parent: %s', |
||
80 | $master->id() |
||
81 | )); |
||
82 | } |
||
83 | } |
||
84 | |||
85 | $this->master = $master; |
||
86 | |||
87 | $this->resetHierarchy(); |
||
88 | |||
89 | return $this; |
||
90 | } |
||
91 | |||
92 | /** |
||
93 | * Retrieve this object's immediate parent. |
||
94 | * |
||
95 | * @return HierarchicalInterface|null |
||
96 | */ |
||
97 | public function getMaster() |
||
98 | { |
||
99 | return $this->master; |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * Determine if this object has a direct parent. |
||
104 | * |
||
105 | * @return boolean |
||
106 | */ |
||
107 | public function hasMaster() |
||
108 | { |
||
109 | return ($this->getMaster() !== null); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * Determine if this object is the head (top-level) of its hierarchy. |
||
114 | * |
||
115 | * Top-level objects do not have a parent (master). |
||
116 | * |
||
117 | * @return boolean |
||
118 | */ |
||
119 | public function isTopLevel() |
||
120 | { |
||
121 | return ($this->getMaster() === null); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Determine if this object is the tail (last-level) of its hierarchy. |
||
126 | * |
||
127 | * Last-level objects do not have a children. |
||
128 | * |
||
129 | * @return boolean |
||
130 | */ |
||
131 | public function isLastLevel() |
||
132 | { |
||
133 | return !$this->hasChildren(); |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * Retrieve this object's position (level) in its hierarchy. |
||
138 | * |
||
139 | * Starts at "1" (top-level). |
||
140 | * |
||
141 | * The level is calculated by loading all ancestors with {@see self::hierarchy()}. |
||
142 | * |
||
143 | * @return integer |
||
144 | */ |
||
145 | public function hierarchyLevel() |
||
146 | { |
||
147 | $hierarchy = $this->hierarchy(); |
||
148 | $level = (count($hierarchy) + 1); |
||
149 | |||
150 | return $level; |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * Retrieve the top-level ancestor of this object. |
||
155 | * |
||
156 | * @return HierarchicalInterface|null |
||
157 | */ |
||
158 | public function toplevelMaster() |
||
159 | { |
||
160 | $hierarchy = $this->invertedHierarchy(); |
||
161 | if (isset($hierarchy[0])) { |
||
162 | return $hierarchy[0]; |
||
163 | } else { |
||
164 | return null; |
||
165 | } |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * Determine if this object has any ancestors. |
||
170 | * |
||
171 | * @return boolean |
||
172 | */ |
||
173 | public function hasParents() |
||
174 | { |
||
175 | return !!count($this->hierarchy()); |
||
176 | } |
||
177 | |||
178 | /** |
||
179 | * Retrieve this object's ancestors (from immediate parent to top-level). |
||
180 | * |
||
181 | * @return array |
||
182 | */ |
||
183 | public function hierarchy() |
||
184 | { |
||
185 | if (!isset($this->hierarchy)) { |
||
186 | $hierarchy = []; |
||
187 | $master = $this->getMaster(); |
||
188 | while ($master) { |
||
189 | $hierarchy[] = $master; |
||
190 | $master = $master->getMaster(); |
||
191 | } |
||
192 | |||
193 | $this->hierarchy = $hierarchy; |
||
194 | } |
||
195 | |||
196 | return $this->hierarchy; |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Retrieve this object's ancestors, inverted from top-level to immediate. |
||
201 | * |
||
202 | * @return array |
||
203 | */ |
||
204 | public function invertedHierarchy() |
||
205 | { |
||
206 | $hierarchy = $this->hierarchy(); |
||
207 | return array_reverse($hierarchy); |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * Determine if the object is the parent of the given object. |
||
212 | * |
||
213 | * @param mixed $child The child (or ID) to match against. |
||
214 | * @return boolean |
||
215 | */ |
||
216 | public function isMasterOf($child) |
||
217 | { |
||
218 | $child = $this->objFromIdent($child); |
||
219 | return ($child->getMaster() == $this); |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Determine if the object is a parent/ancestor of the given object. |
||
224 | * |
||
225 | * @param mixed $child The child (or ID) to match against. |
||
226 | * @return boolean |
||
227 | * @todo Implementation needed. |
||
228 | */ |
||
229 | public function recursiveIsMasterOf($child) |
||
230 | { |
||
231 | $child = $this->objFromIdent($child); |
||
232 | |||
233 | return false; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Get wether the object has any children at all |
||
238 | * @return boolean |
||
239 | */ |
||
240 | public function hasChildren() |
||
241 | { |
||
242 | $numChildren = $this->numChildren(); |
||
243 | return ($numChildren > 0); |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * Get the number of children directly under this object. |
||
248 | * @return integer |
||
249 | */ |
||
250 | public function numChildren() |
||
251 | { |
||
252 | $children = $this->children(); |
||
253 | return count($children); |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * Get the total number of children in the entire hierarchy. |
||
258 | * This method counts all children and sub-children, unlike `numChildren()` which only count 1 level. |
||
259 | * @return integer |
||
260 | */ |
||
261 | public function recursiveNumChildren() |
||
262 | { |
||
263 | // TODO |
||
264 | return 0; |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * @param array $children The children to set. |
||
269 | * @return HierarchicalInterface Chainable |
||
270 | */ |
||
271 | public function setChildren(array $children) |
||
272 | { |
||
273 | $this->children = []; |
||
274 | foreach ($children as $c) { |
||
275 | $this->addChild($c); |
||
276 | } |
||
277 | return $this; |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * @param mixed $child The child object (or ident) to add. |
||
282 | * @throws UnexpectedValueException The current object cannot be its own child. |
||
283 | * @return HierarchicalInterface Chainable |
||
284 | */ |
||
285 | View Code Duplication | public function addChild($child) |
|
286 | { |
||
287 | $child = $this->objFromIdent($child); |
||
288 | |||
289 | if ($child instanceof ModelInterface) { |
||
290 | if ($child->id() === $this->id()) { |
||
291 | throw new UnexpectedValueException(sprintf( |
||
292 | 'Can not be ones own child: %s', |
||
293 | $child->id() |
||
294 | )); |
||
295 | } |
||
296 | } |
||
297 | |||
298 | $this->children[] = $child; |
||
299 | |||
300 | return $this; |
||
301 | } |
||
302 | |||
303 | /** |
||
304 | * Get the children directly under this object. |
||
305 | * @return array |
||
306 | */ |
||
307 | public function children() |
||
308 | { |
||
309 | if ($this->children !== null) { |
||
310 | return $this->children; |
||
311 | } |
||
312 | |||
313 | $this->children = $this->loadChildren(); |
||
314 | return $this->children; |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * @return array |
||
319 | */ |
||
320 | abstract public function loadChildren(); |
||
321 | |||
322 | /** |
||
323 | * @param mixed $master The master object (or ident) to check against. |
||
324 | * @return boolean |
||
325 | */ |
||
326 | public function isChildOf($master) |
||
327 | { |
||
328 | $master = $this->objFromIdent($master); |
||
329 | if ($master === null) { |
||
330 | return false; |
||
331 | } |
||
332 | return ($master == $this->getMaster()); |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * @param mixed $master The master object (or ident) to check against. |
||
337 | * @return boolean |
||
338 | */ |
||
339 | public function recursiveIsChildOf($master) |
||
340 | { |
||
341 | if ($this->isChildOf($master)) { |
||
351 | |||
352 | /** |
||
353 | * @return boolean |
||
354 | */ |
||
355 | public function hasSiblings() |
||
360 | |||
361 | /** |
||
362 | * @return integer |
||
363 | */ |
||
364 | public function numSiblings() |
||
369 | |||
370 | /** |
||
371 | * Get all the objects on the same level as this one. |
||
372 | * @return array |
||
373 | */ |
||
374 | public function siblings() |
||
390 | |||
391 | /** |
||
392 | * @param mixed $sibling The sibling to check. |
||
393 | * @return boolean |
||
394 | */ |
||
395 | public function isSiblingOf($sibling) |
||
400 | |||
401 | /** |
||
402 | * @param mixed $ident The ident. |
||
403 | * @throws InvalidArgumentException If the identifier is not a scalar value. |
||
404 | * @return HierarchicalInterface|null |
||
405 | */ |
||
406 | private function objFromIdent($ident) |
||
442 | |||
443 | /** |
||
444 | * Retrieve an object from the storage source by its ID. |
||
445 | * |
||
446 | * @param mixed $id The object id. |
||
447 | * @return null|HierarchicalInterface |
||
448 | */ |
||
449 | private function loadObjectFromSource($id) |
||
460 | |||
461 | /** |
||
462 | * Retrieve an object from the cache store by its ID. |
||
463 | * |
||
464 | * @param mixed $id The object id. |
||
465 | * @return null|HierarchicalInterface |
||
466 | */ |
||
467 | private function loadObjectFromCache($id) |
||
476 | |||
477 | /** |
||
478 | * Add an object to the cache store. |
||
479 | * |
||
480 | * @param ModelInterface $obj The object to store. |
||
481 | * @return HierarchicalInterface Chainable |
||
482 | */ |
||
483 | private function addObjectToCache(ModelInterface $obj) |
||
489 | |||
490 | /** |
||
491 | * Retrieve the object model factory. |
||
492 | * |
||
493 | * @return \Charcoal\Factory\FactoryInterface |
||
494 | */ |
||
495 | abstract public function modelFactory(); |
||
496 | |||
497 | /** |
||
498 | * @return string |
||
499 | */ |
||
500 | abstract public function id(); |
||
501 | |||
502 | /** |
||
503 | * @return string |
||
504 | */ |
||
505 | abstract public function objType(); |
||
506 | } |
||
507 |
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.