Вопрос

Having come across the concept of immutable objects recently, I would like to know the best practices for controlling access to the state. Even though the object oriented part of my brain makes me want to cower in fear at the sight of public members, I see no technical issues with something like this:

public class Foo {
    public final int x;
    public final int y;

    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}

I would feel more comfortable declaring the fields as private and providing getter methods for each but this seems overly complex when the state is explicitly read only.

What is the best practice for providing access to the state of an immutable object?

Это было полезно?

Решение

It depends entirely on how you're going to use the object. Public fields aren't inherently evil, it's just bad to default everything to being public. For example the java.awt.Point class makes its x and y fields public, and they aren't even final. Your example seems like a fine use of public fields, but then again you might not want to expose all of the internal fields of another immutable object. There is no catch-all rule.

Другие советы

I have thought the same in the past but usually end up making variables private and using getters and setters so that later on I'll still have the option of making changes to the implementation while keeping the same interface.

This did remind me of something I read recently in "Clean Code" by Robert C. Martin. In chapter 6 he gives a slightly different perspective. For example, on page 95 he states

"Objects hide their data behind abstractions and expose functions that operate on that data. Data structure expose their data and have no meaningful functions."

And on page 100:

The quasi-encapsulation of beans seems to make some OO purists feel better but usually provides no other benefit.

Based on the code sample, the Foo class would seem to be a data structure. So based on what I understood from the discussion in Clean Code (which is more than just the two quotes I gave), the purpose of the class is to expose data, not functionality, and having getters and setters probably does not do much good.

Again, in my experience, I have usually gone ahead and used the "bean" approach of private data with getters and setters. But then again, no one ever asked me to write a book about how to write better code so maybe Martin has something to say.

If your object is of local-enough usage that you don't care about the issues of breaking API changes for it in the future, there is no need to tack getters on top of the instance variables. But this is a general subject, not specific to immutable objects.

The advantage of using getters comes from one extra layer of indirection, which may come in handy if you are designing an object which will be widely used, and whose utility will extend into unforseeable future.

Regardless of immutability, you're still exposing the implementation of this class. At some stage you'll want to change the implementation (or perhaps produce various derivations e.g. using the Point example, you may want a similar Point class using polar coordinates), and your client code is exposed to this.

The above pattern may well be useful, but I'd generally restrict it to very localised instances (e.g. passing tuples of information around - I tend to find that objects of seemingly unrelated info, however, either are bad encapsulations, or that the info is related, and my tuple transforms into a fully-fledged object)

The big thing to keep in mind is that function calls provide a universal interface. Any object can interact with other objects using function calls. All you have to do is define the right signatures, and away you go. The only catch is that you have to interact solely through these function calls, which often works well but can be clunky in some cases.

The main reason to expose state variables directly would be to be able to use primitive operators directly on these fields. When done well, this can enhance readability and convenience: for example, adding Complex numbers with +, or accessing a keyed collection with []. The benefits of this can be surprising, provided that your use of the syntax follows traditional conventions.

The catch is that operators are not a universal interface. Only a very specific set of built-in types can use them, these can only be used in the ways that the language expects, and you cannot define any new ones. And so, once you've defined your public interface using primitives, you've locked yourself into using that primitive, and only that primitive (and other things that can be easily cast to it). To use anything else, you have to dance around that primitive every time you interact with it, and that kills you from a DRY perspective: things can get very fragile very quickly.

Some languages make operators into a universal interface, but Java doesn't. This is not an indictment of Java: its designers chose deliberately not to include operator overloading, and they had good reasons to do so. Even when you're working with objects that seem to fit well with the traditional meanings of operators, making them work in a way that actually makes sense can be surprisingly nuanced, and if you don't absolutely nail it, you're going to pay for that later. It is often much easier to make a function-based interface readable and usable than to go through that process, and you often even wind up with a better result than if you'd used operators.

There were tradeoffs involved in that decision, however. There are times when an operator-based interface really does work better than a function-based one, but without operator overloading, that option just isn't available. Trying to shoehorn operators in anyway will lock you into some design decisions that you probably don't really want to be set in stone. The Java designers thought that this tradeoff was worthwhile, and they might even have been correct about that. But decisions like this don't come without some fallout, and this kind of situation is where the fallout hits.

In short, the problem isn't exposing your implementation, per se. The problem is locking yourself into that implementation.

