简体   繁体   中英

How to set cardinalities in Java field?

Let us suppose we have the following class:

public class MyClass{
    List<String> list;

    void method() {
    }
}

Each object of this class has a list of strings, but what if we want to set the cardinality? For example, I want to force this class to have at least 2 strings in that list.

Is there a general pattern to represent the cardinalities on fields?

You simply need to make sure that there are at least 2 elements in list , by some means. There is no standard or simple way of doing this.

This is known as an invariant of the class. It is your responsibility as the person who writes the class to ensure that this invariant is preserved at all times. In particular:

  • You need to document the invariant that there are at least 2 elements in the list.
  • You need to ensure that the list contains at least two elements by the time the constructor finishes. The instance should be "valid" (in the sense that its documented invariant are true) when the constructor finishes.
  • You need to ensure that all code within the class honors the invariant when it manipulates the list - you are allowed to temporarily make it invalid, but you must ensure that it is not possible to observe your instance in an invalid state.

    In the single-threaded cases, this simply means that the invariant must be true once the public method returns (note that this includes if an exception occurs); but if your code is designed to be used in a multithreaded way, you must also ensure that no thread can ever see your instance in a state where the invariant is false (eg you may need synchronized blocks, or ensure that updates are atomic).

  • You need to ensure that subclasses of your class are unable to make the invariant false; or document clearly that it is the responsibility of people writing subclasses to ensure that the invariant remains true.

    There is a great item in Effective Java 2nd Ed about this: "Design and document for inheritance or else prohibit it".

  • You need to ensure that nothing outside your class is able to access the reference to the list. For example, your code makes the list visible to other classes in the same package - any of these classes could call theInstance.list.clear() , and invalidate your invariant.

    It's pretty hard to prevent this absolutely - for example, it could be possible for malicious actors to invalidate the invariant using reflection. You can prevent this, but it's a question of weighing the effort of identifying and blocking such methods vs the actual cost of the invariant becoming false (this strongly depends upon how this class is used).

By far the easiest way to enforce an invariant is on an immutable class . If it's not possible to change the observable state of an instance, it's not possible to invalidate the invariant. Then, all you need to worry about is a) making sure that the class is immutable; b) making sure that the invariant is true once the constructor returns. All of the other points above then simply fall away.

Is there a general pattern to represent the cardinalities on fields?

Obviously, you can represent the cardinalities using integer fields, either in the objects themselves, or at the meta level. But that's not much help if you cannot enforce them.

There is no general pattern for that. @Andy Turner's answer provides a good summary of the alternatives on enforcement of cardinalities. I just want to add a couple of points:

  1. Attempting to enforce the cardinality constraints via static typing is unlikely to work. The Java type system is not rich enough to do this in a pleasant way 1 .

  2. Construction of objects that have fields that have minimum cardinalities can be tricky, especially if there are potentially circularities involving those fields.

One way to deal with construction is to separate the lifecycle of the objects into a "construction" phase and a "completed" phase. During the construction phase, the constraints are relaxed, to allow the construction to be performed in stages. At some point, a "completed" switch is "flipped". At that point 1) the cardinality constraints are checked, and 2) the behavior of mutating operations is changed to prevent changes that would violate cardinality.

This can be implemented using public constructors and a public method to "flip the switch". Alternatively, you can implement this using the builder pattern ; eg

  • make the constructors private
  • use alternative private means to side-step the cardinalities while building
  • check the cardinalities and flip the switch (it one is needed) in the build() method.

Another approach is to allow fields to be below cardinality, but only allow items to be added to the fields when they are in that state. In other words, this is the "flip the switch" approach without an explicit switch. The downside is that a client needs to test if the cardinality constraint is in force yet; ie the constraint is weak.


1 - In theory, you could implement a ListOfTwoOrMore interface, etcetera, but that would bring a raft of new problems.

One way to do that is use a different type for the field. Now it has type List<String> which is a collection that can contain 0 or more elements. You can change it to a type which represents a list that contains 2 or more elements.

You could use var-args in constructor.

public MyClass(String s1, String s2, String... rest){
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM