Hi,
On Sat, May 18, 2013 at 11:11:19AM +0300, Niklas Laxström wrote:
They proposed a solution (work around in Wikibase) to allow writing better code in Wikibase. [...] But as a reader I did not understand how it works
I was also surprised to realize that although the report constantly talks about a lack of dependency injection {,mechanism,facility} (in their meaning), their solution is not dependency injection (in the common meaning), but a service locator (in the common meaning) :-)
Seeing a separate service locator in a friendly extension comes even as a greater surprise to me.
Although it's already close to 10 years old, to me, Martin Fowler's article on dependency injection [1] is still a great primer on “how it works”. The article provides nice short code snippets (Java, but they should be easily readable by any PHP developer) and diagrams on how things get created in different scenarios.
or what would be the practical gains for the code complexity that the solution itself introduces.
I am probably biased, but for me, implementing only the service locator solution they propose, comes with little gain.
Service locators are “only” a tool assisting in decoupling direct dependencies between objects. That's good already, but service locators do not help with composition of objects, which is most of the work. They also do not help/allow to read off dependencies from objects (as for example constructor injection would).
Additionally, MediaWiki already partly uses service locators (Getting a Database connection). Nothing to gain in such parts.
Through service locators' additional indirection, they slow down code a bit, but are a first step to allow for easier testing.
The real paradigm shift would be to go for dependency injection (in the common meaning) and start using an inversion of control container.
* What would that cost us? Some performance, due to the decoupled dependencies.
* What would that buy us? Testable code.
Example? Example. When implementing a unit test for a plain project renaming in gerrit, I started stubbing the required objects and the test reached ~1000 lines of code (a single test! :-( ) although I was only 3/4 through with setting up the required stubs.
This insane amount of code for a single test did not come as a surprise. After all, the function under test had to move the project around in the database, move it in the file system, add git commits to child projects, ... and to test in isolation, you'll just have to stub all that out.
And every further test of a different path through the same method, would mean adding ~1300 lines of code to the test case.
So, since gerrit already embraces dependency injection, IoC, and coding by contract, I could break the project renaming code into smaller objects, and let the IoC container do the composition on its own. Thereby, testing each of the parts in full isolation is easier. And variations in code paths can be tested right where they are. It's no longer needed to test the whole code as one blob.
For this concrete example, testing only the top-level method (assuming sub-tasks meet the contract) requires 16 tests. For the unrefactored code, that'd be ~20k unmaintainable lines for the tests alone. After the refactoring for dependency injection and IoC, the whole file of the test case including header, imports, ... (and the tests) is ~0.5k lines.
Of course, we can always refactor code even without dependency injection or IoC containers. Everyone is doing that :-) “Dependency Injection” is just the hip name for a series of patterns that allow to decouple objects. And IoC containers just make it easy to stitch them back together at run-time.
Best regards, Christian
[1] "Inversion of Control Containers and the Dependency Injection pattern". Available at http://www.martinfowler.com/articles/injection.html