I am attempting to refactor a component of a large project, which currently has a lot of dependencies to global state of the project environment. The goal (for my taste) is a "SOLID" architecture, where everything can be properly tested.
Sometimes the dependency may simply be a string or value: A specific directory, a machine name, sometimes maybe an array.
However, the dependency can not be injected as a simple value at construction time, because the value might not be available yet.
The value might be coming from a dependency injection container, from the configuration system, etc. Currently the class itself calls to legacy functions or static methods of these subsystems, to get the respective values. This is bad because of the global state dependency, and because the component knows too much about these subsystems.
Idea
So my idea was to create one "data provider" interface per dependency, with different implementations for "real" and "fake". An interface expresses one of the needs of a component that depends on it, not more. Thus, most of them have just one method. Some are being reused, but not a lot.
The main functionality is also split into sub-components with interfaces. Some of them have cache or buffer decorators. One part is split into a chain of processors, which implement a "(Name)ProcessorInterface".
Problem: Too many classes and interfaces.
This works great, and everything is super-testable.
I use the same style all the time in my own projects, and would not hesitate to do it again here.
But here it is not my choice. This is a huge OSS project, with a large community of developers, which I need to convince, if I want to do it this way. And the rest of the code, for the most part, does not look like this.
The number of classes and interfaces is getting very high. And a lot of them are just "internal", so they are not meant as part of a public API.
Here is one example of classes I am/was going to add. This is too big to post here. The original was simply a call to drupal_get_profile()
, which of course makes testing a lot more difficult.
Either I am wrong, or I need a better way to sell this.
Questions
To summarize the main questions:
- Is it a problem if some interfaces are not meant for the "public API", but for "internal" use only?
- Is there a problem there are too many classes? Should complexity be measured in number of classes, or average size of each individual class?
- Is there a way to reduce the number of classes and still have the benefits of a "solid" architecture?
I have my own answer for most of these, but I have the feeling this is not sufficient to convince anyone.
More thoughts
An alternative would be to have more "generic" interfaces, e.g. "StringProviderInterface" instead of "ProfileNameInterface". But then I would lose the semantic specificity. Just because they all return strings, does not make them exchangeable.
Maybe the interfaces will be reused more often, if more code transforms into this style.
Update
As the discussion is going on, a major and repeated concern that I hear is that every new interface needs to be backwards compatible, even if it is meant for something "internal". I technically agree with this, but I think the same could be said about classes and public methods. So I wonder if this is really a valid concern. Maybe the real problem is to have awkward one-off interfaces dangling around.
Lazy<T>
is a good choice. Instead of passing a value, you passLazy<T>
which knows how to get the value later when it is actually required, independently of the (in)availability of the value at construction time.