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 Name 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 Name, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
32 | class Name implements HasParent |
||
33 | { |
||
34 | use InheritableNameAttributesTrait, |
||
35 | FormattingTrait, |
||
36 | AffixesTrait, |
||
37 | DelimiterTrait; |
||
38 | |||
39 | /** |
||
40 | * @var array |
||
41 | */ |
||
42 | protected $nameParts; |
||
43 | |||
44 | /** |
||
45 | * Specifies the text string used to separate names in a name variable. Default is ”, ” (e.g. “Doe, Smith”). |
||
46 | * @var |
||
47 | */ |
||
48 | private $delimiter = ", "; |
||
49 | |||
50 | /** |
||
51 | * @var Names |
||
52 | */ |
||
53 | private $parent; |
||
54 | |||
55 | /** |
||
56 | * @var \SimpleXMLElement |
||
57 | */ |
||
58 | private $node; |
||
59 | |||
60 | /** |
||
61 | * @var string |
||
62 | */ |
||
63 | private $etAl; |
||
64 | |||
65 | /** |
||
66 | * Name constructor. |
||
67 | * @param \SimpleXMLElement $node |
||
68 | * @param Names $parent |
||
69 | */ |
||
70 | public function __construct(\SimpleXMLElement $node, Names $parent) |
||
71 | { |
||
72 | $this->node = $node; |
||
73 | $this->parent = $parent; |
||
74 | |||
75 | $this->nameParts = []; |
||
76 | |||
77 | /** @var \SimpleXMLElement $child */ |
||
78 | foreach ($node->children() as $child) { |
||
79 | |||
80 | switch ($child->getName()) { |
||
81 | case "name-part": |
||
82 | /** @var NamePart $namePart */ |
||
83 | $namePart = Factory::create($child, $this); |
||
84 | $this->nameParts[$namePart->getName()] = $namePart; |
||
85 | } |
||
86 | } |
||
87 | |||
88 | foreach ($node->attributes() as $attribute) { |
||
89 | switch ($attribute->getName()) { |
||
90 | case 'form': |
||
91 | $this->form = (string) $attribute; |
||
92 | break; |
||
93 | } |
||
94 | |||
95 | } |
||
96 | |||
97 | $this->initFormattingAttributes($node); |
||
98 | $this->initAffixesAttributes($node); |
||
99 | $this->initDelimiterAttributes($node); |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * @param array $data |
||
104 | * @param integer|null $citationNumber |
||
105 | * @return string |
||
106 | */ |
||
107 | public function render($data, $citationNumber = null) |
||
108 | { |
||
109 | $text = ""; |
||
110 | if (!$this->attributesInitialized) { |
||
111 | $this->initInheritableNameAttributes($this->node); |
||
112 | } |
||
113 | if ("text" === $this->and) { |
||
114 | $this->and = CiteProc::getContext()->getLocale()->filter('terms', 'and')->single; |
||
115 | } elseif ('symbol' === $this->and) { |
||
116 | $this->and = '&'; |
||
117 | } |
||
118 | |||
119 | $resultNames = []; |
||
120 | |||
121 | $hasPreceding = CiteProc::getContext()->getCitationItems()->hasKey($citationNumber - 1); |
||
122 | $subsequentSubstitution = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstitute(); |
||
123 | $subsequentSubstitutionRule = CiteProc::getContext()->getCitationItems()->getSubsequentAuthorSubstituteRule(); |
||
124 | $useSubseqSubstitution = !is_null($subsequentSubstitution) && !empty($subsequentSubstitutionRule); |
||
125 | $preceding = CiteProc::getContext()->getCitationItems()->get($citationNumber - 1); |
||
126 | |||
127 | |||
128 | if ($hasPreceding && $useSubseqSubstitution) { |
||
129 | /** @var \stdClass $preceding */ |
||
130 | $identicalAuthors = $this->identicalAuthors($preceding, $data); |
||
131 | if ($subsequentSubstitutionRule == SubsequentAuthorSubstituteRule::COMPLETE_ALL) { |
||
132 | if ($identicalAuthors) { |
||
133 | return $subsequentSubstitution; |
||
134 | } else { |
||
135 | $resultNames = $this->getFormattedNames($data, $resultNames); |
||
136 | } |
||
137 | } else { |
||
138 | $resultNames = $this->renderSubsequentSubstitution($data, $preceding); |
||
139 | } |
||
140 | } else { |
||
141 | $resultNames = $this->getFormattedNames($data, $resultNames); |
||
142 | } |
||
143 | |||
144 | $resultNames = $this->prepareAbbreviation($resultNames); |
||
145 | |||
146 | if ($this->etAlUseLast) { |
||
147 | /* When set to “true” (the default is “false”), name lists truncated by et-al abbreviation are followed by |
||
148 | the name delimiter, the ellipsis character, and the last name of the original name list. This is only |
||
149 | possible when the original name list has at least two more names than the truncated name list (for this |
||
150 | the value of et-al-use-first/et-al-subsequent-min must be at least 2 less than the value of |
||
151 | et-al-min/et-al-subsequent-use-first). */ |
||
152 | $this->and = "…"; // set "and" |
||
153 | $this->etAl = null; //reset $etAl; |
||
154 | } |
||
155 | |||
156 | /* add "and" */ |
||
157 | $count = count($resultNames); |
||
158 | if (!empty($this->and) && $count > 1 && empty($this->etAl)) { |
||
159 | $new = $this->and . ' ' . end($resultNames); // add and-prefix of the last name if "and" is defined |
||
160 | $resultNames[count($resultNames) - 1] = $new; //set prefixed last name at the last position of $resultNames array |
||
161 | } |
||
162 | |||
163 | if (!empty($this->and) && empty($this->etAl)) { |
||
164 | switch ($this->delimiterPrecedesLast) { |
||
165 | case 'after-inverted-name': |
||
166 | //TODO: implement |
||
167 | break; |
||
168 | case 'always': |
||
169 | $text = implode($this->delimiter, $resultNames); |
||
170 | break; |
||
171 | case 'never': |
||
172 | if (!$this->etAlUseLast) { |
||
173 | if (count($resultNames) === 1) { |
||
174 | $text = $resultNames[0]; |
||
175 | View Code Duplication | } else if (count($resultNames) === 2) { |
|
|
|||
176 | $text = implode(" ", $resultNames); |
||
177 | } else { // >2 |
||
178 | $lastName = array_pop($resultNames); |
||
179 | $text = implode($this->delimiter, $resultNames) . " " . $lastName; |
||
180 | } |
||
181 | } /*else { |
||
182 | if (count($resultNames) === 1) { |
||
183 | $text = $resultNames[0]; |
||
184 | } else if (count($resultNames) === 2) { |
||
185 | $text = implode(" ", $resultNames); |
||
186 | } else { // >2 |
||
187 | $lastName = array_pop($resultNames); |
||
188 | $text = implode($this->delimiter, $resultNames) . ", " . $lastName; |
||
189 | } |
||
190 | }*/ |
||
191 | |||
192 | break; |
||
193 | case 'contextual': |
||
194 | default: |
||
195 | View Code Duplication | if (count($resultNames) === 1) { |
|
196 | $text = $resultNames[0]; |
||
197 | } else if (count($resultNames) === 2) { |
||
198 | $text = implode(" ", $resultNames); |
||
199 | } else { |
||
200 | $text = implode($this->delimiter, $resultNames); |
||
201 | } |
||
202 | } |
||
203 | } |
||
204 | if (empty($text)) { |
||
205 | $text = implode($this->delimiter, $resultNames); |
||
206 | } |
||
207 | |||
208 | //append et al abbreviation |
||
209 | if (count($data) > 1 && |
||
210 | !empty($resultNames) && |
||
211 | !empty($this->etAl) && |
||
212 | !empty($this->etAlMin) && |
||
213 | !empty($this->etAlUseFirst)) { |
||
214 | |||
215 | $text = $this->appendEtAl($text, $resultNames); |
||
216 | } |
||
217 | |||
218 | /* A third value, “count”, returns the total number of names that would otherwise be rendered by the use of the |
||
219 | cs:names element (taking into account the effects of et-al abbreviation and editor/translator collapsing), |
||
220 | which allows for advanced sorting. */ |
||
221 | |||
222 | if ($this->form == 'count') { |
||
223 | return (int) count($resultNames); |
||
224 | } |
||
225 | |||
226 | return $text; |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | * @param \stdClass $name |
||
231 | * @param int $rank |
||
232 | * @return string |
||
233 | */ |
||
234 | private function formatName($name, $rank) |
||
235 | { |
||
236 | |||
237 | $nameObj = $this->cloneNamePOSC($name); |
||
238 | |||
239 | $useInitials = $this->initialize && !is_null($this->initializeWith) && $this->initializeWith !== false; |
||
240 | if ($useInitials && isset($name->given)) { |
||
241 | $nameObj->given = StringHelper::initializeBySpaceOrHyphen($name->given, $this->initializeWith); |
||
242 | } |
||
243 | |||
244 | // format name-parts |
||
245 | if (count($this->nameParts) > 0) { |
||
246 | /** @var NamePart $namePart */ |
||
247 | foreach ($this->nameParts as $namePart) { |
||
248 | $nameObj = $namePart->render($nameObj); |
||
249 | } |
||
250 | } |
||
251 | $ret = $this->getNamesString($nameObj, $rank); |
||
252 | |||
253 | return trim($ret); |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * @param \stdClass $name |
||
258 | * @param int $rank |
||
259 | * @return string |
||
260 | */ |
||
261 | private function getNamesString($name, $rank) |
||
327 | |||
328 | /** |
||
329 | * @param $name |
||
330 | * @return \stdClass |
||
331 | */ |
||
332 | private function cloneNamePOSC($name) |
||
352 | |||
353 | /** |
||
354 | * @param $text |
||
355 | * @param $resultNames |
||
356 | * @return string |
||
357 | */ |
||
358 | protected function appendEtAl($text, $resultNames) |
||
383 | |||
384 | /** |
||
385 | * @param $resultNames |
||
386 | * @return array |
||
387 | */ |
||
388 | protected function prepareAbbreviation($resultNames) |
||
430 | |||
431 | /** |
||
432 | * @return Names |
||
433 | */ |
||
434 | public function getParent() |
||
438 | |||
439 | /** |
||
440 | * @param $data |
||
441 | * @param \stdClass $preceding |
||
442 | * @return array |
||
443 | */ |
||
444 | protected function renderSubsequentSubstitution($data, $preceding) |
||
499 | |||
500 | /** |
||
501 | * @param \stdClass $preceding |
||
502 | * @param \stdClass $name |
||
503 | * @return bool |
||
504 | */ |
||
505 | public function precedingHasAuthor($preceding, $name) |
||
514 | |||
515 | /** |
||
516 | * @param \stdClass $precedingItem |
||
517 | * @param array $currentAuthor |
||
518 | * @return bool |
||
519 | */ |
||
520 | private function identicalAuthors($precedingItem, $currentAuthor) |
||
533 | |||
534 | /** |
||
535 | * @param $data |
||
536 | * @param $resultNames |
||
537 | * @return array |
||
538 | */ |
||
539 | protected function getFormattedNames($data, $resultNames) |
||
546 | |||
547 | /** |
||
548 | * @return string |
||
549 | */ |
||
550 | public function getForm() |
||
554 | |||
555 | /** |
||
556 | * @return string |
||
557 | */ |
||
558 | public function isNameAsSortOrder() |
||
562 | |||
563 | /** |
||
564 | * @return mixed |
||
565 | */ |
||
566 | public function getDelimiter() |
||
570 | |||
571 | /** |
||
572 | * @param mixed $delimiter |
||
573 | */ |
||
574 | public function setDelimiter($delimiter) |
||
578 | |||
579 | |||
580 | } |
||
581 |
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.