Introduction
Do you think about compatibility of your public API when you modify classes from it? It is especially easy to miss out that something incompatibly changed when you are using Lombok. If you use AllArgsConstructor
annotation it will cause many problems.
What is the problem?
Let's define simple class with AllArgsConstructor
:
@Data @AllArgsConstructor public class Person { private final String firstName; private final String lastName; private Integer age; }
Now we can use generated constructor in spock test:
def 'use generated allArgsConstructor'() { when: Person p = new Person('John', 'Smith', 30) then: with(p) { firstName == 'John' lastName == 'Smith' age == 30 } }
And the test is green.
Let's add new optional field to our Person class - email
:
@Data @AllArgsConstructor public class Person { private final String firstName; private final String lastName; private Integer age; private String email; }
Adding optional field is considered compatible change. But our test fails...
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.github.alien11689.allargsconstructor.Person(java.lang.String, java.lang.String, java.lang.Integer)
How to solve this problem?
After adding field add previous constructor
If you still want to use AllArgsConstructor
you have to ensure compatibility by adding previous version of constructor on your own:
@Data @AllArgsConstructor public class Person { private final String firstName; private final String lastName; private Integer age; private String email; public Person(String firstName, String lastName, Integer age) { this(firstName, lastName, age, null); } }
And now our test again passes.
Annotation lombok.Data
is enough
If you use only Data
annotation, then constructor, with only mandatory (final
) fields, will be generated. It is because Data
implies RequiredArgsConstructor
:
@Data public class Person { private final String firstName; private final String lastName; private Integer age; }
class PersonTest extends Specification { def 'use generated requiredFieldConstructor'() { when: Person p = new Person('John', 'Smith') p.age = 30 then: with(p) { firstName == 'John' lastName == 'Smith' age == 30 } } }
After adding new field email
test still passes.
Use Builder
annotation
Annotation Builder
generates for us PersonBuilder
class which helps us create new Person
:
@Data @Builder public class Person { private final String firstName; private final String lastName; private Integer age; }
class PersonTest extends Specification { def 'use builder'() { when: Person p = Person.builder() .firstName('John') .lastName('Smith') .age(30).build() then: with(p) { firstName == 'John' lastName == 'Smith' age == 30 } } }
After adding email field test still passes.
Conclusion
If you use AllArgsConstructor
you have to be sure what are you doing and know issues related to its compatibility. In my opinion the best option is not to use this annotation at all and instead stay with Data
or Builder
annotation.
Sources are available here.
Thanks for pointing out the difference. I would only like to add that @Data is good only for Value Objects (JavaBeans). It breaks the encapsulation (generating getters and setters) and generate hashCode based on every single field.
ReplyDeleteFor Entities (in the sense of DDD) you wouldn't want to use @Data annotation at all.