如何防止可变对象的修改

huangapple go评论57阅读模式
英文:

How to prevent modification of mutable object

问题

假设我有一个简单的类:

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) {
  // 通过下面的代码改变对象的状态
  some.setSomething(new Something());
}

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

条件:你不能修改 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("无法修改对象!");
  }
}

方法:

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

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

change(new SomeProxy());

我们会得到一个异常。

在 Java 中是否有机制可以解决这个问题,而不需要创建代理?

英文:

Let's suppose I have a simple class:

class Some {
  private Something something;

  public Some() {}

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

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

and I have a method:

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

How can I prevent the state of the Some object from changing inside the change() method?

Condition: you can't modify the Some class (i.e. remove the setSomething() method, make the something field as final, declare the class as final)

Solution I've found:

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!");
  }
}

Method:

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

And then if we call the change() method like this:

change(new SomeProxy());

we will get an exception.

Is where in java mechanism that could help with that problem and prevent the object from being modified inside the change() method without creating a proxy?

答案1

得分: 6

没有setter的接口

我同意 @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) {
  // 无法通过编译,接口中没有setSomething方法
  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) {
  // 无法通过编译,接口中没有setSomething方法
  some.setSomething(new Something());
}

这种方法不会对Some类进行任何修改,但为了正常工作,您必须确保客户端代码无法访问Some类。您可以通过Java 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) {
  // 无法通过编译:
  other.setSomething(new Something());
  // 无法通过编译:
  other.some.setSomething(new Something());
  // 可以通过:
  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类具有控制权。

英文:

An interface without setter

I agree with the comment of @kaya3, just define an interface without the setter and give that to your function:

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

Please notice that you still modify the Some class, you make it implement an interface and if that class is from a lib and you really can't touch it, it doesn't work. I used it because you did it yourself, but still this isn't a great solution if you really can't modify the class.

Still this is great solution to segregate interface and implementation. The interface is part of your public API and nobody can modify the object with the public interface. So the API always return instances of the interface but use the concrete implementation inside.

A proxy that really don't touch the original class

You can go a step further by not making Some implementing the interface and introduce back your proxy if the class if from an external 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());
}

This way you never changed the Some class one bit but to work properly you must ensure the the Some class isn't visible from the client code. You can achieve that thanks to jave 9 modules, thanks to OSGI or thanks to runtime dependency in maven.

The proxy is just a specific case of a more generic and common one

If you think about it, it is specific case of just having a class with a private field for its own use, and publishing part of that object behavior but not all. This class doesn't need to do only that. She may have a purpose that isn't being a proxy but just happen to have a Some instance to do its job. She control the Some instance, can call the setter because she is in control, but for the outside world, she only give access to the setter.

This is the general case. That class does implement a concept/feature and encapsulate properly its internal state and implementation. And if you pass that class to any function, nobody can modify the interal state in an unwanted way:

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

Please prefer immutable design in the general case

It is usually recognized that an immutable design is overall better in many cases. So allows the constructor to get the Something() instance, and then make the field final and provide no setter. You'll find that in many case you don't need the setter at all and can just create the immutable object when the Something instance is already available. It is much simpler and robust design. If you API need to "change" to a new Something, you could always also create a new Some instance:

class Some {
  private final Something something;

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

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

This has benefits:

  • you can add the equals/hashCode and make it participate to maps/set collections safely, you can even compute the hash at construction time and cache it for improved perf (helped us a lot on perf sensitive code)
  • you can share the object arround threads safely
  • you prevent bug occuring by a client calling that setter, so you can pass it anywhere without any risk of bug.
  • you don't need a complex proxy at all, making a copy or deal with exceptions.

But of course this imply you are in control of the Some class.

huangapple
  • 本文由 发表于 2020年9月26日 15:32:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/64075057.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定