Using Lombok with CUBA Platform we cannot use @Builder annotation for entities, as these should be instantiated by a factory, defined in CUBA documentation. There is no clear way of how it can be achieved.
Factory pattern is one of the most used ones, so it would be great to be able to specify class and method or a separate factory class in Builder annotation.
The builder and the factory patterns serve different purposes. The first is typically used when you want to create (potentially immutable) instances of a well-known class that has many fields that must be populated. The latter is typically used when the user of the factory should not know the actual implementation to instantiate.
Furthermore, there are technical problems: The factory method is not generated by Lombok, so the order of its parameters can easily differ from the order that @Builder expects. This leads to difficult to solve compilation errors, or - even worse - runtime bugs when arguments are getting assigned to wrong fields.
However, there is a relatively simple solution: Customize your build() method to call the factory instead of the constructor.
The builder and the factory patterns serve different purposes.
Let me disagree. Factory pattern is the way to instantiate an object, nothing more. It is an alternative to new operator. E.g. you call MyObject o = MyFactory.instantiate(MyObject.class) instead of MyObject o = new MyObject() - that's it. It has nothing to do with initialization, where Builder pattern comes in power.
Customize your build() method to call the factory instead of the constructor.
This is not easy at all. The only way is to call super.build(), then instantiate an object using Factory, and then manually move all the values. The worst part here is that it is not generic and you'll need to specify it for each entity. Also, object instantiation will happen twice via new and via the factory; after that, it will be required to copy all fields manually. Each change in fields will lead to the routine, that Lombok is trying to minimize - nonsense.
Furthermore, there are technical problems
Introduce your own interface for LombokFactory with the only build method with no parameters and potential problems will be solved. Why not?
Lombok is not present at runtime. Thus you can't introduce new types in Lombok that can be used in the code.
However, I still do not really understand what you want to achieve (and how). I think you have to provide some example code: first with your suggested Lombok annotation extension, second with the generated (pure Java) code.
PS: @Builder cannot use setters, because classes may not have any or fields are final, e.g. when instances should be immutable.
Lombok is not present at runtime
Right, good point.
However, I still do not really understand what you want to achieve (and how)
See, I would like to have a builder for my JPA entity. However, due to the documentation, it should be created by dataManager.create(NewEntity.class). Your builder always calls new and there is no way to make it call the right method for instantiation. Therefore, amazing builder functionality, provided by Lombok cannot be applied :(.
There is the most straightforward way to solve it from my perspective:
@NamePattern("%s|foo")
@Table(name = "LOMBOKTEST_NEW_ENTITY")
@Entity(name = "lomboktest_NewEntity")
@Builder(factoryMethod = "instantiate")
@Data
public class NewEntity extends StandardEntity {
private static final long serialVersionUID = -2388548657648937061L;
private static NewEntity instantiate() {
// defined by user
return AppBeans.get(DataManager.class).create(NewEntity.class);
}
@Column(name = "FOO")
@NotNull
protected String foo;
}
Unfortunately, this can only work when there are setters available or at least all fields are non-final. This is a severe limitation, because @Builder is often used for immutable classes. Currently, @Builder passes all its values to an all-args constructor. Introducing a different behavior would require a huge amount of work. And it could only be used for your usecase (when having a factory), because of classes with final fields, and because of backwards-compatibility. Thus, Lombok has to keep the existing way too. I'm not a maintainer, but I'm quite sure the project owners do not want that.
BTW: As mentioned before, you can do it manually:
@Builder
@Data
public class NewEntity extends StandardEntity {
public static class NewEntityBuilder {
public NewEntity build() {
NewEntity i = AppBeans.get(DataManager.class).create(NewEntity.class);
i.setFoo(foo);
return i;
}
}
protected String foo;
}
Exactly, now do it with 35 fields. And always remember to add new fields and remove old ones. Nonsense.
As for your point with immutability, it doesn't mention anywhere in the documentation https://projectlombok.org/features/Builder. This approach with all args constructor is very limited and this should be mentioned in bold.
Even if this approach was limited, it is one of only two ways how the builder pattern works when there are final fields. (The other is having the builder as parameter of the constructor, but this wouldn't help you in this case, either.) Immutability is very frequently used, especially when using functional programming, streams etc. Not allowing that with @Builder would render the whole thing near-useless. So I don't see an alternative here.
Still, the very big case is JPA entities, which are not mutable.
Yes, JPA is a use-case, but is it big enough that it will be worth the implementation and maintenance burden? As I said: I'm not a project maintainer, I just contributed the @SuperBuilder feature. So let's wait for an evaluation from the project owners (typically on Monday evenings).
(BTW: For which JPA implementations is this a problem? At least Spring JPA allows creating instances using a regular constructor.)
You can put @Builder on a static method, and it can do whatever you want it to.
If that is not sufficient for this case, then the answer is a no. As Jan Rieke explained, this is far too involved.
We could consider a feature request to add a feature specifically for this CUBA thing in package lombok.extern.cuba, but it has zero chance of being taken up by us without a complete pull request with docs, tests, and all relevant implementations, and please contact us before you consider it; that sounds like a thing that has a massive support burden which we don't want to take up. We would probably require a legal obligation stapled to the PR that you'll support it for at least 5 years. I know that's a bit harsh, but the support load is, as you can see by the bug tracker, already backbreaking.
This sounds very reasonable. Thank you for the provided explanation.
Most helpful comment
Even if this approach was limited, it is one of only two ways how the builder pattern works when there are final fields. (The other is having the builder as parameter of the constructor, but this wouldn't help you in this case, either.) Immutability is very frequently used, especially when using functional programming, streams etc. Not allowing that with
@Builderwould render the whole thing near-useless. So I don't see an alternative here.