The architectural complexity of a system is directly related to the number, and in some sense - the quality of dependencies in this system. But more, of course, with the number.
Although we would like to believe the first intuitive assumption that this dependency is linear, where each new dependency increases some abstract COMPLEXITY counter by 1, in practice, this is not the case.
It is almost always exponential growth.
Many engineering principles aim to make our systems more reliable in operation and simpler to understand.
In light of the complexity topic, mainstream development recommends developers manage dependencies in the project following a simple rule - "Depend only on what is *really* necessary."
Seems pretty simple. In a more engineering interpretation, we're talking about the I in SOLID, which stands for - Interface Segregation Principle.
Don't be fooled by the definition itself, because ISP implies not only interface dependencies, but also dependencies between libraries, functions, classes, data, and so on.
Like all other elements of SOLID, ISP can be quickly and intuitively understood by somehow experienced developers. However, it seems that applying ISP in practice is much harder than talking about its meaning.
We've all been there - implementing some class, and having the task to implement only specific functionality, like searching by ID, and then we all often get this brilliant idea - "I need to add methods for adding and deleting right away, they'll be needed anyway!"
The mental reasoning from a purely human point of view is understandable and may be justified, for example - it seems very convenient to give another developer/user a larger, richer interface!
Of course, this is already a violation of ISP.
Following the logic above, as a result, already at the testing stage of what we've written, we'll bump into interesting moments - how do we know that to test our functionality we need to test only _this_ search method (mock what's related to it, write fixtures, etc.), and not 2 other methods that we wrote?
Well, it's simple, solely by knowing that currently only _this_ method is used in the implementation!
As a result, we wrote N extra methods and wrote unnecessary fixtures/mocks N times for each, and in the tests themselves we're testing the implementation. The latter point, of course, is always cool at the moment but loses most of the value and essence of testing - testing the behavior of the system.
At this point, I thought that TDD might help us here; when we turn the "default" development process inside out and implement only what we described in the tests. However, nothing still prevents us from showing mindset persistence and immediately writing tests for "extra" methods. See the trap?
Whether with TDD or just test coverage - we need to think not about implementation and test not the implementation, but behavior. Sorry for this sidekick, today we're not talking about testing.
Returning to the topic of ISP, instead of bloating the interface, in our arsenal we have exactly the opposite trick - organizing many small interfaces, each of which provides up to calling just one function/method.
This is a cognitively simple trick for any developer in terms of implementation, and quite valuable in terms of result - now services can declare and explicitly depend only on what they need.
As a substantial bonus - the code becomes more visual and readable, and when writing tests, we may not need to dig into the implementation at all.
In some sense, this approach leaves room for super-impulsive developers to immediately define all the "mini" interfaces for all the "definitely will be needed soon" methods. The main savior thing is not to stuff them into the implementation without necessity and ruthlessly delete them in time during self-code-review when you realize that it's not used in working implementation :)
Ok, Ok! You may keep it!!! Just not stuff them all into the implementation!
Perhaps reading these lines, you already have a question - "What?! Do I need to implement each such interface separately?".
Well, of course not.
In C# and Java, one class can implement multiple interfaces, in Python there is multiple inheritance "for free." In Go... in Go, everything is as flat and explicit as possible (even it's called "implicit implementation") - and interfaces are automatically "stretched" over structures implementing their methods.
In Rust, you can explicitly implement several traits for a structure.
¯\_(ツ)_/¯
With new requirements, for example, initially, we needed to implement search by ID, and now by name or some other field, the approach with "mini" interfaces is still applicable and flexible enough.
At the same time, depending on the language used, we can avoid listing implemented interfaces as separate fields - using generics (Java/C#/Rust), interface composition (Go), and multiple inheritance (Python. Yes, again.)
The approach we've considered, while good, probably causes very mixed feelings among developers who love and constantly use IoC containers.
IoC containers are good for automatic injection of interfaces, and these interfaces almost always... how to put it mildly... "VERY HUGE."
The "mini" interfaces may be far from convenient, beautiful, or justified to inject using containers (underline as appropriate.). Especially when generics have come into use.
Injecting something like a generic interface using IoC containers will inevitably be complex, and opaque, exactly opposite to the clarity and simplicity we, as professional engineers, would love to follow.
In mainstream programming, IoC containers are used as magic boxes to make the developer's life easier by automating the creation of necessary objects.
So, IoC containers make the programmer's life easier (seemingly) but lead to the bloating of interfaces. Bloated interfaces practically automatically lead to leakage of dependencies, and quite often - unnecessary dependencies.
In addition, these dependencies may be completely unobvious when a class receives injections of such interfaces.
As a result, behind the abstraction of the IoC container, we simply blur the real abstraction, lose clear boundaries, lose the ability to simply enough track and reason about real dependencies, and, ultimately, debug the system normally, without risking our mental health.
It turns out that almost any codebase with massive use of IoC containers is one continuous violation of ISP.
And do we just turn a blind eye to this?