I like the builder pattern (example https://stackoverflow.com/a/1953567) for various reasons, e. g. the fact that I can work with immutable objects and that I can control the object creation in the way that no invalid objects can be created.
However, I try to follow the paradigm "program to interfaces, not implementations" (example https://stackoverflow.com/a/2697810).
I figured, these two guidelines do not play well together.
If I have an interface Person
and a class PersonImpl
and a builder PersonImplBuilder
that builds a PersonImpl
. I now can assure that every instance of PersonImpl
is valid and immutable. But every return value and particularly every method parameter in my API uses the interface. So I can not depend on a valid object.
Am I missing something respectively is there another way of combining these two very useful guidelines?
EDIT
Some code to clarify.
In this example the builder is useless in terms of ensuring validity and/or imutability of the object in my API. It does only guarantee that any object of PersonImpl
is valid (and by the way that only works because PersonImpl
ist declared as final). But I can not control if a client is actually using my safely constructed PersonImpl
object or any other implementation of the Person
interface.
public interface Person {
LocalDate getBirthday();
}
public final class PersonImpl implements Person {
private final LocalDate birthday;
private PersonImpl(PersonImplBuilder builder) {
this.birthday = builder.birthday;
}
@Override
public LocalDate getBirthday() {
return birthday;
}
}
public class PersonImplBuilder {
private LocalDate birthday;
public LocalDate getBirthday() {
return birthday;
}
public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}
public PersonImpl build() {
if(birthday.isAfter(LocalDate.now().minusYears(21).minusDays(1))) {
throw new IllegalStateException("Person must be 21 years or above");
}
return new PersonImpl(this);
}
}
// this is my API
public interface PersonService {
void doSomeAdultStuff(Person person);
}
public class PersonServiceImpl implements PersonService {
//...
}
public void maliciousMethod() {
PersonService service = new PersonServiceImpl();
service.doSomeAdultStuff(new Person() {
@Override
public LocalDate getBirthday() {
return LocalDate.now();
}
});
}