How to follow Effective Java advice?
Effective Java describes general best practices that better be followed when it's possible. But it considers pure java, not any framework features.
Framework defines the architecture of the project and you should follow these rules. The framework has it's own best practices.
Immutable objects are good because they are inherently thread-safe. Their invariants are established by the constructor, and if their state cannot be changed, these invariants always hold. But there is no strict rule that every object should be immutable, sometimes it is not possible in scope of the given task.
Builders and factory patterns are still good and could be used in scope of a Spring project
. I used both Spring dependencies and Factory
patterns in real project, because the Factory
still allows you to @Autowire
objects.
As an overall example, I used Spark functions in Spring project. Some of the functions were @Autowire
Spring services. But the functions themselves were not Spring services. You cannot create them by new Function()
because then Spring will be not able to autowire
the Service. But with Factory, you can help Spring to do so.
There are many good design principles like SOLID, DRY, KISS
, design patterns which are usually helpful and allow you organize the code much better. But sometimes, in real life, you simple just cannot apply all of them to your particular case. The main rule here is that you should not absolutize any of best practices and find a middle ground between achievement of final goal and applying the best practices.
The are several dimensions to consider here:
- Sometimes best practices coming from different sources simply don't fit together nicely. But it is always a good best practice ... to follow best practices (to not surprise your readers). Meaning: when Spring comes with all these "guidelines", then you follow them. Even when that means violating "more generic" best practices.
- Also keep in mind that technical restrictions often affect what you as a developer can do. As in: Java is as it is. So, when a framework wants to create + fill objects using a default constructor, and then reflection ... well, that is like the only choice you have in Java. Other languages might enable you to do such things in a more concise way, but in Java, as said: your options are simply limited.
Thus, the best approach is: regard "best practices" like multiple circles drawn around the same central point. You first focus on those that line up directly with the technology you are using (say Spring). Then you can check "what else is there", and you try to follow these ideas. But something that is emphasized by the innermost circle always trumpets things derived from "further outside".
Design is a means to an end, design patterns are standard solutions to common problems.
Design books should not be read as "always to do this", but as "this is a standard solution for this problem". If you don't have the problem, you don't need to solve it. And sometimes your particular context may permit an easier or otherwise better solution.
So let's see why Joshua Bloch recommends these items, shall we? (Sorry for the occasional inaccuracy, I'm paraphrasing from memory here)
decreasing mutability, making fields final
Immutable state is referentially transparent and therefore easier to reason about. It is also inherently thread-safe.
... but databases hold mutable data. A database application must therefore deal with mutable data, and the clearest way to express this is using mutable objects.
To help with concurrent mutation, databases shield the application from concurrent changes by means of transactions.
That is, immutable objects, and transaction scoped objects are different solutions for the problem of reasoning about concurrent mutations.
refuse default constructors
When working with objects, we generally want them to be fully initialized.
We can enforce this by writing a constructor that initializes the object.
... but state persisted in a database has already been initialized. By providing a default constructor, the framework can recreate the object while bypassing initialization.
That is, we can ensure that objects are initialized by initializing them in a constructor, or by having a framework recreate initialized objects.
(BTW, complex JPA entities often use both approaches: they have a public constructor for initializing, and a package-visible constructor for recreating)
disabling inheritance
When you are writing a library, you need to be able to change your code without breaking clients, and you may even need to prevent malicious clients from breaking your code. Inheritance can interfere with both unless carefully managed.
When you are writing an application, your code is not usually subclassed by other teams, making it easy to change sub classes when changing the super class.
That is, you can prevent super class changes from breaking sub classes by preventing subclassing, by restricting yourself to superclass changes that can not break subclasses, or by changing subclasses when they break.
How should I deal with it? Is the book useless in practice?
By considering these standard solutions to common problems, when you encounter these problems.