Still Life with Carafe, Bottle, and Fruit, 1906 - Paul Cézanne

A few months ago I wrote about persistence of self-sufficient objects in the context of pure object oriented programming. I used repositories to represent the collections of these objects, resulting in two distinct categories of domain objects: collections and individuals. I compared this approach to Yegor Bugayenko's SQL Speaking Objects, where these two categories also exist. However, for a long time this separation seemed artificial to me, at least as long as we need it only to implement persistence. In pure object oriented world, each object should represent a real world entity. Is a collection something that really exists as a real entity over and above the individual objects that make it up, such that it requires a separate class to represent it? "If a thing can be done adequately by means of one", wrote Thomas Aquinas, "it is superfluous to do it by means of several; for we observe that nature does not employ two instruments where one suffices". Kent Beck's fourth rule of simple design - fewest elements - is consistent with this. We know we can eliminate controllers, services, factories, managers, helpers and other obscure technical "objects" which don't exist in the real world. Can we eliminate "collection objects" as well? I think we can.

To be sure, taking things too far can result in oversimplified models, and there are good arguments for collection entities, e.g. see this excellent post by Mihai. However, if you are interested in how they can be eliminated anyway, read on. I'm still experimenting with this idea - we don't have anything in production yet, so this is going to be more of an experimental and philosophical post, read critically and comment of course.

Bottles on a shelf

Let's say we want a bottle of wine. We know the cellar where it is stored, we know the shelf and we know where on the shelf (in which position) it is. Do we need to postulate a collection of them to get it from?

Bottles bottles = new BottlesInCellar(cellar);
Bottle bottle = bottles.find(shelf, position);

Should we not be able to just declare the bottle directly?

Bottle bottle = new WineBottle(cellar, shelf, position);

Turns out we can do it, but it's a bit more tricky than it might seem at first glance. The most common reason for introducing collection entities, as far as I can tell, is to handle situations where a new bottle needs to be stored in cellar, or where somebody requests a bottle which doesn't exist. Somebody needs to have these responsibilities, and some sort of collection (a "service" object is most certainly out of the question) seems a natural candidate for this.

Bottles bottles = new BottlesInCellar(cellar);

// 'Bottles' object can choose the best shelf and position.
bottles.add(new WineBottle());

// 'Bottles' can return Optional, so situations with
// nonexistent bottles are covered.
Optional<Bottle> bottle = bottles.find(shelf, position);

Let's try it without collections. Say, we want to store a new wine bottle in cellar. Could this work?

new WineBottle(cellar, shelf, position);

What if we want to retrieve a bottle from the cellar and drink it? We know the coordinates, so we can do this:

Bottle bottle = new WineBottle(cellar, shelf, position);
bottle.drink();

Wait a moment. We just used new WineBottle(cellar, shelf, position) to store a new bottle in cellar. And now we're using it to get one from cellar. Turns out new WineBottle(cellar, shelf, position) is ambiguous. Is it a new bottle to be stored in cellar, or is it an existing bottle which already was in the cellar? What if there was no bottle on the shelf where we hoped it would be?

In order to make sense of it, we need to introduce a bit of Aristotelian metaphysics, namely, potency and act.[1] Aristotle famously separated modes of existence into potency and act as a response to Parmenides, who claimed that change is impossible because it implies some new effect coming into existence, an effect which did not exist before - it was nothing before it was something. But since something can't come from nothing, so claims Parmenides, change is impossible. Aristotle's reply was that while the effect did not exist actually before the change occurred, it did exist potentially - it wasn't really nothing. If it was, it would indeed be impossible for the change to happen.

We can use this for our bottles. new WineBottle(cellar, shelf, position) is a bottle which is in the cellar potentially, but actually it might still be in the wine shop. We could try going to the cellar and bottle.drink()'ing it, but unless it's actually there, we will get an exception (or 404, or whatever). Or we can actualize it by buying it and bottle.store()'ing it in the cellar.

There are still some other issues (e.g. immutability), so let's look at a bit more realistic example.

Payments

Let's say we handle payment initiations. We receive a JSON with payment data and we need to initiate the payment (store it to be executed later). We also can be asked to display the latest state of initiated payments in JSON. So we define this interface:

public interface Payment {
    void initiate();
    Json json();
}

