首页 > 解决方案 > 如何防止修改可变对象

问题描述

假设我有一个简单的类:

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()方法:

change(new SomeProxy());

我们会得到一个例外。

java机制中的什么地方可以帮助解决这个问题并防止对象在change()方法内部被修改而不创建代理?

标签: java

解决方案


没有设置器的接口

我同意@kaya3 的评论,只需定义一个没有设置器的接口并将其提供给您的函数:

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 类,您使其实现了一个接口,如果该类来自一个库并且您真的无法触摸它,它就不起作用。我使用它是因为你自己做了,但如果你真的不能修改类,这仍然不是一个很好的解决方案。

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

一个真正不触及原始类的代理

如果类来自外部 API,您可以通过不让 Some 实现接口并引入代理来更进一步:

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 实例来完成它的工作。她控制 Some 实例,可以调用 setter 因为她处于控制之中,但对于外部世界,她只授予对 setter 的访问权限。

这是一般情况。该类确实实现了一个概念/功能并正确封装了它的内部状态和实现。如果你将该类传递给任何函数,没有人可以以不需要的方式修改内部状态:

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。您会发现,在很多情况下,您根本不需要 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 并使其安全地参与映射/集合集合,您甚至可以在构建时计算哈希并将其缓存以提高性能(在性能敏感代码方面帮助了我们很多)
  • 您可以安全地围绕线程共享对象
  • 您可以防止客户端调用该 setter 发生错误,因此您可以将它传递到任何地方而没有任何错误风险。
  • 您根本不需要复杂的代理,复制或处理异常。

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


推荐阅读