Boaris
Boaris

Reputation: 5216

Consumer Constructor Pitfalls

Look at the following code:

class Person {

    String name;
    int age;

    Person(Consumer<Person> consumer) {
        consumer.accept(this);
    }

}

As you can see, I'm using "consumer constructor", so I can create a person like this:

var person = new Person(p -> {
    p.name = "John";
    p.age = 30;
})

Seems like this approach is much better than builder pattern or all-arguments constructor.

4 years have passed since Java 8 was released but nobody uses consumer constructors (at least I haven't seen it before).

I do not understand why? Is this approach has some pitfalls or limitations?

I have found one, but I don't think it is critical:

class AdvancedPerson extends Person {

    String surname;

    AdvancedPerson(Consumer<AdvancedPerson> consumer) {
        super(); // <-- what to pass?
        consumer.accept(this);
    }

}

Sure we can create a no-arguments constructor in Person and just call it in AdvancedPerson consumer constructor. But is this a solution?

So what do you think about it?
Is it safe to use consumer constructors?
Is this a substitute for builders and all-argument constructors? Why?

Upvotes: 9

Views: 1130

Answers (3)

Bj&#246;rn Zurmaar
Bj&#246;rn Zurmaar

Reputation: 829

It's neither safe nor elegant from my point of view. There are several reasons against this approach: The worst thing about it is that it not only allows but also forces you to let the this reference escape while the object is not initialized yet. This has several severely bad implications:

  • The consumer will have a reference to an object who is in a mid constructor call. The consumer can then do all kind of evil things like e.g. call methods overridden by another child class.
  • Also the consumer may be able to see the object in a partial state, e.g. some member initialized while other aren't. This can get even worse when multi-threading is involved.
  • Immutable objects are impossible to create with this approach as you have to allow the consumer to meddle with the internals of the object under construction.
  • Last but not least you put the burden of being responsible for guaranteeing for the class' invariants to the consumer. This is definitely something a class should be responsible for itself.

See the second chapter of effective java for best practices concerning creating objects.

Upvotes: 12

Andrew
Andrew

Reputation: 49606

It seems like I don't control the process of initialisation at all.

I don't know how many fields have been set, what their values are. I don't know which methods (and how many) a consumer has called. It looks like I break the encapsulation rules.

I also expose the this reference for the outer world.
The caller may do with this whatever they want. And I won't be notified about that. Multithreading has a term called 'improper publication'. I would use it here.

Upvotes: 8

rgettman
rgettman

Reputation: 178253

Consumer constructors appear to present a concise way in Java to write code that in other languages would be referred to as "properties". Just pass in a Consumer that initializes the state of the object.

But this cannot work with proper class design, which includes encapsulation. Here, this means that name and age should be private. This means that the Consumer no longer has access to the instance variables and no longer compiles.

Encapsulation and the Builder Pattern both have the advantage that invalid states of the objects are checked and rejected.

If Java had the concept of properties, where code such as obj.prop1 = value; and variable = obj.prop2; in reality called methods, then this would not be a concern. But it doesn't have this concept.

The pitfall you have found could be worked around by passing a Consumer for each superclass as well as the class in question, e.g.:

AdvancedPerson(Consumer<Person> pConsumer, Consumer<AdvancedPerson> consumer) 
{
    super(pConsumer); // <-- what to pass?
    consumer.accept(this);
}

However, that means that a user must pass in a different Consumer for every superclass, which doesn't make much sense.

In addition, passing this from a constructor is not a good idea; this is an instance leak, where outside code can access an object that isn't fully constructed yet.

Encapsulation, this leaking, and the complication of multiple Consumers in a class hierarchy are all good reasons not to use consumer constructors.

Upvotes: 4

Related Questions