【问题标题】:How to enforce encapsulation of immutable class in Scala?如何在Scala中强制封装不可变类?
【发布时间】:2021-05-20 20:19:59
【问题描述】:

我正在尝试在 Dart 中编写不可变代码。 Dart 并没有真正考虑到不变性,这就是为什么我需要编写大量样板来实现不变性。因此,我对围绕不变性概念构建的 Scala 等语言如何解决这个问题产生了兴趣。

我目前在 Dart 中使用以下类:

class Profile{
  List<String> _inSyncBikeIds = []; // private field
  String profileName; // public field

  Profile(this.profileName); // You should not be able to pass a value to _inSyncBikeIds

  void synchronize(String bikeId){
    _inSyncBikeIds.add(bikeId);
  }

  bool isInSync(String bikeId){
    return _inSyncBikeIds.contains(bikeId);
  }

  void reset(){
    _inSyncBikeIds = [];
  }
}

immutable 中的同一个类:

class Profile{
  final List<String> _inSyncBikeIds = []; // private final field
  final String profileName; // public final field

  factory Profile(String profileName) => Profile._(profileName); // You should not be able to pass a value to _inSyncBikeIds

  Profile._(this._inSyncBikeIds, this.profileName); // private contructor

  Profile synchronize(String bikeId){
    return _copyWith(inSyncBikeIds: _inSyncBikeIds.add(bikeId);
  }

  bool isInSync(String bikeId) {
    return _inSyncBikeIds.contains(bikeId);
  }

  Profile reset(){
    return _copyWith(inSyncBikeIds: []);
  }

  Profile copyWith({
    String profileName,
  }) {
    return _copyWith(profileName: profileName)
  }

  Profile _copyWith({
    String profileName,
    List<Id> inSyncBikeIds,
  }) {
    return Profile._(
        profileName: profileName ?? this.profileName,
        inSyncBikeIds: inSyncBikeIds ?? _inSyncBikeIds);
  }
}

到目前为止,我从 Scala 了解到的是,对于每个类,都会自动创建一个 copy 方法。为了能够使用copy 方法更改字段,它需要成为构造函数的一部分。

我希望字段 _inSyncBikeIdsfinal(在 Scala 中为 val)。为了更改字段_inSyncBikeIds 的值,我需要创建对象的副本。但是为了使用copy方法,改变字段,它需要是类的构造函数的一部分,比如class Profile(private val _inSyncBikeIds, val profileName)。但这会破坏封装,因为每个人都可以创建一个对象并初始化_inSyncBikeIds。就我而言,_inSyncBikeIds 在初始化后应该始终是一个空列表。

三个问题:

  • 如何在 Scala 中解决这个问题?
  • 当我在类内部使用copy方法时,可以使用copy方法更改私有字段吗?
  • Scala 中的 copy 方法是否也复制私有字段(即使它们不是构造函数的一部分,当然也不能改变该私有字段)?

【问题讨论】:

  • 听起来你在这里混淆了很多想法。并非 Scala 中的每个类都得到copy;只有 case 类 可以免费获得它,并且这些类专门用于您希望 数据公开且不可变的情况。如果您要封装私有数据,您确实需要案例类。你会使用一个普通的class,你要么不会得到copy,要么你会手动实现它来做任何有意义的事情。
  • @SilvioMayolo 谢谢!我明白,所以即使在 Scala 中,我也需要编写自己的复制方法,以实现所有类的真正不变性:(
  • 您希望该字段是不可变的,并且“初始化为空”。这意味着它永远是空的(你不能改变它,因为它是不可变的),不是吗?
  • 我希望能够制作对象的副本,并将字符串添加到列表中。类似于synchronize 方法。
  • 你不能在 dart 中有一个私有构造函数吗?如果它是一个东西,这似乎是一个明显的解决方案,不是吗?

标签: scala immutability


【解决方案1】:

Scala 的传统倾向于将不可变数据视为免费共享的许可证(因此默认情况下是公开的等)。封装的解释更多的是对象之外的代码不能直接改变数据:不可变的数据,无论可见性如何,都满足了这一点。

可以通过将abstract 设置为abstract 来抑制copy 自动生成的方法(几乎总是sealed abstract 带有private 构造函数)。这通常用于使 apply/copy 方法返回不同的类型(例如,将验证失败编码为值而不抛出异常(如 require 会)),但它可以用于您的目的

sealed abstract case class Profile private(private val _inSyncBikeIds: List[String], profileName: String) {
  def addBike(bikeId: String): Profile = Profile.unsafeApply(bikeId :: _inSyncBikeIds, profileName)

  // Might consider using a Set...
  def isInSync(bikeId: String): Boolean = _inSyncBikeIds.contains(bikeId)

  def copy(profileName: String = profileName): Profile = Profile.unsafeApply(_inSyncBikeIds, profileName)
}

object Profile {
  def apply(profileName: String): Profile = unsafeApply(Nil, profileName)
  private[Profile] def apply(_inSyncBikeIds: List[String], profileName: String): Profile = new Profile(_inSyncBikeIds, profileName) {}
}

unsafeApply 更常见于作为价值用例的验证,但它的主要目的是将抽象 Profile 的具体实现限制为仅匿名实现;这种单态性对运行时性能有有益的影响。

注意:case classes 是 Serializable,因此存在 Java 序列化漏洞:在应用程序代码中,这可以通过 从不使用 Java 序列化来解决,因为它已损坏,但它弥补了被损坏的缺陷完全邪恶(即,如果您有一个使用 Java 序列化的 Scala 应用程序,您可能应该重新评估导致您出现这种情况的选择)。

没有办法在 JVM 字节码 AFAIK 中编码 sealedness(Scala 使用注解 IIRC,因此 Scala 会将 Profile 的扩展限制到该编译单元,但是,例如,Kotlin 不会),也不是private[Profile] 访问控制以非 Scala 的 JVM 语言将强制执行的方式编码(unsafeApply 方法实际上是字节码中的 public)。同样,在应用程序代码中,显而易见的问题是“你为什么要从 Java/Kotlin/Clojure/...中使用它?”。在库中,您可能需要做一些骇人听闻的事情,例如抛出异常,捕获它并检查堆栈的顶部帧,如果不是 hunky-dory,则再次抛出。

【讨论】:

    【解决方案2】:

    我不知道在 dart 中是否可行,但在 scala 中,这将通过私有构造函数完成:

       class Profile private (val _foo: Seq[String], val bar: String) {
          def this(bar: String) = this(Nil, bar)
        }
    

    这让您可以定义

       private copy(foo: Seq[String], bar: String) = new Profile(foo, bar)
    

    这很好只要类是final。如果您将其子类化,则会出现错误:Child.copy() 返回Parent 的实例,除非您在每个子类中覆盖copy,但没有好的方法可以强制执行它(Scala 3 诚然对此有一些改进)。

    您提到的生成的copy 方法仅适用于案例类。但是子类化一个案例类会导致一些even more interesting results

    真的虽然很少有用。例如,查看您的代码,如果我正确阅读了问题,您希望用户 能够做到 Profile(List("foo"), "bar")Profile("bar").synchronize("foo") 仍然可能,即使它产生完全相同的结果。这似乎没什么用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-24
      • 2013-10-05
      • 2015-04-26
      • 2017-05-14
      • 2016-11-03
      相关资源
      最近更新 更多