The implementation will need to have two coordinates: some data store and an ID, and some content. In the wine bottles example above we skipped the content to make it simpler and more to the point. In a payment request the content is JSON, in a wine bottle the content would have been wine (and perhaps the bottle itself).

So we can define the three fields:

public final class PersistedPayment implements Payment {
    private final UUID id;
    private final JsonStore store;
    private final Json content;
    
    . . .
}

Initiating is easy:

@Override
public void initiate() {
    if (content.isMissing()) throw new NoContentException();
    store.save(id, content);
}

Displaying itself is a bit more tricky because the Payment has to know whether it is actually in the store, or whether it was just received via HTTP and hasn't been initiated yet. In the latter case it would be initiated only potentially. If it was received via HTTP with the intent to initiate it, it must already have the data and doesn't need to look for it in store. In fact, there is no data in store, because it hasn't actually been initiated yet.

@Override
public Json json() {
    return content.isMissing()
        ? store.byId(id)   // This object doesn't hold data, must be in store.
        : content;         // This object holds the data, here it is.
}

And so it will have two constructors: one which takes content and one which doesn't. The former is for declaring a potential payment to be executed (which is why it takes content) and the latter is for declaring a payment which is either in act (has actually been initiated) or in potency (has not actually been initiated). Since we don't give the second constructor any content, we won't know whether it is in act until we ask it to display itself.

The whole payment might look like this:

public final class PersistedPayment implements Payment {
    private final UUID id;
    private final JsonStore store;
    private final Json content;

    public PersistedPayment(UUID id, JsonStore store) {
        this(id, store, new Json.Missing());
    }

    public PersistedPayment(UUID id, JsonStore store, Json content) {
        this.id = id;
        this.store = store;
        this.content = content;
    }

    @Override
    public void initiate() {
        if (content.isMissing()) throw new NoContentException();
        store.save(id, content);
    }

    @Override
    public Json json() {
        return content.isMissing()
            ? store.byId(id)   // This object doesn't hold data, must be in store.
            : content;         // This object holds the data, here it is.
    }
}

When we get a request to execute new payment, we can do:

UUID id = new UUID.randomUUID();
Payment payment = new PersistedPayment(id, store, content).initiate();
return new HttpCreated(id, payment.json());

And when we get a request to show the latest state of some payment:

Payment payment = new PersistedPayment(id, store);
return payment.json().isMissing()
    ? new HttpBadRequest()
    : new HttpOk(payment.json());

State updates

Payment might need to be authorized before the system can proceed with execution. This would change the payment's state. If Payment's content is in store, it needs to be updated there. If Payment's content is in the object itself, it needs to be reflected there as well, because otherwise the json() method would return a stale presentation. We can modify the content in store, but we can't assign a new Json to content field because it's final and must stay that way in order to preserve immutability. We could modify the Json already stored there (assuming it's mutable), but that would violate Oracle's definition of immutability. Even if we don't agree with this definition, it would still mean logic duplication, because we would have to update both store and field (assuming the content is in both). The solution is to create an immutable copy with an updated state which would represent the same entity, only at a different point in time.

We need a new method in the interface.

public interface Payment {
    void initiate();
    Payment authorized();
    Json json();
}

And the implementation could look like this:

@Override
public Payment authorized() {
    if (json().isMissing()) throw new NoContentException();
    store.save(id, json().with("status", "authorized"));
    return new PersistedPayment(id, store);
}

First we update it in the store, and then we return a new object which represents the actual payment in store, specified by two coordinates: the store and the ID in the store. Note that we use the json() method to reference the current content of the payment, and not the content field directly, because it might be missing.

What can be taken from this

Here we took a look at a pretty simple example. There were no validations, business rules or data transformations. They were not the point and can be easily added. The ternary conditional in json() method is a slight smell (like all explicit forks in control flow are), but it can be replaced with polymorphism. That would make the object slightly more complicated, but perhaps it would pay off in larger applications where more decomposition is needed.

This is a thought experiment in code. Don't take it too seriously. I have implemented it in a small application and it works quite well, but it hasn't been deployed to production yet. I haven't tried it in larger applications - obviously anyone who might want to try it in their own code should know what they are doing and not implement it blindly. Perhaps there are good reasons to model collections of domain entities. Nonetheless, if for any reason someone might want to eliminate them from their model and use only individual entities - it is possible.


  1. It's "potentiality" and "actuality" in modern English, but I like the traditional terminology. ↩︎