How to deal encapsulated a field that is a list of immutable elements?

Asked

Viewed 59 times

4

Imagine that you have an object and one of its fields is a list of immutable elements.

You want to make this field available to the clients of this object in a controlled way, after all a list can be changed and receive or lose elements, invalidating the object and violating the principle of encapsulation.

What to make available to the customer? Depending on the business rule, you will respond. But generally consider some options.

One option is to provide a getter method that accesses a copy of the list. Thus, having the elements already immutable, the original list is also immutable.

Another option is to make available a getter that accesses the elements one by one: getElement(int posicao).

Another option is to provide an iterator.

The question is, does this last option make sense? I usually include it among the possible options but I give up, thinking that the design does not look good. But I think back and forth about her. So this question. What do you think?

  • Related: https://www.martinfowler.com/bliki/EncapsulatedCollection.html

1 answer

5


From the point of view of encapsulation, I normally consider that the best approach is the following:

public class SuaClasse {

    // Embora AlgumaCoisa seja imutável, a lista em si é mutável.
    private List<AlgumaCoisa> algumaLista;

    public SuaClasse() {
        // ...
    }

    public List<AlgumaCoisa> getAlgumaLista() {
        return List.copyOf(algumaLista);
    }

    public void adicionarItem(AlgumaCoisa adicionando) {
        this.algumaLista.add(adicionando);
    }
}

The method List.copyOf(Collection<E>) added in Java 10 and creates an immutable copy of a given list. If the given list is modified in the future, the copy will not be changed.

In older versions of Java, I used this (in this case, Java 7):

    public List<AlgumaCoisa> getAlgumaLista() {
        return Collections.unmodifiableList(new ArrayList<>(algumaLista));
    }

Anyway, the internal list never is directly exposed to any other class that wants to access it. This ensures encapsulation. If any external class is interested in modifying the contents of that instance list x, use any of the methods described by x, which justifies the fact that the returned list is immutable.

It is also justified to be a copy because subsequent changes through the method adicionarItem should not mysteriously be reflected in lists that although have been obtained previously from the instance x, are in codes no longer interested in x proper.

And even if a copy is returned, any attempt to modify that copy directly is at least one code Smell or a decoy for bugs. So, even though it’s a copy, it still has to be immutable.

Use Iterator directly is difficult. The Iterator has limitations of not being able to be iterated more than once (unless you copy all the items in a list, but then it would be easier to already give this list previously built) and also has limitation of not being able to determine the size of the list. In addition, the Iterator is something quite outdated, because now exists the Stream.

There are some scenarios where it makes sense to return a Stream instead of a list. An example is when one is going through the records of a database or a file line by line. In that case, you do it:

public Stream<AlgumaCoisa> {
    // Faz alguma coisa.
}

But this is only justified when it is not desired that all items are pre-prepared in memory because of performance or memory savings. If this is not the case, the list is returned.

Finally, one can still be concerned that the getter may get heavy to run, because whenever it is called, a new copy of the list will be made even if it is identical to the copy produced in the previous call to the method. So you can do that:

public class SuaClasse {

    // Embora AlgumaCoisa seja imutável, a lista em si é mutável.
    private List<AlgumaCoisa> algumaLista;
    private List<AlgumaCoisa> algumaListaImutavel;

    public SuaClasse() {
        // ...
    }

    public List<AlgumaCoisa> getAlgumaLista() {
        if (algumaListaImutavel == null) algumaListaImutavel = List.copyOf(algumaLista);
        return algumaListaImutavel;
    }

    public void adicionarItem(AlgumaCoisa adicionando) {
        this.algumaListaImutavel = null;
        this.algumaLista.add(adicionando);
    }
}
  • I usually do return new ArrayList<>(algumaLista); to create the copy, because I don’t see the obligation of the list to be "exactly" immutable, but I may be wrong. And thinking here, some Iterators allow removal of elements from the list, this is also a point against using Iterators.

  • "And even if a copy is returned, any attempt to modify that copy directly is at least a Smell code or a decoy for bugs. Therefore, even though it is a copy, it still has to be immutable." Now I’ve seen this. Can you elaborate? Why a Smell code or decoy for bugs? Because it ceases to reflect exactly the content of the original field at that time?

  • 1

    @Piovezan Why normally, when this happens, it is an attempt to modify the original list, trying to violate the encapsulation (and even if it doesn’t work out, it is still a problem). Not necessarily that, but often it is. Moreover, there is a semantic question, for often this list represents a part of the state of an object at the time the getter was called. Trying to change it directly means at least that there is some other semantics involved (and probably an uncertain and ill-defined one). [continue]

  • 1

    [continuation] In addition, relying on the fact that the returned list is mutable means relying on an implementation detail of another class that is not formally defined by signing its methods, which is something that tends to produce fragile code (i.e., prone to break up in the face of internal changes in other classes).

  • 1

    Anyway, it is rarely correct to modify directly the returned list, even if it does not change the original. If someone is trying to do this, they are probably going to be screwing up, and so it is better to stop it soon, as it is better to make an exception soon than to have to break their heads, waste time and have annoyances afterwards with debugging.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.