Object Oriented Programming: Accessor methods
Tue, May 10, 2016 ❝Accessor methods in Object Oriented Programming.❞Contents
In a follow up post to Object Oriented Programming: Curiosity of access modifiers we look at the purpose of accessor methods and how these should be used. We will point out some common mistakes in the use of accessor methods that need not happen. In correcting these mistakes, it is sometimes possible to get more support from the compiler or type system for the rest of your implementation.
The reason for accessors
As discussed earlier, there exists the notion of Encapsulation. By separating internal state we avoid incorrect usage logic from corrupting the object.
Now, even though we prevent internal state from being accessed by the user, we may need to grant partial access to the internal state. For example, we may want to let users read the data of an object but prevent them from writing data.
Many classes contain the so-called “getters and setters”. Methods starting with ‘get’ or ‘set’, concatenated with the field name, which resp. provide read and write access to that particular field of the internal state.
Getters and setters
Getters are present to provide read access to a field of the internal state. Setters are present to provide write access. The fields are not directly accessible and only through the help of these proxies are we able to access them. The advantage here is that these “getters” and “setters” are also implementation logic, so we can control exactly what happens when reading or writing.
Note that this is limited: we only control read/write access to the reference. Once the reference is acquired, the user can use any feature that the object provides. A similar case holds for write access. We enable writing to the field of the internal state through the provided implementation logic. It is however possible to provide bad values. Therefore we use the accessor methods to restrict access to the internal state to a subset of possibilities that is controllable.
Restricted access(ors)
There are a number of variations where we correctly restrict access using accessor methods.
-
Restricted write access.
Any value that is provided for assignment to the internal state is verified for validity. Not all possible values are necessarily considered valid values. In some cases, we abort prior to assignment with anIllegalArgumentException
to signal that this value is bad. Similarly, if the object is in an unexpected state in which this method should not be used, we throw anIllegalStateException
. -
Restricted read access.
As noted before, we can only regulate access to the reference. Once the reference is acquired, a user can still use any feature provided by the object. We can control usage by providing the caller with a “handicapped” (wrapped) instance. For example, we can expose a list, but whenever we do we wrap it as an unmodifiableList. This way we can be sure that the list instance is not modified (incorrectly). We preserve consistency and still expose all other features of this particular part of the internal state.
Accessing a field directly
A use of fields that is not often discussed, is that of defining fields as publicly accessible. Encapsulation is the characteristic that allows you to control the internal state to ensure that the state is always consistent. Some fields may contain any value and therefore are always consistent. Or rather, some fields need not be controlled because any implementation logic can handle any state at any time. At this point, the logic is only using the field. It would make perfect sense to make the field publicly accessible.
I have found 2 variations of this, so far:
-
Publicly accessible field, non-final: At any moment we can read and write the field. Internal state consistency cannot rely on specific values for this field. Implementation logic may still use the field in whatever state it is at that particular moment. Access is read + write.
-
Publicly accessible field, final: At construction time we assign a value. The value may originate from the user provided through the constructor, or it may be determined inside the constructor. In any case, even though the field is publicly accessible, because it is declared final it cannot be modified after initial assignment. This is perfect for immutable values as we can provide simple direct access without risk of unintended modifications.
Directly exposing fields is not that common in every object-oriented language. Go leverages this method quite extensively, for example in the use of configuration structures. Java, on the other hand, always tends to work through accessor methods, even if it is not strictly necessary.
Of course it depends on your application whether this method is suitable or not. For internal use inside your library it is a perfectly valid use case. On the other hand, when crossing abstraction boundaries or as a concrete type provided on behalf of an interface, this is less suitable. Nevertheless, not every application ends up at an abstraction boundary.
Variations of field dependency
Internal state may contain any number of fields. Not every field needs to have the very same significance for the internal state of the object. There are a number of variations of field dependency. Depending on the type of variation, we tighten or relax control of the field. Field management is more elaborate in cases that rely heavily on the field’s availability and its exact state to match its own internal state.
-
Completely independent
The field is publicly accessible for reading and writing. Implementation logic only tries the field for usage only, if it is available at all, since the field contents could change at any time. This is the more opportunistic use case, where a reference is addressed if it is available, but in any case it is a secondary service, as object consistency does not rely on it. -
Dependence on availability/for usage, but final
The field is publicly accessible, but read-only. Implementation logic depends on the field being available. As the field is declared final, usage can in no way interfere with availability. The constructor can ensure that the field is correctly initialized from the start. Thefinal
declaration ensures this does not change afterwards. The implementation logic is not dependent on the state of the field, so unrestricted exposure to the user poses no risk. -
Dependence on availability/for usage, but non-final
The field is not publicly accessible and not declaredfinal
. Read accessor method exposes access for usage purposes. Write accessor, if provided, will control values that are being assigned. This way we can preventnull
references and invalid instances from being assigned. The implementation logic can be sure that any access to field is consistent with current state. The implementation logic does not rely on specific state of the instance in the field. -
Dependence on availability and state of object
The field is not publicly accessible and may be declaredfinal
. As the internal state of the object relies on a specific state of the instance in the field, we provide only restricted/controlled access. A read accessor method may be provided to expose the field for usage purposes, likely restricted in some ways such as by wrapping it to make it unmodifiable, in order to control consistency. A write accessor method may be provided, although not necessary. If provided, it is likely restricted to ensure compatibility with the internal state.
Note that above descriptions assume that user access is required. For fields that need not be exposed to the user, we can trivially lower the access level to protected, package-private or private, and remove accessor methods. Similarly, with reduced access, it becomes trivial to manage state as there are no outside influences. Even so, it is good to declare fields immutable if assignment is not allowed to enforce this behavior.
The object should be in complete control of its own internal state. Any interference with state consistency by usage logic must be prevented, through restricting access and restricting usage.
Restricted access for inter-field consistency
As mentioned in earlier sections, we leverage restricted access as a mechanism to guarantee consistency within an object. When we discuss consistency in this context, we refer to consistency between multiple fields. Each field, given that it contains an object itself, should already ensure its own consistency. However, a single field cannot ensure that it is consistent with the data in other fields. The inter-field consistency.
That is what the implementation logic of the encompassing object is for. The implementation logic of the encompassing object is there to ensure that the encompassing object is consistent, meaning that at any time the data in all fields match and are in a usable state. A single field manages itself, but we need the encompassing object to ensure data consistency outside of the single-field scope.
-
Wrap field with utility that restricts access to ensure consistency of single field (intra-field consistency).
-
Write (accessor) methods to ensure that all fields are in sync with every use of the object.
Note that if you only need to guarantee consistency of a single field, we can suffice with wrapping the field (possibly upon returning) with a decorator (utility) that enforces the extra constraints. For more elaborate constraints, we use implementation logic on the encompassing object.
Dubious accessor methods
There are some variations of dubious getters and setters, accessor methods that are probably not used correctly.
Unrestricted “getter” and “setter” for same field
The nature of the accessor method is to control access to a field that is part of the internal state. However, with both the plain “getter” and plain “setter” being available no control is being exacted. (“plain” meaning no additional restriction is set) This kind of field does not need encapsulation and could just have been publicly accessible.
Now, to be fair, there is one caveat to using a public
field. It is not possible to define an interface on public fields. Therefore, by using a public field, you will not be able to define an interface method for accessing that particular field. However, interfaces typically do not focus on which raw data can be retrieved but rather on more complicated operations.
Private getters/setters
A not so common pattern, but it is seen, is the private getters and setters. This does not make sense, since implementation logic is already able to fully access the internal state.
There are (at least) 2 variations:
-
Getter/setter adds restrictions. In this case we should abstract a subtype. In the current situation we cannot prevent implementation logic from accessing the field directly. By extracting a separate class for this field/data together with its accessor methods and other implementation logic, we can leverage encapsulation in order to guarantee that the “getter” and/or “setter” is called.
-
Getter/setter is plain getter/setter with no added restrictions. This method is completely redundant. In addition, if a private “setter” is defined, it may block you from declaring the field in the internal state as final, since the setter is assigning to it. In case you only need to assign it once, consider assigning to the field directly and declaring the field final such that you can better leverage the type system and compiler to better control the state.
There is no valid use case for a private getter or setter.
Fields that should not be set
There are a number of cases where it does not make sense for fields to be writable. A clear example of this is with the use of Collections. Most use cases of Collections should only be used. I am referring to “usage” as defined in an earlier post: Implementation and usage.
The internal state contains a collection type, say List
. The list itself can be declared final. We only need to provide a “getter” accessor method. Any usage pattern related to this field of the internal state will be for acquiring the reference and subsequent usage only. The “getter” is sufficient, as the list instance already provides all required methods. With final
declared and a guaranteed assignment in the constructor, you know that the field is never null
.