Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

DIP: Takeaways

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $8.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The two rules of the dependency inversion principle give us clear instructions on how two classes - like CommentSpamManager and RegexSpamWordHelper - should interact.

"Inversion"? What got Inverted?

But before we talk about the pros and cons of DIP... why is this called dependency inversion? What is the "inversion"?

This took me a long time to wrap my head around. I expected that dependency inversion somehow meant that the two classes literally started depending on each other in some... different way. Like suddenly we would inject the CommentSpamManager into RegexSpamWordHelper... instead of the other way around, actually "inverting" the dependency.

But, as you can see... that is not the case. On a high level, these two classes depend on each other in the exact same way as they always did: the low level, details class - RegexSpamWordHelper - is injected into the high-level class - CommentSpamManager.

The "inversion" part is... more of an abstract concept. Before we refactored our code to create and use the interface, I would have said:

CommentSpamManager depends on RegexSpamWordHelper. If we decide to modify RegexSpamWordHelper, we will then need to update CommentSpamManager to make it work with those changes. RegexSpamWordHelper is the boss.

But after the refactoring, specifically, after we created an interface based on the needs of CommentSpamManager, I would now say this:

CommentSpamManager depends on any class that implements CommentSpamCounterInterface. In reality, this is the RegexSpamWordHelper class. But if we decided to refactor how RegexSpamWordHelper works, it would still be responsible for implementing CommentSpamCounterInterface. In other words, when RegexSpamWordHelper changes, our high level CommentSpamManager class will not need to change.

That is the inversion: it's an inversion of control: a "reversal" of who is in charge. Thanks to the new interface, the high-level class - CommentSpamManager - has taken control over what its dependency needs to look like.

Pros and Cons of DIP

So now that we understand the dependency inversion principle, what are its benefits?

Simply put: DIP is all about decoupling. CommentSpamManager is now decoupled from RegexSpamWordHelper. We could even replace it with a different class that implements this interface without touching any code from the high-level class.

This is one of the core strategies to writing "framework agnostic" code. In this situation, developers create interfaces in their code and only depend on those interfaces, instead of on the interfaces or classes from whatever framework they're using.

However, in my code, I rarely follow the dependency inversion principle. Well, let me clarify. If I were working on an open source, reusable library, like Symfony itself, I would definitely create interfaces, like we just did. Why? Because I want to allow the users of my code to replace this service with some other class, like maybe someone wants to replace our simple RegexSpamWordHelper in their app with a class that uses an API to find these spam words.

But if I were writing this in my own application, I would skip creating the interface: I would make my code look like it originally did with CommentSpamManager relying directly on RegexSpamWordHelper with no interface.

Most Dependencies Don't Need Inverting

Why? As Dan North points out in his blog post: not all dependencies need to be inverted. If something you depend on will truly need to be swapped out for a different class or implementation later, then that dependency is almost more of an "option". If we had that situation, we probably would want to apply DIP. By creating and type-hinting an interface, we're saying:

Please pass me the "option" that you would like to use for counting spam words.

But, most of the time, to partially quote Dan:

Dependencies aren't options: they're just the way we are going to count spam words in this situation.

If you followed DIP perfectly, you end up with a code base with a lot of interfaces which are implemented by only one class each. That adds flexibility... which you likely won't need. The "cost" is misdirection: your code is harder to follow.

For example, in CommentSpamManager, it now takes a bit more work to figure out which class counts the spam words and how everything is working. And if you ever do try to change a dependency to use a different, concrete class, you might discover that, even though you followed DIP, it's not so easy change!

For example, changing from one database system to another is probably going to be an ugly job... even if you created an interface to abstract away the differences beforehand. It might still be worth doing... if you do think your database will change, but it's not a silver bullet that will make that an easy task.

So my advice is this: unless you're writing code that will be shared across projects, do not create an interface until you have more than one class that would implement it... which we actually saw earlier with our scoring factors. This is a perfectly nice use of interfaces.

But! I fully admit that not everyone agrees with my opinion on this! And if you do disagree, awesome! Do what you think is best. There are plenty of smart people out there that do create extra interfaces in their code to decouple from whatever frameworks or libraries they're using. I'm just not one of them.

SOLID in Review

Ok friends, that's it! We are done with the SOLID principles! Let's do a quick recap... using our simplified definitions.

One: the single responsibility principle says:

Write classes so that your code "fits in your head".

Two: the open-closed principle says:

Design your classes so that you can change their behavior without changing their code.

This is never entirely possible... and in my app code, I rarely follow this.

Three: the Liskov substitution principle says:

If a class extends a base class or implements an interface, make your class behave like it is supposed to.

PHP protects against most violations of this principle by throwing syntax errors.

Four: the interface segregation principle says:

If a class has a large interface - so a lot of methods - and you often inject the class and only use some of these methods - consider splitting your class into smaller pieces.

And five: the dependency inversion principle says:

Prefer type-hinting interfaces and allow each interface to be designed for the "high level" class that will use it, instead of for the low-level class that will implement it.

In my app, I do type-hint interfaces whenever they exist, usually because services from Symfony or other libraries provide an interface. But I don't create my own interfaces until I have multiple classes that need to implement them.

My opinions are, of course, just that: opinions! And I tend to be much more pragmatic than dogmatic... for better or worse. People will definitely disagree... and that's great! SOLID forces us to think critically.

Also the SOLID principles aren't the only "game" in town when it comes to writing clean code. There are design patterns, composition over inheritance, the law of demeter and other principles to guide your path.

If you have any questions or ideas, as always, we would love to hear from you down in the comments.

Alright, friends, seeya next time!

Leave a comment!

6
Login or Register to join the conversation
Przemysław L. Avatar
Przemysław L. Avatar Przemysław L. | posted 1 year ago

