【问题标题】:How to prevent direct access to private members outside of getters/setters in the same class?如何防止直接访问同一类中 getter/setter 之外的私有成员?
【发布时间】:2020-09-21 00:36:03
【问题描述】:

挑战

给定一个具有重要 getter 的类和一些其他内部方法访问相同的 private 成员字段:

有效示例

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();
    }
}

我想禁止在getValueLength() 方法中绕过getter。 getValueLength() 的以下实现应该会产生某种错误(单元测试失败、checkstyle 错误或其他任何可以以通用方式自动化的错误):

方法无效

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

背景

在我的实际代码中,getter 内部进行了一些延迟加载,仅在第一次访问时才加载实际值。先调用getValueLength() 可能会直接导致NullPointerException 或者基于缺失值执行错误操作。

这样做的动机是确保未来的开发人员不会忘记只使用 getter 方法而永远不会直接访问成员。由于这是一个孤立的问题(即仅在专门添加这种延迟加载的情况下),如果需要一些额外的注释是可以接受的,例如在成员本身上,它应该只在选定的方法中访问,并且这些方法也被标记 - 或特定的 checkstyle cmets 启用/禁用特定规则。

【问题讨论】:

  • 为什么不将value 初始化为"default" 而不是null?不再有 NPE,也不再有惰性初始化
  • 必须在同一个班级吗?您可以将 value 包装在 LazyValue 类中
  • value 可能总是有一个不直观的名称,例如 $value$,这会让未来的开发人员在访问它之前再看一遍
  • 封装的重点是对类的使用者隐藏实现细节。当您 从实现本身 隐藏这些细节时,这没有多大意义,所以我认为 OP 的问题更多是“我如何确保我不会意外使用这个字段而不是我的 getter在做这堂课的时候”。为此,删除惰性初始化或包装在一个对象中(以描述它的作用)将是最好的imo。不喜欢晦涩的变量名。
  • (1) 根据“背景”部分,预先初始化值会破坏延迟加载大量几乎不使用的数据的目的,在这个特定示例中,可以设置值稍后转至null (2) 包装成LazyValue 可能是一种选择,但由于返回类型不同,会阻止我们的工具检测到getter 和setter,(3) 奇怪的名称会与其他现有的规则,不是那么好。开发者不知道这个名字为什么这么奇怪。

标签: java getter-setter checkstyle


【解决方案1】:

没有办法保护成员免受其自己的类的影响,但是您可以保护成员免受其子类/子类的影响。例如:

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();
    }
}

这样TestType仍然可以使用setValue()getValue(),但是尝试直接访问this.value是行不通的,因为valueTestParent中有私有访问权限

【讨论】:

  • 这在某些情况下可能是一个很好的答案,但请记住,将此逻辑应用于enum 或多重继承(继承关联,而不是访问)会遇到障碍。在@ChristianScott 的评论之后,您可以改为将字段包装在一个对象中,这样获得它的唯一方法就是通过 getter。
  • 子类型的公平点(这就是为什么成员在问题中已经是private)。不幸的是,这并不能回答我的问题。希望有一些方法可以通过一些工具来实现它。
【解决方案2】:

只是把这个答案写成表格:

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;
    }
}

然后,将其应用于您的班级:

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();
    }
}

现在,当您在 TestType 的范围内编写代码时:

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

如您所见,这样做也使TestType#getValue 变得多余,您可以简单地允许value 成为protected 成员(在这种情况下,我会删除这些设置器并使其不可变)。

【讨论】:

  • LoadableVar 类中,可能需要将val 包装成Optional 或采取其他措施以允许加载的值是null,而不执行可能资源匮乏Supplier 反复。
【解决方案3】:

没有办法在代码中强制执行。

但是,由于您在评论中提到“希望有某种方法可以通过某种工具来实现它”,并且问题标记为,我相信您可以创建自定义规则:

<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>

然后你会想要禁止检查你自己的“有效”行:

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

在代码中:

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
}

(还有其他方法可以使用 cmets 来抑制 Checkstyle 的检查,您可能会发现这些检查在视觉上更令人愉悦。请参阅 filters 文档。)

当实际运行 Checkstyle 时,您可能希望将其限制在那个源文件中。例如,如果使用 Ant,您可能会这样做:

<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>

【讨论】:

  • 感谢您的投入和努力。所以这意味着我必须为这种模式的每次出现专门制定这样的 checkstyle 规则,并为相应的文件显式地执行它。听起来工作量很大。我希望有一个更通用的解决方案。我想如果这些东西遵循特定的命名约定,它可能是可行的,但我倾向于 LazyValue 包装方法。
  • 是的,工作量很大。我自己的首选解决方案是简单地将 javadoc 添加到强调任何人都不应直接访问它的私有字段。我希望开发人员在使用它之前注意任何代码元素的 javadoc。但是,有时我是一个理想主义者。
猜你喜欢
  • 2020-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-19
  • 1970-01-01
  • 2021-12-25
相关资源
最近更新 更多