如何防止可变对象的修改

时间:2020-09-26 07:32:36

标签: java

假设我有一个简单的课:

class Some {
  private Something something;

  public Some() {}

  public Something getSomething() {
    return this.something;
  }

  public void setSomething(Something something) {
    this.something = something;
  }
}

我有一个方法:

void change(Some some) {
  //change state of object via
  some.setSomething(new Something());
}

如何防止Some对象的状态在change()方法内更改?

条件:您无法修改Some类(即删除setSomething()方法,将something字段设置为final,将该类声明为final

我找到的解决方案:

interface ISome {
  void setSomething(Something something);
  Something getSomething();
}

class Some implements ISome {
  private Something something;

  public Some() {}

  public Something getSomething() {
    return this.something;
  }

  public void setSomething(Something something) {
    this.something = something;
  }
}

class SomeProxy implements ISome {
  private ISome some;
  
  public SomeProxy(ISome some) {
    this.some = some;
  }

  public Something getSomething() {
    return some.getSomething();
  }

  public void setSomething(Something something) {
    throw new IllegalOperationException("You cant modify an object!");
  }
}

方法:

void change(ISome some) {
  some.setSomething(new Something()); // 
}

然后,如果我们像这样调用change()方法:

void(new SomeProxy());

我们会得到一个例外。

java机制中的哪一个可以帮助解决该问题并防止在不创建代理的情况下在change()方法内修改对象?

1 个答案:

答案 0 :(得分:1)

无设置器的界面

我同意@ kaya3的评论,只需定义一个不带setter的接口并将其提供给您的函数即可:

interface ISome {
  Something getSomething();
}

class Some implements ISome {
  private Something something;

  public Some() {}

  public Something getSomething() {
    return this.something;
  }

  public void setSomething(Something something) {
    this.something = something;
  }
}


void change(ISome some) {
  //Doesn't compile there no method setSomething in the interface
  some.setSomething(new Something());
}

请注意,您仍在修改Some类,使它实现一个接口,如果该类来自lib并且您真的不能触摸它,则它不起作用。我使用它是因为您是自己做的,但是如果您真的不能修改类,那么这仍然不是一个很好的解决方案。

仍然,这是隔离接口和实现的绝佳解决方案。该接口是公共API的一部分,没有人可以使用公共接口修改对象。因此,API总是返回接口的实例,但在内部使用具体的实现。

真正不影响原始课程的代理

您可以通过不做一些实现该接口的步骤来进一步操作,如果该类来自外部API,请重新引入您的代理:

class Some {
  private Something something;

  public Some() {}

  public Something getSomething() {
    return this.something;
  }

  public void setSomething(Something something) {
    this.something = something;
  }
}

class SomeProxy implements ISome {
  private Some some;
  
  public SomeProxy(Some some) {
    this.some = some;
  }

  public Something getSomething() {
    return some.getSomething();
  }
}


void change(ISome some) {
  //Doesn't compile there no method setSomething in the interface
  some.setSomething(new Something());
}

这样一来,您无需再更改Some类,但要正常工作,您必须确保Some类在客户端代码中不可见。借助jave 9模块,OSGI或maven中的运行时依赖性,您可以实现这一目标。

代理只是更通用和常见的一种具体情况

如果您考虑一下,这是一种特殊情况,即只有一个带有私有字段的类供其自己使用,然后发布该对象行为的一部分,而不是全部。此类不需要这样做。她的目的可能不是成为代理,而是恰好有一个Some实例来完成其工作。她控制“某人”实例,因为她在控制中,所以可以呼叫设置器,但是对于外部世界,她只允许访问设置器。

这是一般情况。该类确实实现了概念/功能,并正确封装了其内部状态和实现。而且,如果您将该类传递给任何函数,则没人会以不想要的方式修改内部状态:

class OtherBehavior {
  private Some some;
  private OtherThing thing;
  [...]

  public Something getSomething() {
    return some.getSomething();
  }

  public void doStuff() {
    [...]
  }
  [...]
}

void change(OtherBehavior other) {
  // Doesn't compile:
  other.setSomething(new Something()); 
  // Doesn't compile:
  other.some.setSomething(new Something());
  // Ok:
  other.getSomething();
}

在一般情况下,请首选不变设计

通常认为,在许多情况下,不变的设计总体上更好。因此,允许构造函数获取Something()实例,然后将字段定为final,并且不提供setter。您会发现,在许多情况下,您根本不需要设置器,并且只要Something实例可用就可以创建不可变对象。这是更简单,更可靠的设计。如果您的API需要“更改”为新的Something,则始终可以创建一个新的Some实例:

class Some {
  private final Something something;

  public Some(Something something) {
    this.something = something;
  }

  public Something getSomething() {
    return this.something;
  }
}

这有好处:

  • 您可以添加equals / hashCode并使其安全地参与到map / set集合中,甚至可以在构造时计算哈希并将其缓存以提高性能(对性能敏感代码有很大帮助)
  • 您可以安全地共享对象周围的线程
  • 您可以防止客户端调用该setter发生错误,因此您可以将其传递到任何地方而没有任何错误风险。
  • 您根本不需要复杂的代理,只需复制或处理异常即可。

但是,这当然意味着您可以控制Some类。