Having getters and setters does not in itself break encapsulation. What does break encapsulation is automatically adding a getter and a setter for every data member (every field, in java lingo), without giving it any thought. While this is better than making all data members public, it is only a small step away.
The point of encapsulation is not that you should not be able to know
or to change the object's state from outside the object, but that you
should have a reasonable policy for doing it.
Some data members may be entirely internal to the object, and should
have neither getters nor setters.
Some data members should be read-only, so they may need getters but
not setters.
Some data members may need to be kept consistent with each other. In
such a case you would not provide a setter for each one, but a single
method for setting them at the same time, so that you can check the
values for consistency.
Some data members may only need to be changed in a certain way, such
as incremented or decremented by a fixed amount. In this case, you
would provide an increment()
and/or decrement()
method, rather
than a setter.
Yet others may actually need to be read-write, and would have both a getter
and a setter.
Consider an example of a class Person
. Let's say a person has a name, a social security number, and an age. Let's say that we do not allow people to ever change their names or social security numbers. However, the person's age should be incremented by 1 every year. In this case, you would provide a constructor that would initialize the name and the SSN to the given values, and which would initialize the age to 0. You would also provide a method incrementAge()
, which would increase the age by 1. You would also provide getters for all three. No setters are required in this case.
In this design you allow the state of the object to be inspected from outside the class, and you allow it to be changed from outside the class. However, you do not allow the state to be changed arbitrarily. There is a policy, which effectively states that the name and the SSN cannot be changed at all, and that the age can be incremented by 1 year at a time.
Now let's say a person also has a salary. And people can change jobs at will, which means their salary will also change. To model this situation we have no other way but to provide a setSalary()
method! Allowing the salary to be changed at will is a perfectly reasonable policy in this case.
By the way, in your example, I would give the class Fridge
the putCheese()
and takeCheese()
methods, instead of get_cheese()
and set_cheese()
. Then you would still have encapsulation.
public class Fridge {
private List objects;
private Date warranty;
/** How the warranty is stored internally is a detail. */
public Fridge( Date warranty ) {
// The Fridge can set its internal warranty, but it is not re-exposed.
setWarranty( warranty );
}
/** Doesn't expose how the fridge knows it is empty. */
public boolean isEmpty() {
return getObjects().isEmpty();
}
/** When the fridge has no more room... */
public boolean isFull() {
}
/** Answers whether the given object will fit. */
public boolean canStore( Object o ) {
boolean result = false;
// Clients may not ask how much room remains in the fridge.
if( o instanceof PhysicalObject ) {
PhysicalObject po = (PhysicalObject)o;
// How the fridge determines its remaining usable volume is a detail.
// How a physical object determines whether it fits within a specified
// volume is also a detail.
result = po.isEnclosedBy( getUsableVolume() );
}
return result;
}
/** Doesn't expose how the fridge knows its warranty has expired. */
public boolean isPastWarranty() {
return getWarranty().before( new Date() );
}
/** Doesn't expose how objects are stored in the fridge. */
public synchronized void store( Object o ) {
validateExpiration( o );
// Can the object fit?
if( canStore( o ) ) {
getObjects().add( o );
}
else {
throw FridgeFullException( o );
}
}
/** Doesn't expose how objects are removed from the fridge. */
public synchronized void remove( Object o ) {
if( !getObjects().contains( o ) ) {
throw new ObjectNotFoundException( o );
}
getObjects().remove( o );
validateExpiration( o );
}
/** Lazily initialized list, an implementation detail. */
private synchronized List getObjects() {
if( this.list == null ) { this.list = new List(); }
return this.list;
}
/** How object expiration is determined is also a detail. */
private void validateExpiration( Object o ) {
// Objects can answer whether they have gone past a given
// expiration date. How each object "knows" it has expired
// is a detail. The Fridge might use a scanner and
// items might have embedded RFID chips. It's a detail hidden
// by proper encapsulation.
if( o implements Expires && ((Expires)o).expiresBefore( today ) ) {
throw new ExpiredObjectException( o );
}
}
/** This creates a copy of the warranty for immutability purposes. */
private void setWarranty( Date warranty ) {
assert warranty != null;
this.warranty = new Date( warranty.getTime() )
}
}
Type safety is a very minor reason to use first-class collections. From your link:
Rule 4: First class collections
Application of this rule is simple: any class that contains a collection should contain no other member variables. Each collection gets wrapped in its own class, so now behaviors related to the collection have a home. You may find that filters become a part of this new class. Also, your new class can handle activities like joining two groups together or applying a rule to each element of the group.
The idea here is if you find yourself searching, filtering, validating, or anything beyond add/remove/iterate semantics on a collection, the code is asking you to put it in its own class. If you need to update just one value (after a search), that probably goes in the collection class.
The reasoning for this is pretty simple, collections tend to get passed around. Soon enough, 4 different classes have their own SearchByID()
method. Or you get return values like Map<Integer, String>
with the context of what's stored in that map stripped away. A first-class collection is a simple solution that costs a single source file. In practice, once those are in place (they're very easy to write unit tests for as well), any change dealing with the collection is easy to handle, like when SearchByID
needs to take a GUID instead of an int.
Best Answer
Books can be wrong or misleading. It is certainly true that having getters and setters for every field provides little more encapsulation than having those fields be public, and it's unfortunate that this kind of design is very widespread.
But it's nonsense to conclude that getters and setters should be avoided altogether. Instead, they should be added deliberately where appropriate, as part of the API design of the class:
A Comparator could be seen as an example for case 2, especially if the Comparator is deliberately used instead of having the class implement Comparable. This indicates that the comparison logic is external to the class, and it depends on the field, so the field is useful outside the class.
A compromise might be to have a package private (default visibility) getter and keep the comparator in the same package - or even have it as an inner class that accesses the private field. That would be appropriate if you want the class itself to offer a choice of different sort orders.
If your only concern is that the sort order may change globally in the future, I'd actually stay with
Comparable
- it's still a change in once place, no need to separate it artificially.