If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeReady for principle number 4? It's the interface segregation principle - or ISP. It says:
Clients should not be forced to depend on interfaces that they do not use.
That's not a bad definition! But I want to clarify that word "interface". It is not necessarily referring to a literal interface. It's referring to the abstract concept of an interface, which generally means "the public methods" of a class... even if it doesn't technically implement an interface. The meaning of interface here is: the "stuff that you can do with an object" when I give it to you.
So let me try to give this an even simpler definition:
Build small, focused classes instead of big, giant classes.
This definition reminds me a lot of the single responsibility principle... and that's true! But the interface segregation principle kind of looks at this from the other direction: from the perspective of who uses the class, not from the perspective of the class itself. Again, the original definition is:
Clients should not be forced to depend upon interfaces - so basically methods - that they do not use.
For example, suppose you've accidentally built a giant class called ProductManager
with a ton of methods on it. Whoops! Then, somewhere in your code, you need to call just one of those methods. This other class is called the "client" because it is using our giant ProductManager
class. And unfortunately, even though it only needs one method from the ProductManager
, it needs to inject the whole giant object. It's forced to depend on an object whose interface - whose public methods - are many more than it actually needs.
Why is this a problem? Let's answer that question a bit later after we play with a real world example. Because... management has asked us to make yet another change to our believability score system! If a sighting receives a score of less than 50 points... but it has three or more photos, we will give it a boost: 5 extra points per photo. This... was not a change we anticipated! Darn! Our scoring factors do have the ability to add to the score... but they don't have the ability to see the final score and then modify it.
No problem: let's add a second method to the interface that has the ability to do that. Call it, how about, public function adjustScore()
. In this case, it's going to receive the int $finalScore
that's just been calculated and the BigFootSighting
that we're scoring. It will return the new int
final score. You can add some PHPDoc above this to better explain the purpose of the method if you want.
... lines 1 - 6 | |
interface ScoringFactorInterface | |
{ | |
... lines 9 - 15 | |
public function adjustScore(int $finalScore, BigFootSighting $sighting): int; | |
} |
In a minute, we're going to call this from inside of SightingScorer
after the initial scoring is done. But first, let's open PhotoFactor
and add the new bonus logic.
At the bottom, go to Code -> Generate - or Command + N on a Mac - select "Implement Methods" and implement adjustScore()
. Say $photosCount = $sighting->getImages()
- don't forget to count these - then if the $finalScore
is less than 50 and $photosCount
is greater than two - the $finalScore
should get plus equals $photosCount * 5
. At the bottom, return $finalScore
.
... lines 1 - 6 | |
class PhotoFactor implements ScoringFactorInterface | |
{ | |
... lines 9 - 22 | |
public function adjustScore(int $finalScore, BigFootSighting $sighting): int | |
{ | |
$photosCount = count($sighting->getImages()); | |
if ($finalScore < 50 && $photosCount > 2) { | |
$finalScore += $photosCount * 5; | |
} | |
return $finalScore; | |
} | |
} |
New logic done! But now... what do we do with all the other classes that implement ScoringFactorInterface
? Unfortunately, for PHP to even run, we do need to add the new method to each class. But we can just make it return $finalScore
.
So at the bottom of CoordinatesFactor
, go back to Code -> Generate - select "Implement Methods", generate adjustScore()
, and return $finalScore
.
... lines 1 - 6 | |
class CoordinatesFactor implements ScoringFactorInterface | |
{ | |
... lines 9 - 24 | |
public function adjustScore(int $finalScore, BigFootSighting $sighting): int | |
{ | |
return $finalScore; | |
} | |
} |
Copy, this close CoordinatesFactor
, go to DescriptionFactor
and add it to the bottom. Do the same thing inside of TitleFactor
.
... lines 1 - 6 | |
class TitleFactor implements ScoringFactorInterface | |
{ | |
... lines 9 - 24 | |
public function adjustScore(int $finalScore, BigFootSighting $sighting): int | |
{ | |
return $finalScore; | |
} | |
} |
Finally, we can update SightingScorer
. Add a second loop after calculating the score: for each $this->scoringFactors
as $scoringFactor
, this time say $score = $scoringFactor->adjustScore()
... and pass in $score
and $sighting
.
... lines 1 - 8 | |
class SightingScorer | |
{ | |
... lines 11 - 20 | |
public function score(BigFootSighting $sighting): BigFootSightingScore | |
{ | |
... lines 23 - 27 | |
foreach ($this->scoringFactors as $scoringFactor) { | |
$score = $scoringFactor->adjustScore($score, $sighting); | |
} | |
... lines 31 - 32 | |
} | |
} |
Done! By the way, you might argue that the order of scoring factors is now relevant. That's true! But... we're not going to worry about that for simplicity... and because that isn't relevant to this principle. But, there is a way to give a tagged service a higher priority in Symfony so that it is passed earlier or later than other scoring factors.
If, at this point, something is itching you, that might be because we just violated the open-closed principle! We had to modify the score()
method in order to add this new behavior. But that's okay! It highlights the tricky nature of OCP: we didn't anticipate this kind of change! You can't "close" a class against all kinds of changes: you can only close it against the changes that you correctly predict.
Looking at our new interface and the classes that implement it, you can probably feel that it's not... ideal that all of these classes need to implement this method... even though they don't really care about it. Next: we're going to make this even more obvious, refactor to a better solution, and finally discuss the key takeaways from the interface segregation principle.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.1
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2", // 2.3.1
"doctrine/doctrine-migrations-bundle": "^3", // 3.1.1
"doctrine/orm": "^2", // 2.8.4
"knplabs/knp-time-bundle": "^1.15", // v1.16.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^6.0", // v6.1.2
"symfony/console": "5.2.*", // v5.2.6
"symfony/dotenv": "5.2.*", // v5.2.4
"symfony/flex": "^1.9", // v1.18.7
"symfony/form": "5.2.*", // v5.2.6
"symfony/framework-bundle": "5.2.*", // v5.2.6
"symfony/http-client": "5.2.*", // v5.2.6
"symfony/mailer": "5.2.*", // v5.2.6
"symfony/property-access": "5.2.*", // v5.2.4
"symfony/property-info": "5.2.*", // v5.2.4
"symfony/security-bundle": "5.2.*", // v5.2.6
"symfony/serializer": "5.2.*", // v5.2.4
"symfony/twig-bundle": "5.2.*", // v5.2.4
"symfony/validator": "5.2.*", // v5.2.6
"symfony/webpack-encore-bundle": "^1.6", // v1.11.1
"symfony/yaml": "5.2.*", // v5.2.5
"twig/cssinliner-extra": "^3.3", // v3.3.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/twig": "^2.12|^3.0" // v3.3.0
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.2", // 3.4.0
"fakerphp/faker": "^1.13", // v1.14.1
"symfony/debug-bundle": "^5.2", // v5.2.4
"symfony/maker-bundle": "^1.13", // v1.30.2
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/stopwatch": "^5.2", // v5.2.4
"symfony/var-dumper": "^5.2", // v5.2.6
"symfony/web-profiler-bundle": "^5.2" // v5.2.6
}
}