I tried to squeeze it into a comment, as it technically does not answer the question about whether this is a "Bad Practice™", but this term is hard to define, and thus, it is nearly impossible to give an answer anyhow...
This eventually seems to boil down to the question of how to Make java methods visible to only specific classes (and there are similar questions). The getter/setter should only be available to one particular class - namely, to the visitor.
You used very generic names and descriptions in the question, and it's hard to say whether this makes sense in general.
But some points to consider:
One could argue that this defeats the encapsulation in general. Everybody could write such a visitor and obtain access to the get/set methods. And even though this would be a ridiculous hack: If people want to achieve a goal, they will do things like that! (sketeched in Appendix 1 below)
More generally, one could argue: Why is only the visitor allowed to access the setter/getter, and other classes are not?
One convincing reason to hide getter/setter methods behind Supplier
/Consumer
instances could be related to visibility and the specificness of classes (elaborated in Appendix 2). But since the visitor always has the dependency to the visited class, this is not directly applicable here.
One could argue that the approach is more error prone. Imagine the case that either the setter or the getter are null
, or that they belong to different instances. Debugging this could be awfully hard.
As seen in the comments and other answer: One could argue that the proposed approach only complicates things, and "hides" the fact that these are actually setter/getter methods. I wouldn't go so far to say that having setter/getter methods in general already is a problem. But your approach is now to have setter-setters and getter-setters in a visitor. This extends the state space of the visitor in a way that is hard to wrap the head around.
To summarize:
Despite the arguments mentioned above, I would not call it a "bad practice" - also because it is not a common practice at all, but a very specific solution approach. There may be reasons and arguments to do this, but as long as you don't provide more details, it's hard to say whether this is true in your particular case, or whether there are more elegant solutions.
Update
For the added details: You said that
inconsistencies could arise as users change these variables
It is usually the responsibility of a class to manage its own state space in a way that makes sure that it is always "consistent". And, in some sense, this is the main purpose of having classes and encapsulation in the first place. One of the reasons of why getters+setters are sometimes considered as "evil" is not only the mutability (that should usually be minimized). But also because people tend to expose properties of a class with getters+setters, without thinking about a proper abstraction.
So specifically: If you have two variables x
and y
that depend on one another, then the class should simply not have methods
public void setX(int x) { ... }
public void setY(int y) { ... }
Instead, there should (at best, and roughly) be one method like
public void setState(int x, int y) {
if (inconsistent(x,y)) throw new IllegalArgumentException("...");
...
}
that makes sure that the state is always consistent.
I don't think that there is a way of cleanly emulating a C++ friend
function. The Consumer
/Supplier
approach that you suggested may be reasonable as a workaround. Some (not all) of the problems that it may cause could be avoided with a slightly different approach:
The package org.example
contains your main class
class A {
private int v;
private int w;
public void accept(SomeVisitor visitor) {
// See below...
}
}
And the package org.example
also contains an interface. This interface exposes the internal state of A
with getter+setter methods:
public interface InnerA {
void setV(int v);
int getV();
void setW(int w);
int getW();
}
But note that the main class does not implement this interface!
Now, the visitors could reside in a different packakge, like org.example.visitors
. And the visitor could have a dedicated method for visiting the InnerA
object:
public class SomeVisitor extends ParentVisitor {
@Override
public void visit(A a) {
...
}
@Override
public void visit(InnerA a) {
// Code that will, possibly, read and modify A.x
...
}
The implementation of the accept
method in A
could then do the following:
public void accept(SomeVisitor visitor) {
visitor.accept(this);
visitor.accept(new InnerA() {
@Override
public void setX(int theX) {
x = theX;
}
@Override
public int getX() {
return x;
}
// Same for y....
});
}
So the class would dedicatedly pass a newly created InnerA
instance to the visitor. This InnerA
would only exist for the time of visiting, and would only be used for modifying the specific instance that created it.
An in-between solution could be to not define this interface, but introduce methods like
@Override
public void visit(Consumer<Integer> setter, Supplier<Integer> getter) {
...
}
or
@Override
public void visit(A a, Consumer<Integer> setter, Supplier<Integer> getter) {
...
}
One would have to analyze this further depending on the real application case.
But again: None of these approaches will circumvent the general problem that when you provide access to someone outside of your package, then you will provide access to everyone outside of your package....
Appendix 1: A class that is an A
, but with public getter/setter methods. Goodbye, encapsulation:
class AccessibleA extends A {
private Consumer<Integer> setter;
...
AccessibleA() {
EvilVisitor e = new EvilVisitor();
e.accept(this);
}
void setSetter(Consumer<Integer> setter) { this.setter = setter; }
...
// Here's our public setter now:
void setValue(int i) { setter.accept(i); }
}
class EvilVisitor {
private AccessibleA accessibleA;
...
public void setSetter(Consumer<Integer> setter) {
accessibleA.setSetter(setter);
}
...
}
Appendix 2:
Imagine you had a class like this
class Manipulator {
private A a;
Manipulator(A a) {
this.a = a;
}
void manipulate() {
int value = a.getValue();
a.setValue(value + 42);
}
}
And now imagine that you wanted to remove the compile-time dependency of this class to the class A
. Then you could change it to not accept an instance of A
in the constructor, but a Supplier
/Consumer
pair instead. But for a visitor, this does not make sense.