Actually, it breaks encapsulation to expose any property of an object in any way -- every property is an implementation detail. Just because everybody does this doesn't make it right. Using accessors and mutators (getters and setters) doesn't make it any better. Rather, the CQRS patterns should be used to maintain encapsulation.

I know only one prop to have getters for final properties. It is the case when you'd like to have access to the properties over an interface.

    public interface Point {
       int getX();
       int getY();
    }

    public class Foo implements Point {...}
    public class Foo2 implements Point {...}

Otherwise the public final fields are OK.

The class that you have developed, should be fine in its current incarnation. The issues usually come in play when somebody tries to change this class or inherit from it.

For example, after seeing above code, somebody thinks of adding another member variable instance of class Bar.

public class Foo {
    public final int x;
    public final int y;
    public final Bar z;

    public Foo( int x, int y, Bar z) {
        this.x = x;
        this.y = y;
    }
}

public class Bar {
    public int age; //Oops this is not final, may be a mistake but still
    public Bar(int age) {
        this.age = age;
    }
}

In above code, the instance of Bar cannot be changed but externally, anybody can update the value of Bar.age.

The best practice is to mark all fields as private, have getters for the fields. If you are returning an object or collection, make sure to return unmodifiable version.

Immunatability is essential for concurrent programming.

An object with public final fields that get loaded from public constructor parameters effectively portrays itself as being a simple data holder. While such data holders aren't particularly "OOP-ish", they are useful for allowing a single field, variable, parameter, or return value to encapsulate multiple values. If the purpose of a type is to serve as a simple means of gluing a few values together, such a data holder is often the best representation in a framework without real value types.

Consider the question of what you would like to have happen if some method Foo wants to give a caller a Point3d which encapsulates "X=5, Y=23, Z=57", and it happens to have a reference to a Point3d where X=5, Y=23, and Z=57. If the thing Foo has is known to be a simple immutable data holder, then Foo should simply give the caller a reference to it. If, however, it might be something else (e.g. it might contain additional information beyond X, Y, and Z), then Foo should create a new simple data holder containing "X=5, Y=23, Z=57" and give the caller a reference to that.

Having Point3d be sealed and expose its contents as public final fields will imply that methods like Foo may assume it's a simple immutable data holder and may safely share references to instances of it. If code exists that make such assumptions, it may be difficult or impossible to change Point3d to be anything other than a simple immutable data holder without breaking such code. On the other hand, code which assumes Point3d is a simple immutable data holder can be much simpler and more efficient than code which has to deal with the possibility of it being something else.

You see this style a lot in Scala, but there is a crucial difference between these languages: Scala follows the Uniform Access Principle, but Java doesn't. That means your design is fine as long as your class doesn't change, but it can break in several ways when you need to adapt your functionality:

  • you need to extract an interface or super class (e.g. your class represents complex numbers, and you want to have a sibling class with polar coordinate representation, too)
  • you need to inherit from your class, and information becomes redundant (e.g. x can be calculated from additional data of the sub-class)
  • you need to test for constraints (e.g. x must be non-negative for some reason)

Also note that you can't use this style for mutable members (like the infamous java.util.Date). Only with getters you have a chance to make a defensive copy, or to change representation (e.g. storing the Date information as long)

I use a lot constructions very similar to the one you put in the question, sometimes there are things that can be better modeled with a (sometimes inmutable) data-strcuture than with a class.

All depends, if you are modeling an object, an object its defined by its behaviors, in this case never expose internal properties. Other times you are modeling a data-structure, and java has no special construct for data-structures, its fine to use a class and make public all the properties, and if you want immutability final and public off course.

For example, robert martin has one chapter about this in the great book Clean Code, a must read in my opinion.

In cases where the only purpose is to couple two values to each other under a meaningful name, you may even consider to skip defining any constructors and keep the elements changeable:

public class Sculpture {
    public int weight = 0;
    public int price = 0;
}

This has the advantage, to minimize the risk to confuse the parameter order when instantiating the class. The restricted changeability, if needed, can be achieved by taking the whole container under private control.

Just want to reflect reflection:

Foo foo = new Foo(0, 1);  // x=0, y=1
Field fieldX = Foo.class.getField("x");
fieldX.setAccessible(true);
fieldX.set(foo, 5);
System.out.println(foo.x);   // 5!

So, is Foo still immutable? :)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top