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 SubscribeThe big thing that OCP wants us to take away from this conversation is this: try to imagine the future changes you are most likely to need to make, and architect, your code so that you will be able to make those changes without modifying existing classes.
We showed one common pattern to do this: by injecting an array or - iterable - of services instead of hardcoding all the logic right inside the class. There are also other patterns that you can use to accomplish OCP, including the "strategy pattern" - which is similar to what we did, but where you allow just one service to be passed in to handle some work - and the template method pattern. All of these are different flavors of the same thing: allowing functionality to be passed into a class, instead of living inside the class.
But the truth is, I don't love OCP. And I've got three reasons. First, even Uncle Bob - the father of the SOLID principles - knows that OCP is a "lie". OCP promises that, if you follow it correctly, you will never need to mess around with your old code. But a system can't be 100% OCP-compliant. Our SightingScorer
class is "closed" against the change of "adding new scoring factors". But what would happen if we suddenly needed a scoring factor to be able to multiply the existing score by a number... instead of just adding to it.
... lines 1 - 8 | |
class SightingScorer | |
{ | |
... lines 11 - 20 | |
public function score(BigFootSighting $sighting): BigFootSightingScore | |
{ | |
$score = 0; | |
foreach ($this->scoringFactors as $scoringFactor) { | |
$score += $scoringFactor->score($sighting); | |
} | |
return new BigFootSightingScore($score); | |
} | |
} |
This unexpected change would require us to, yup, modify the code in SightingScorer
. If we had anticipated this change, we could have added an abstraction to SightingScorer
to protect us from this new kind of change. But no one can perfectly predict the future: we can do our best... but often, we'll be wrong.
Of course, just because a principle isn't perfect doesn't meant we should never use it. But that leads me to the second reason that I don't love OCP: It creates unnecessary abstractions... which make our code harder to understand.
SightingScorer
is now closed against new scoring factors, which means we can add new scoring factors to our system without modifying the class. But at what cost? I can no longer open up this class and quickly understand how the believability score is calculated. Now I need to dig around to figure out which factors are injected... then go look at each individual factor class.
If you have a large team, being able to separate things into smaller pieces like this becomes more desirable. But, for example here at SymfonyCasts - with our brave team of about four - we would probably not make this change. It adds misdirection to our code, with a limited benefit.
And that leads me to my third and final reason for not loving OCP. And this one comes from Dan North's blog post.
He argues that the open-closed principle comes from an era when changes were expensive because of the need to compile a code, the fact that we hadn't really mastered the science of refactoring code yet, and because version control was done with CVS, which according to him, added to a mentality of wanting to make changes by adding new code, instead of modifying existing code.
In other words... OCP is a dinosaur! Dan's advice, which I agree with, is quite different than OCP. He says:
If you need code to do something else, change the code to make it do something else.
Quoting Dan, he says:
Code is not an asset to be carefully shrink-wrapped, and preserved, but a cost, a debt. All code is cost. So if I can take a big pile of existing code and replace it with smaller, more specific costs, than I'm winning at code.
I love that.
So how do I personally navigate OCP in the real world? It's pretty simple. If I'm building an open source library where the people who use my code will literally not be able to modify it, then I do follow a pattern like we used in SightingScorer
whenever I identify a change that a user might need to make. This gives my users the ability to make that change... without modifying the code in the class... which would be impossible for them.
But if I'm coding in a private application, I'm much more likely to keep all the code right inside the class. But this is not an absolute rule. Separating the code makes it easier to unit test and can help us follow the advice from SRP: writing code that "fits in your head". Larger teams will also probably want to split things more readily than smaller teams. As with all the SOLID principles, do your best to write simple code and... don't overthink it.
Next, let's turn to SOLID principle number three: the Liskov Substitution Principle.
It should be pointed out that OCP is *not only* useful for library devs (writing for a third party audience) but also for app devs (or even solo devs) that are building their own tooling/utility code (that would be extended) for use in other projects.
Beginners may not realize that they are actually "customers" of their own libraries or that OCP "thinking" can be applied to their own codebase (at least for generic tooling that can be used across projects).
Hey Fox C.
Thanks for your input, and I agree with you. OCP can be used for other reasons than just writting a third party library. I'd say, OCP comes handy when developing a "tool" that will be used by different programs (service classes), so you can encapsulate the main logic of the code and bring a way to extend its functionality to their clients.
Cheers!
// 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
}
}
I love the way you explain things! Many thanks for your simple way of delivering the information!