简体   繁体   中英

How to prevent direct access to private members outside of getters/setters in the same class?

Challenge

Given a class with a non-trivial getter and some other internal method accessing the same private member field:

Valid Example

class TestType {
    private String value;

    public String getValue() {
        return this.value == null || this.value.isEmpty() ? "default" : this.value;
    }

    public void setValue(String newValue) {
        this.value = newValue;
    }

    public int getValueLength() {
        return this.getValue().length();
    }
}

I want to forbid the by-passing of the getter in the getValueLength() method. The following implementation of getValueLength() should produce some kind of error (unit test failure, checkstyle error, or whatever else can be automated in a generic way):

Invalid Method

    public int getValueLength() {
        // ERROR: possible NullPointerException
        return this.value.length();
    }

Background

In my real-life code there is some lazy-loading going on inside the getter that only loads the actual value when it is being accessed for the first time. Calling getValueLength() first may result in a NullPointerException directly or perform wrong actions based on the missing value.

The motivation is to ensure that future developers don't forget to only use the getter method and never access the member directly. Since this is an isolated issue (ie only where this kind of lazy-loaded is being specifically added) it is acceptable if some extra annotations are required, eg on the member itself that it should only be accessed in selected methods and those methods to be marked as well -- or specific checkstyle comments enabling/disabling specific rules.

Just to put this answer to form:

class LoadableVar<T> {

    private T val;
    private Supplier<T> loader;

    public LoadableVar(Supplier<? extends T> loader) {
        this.loader = loader;
    }

    public T get() {
        if (this.val == null) {
            //see: volatile and double-locking if multithreading
            this.val = this.loader.get();
        }
        return this.val;
    }

    public void set(T overwrite) { //WARN: ignores the loader!
        this.val = overwrite;
    }
}

Then, in applying it to your class:

class TestType {
    private final LoadableVar<String> value;

    public TestType() {
        //can also be passed into the class, or done however you desire
        this.value = new LoadableVar<>(() -> /* load string from i/o, etc */);
    }

    public String getValue() {
        return this.value.get();
    }

    public void setValue(String newValue) {
        this.value.set(newValue); //I don't think this should be settable, personally
    }

    public int getValueLength() {
        return this.getValue().length();
    }
}

Now when you are writing code within TestType's scope:

String s;
s = this.value; //compile error!
s = this.getValue(); //OK
s = this.value.get(); //OK

As you can see, in doing it this way you've also made TestType#getValue redundant, you could simply allow value to be a protected member (and in which case, I'd remove those setters and make it immutable).

There is no way to enforce this in code.

However, since you mention in a comment, “Hopefully there is some way to achieve it via some tool,” and the question is tagged with , I believe you can create a custom rule:

<module name="Regexp">
    <property name="id" value="valueField"/>
    <property name="format" value="\bvalue\b"/>
    <property name="illegalPattern" value="true"/>
    <property name="ignoreComments" value="true"/>
    <property name="message" value="Do not use 'value' field directly;  use getValue() instead."/>
</module>

You would then want to suppress that check for your own “valid” lines:

<module name="SuppressWithNearbyCommentFilter">
    <property name="idFormat" value="valueField"/>
</module>

And in the code:

private String value;   // SUPPRESS CHECKSTYLE

public String getValue() {
    return this.value == null || this.value.isEmpty() ? "default" : this.value; // SUPPRESS CHECKSTYLE
}

public void setValue(String newValue) {
    this.value = newValue;  // SUPPRESS CHECKSTYLE
}

(There are other ways to suppress Checkstyle's checks with comments, which you may find more visually pleasing. See the filters documentation.)

When actually running Checkstyle, you probably want to restrict it to that one source file. For instance, if using Ant, you might do:

<taskdef resource="com/puppycrawl/tools/checkstyle/ant/checkstyle-ant-task.properties"
     classpath="tools/checkstyle-8.33-all.jar"/>

<checkstyle config="checkstyle.xml">
    <fileset dir="src/main/java" includes="**/TestType.java"/>
</checkstyle>

There is not a way to protect a member from its own class, however you can protect a member from its children/subclasses. For example:

class TestParent {
    private String value;

    public String getValue() {
        return this.value == null || this.value.isEmpty() ? "default" : this.value;
    }

    public void setValue(String newValue) {
        this.value = newValue;
    }

}

class TestType extends TestParent {
    public int getValueLength() {
        return this.getValue().length();
    }
}

This way TestType can still use setValue() and getValue() , but trying to directly access this.value will not work because value has private access in TestParent

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