Practical PHP Refactoring: Extract Interface
Join the DZone community and get the full member experience.
Join For FreeA concrete class still defines an implicit interface by itself, as the set of its public methods. When the called interface is a subset of this, or it is depended upon in multiple places, it is interesting to make it explicit.
The Extract Interface refactoring creates an interface from an existing concrete class.
Why?
Interfaces are not strictly necessary for computation: PHP is already Turing-complete without them. Interfaces describe what a collaborator should accomplish, while classes describe how, with all the necessary code. However, apart from documenting a contract between two classes, they have several benefits.
In the realm of TDD, they enable outside-in testing with mocks: this means you can test-driver a class together with the interfaces it depends upon, even if the real implementation of its collaborators do not exist, or do not implement them, yet.
Outside-in TDD helps shaping interfaces from the point of view of the caller instead of the callee. As such, these interfaces should reside in a separate folder/namespace from the implementing code (for the Dependency Inversion Principle).
What if a class has multiple calling points? It can possibly expose multiple interfaces, where every client depends exactly at most one of them and not on any additional methods.
Steps
- Create an empty interface. If you can only think of a name such as Set and ISet for a class and interface, start by saving the good name for the interface: Set and TreeBasedSet is better than the former case.
- Declare common operations in the interface, with method signatures identical to the original. In case only some operations are called by the client, only this subset should be copied in the interface.
- Add implements keywords to tie existing concrete classes to the interface.
- Simplify the client code by making it dependent on the interface where possible.
Examples of the last step are multiple:
- the tests can now use a mock or a stub easily (even coded by hand instead of a generated one), since starting from interface you'll have a small set of methods to override.
- Type hints can be written referring to the interface name whenever the client code calls only the methods listed in the interface.
- renaming the interface and the concrete classes to reflect a role (in the interface) and implementation peculiarities (in the concrete classes).
- Add or drop methods in the contract to fit the desires of the caller.
- Extract some functionality into a Decorator or a Composite, which are multiple implementation of an interface.
Example
In the initial state, the Money presenter object is depending on a concrete class, EuroLocale.
<?php class ExtractInterface extends PHPUnit_Framework_TestCase { public function testShouldDisplayAMoneyAmount() { $locale = new EuroLocale(); $money = new Money("42"); $this->assertEquals("42 €", $money->display($locale)); } } class EuroLocale { public function format($amount) { return $amount . ' €'; } } class Money { private $amount; /** * @param string $amount to keep precision */ public function __construct($amount) { $this->amount = $amount; } public function display(EuroLocale $locale) { return $locale->format($this->amount); } }
We create an interface, Locale, and just the single public method is extracted. Anything more would not be shown in this example, and won't be brought up into the interface.
interface Locale { /** * @return string */ public function format($amount); }
We add an implements keyword, and simplify the type hint dependency to target just Locale.
interface Locale { /** * @return string */ public function format($amount); } class EuroLocale implements Locale { public function format($amount) { return $amount . ' €'; } } class Money { private $amount; /** * @param string $amount to keep precision */ public function __construct($amount) { $this->amount = $amount; } public function display(Locale $locale) { return $locale->format($this->amount); } }
Usually a dependency on a concrete class, which may have a dozen different methods, does not let us refactor tests introducing Test Doubles. This happens because we are unsure about which methods we should redefine: which are called in this test method? And by Money objects in general?
Now that we have an interface, we define explicitly that only format() is called, even if EuroLocale may have many others. So we can break the tests in two, one targeting Money and the other EuroLocale. Note the order of the unit tests: they are completely independent, so we can test (and thus develop) Money first.
<?php class ExtractInterface extends PHPUnit_Framework_TestCase { public function testShouldFormatItsAmountBeforeDisplayingIt() { $locale = $this->getMock('Locale'); $locale->expects($this->once())->method('format')->with("42")->will($this->returnValue('42 SIMBOL')); $money = new Money("42"); $this->assertEquals("42 SIMBOL", $money->display($locale)); } public function testShouldFormatAnAmountWithTheEuroSighn() { $locale = new EuroLocale(); $this->assertEquals("42 €", $locale->format("42")); } }
In the real world, there would have been dozens of tests involving both objects, with a lot of setup code and machinery. Interfaces let us break direct dependencies and test classes in real isolation, an approach that scales better to many tests. For example, in the final version every test for a new formatting options (like 42.00 or 10,000.00 EUR) needs only to create a string instead of a Money object. Equivalently, any test for a new user of format() does not have to care about particular formatting rules.
Opinions expressed by DZone contributors are their own.
Comments