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 Page 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 Page, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
11 | class Page extends Model |
||
12 | { |
||
13 | |||
14 | /** @var Project The project that this page belongs to. */ |
||
15 | protected $project; |
||
16 | |||
17 | /** @var string The page name as provided at instantiation. */ |
||
18 | protected $unnormalizedPageName; |
||
19 | |||
20 | /** @var string[] Metadata about this page. */ |
||
21 | protected $pageInfo; |
||
22 | |||
23 | /** @var string[] Revision history of this page */ |
||
24 | protected $revisions; |
||
25 | |||
26 | /** |
||
27 | * Page constructor. |
||
28 | * @param Project $project |
||
29 | * @param string $pageName |
||
30 | */ |
||
31 | public function __construct(Project $project, $pageName) |
||
32 | { |
||
33 | $this->project = $project; |
||
34 | $this->unnormalizedPageName = $pageName; |
||
35 | } |
||
36 | |||
37 | /** |
||
38 | * Get basic information about this page from the repository. |
||
39 | * @return \string[] |
||
40 | */ |
||
41 | protected function getPageInfo() |
||
42 | { |
||
43 | if (empty($this->pageInfo)) { |
||
44 | $this->pageInfo = $this->getRepository() |
||
1 ignored issue
–
show
|
|||
45 | ->getPageInfo($this->project, $this->unnormalizedPageName); |
||
46 | } |
||
47 | return $this->pageInfo; |
||
48 | } |
||
49 | |||
50 | /** |
||
51 | * Get the page's title. |
||
52 | * @return string |
||
53 | */ |
||
54 | public function getTitle() |
||
55 | { |
||
56 | $info = $this->getPageInfo(); |
||
57 | return isset($info['title']) ? $info['title'] : $this->unnormalizedPageName; |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * Get this page's database ID. |
||
62 | * @return int |
||
63 | */ |
||
64 | public function getId() |
||
65 | { |
||
66 | $info = $this->getPageInfo(); |
||
67 | return isset($info['pageid']) ? $info['pageid'] : null; |
||
68 | } |
||
69 | |||
70 | /** |
||
71 | * Get this page's length in bytes. |
||
72 | * @return int |
||
73 | */ |
||
74 | public function getLength() |
||
79 | |||
80 | /** |
||
81 | * Get HTML for the stylized display of the title. |
||
82 | * The text will be the same as Page::getTitle(). |
||
83 | * @return string |
||
84 | */ |
||
85 | public function getDisplayTitle() |
||
86 | { |
||
87 | $info = $this->getPageInfo(); |
||
88 | if (isset($info['displaytitle'])) { |
||
89 | return $info['displaytitle']; |
||
90 | } |
||
91 | return $this->getTitle(); |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Get the full URL of this page. |
||
96 | * @return string |
||
97 | */ |
||
98 | public function getUrl() |
||
99 | { |
||
100 | $info = $this->getPageInfo(); |
||
101 | return isset($info['fullurl']) ? $info['fullurl'] : null; |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * Get the numerical ID of the namespace of this page. |
||
106 | * @return int |
||
107 | */ |
||
108 | public function getNamespace() |
||
109 | { |
||
110 | $info = $this->getPageInfo(); |
||
111 | return isset($info['ns']) ? $info['ns'] : null; |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Get the number of page watchers. |
||
116 | * @return int |
||
117 | */ |
||
118 | public function getWatchers() |
||
119 | { |
||
120 | $info = $this->getPageInfo(); |
||
121 | return isset($info['watchers']) ? $info['watchers'] : null; |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Whether or not this page exists. |
||
126 | * @return bool |
||
127 | */ |
||
128 | public function exists() |
||
129 | { |
||
130 | $info = $this->getPageInfo(); |
||
131 | return !isset($info['missing']) && !isset($info['invalid']); |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Get the Project to which this page belongs. |
||
136 | * @return Project |
||
137 | */ |
||
138 | public function getProject() |
||
142 | |||
143 | /** |
||
144 | * Get the language code for this page. |
||
145 | * If not set, the language code for the project is returned. |
||
146 | * @return string |
||
147 | */ |
||
148 | public function getLang() |
||
157 | |||
158 | /** |
||
159 | * Get the Wikidata ID of this page. |
||
160 | * @return string |
||
161 | */ |
||
162 | public function getWikidataId() |
||
171 | |||
172 | /** |
||
173 | * Get the number of revisions the page has. |
||
174 | * @param User $user Optionally limit to those of this user. |
||
175 | * @return int |
||
176 | */ |
||
177 | public function getNumRevisions(User $user = null) |
||
188 | |||
189 | /** |
||
190 | * Get all edits made to this page. |
||
191 | * @param User|null $user Specify to get only revisions by the given user. |
||
192 | * @return array |
||
193 | */ |
||
194 | public function getRevisions(User $user = null) |
||
204 | |||
205 | /** |
||
206 | * Get the statement for a single revision, |
||
207 | * so that you can iterate row by row. |
||
208 | * @param User|null $user Specify to get only revisions by the given user. |
||
209 | * @return Doctrine\DBAL\Driver\PDOStatement |
||
210 | */ |
||
211 | public function getRevisionsStmt(User $user = null) |
||
215 | |||
216 | /** |
||
217 | * Get various basic info used in the API, including the |
||
218 | * number of revisions, unique authors, initial author |
||
219 | * and edit count of the initial author. |
||
220 | * This is combined into one query for better performance. |
||
221 | * Caching is intentionally disabled, because using the gadget, |
||
222 | * this will get hit for a different page constantly, where |
||
223 | * the likelihood of cache benefiting us is slim. |
||
224 | * @return string[] |
||
225 | */ |
||
226 | public function getBasicEditingInfo() |
||
230 | |||
231 | /** |
||
232 | * Get assessments of this page |
||
233 | * @return string[]|false `false` if unsupported, or array in the format of: |
||
234 | * [ |
||
235 | * 'assessment' => 'C', // overall assessment |
||
236 | * 'wikiprojects' => [ |
||
237 | * 'Biography' => [ |
||
238 | * 'assessment' => 'C', |
||
239 | * 'badge' => 'url', |
||
240 | * ], |
||
241 | * ... |
||
242 | * ], |
||
243 | * 'wikiproject_prefix' => 'Wikipedia:WikiProject_', |
||
244 | * ] |
||
245 | */ |
||
246 | public function getAssessments() |
||
320 | |||
321 | /** |
||
322 | * Get CheckWiki errors for this page |
||
323 | * @return string[] See getErrors() for format |
||
324 | */ |
||
325 | public function getCheckWikiErrors() |
||
329 | |||
330 | /** |
||
331 | * Get Wikidata errors for this page |
||
332 | * @return string[] See getErrors() for format |
||
333 | */ |
||
334 | public function getWikidataErrors() |
||
372 | |||
373 | /** |
||
374 | * Get Wikidata and CheckWiki errors, if present |
||
375 | * @return string[] List of errors in the format: |
||
376 | * [[ |
||
377 | * 'prio' => int, |
||
378 | * 'name' => string, |
||
379 | * 'notice' => string (HTML), |
||
380 | * 'explanation' => string (HTML) |
||
381 | * ], ... ] |
||
382 | */ |
||
383 | public function getErrors() |
||
392 | |||
393 | /** |
||
394 | * Get all wikidata items for the page, not just languages of sister projects |
||
395 | * @return int Number of records. |
||
396 | */ |
||
397 | public function getWikidataItems() |
||
401 | |||
402 | /** |
||
403 | * Count wikidata items for the page, not just languages of sister projects |
||
404 | * @return int Number of records. |
||
405 | */ |
||
406 | public function countWikidataItems() |
||
410 | |||
411 | /** |
||
412 | * Get number of in and outgoing links and redirects to this page. |
||
413 | * @return string[] Counts with the keys 'links_ext_count', 'links_out_count', |
||
414 | * 'links_in_count' and 'redirects_count' |
||
415 | */ |
||
416 | public function countLinksAndRedirects() |
||
420 | |||
421 | /** |
||
422 | * Get page views for the given page and timeframe. |
||
423 | * @param string|DateTime $start In the format YYYYMMDD |
||
424 | * @param string|DateTime $end In the format YYYYMMDD |
||
425 | * @return string[] |
||
426 | */ |
||
427 | public function getPageviews($start, $end) |
||
431 | |||
432 | /** |
||
433 | * Get the sum of pageviews over the last N days |
||
434 | * @param int [$days] Default 30 |
||
435 | * @return int Number of pageviews |
||
436 | */ |
||
437 | public function getLastPageviews($days = 30) |
||
448 | } |
||
449 |
Let’s take a look at an example:
In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.
Available Fixes
Change the type-hint for the parameter:
Add an additional type-check:
Add the method to the parent class: