✨AI全文摘要
The document discusses the challenges and benefits of using a Dependency Injection (DI) framework in a Java project. It highlights the two main options for dependency management: introducing a full-blown DI framework or using traditional object instantiation or simple factory pattern, which can be time-consuming and cumbersome. The author uses the example of a simple Java project where the interface and implementation class pattern was used to decouple the interface and implementation, but it also introduced complexity. The document suggests using delegation and classpath scan capability to achieve minimal dependencies and extra code, and provides a code example to help understand the process.
Why
If you have got used to using DI framework (like Spring) to manage and inject dependencies, and now you have to start a new, relatively simpler, but not so simple project, you have 2 options to work with in terms of dependency management:
- introduce a full blown DI framework and waste plenty of time in numerous and verbose configurations
- use traditional object instantiation or simple factory pattern and make the code clumsy and hard to modify
I encountered this sort of problem a week ago when I was writing the demo project for architecture class, which was only a simple JavaFX project that had a simple dependency hierarchy. What was different in this project from other simple and less designed project was that instead of using class directly, it used interface and implementation class pattern in order to match the class diagram and meet the course requirement. Using interface decoupled the interface and implementation, which was a good practice, but also introduced complexity in using this code, since it would be impossible to new an interface.
As mentioned before, excluding DI framework option, two patterns could be applied: object instantiation and factory pattern. Kotlin also supports singleton object declaration which was also a good choice. All of them, however, made the code less elegant:
Pattern 1: object instantiation
Pattern 2: simple singleton factory pattern
Pattern 2.1: simple singleton factory pattern using companion object
Pattern 2.2: singleton using singleton object declaration
All of these problems were of little importance, but to an extent affected my coding experiences. What I would like was minimal dependencies and minimal extra code.
Luckily, with the help of delegation and classpath scan capability provided by classgraph, we could achieve the minimality, with the introduction of only three simple APIs:
Service
annotation to annotate service interfaceServiceImpl
annotation to annotate implementationdi
function for delegation
How It is Done?
Here is the code with some comments to help understanding.
Before getting started, install classgraph in your project with Maven or Gradle to have all the Services and ServiceImpls automatically scanned and configured like Spring Boot would do.
What's Good?
- Singleton injection
All injected instances are singleton, which is how DI is usually used.
- new support
You can also simply new an object (which should not be annotated with @ServiceImpl) if singleton injection is not enough, or context parameters are required when using a service. New-ed object can still use its dependencies. No extra learning needed.
- Circular and hierarchy Dependency
Unlike commonly used constructor injection and setter injection, dependent object are fetched dynamically from the container when the property is being accessed. Nothing will be injected during the instantiation of objects. So there would be no problem to have circular and hierarchy dependency structure at all.
- Structure your project as you wish
With other DI frameworks (like autofac and Spring), the whole application must be structured from the ground up and configured in guidance of the DI framework of your choice , which is time-wasting and totally a overkill if a simple application is all what you need.
Limitations
- Can not use dependency in
init
block
All services are instantiated randomly (to be more precise, in the order of time when it is scanned), regardless of their dependency relationships. For example, if A uses B, it is possible that B is instantiated earier than A, in which case access to B inside A's init
block would cause NotProvidedException
since at that time A instance has not been in the container. This problem can be solved with prior dependency relationship analysis or custom after-instantiation hook.
- No advanced features like object lifetime control, scope, test...
It should be emphasized that our 50 lines of code only targets to small application and is not for production, so advanced features are never considered. Use full blown DI framework if your application is serious.
My Thoughts on Kotlin
My first impression to Kotlin was a mixed bag:
It had lots of features that C# and Java did't, which does improve coding experience a lot: strict nullity check, pattern matching(which C# is introducing), delegation, function type ((Int, String) -> String
, which is more intuitive than Action
delegations in C#, and various and differently named built-in functional interfaces (like Consumer
, Producer
) in Java which are hard to remember), inline functons and more;
Some parts of language seemed confusing at first, but later was proved to be excellent. For example lambda in Kotlin needs to be wrapped with a brace({}
) pair (like func(arr, {a, b -> a < b})
(just an example)), where Java doesn't (func(arr, (a, b) -> a < b)
). However with the ability to write the last lambda parameter outside of parentheses (func(arr) {a, b -> x a < b}
), the extra abilities it brings overweigh the disadvantages.
For example, with the help of TornadoFX, an excellent JavaFX framework for Kotlin, building JavaFX views can be done directly in Kotlin elegantly, which is even better than FXML, since strongly-typed-ness makes coding way less error-prone, verbose and intuitive than XML.
Gradle also supports Kotlin as its DSL alongside Groovy, which also proves the benefits of this grammar are phenomenal.
Of course Kotlin has some problems, like the grammar of anonymous object is more verbose than Java's (where just a lambda would be enough) and some type system related problems thanks to the type erasure of JVM.
But the defects cannot obscure the virtues.
The features (including grammar sugars) Kotlin has have opened my eyes, showing me how a programming language that is modern, well-designed and has no history burdens can be like, how these features and grammar sugars can make coding much more productive and enjoyful, and most importantly, how our ways of thinking a problem can be different.
Everyone knows that programming languages are just tools: that's true, but tools can also influence how we look at problems.
A programmer who only knew procedural programming language would try to solve every problems procedurally, quickly being overwhelmed by exponentially growing complexity; A OOP only programmer would wrap everything into objects, resulting in piles of useless boilerplate codes and a class hierarchy so complicated that only god could understand it; A FP originalist would try to use FP on everything, ignoring the nature of the problem, the communities of selected technologies and his/her teammates, all leading to the failure of the project.
Learning different languages can bring different angles to view a problem and different tools to solve a problem, and that is valuable for any programmers.