ISP/OCP on too simple an example devolves to SRP, and that is entirely fine. It tells use that most cases aren't that complex, so we avoid unintentional complexity.

ISP to shine need example where two different clients need different combination of interfaces created by splitting big one. Like one client need object that implements Interface A, but other client needs one that implement Interface B, but yet another one needs Interface A and interface B. This then leads to thinking about interfaces as capabilities, and about client code using interfaces as constraints on what capabilities its input needs to provide.
Not something that will show up in every application. However if it is correct abstraction its very powerful.

OCP to shine need example that is Expression Problem or comes close to it. There we start to see that some domains require degrees of freedom and also that its responsibility of developer to eliminate those freedoms that are useless for application. However Expression Problem is something that library code must deal with.

My biggest gripe with SOLID is that its acronym. Therefore wording of each rule is set in stone forever. Therefore if we get a better replacement , we can't just replace old principle, but instead we need to introduce this murky translation. Course was awesome at shining light on that in-between meaning! Thank you.

Reply

Hey Przemysław L.

Thanks for your insight, it's very valuable. Cheers!

Reply
Marcin Avatar

Ok, I should wait with my congratulations until the last chapter. I cannot agree with the takeaways you talked about. Interfaces are not only about DIP rule.

First thing I want to say it that we should learn junior developers to think about the code in more abstract way. When coding, one should not care about the implementation but the contract with an abstraction e.g. "I need any class that can count spam words. I don't care how this class does the job." and implement the high level class without caring about the concrete implementation. Then everything what has to be done is a proper container configuration. Encouraging them to inject concrete class because "it's not an option" has long term disadvantages. They are not experienced developers enough to decide if a dependency is an option.

Second thing. Interfaces are very helpful when you want to tweak dependencies based on environment. Traceable/Debuggable classes are good examples in dev env. Mocks are good examples in both dev and test environment. In test env interfaces are very helpful when you have to write stubs.

Third thing. I had many tasks when I had to tweak into/add behaviour to some existing code but I just wasn't able to because the whole process was so tighten up by the concrete classes depending on other concrete classes and so on. If I had an interface I could have done something but I had to end up with violating some rules (including my personal rule not to write bad code).
It's like a snowball. Every next bad line of code, bad design speeds the snowball up.

Reply

Hey Fabien, thanks for your feedback, you mentioned a few good points about when introducing an interface may be helpful. The key thing of SOLID is to have a set of ground rules that may guide you in designing your application. It should aid you at making a design desicion but without forcing your application to be SOLID compliant, and you should be aware of when it's better to violate a pricinple than following it.
Besides that, I'd like to say that it's better to wait for introducing an interface into your application (I'm talking about your own application's code, not about a reusable library/bundle) because everytime you add a new interface into your application it will aslo increase its complexity. So, my rule for adding interfaces is to wait until you have at least 2 different implementations for the interface I want to create. I prefer to add tests and not over-invest in my design, and later, if needed, refactor my design as much as required (an easy thing to do when you have tests covering your back).

Cheers!

Reply
Marcin Avatar

Thank you for this tutorial. I wish it wasn't only 18 episodes long. I think the topics like SOLID, Design Patterns et cetera should deserve more attention. Those require focus, thinking and a lot of hours worked but the benefits are huge. It's something one has to understand. On the other side we have tools/tutorials about things a developer has to learn rather than understand and this it what I diagnose as a main problem - people get to much features out of the box and as a reason they are not forced to think. They don't learn how something works but how to use it. I see this problem a lot even with medium or senior (btw. what does it mean nowadays?) developers.

Reply

Hey Fabien,

Haha, yeah, it's always sad when the course ends :) Our mission is to explain complex things short and easy, I hope we were able to do so in this tutorial. Design Patterns deserve its own course, and we will release it someday (not specific estimations yet). This course covered only SOLID principles. For each principle in SOLID we tried to make up a good example to make it easier to understand, we didn't explain the theory only. Tutorial length is always a balance between too long tutorials where you start losing the main idea and too short where some topics may be not covered. If you think you didn't get something from this tutorial, try to rewatch it again in a while, this may help. Or, you can always ask your questions regarding the tutorial in comments below videos.

> They don't learn how something works but how to use it. I see this problem a lot even with medium or senior (btw. what does it mean nowadays?) developers.

Good tools gives you more power to complete your task simpler and faster. It also a balance. And if you're curious - you still may look at the tool's source code to see how it works behind the scene and to understand it better. Thankfully we're talking about Open Source Software and it's available to everyone. So, if you want to learn and want to know more - you can do this, nothing stops you. Anyway, you have to think about the task you have to solve, probably not all the levels as if you have a tool - low-level already was thought by the devs who designed that tool for you. But you still have to think up-level how to integrate that tool into your project and solve the task. You can refuse some low-level tools and write all that code yourself, but this way you would spend much more time on a simple task than you could. And it depends on your if it's good for you or no. If you want to learn - great, go for it. If you have some deadlines and have to make that feature work fast - probably writing it from scratch would be overkill when there's a ready-to-use solution. The downside of writing low-level things is that you at least have to test them. Usually tools are well tested and well written, because many devs are looking at the source code and constantly improving it, reporting bugs, fixing them - it's difficult to achieve when you work on that by yourself. But for better understanding and better learning - I think it would be good.

So, if you want to know more about how tools are working behind the scene - start looking at the source code, try to understand it first, then try to find bottleneck parts of the code, and if you were lucky - try to report it by creating an issue or improve it by sending a PR. In other words, you can try to become a maintainer of that tool, and I bet you will learn a lot from it.

Anyway, thank you for this feedback and your interest in SymfonyCasts tutorials!

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice