Practical PHP Refactoring: Replace Magic Number with Symbolic Constant
Join the DZone community and get the full member experience.
Join For FreeIn the scenario of today, we have a literal number, such as 42, scattered across the code base. Alternatively, this number may be just written in a single place, but buried into many lines of code that make difficult to understand if that's the single place where a change to its value can be made.
The refactoring hides the number behind a constant, for documentation purposes and to eliminate duplication. It also targets all the other literal or calculated values which depend on the original magic number.
Why should I introduce an additional constant?
For starters, when you want to eliminate the duplication of the literal value in different place. If the number is complex enough (3.14, 0xCAFEBABE) you may think that you can grep for it in case it changes; however, the only acceptable magic numbers to use directly in code are 0, 1, and 2 in some cases.
The usage of literal values is problematic especially when there are dependencies between them. An old example is that in a data structure representing a deck of 52 cards, to access the last element you will write:
$deck[51]
which won't be found by grep or any other tool without artificial intelligence. Another issue is the different versions of the number:
3.14 3.1415 3.14159
In short, replacing all the numbers instances and appearances in dependent expressions with a constant will centralize the value and make it far easier to change.
The second motivation for introducing a constant is that it expresses a concept (second rule of simple design, XP) that serves to render the code self-explaining: a constant has a name that describes its usage. In a sense, you focus on the role the constant plays (number of cards) instead of its particular implementation (52).
When we say that we don't want comments, we want however code with symbolic constants instead of numbers pulled out of an hat: otherwise comments are necessary to explain their meaning.
This refactoring usually comes with no cost in performance, so focusing on this issue would be a micro-optimization. The refactoring cites a "constant" instead of a private field or something similar, just because of performance optimizations that can be made on constants in many languages, substituting them in the code at compile time.
Alternatives
Fowler suggests looking for alternatives before creating a constant. An alternative means that we could write code that does not assume a particular value of the magic number, but it's still correct:
$deck[count($deck)-1] // access the last element of an array
Other examples of alternatives include replacing a type code with a class (with a refactoring we will see later in this series) and use already existing constants. The PHP core provides constants for many mathematical quantities:
M_PI M_LN2 M_PI_2 M_SQRT2
Steps
- Declare a constant: its value should be the magic number.
- Find all occurrences of the literal number: pay attention also to literals that may be a function of the magic number, like 51 in the above example. Unfortunately, there are no algorithmic ways for finding them (apart from tests that fail when you change the constant.)
- Check if the literal is an instance of the magic number or a collision. In our example, 52 may also be the number of allowed vehicle types or the Italian tax rate. Change the matching numbers to a reference to the constant.
After the changes, the test should work flawlessly. This is a small scale refactoring, so no test should change.
You can also try, as said in step 2, to change the value of the constant containing the magic number and verify that the whole system still passes the tests. In that case, it means nor the tests nor unreviewed parts of the application still have the literal (or a function of it) embedded in their code.
Example
In the example, we start from a class with various magic numbers hidden in it: 4 suits, 13 cards for each suit, and 52 as the total number.
<?php class ReplaceMagicNumberWithSymbolicConstant extends PHPUnit_Framework_TestCase { public function testDeckIsFilledWithCardsInitially() { $deck = new Deck(); $this->assertEquals(52, count($deck)); } public function testDeckCanDrawAllItsCards() { $deck = new Deck(); for ($i = 0; $i < 52; $i++) { $card = $deck->draw(); $this->assertGreaterThanOrEqual(1, $card); $this->assertLessThanOrEqual(13, $card); } $this->assertEquals(0, count($deck)); } } class Deck implements Countable { private $cards; public function __construct() { $this->cards = array(); for ($i = 0; $i < 4; $i++) { $this->cards = array_merge($this->cards, range(1, 13)); } } public function count() { return count($this->cards); } public function draw() { return array_shift($this->cards); } }
We create the constants, and plan to derive 52 from the other values:
class Deck implements Countable { const RANGE = 13; const SUITS = 4; private $cards; public function __construct() { $this->cards = array(); for ($i = 0; $i < 4; $i++) { $this->cards = array_merge($this->cards, range(1, 13)); } } public function count() { return count($this->cards); } public function draw() { return array_shift($this->cards); } }
We replace the occurrences with references to the constant. In the test, we use a quick calculation to find out the total number of cards.
<?php class ReplaceMagicNumberWithSymbolicConstant extends PHPUnit_Framework_TestCase { private $totalCards; public function setUp() { $this->totalCards = Deck::SUITS * Deck::RANGE; } public function testDeckIsFilledWithCardsInitially() { $deck = new Deck(); $this->assertEquals($this->totalCards, count($deck)); } public function testDeckCanDrawAllItsCards() { $deck = new Deck(); for ($i = 0; $i < $this->totalCards; $i++) { $card = $deck->draw(); $this->assertGreaterThanOrEqual(1, $card); $this->assertLessThanOrEqual(Deck::RANGE, $card); } $this->assertEquals(0, count($deck)); } } class Deck implements Countable { const RANGE = 13; const SUITS = 4; private $cards; public function __construct() { $this->cards = array(); for ($i = 0; $i < self::SUITS; $i++) { $this->cards = array_merge($this->cards, range(1, self::RANGE)); } } public function count() { return count($this->cards); } public function draw() { return array_shift($this->cards); } }
Opinions expressed by DZone contributors are their own.
Comments