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 Amount 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 Amount, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 12 | class Amount |
||
| 13 | { |
||
| 14 | /** |
||
| 15 | * @var string |
||
| 16 | */ |
||
| 17 | private $amount; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * @var array Substitution map for signal strings |
||
| 21 | */ |
||
| 22 | private static $signals = [ |
||
| 23 | '0'=>'å', '1'=>'J', '2'=>'K', '3'=>'L', '4'=>'M', '5'=>'N', '6'=>'O', '7'=>'P', '8'=>'Q', '9'=>'R' |
||
| 24 | ]; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * Create amount from numerical string |
||
| 28 | * |
||
| 29 | * @throws InvalidArgumentException If $amount is not valid |
||
| 30 | */ |
||
| 31 | 381 | public function __construct(string $amount) |
|
| 39 | |||
| 40 | /** |
||
| 41 | * Create amount from integer or floating point number |
||
| 42 | * |
||
| 43 | * It is important to note that computers internally use the binary floating |
||
| 44 | * point format and cannot accurately represent a number like 0.1, 0.2 or |
||
| 45 | * 0.3 at all. Using floating point numbers leads to a loss of precision. |
||
| 46 | * For example `floor((0.1+0.7)*10)` will usually return 7 instead of the |
||
| 47 | * expected 8, since the internal representation will be something like |
||
| 48 | * 7.9999999999999991118.... |
||
| 49 | * |
||
| 50 | * For this reason floats should never ne used to store monetary data. This |
||
| 51 | * method exists for rare situations when converting from native formats is |
||
| 52 | * inevitable. Unless you know what you are doing it should NOT be used. |
||
| 53 | * |
||
| 54 | * @param int|float $number |
||
| 55 | * @throws InvalidArgumentException If $number is not an integer or float |
||
| 56 | */ |
||
| 57 | 19 | public static function createFromNumber($number, int $precision = -1): Amount |
|
| 74 | |||
| 75 | /** |
||
| 76 | * Create amount from a formatted string |
||
| 77 | */ |
||
| 78 | 4 | public static function createFromFormat(string $amount, string $point = '.', string $sep = ''): Amount |
|
| 88 | |||
| 89 | /** |
||
| 90 | * Create amount from signal string |
||
| 91 | * |
||
| 92 | * Signal strings does not contain a decimal digit separator. Instead the |
||
| 93 | * last two digits are always considered decimals. For negative values the |
||
| 94 | * last digit is converted to an alphabetic character according to schema: |
||
| 95 | * 0 => å, 1 => J, 2 => K, ... 9 => R. |
||
| 96 | * |
||
| 97 | * @throws InvalidArgumentException If $signalStr is not a valid signal string |
||
| 98 | */ |
||
| 99 | 12 | public static function createFromSignalString(string $signalStr): Amount |
|
| 117 | |||
| 118 | /** |
||
| 119 | * Get the raw stored amount |
||
| 120 | */ |
||
| 121 | 213 | public function getAmount(): string |
|
| 125 | |||
| 126 | /** |
||
| 127 | * Get new Amount rounded to $precision number of decimal digit using $rounder |
||
| 128 | */ |
||
| 129 | 201 | public function roundTo(int $precision = -1, Rounder $rounder = null): Amount |
|
| 138 | |||
| 139 | /** |
||
| 140 | * Get amount as string |
||
| 141 | */ |
||
| 142 | 197 | public function getString(int $precision = -1, Rounder $rounder = null): string |
|
| 150 | |||
| 151 | /** |
||
| 152 | * Get amount as string |
||
| 153 | */ |
||
| 154 | 2 | public function __tostring(): string |
|
| 158 | |||
| 159 | /** |
||
| 160 | * Get amount as integer |
||
| 161 | */ |
||
| 162 | 1 | public function getInt(Rounder $rounder = null): int |
|
| 166 | |||
| 167 | /** |
||
| 168 | * Get amount as float |
||
| 169 | * |
||
| 170 | * It is important to note that computers internally use the binary floating |
||
| 171 | * point format and cannot accurately represent a number like 0.1, 0.2 or |
||
| 172 | * 0.3 at all. Using floating point numbers leads to a loss of precision. |
||
| 173 | * For example `floor((0.1+0.7)*10)` will usually return 7 instead of the |
||
| 174 | * expected 8, since the internal representation will be something like |
||
| 175 | * 7.9999999999999991118.... |
||
| 176 | * |
||
| 177 | * For this reason floats should never ne used to store monetary data. This |
||
| 178 | * method exists for rare situations when converting to native formats is |
||
| 179 | * inevitable. Unless you know what you are doing it should NOT be used. |
||
| 180 | */ |
||
| 181 | 2 | public function getFloat(int $precision = -1, Rounder $rounder = null): float |
|
| 185 | |||
| 186 | /** |
||
| 187 | * Get amount as signal string |
||
| 188 | * |
||
| 189 | * Signal strings does not contain a decimal digit separator. Instead the |
||
| 190 | * last two digits are always considered decimals. For negative values the |
||
| 191 | * last digit is converted to an alphabetic character according to schema: |
||
| 192 | * 0 => å, 1 => J, 2 => K, ... 9 => R. |
||
| 193 | */ |
||
| 194 | 11 | public function getSignalString(Rounder $rounder = null): string |
|
| 205 | |||
| 206 | /** |
||
| 207 | * Get new Amount with the value of $amount added to instance |
||
| 208 | */ |
||
| 209 | 6 | View Code Duplication | public function add(Amount $amount, int $precision = -1): Amount |
| 219 | |||
| 220 | /** |
||
| 221 | * Get new Amount with the value of $amount subtracted from instance |
||
| 222 | */ |
||
| 223 | 6 | View Code Duplication | public function subtract(Amount $amount, int $precision = -1): Amount |
| 233 | |||
| 234 | /** |
||
| 235 | * Get new Amount with the value of instance multiplied with $amount |
||
| 236 | * |
||
| 237 | * @param int|float|string|Amount $amount |
||
| 238 | */ |
||
| 239 | 8 | public function multiplyWith($amount, int $precision = -1): Amount |
|
| 249 | |||
| 250 | /** |
||
| 251 | * Get new Amount with the value of instance divided by $amount |
||
| 252 | * |
||
| 253 | * @param int|float|string|Amount $divisor |
||
| 254 | */ |
||
| 255 | 8 | public function divideBy($divisor, int $precision = -1): Amount |
|
| 271 | |||
| 272 | /** |
||
| 273 | * Compare to amount |
||
| 274 | * |
||
| 275 | * @return int 0 if instance and $amount are equal, 1 if instance is larger, -1 otherwise. |
||
| 276 | */ |
||
| 277 | 24 | public function compareTo(Amount $amount, int $precision = -1): int |
|
| 285 | |||
| 286 | /** |
||
| 287 | * Check if instance equals amount |
||
| 288 | */ |
||
| 289 | 10 | public function equals(Amount $amount, int $precision = -1): bool |
|
| 293 | |||
| 294 | /** |
||
| 295 | * Check if instance is less than amount |
||
| 296 | */ |
||
| 297 | 15 | public function isLessThan(Amount $amount, int $precision = -1): bool |
|
| 301 | |||
| 302 | /** |
||
| 303 | * Check if instance is less than or equals amount |
||
| 304 | */ |
||
| 305 | 2 | public function isLessThanOrEquals(Amount $amount, int $precision = -1): bool |
|
| 309 | |||
| 310 | /** |
||
| 311 | * Check if instance is greater than amount |
||
| 312 | */ |
||
| 313 | 8 | public function isGreaterThan(Amount $amount, int $precision = -1): bool |
|
| 317 | |||
| 318 | /** |
||
| 319 | * Check if instance is greater than or equals amount |
||
| 320 | */ |
||
| 321 | 6 | public function isGreaterThanOrEquals(Amount $amount, int $precision = -1): bool |
|
| 325 | |||
| 326 | /** |
||
| 327 | * Check if amount is zero |
||
| 328 | */ |
||
| 329 | 1 | public function isZero(int $precision = -1): bool |
|
| 333 | |||
| 334 | /** |
||
| 335 | * Check if amount is greater than zero |
||
| 336 | */ |
||
| 337 | 1 | public function isPositive(int $precision = -1): bool |
|
| 341 | |||
| 342 | /** |
||
| 343 | * Check if amount is less than zero |
||
| 344 | */ |
||
| 345 | 12 | public function isNegative(int $precision = -1): bool |
|
| 349 | |||
| 350 | /** |
||
| 351 | * Get new Amount with sign inverted |
||
| 352 | */ |
||
| 353 | 1 | public function getInverted(): Amount |
|
| 357 | |||
| 358 | /** |
||
| 359 | * Get new Amount with negative sign removed |
||
| 360 | */ |
||
| 361 | 11 | public function getAbsolute(): Amount |
|
| 365 | |||
| 366 | /** |
||
| 367 | * Allocate amount based on a list of ratios |
||
| 368 | * |
||
| 369 | * @param int[]|float[] $ratios List of ratios |
||
| 370 | * @param int $precision Optional decimal precision used in calculation |
||
| 371 | * @return Amount[] The allocated amounts |
||
| 372 | */ |
||
| 373 | 4 | public function allocate(array $ratios, int $precision = -1): array |
|
| 394 | |||
| 395 | |||
| 396 | /** |
||
| 397 | * Get the default display precision |
||
| 398 | */ |
||
| 399 | 4 | public static function getDisplayPrecision(): int |
|
| 403 | |||
| 404 | /** |
||
| 405 | * Get the default internal precision used in computations |
||
| 406 | */ |
||
| 407 | 45 | public static function getInternalPrecision(): int |
|
| 411 | |||
| 412 | /** |
||
| 413 | * Get default rounding strategy |
||
| 414 | */ |
||
| 415 | 197 | public static function getDefaultRounder(): Rounder |
|
| 419 | |||
| 420 | /** |
||
| 421 | * Get a numerical string from input |
||
| 422 | * |
||
| 423 | * @param int|float|string|Amount $amount |
||
| 424 | * @throws InvalidArgumentException If $amount is does not evaluate to a numberical string |
||
| 425 | */ |
||
| 426 | 11 | private function castToString($amount): string |
|
| 444 | } |
||
| 445 |