【问题标题】:Can Scala constrain an object graph so that only those objects relevant to the context are visible?Scala 能否约束对象图,以便只有与上下文相关的对象可见?
【发布时间】:2012-03-13 10:31:42
【问题描述】:

有没有办法使用 Scala 的类型系统来简洁地指定完整对象图的上下文相关子图?

DCI 认为您通常拥有一个相当复杂的对象图,但在任何一个用例中,您通常只想使用子图。你有一个Foo,它有一个Bar和一个Bat,但是当你在用例1中时,你只关心Bar,而在用例2中时,只关心@987654325 @。

例如,假设您有这个结构,角色 1 用例需要 Foo->Bar->Baz->Bin,角色 2 用例需要 Foo->Bat->Baz->Buz

class Foo{
   val bar = new Bar() //Only relevant to Role 1
   val bat = new Bat() //Only relevant to Role 2 
}

class Bar {
   val baz = new Baz() 
}

class Bat {
   val baz = new Baz()
}

//Relevant to both Role 1 and 2 (via Bar or Bat)
class Baz {
  val bin = new Bin() //Only relevant to Role 1
  val buz = new Buz() //Only relevant to Role 2
}

class Bin{}
class Buz{}

很容易看出如何通过使用特征来限制单个类中的访问:

trait FooInRole1 { def bar : Bar }  //Define accessor in trait
s/Foo/Foo extends FooInRole1/       //Change Foo's declaration to implement trait
val f : FooInRole1 = new Foo        //LHS is i'face, RHS is implementation
//f.bat <--Compile error              Irrelevant field is not available. \o/ 

但是您必须为与用例相关的每个对象重复此模式。 (例如,你需要一个BazInRole1 来访问bin 和一个BazInRole2 来访问biz

我的问题是,是否有某种方法可以避免编写所有这些容易出错、名称空间拥挤的特征。例如,我可以想象这样的代码(不能编译):

class Foo[T] {
  T match { 
    case r1 : Role1 => def bar : Bar[T]
    case r2 : Role2 => def bat : Bat[T]
    case _ => //Nothing
  }
}

val fInRole1 = new Foo[Role1] //Provides Foo->Bar->Baz->Bin
val fInRole2 = new Foo[Role2] //Provides Foo->Bat->Baz->Buz

似乎 Scala 的类型系统具有足够的表现力来做这样的事情,但我无法弄清楚。

【问题讨论】:

  • 我认为这样的事情可以通过类型类来实现。只需使类型类成为对象图上的视图,并仅通过类型类访问和操作其内容。

标签: scala abstraction dci typeclass


【解决方案1】:

不是很简洁,成员也有,就是用不上,不过往这个方向走或许可以接受?

class Foo[R] {
  def bar(implicit ev: R <:< Role1) = new Bar[R] //Only relevant to Role 1
  def bat(implicit ev: R <:< Role2) = new Bat[R] //Only relevant to Role 2
}

【讨论】:

    【解决方案2】:

    如果我正确理解了您的问题(我不确定),您希望Foo 提供barbat 之一,具体取决于Foo 的类型参数。

    我的第一枪是:

    class Bar
    class Bat
    
    trait BarExt { def bar = new Bar }
    trait BatExt { def bat = new Bat }
    
    trait Role
    case object Role1 extends Role
    case object Role2 extends Role
    
    trait RoleProvider[From <: Role, To] {
      def apply(): To
    }
    
    object RoleProvider {
      implicit val r1 = new RoleProvider[Role1.type, Foo[Role1.type] with BarExt] {
        def apply() = new Foo[Role1.type] with BarExt
      }
    
      implicit val r2 = new RoleProvider[Role2.type, Foo[Role2.type] with BatExt] {
        def apply() = new Foo[Role2.type] with BatExt
      }
    }
    
    class Foo[T <: Role]
    
    object Foo {
      def create[T <: Role, To](f: T)(implicit rp: RoleProvider[T,To]): To = rp()
    }
    

    这样

    scala> Foo.create(Role1)
    res1: Foo[Role1.type] with BarExt = RoleProvider$$anon$3$$anon$1@187b2d93    scala> Foo.create(Role1).bar
    
    scala> Foo.create(Role1).bar
    res2: Bar = Bar@7ea4b9da
    
    scala> Foo.create(Role1).bat
    <console>:12: error: value bat is not a member of Foo[Role1.type] with BarExt
                  Foo.create(Role1).bat
    

    scala> Foo.create(Role2).bat
    res3: Bat = Bat@56352b57
    
    scala> Foo.create(Role2).bar
    <console>:12: error: value bar is not a member of Foo[Role2.type] with BatExt
                  Foo.create(Role2).bar
    

    可以通过将相应的声明拉入r1r2 的定义中来摆脱BarExtBatExt,但是我发现使用它“更难”:

    implicit val r1 = new RoleProvider[Role1.type, Foo[Role1.type] { val bar: Bar }] {
      def apply() = new Foo[Role1.type] { val bar = new Bar }
    }
    
    implicit val r2 = new RoleProvider[Role2.type, Foo[Role2.type] { val bat: Bat }] {
      def apply() = new Foo[Role2.type] { val bat = new Bat }
    }
    

    归根结底,我仍然不相信这正是您所要求的,或者是吗?

    【讨论】:

    • 我不认为这正是我想要的。想想 Baz(或者,更糟糕的是,一个包含 20 个具有交叉引用的类的图表)。为了确保 Baz.bin 仅在 Role1 中可用,我不必为每个角色的每个对象定义一个特征 (xExt) 并为每个边编写一个转换 fn 吗?也许这是可以做到的最好的事情,但我希望找到一种方法来简洁地将类型参数从 Foo “级联”到 Baz。这有意义吗?
    • 所以 all 方法应该包含在原始图表中,但是当从某个角色“查看”图表时,只有这些方法的一个子集被公开?我假设“子图”指的是同一组节点,但具有不同的、依赖于类型的角色?
    • 是的,这正是我所希望的。
    • 为每个返回所需类型引用的节点定义一个toRole[T] 方法是否有意义?您可能可以使用与上面示例相同的隐式机制。
    • 是的,something 比如 toRole 或 RoleAdapter[T <: role u typethatcanassumerole :>somewhere 中写“Role1 表示 Foo 和 Bar,Baz 和 Bin 等”。只是您希望该定义明确且位于一个位置。然后,如您所说,如果您有某种 toRole / inRole 工厂,可以在一个地方处理布线。
    【解决方案3】:

    在 DCI 的 this artima article 中,作者提出了一种在 Scala 中获得 DCI 架构的方法,这看起来就像您的目标一样。

    基本思想是在 trait 中定义与您的用例相关的方法,但不是您的方法,而是使用自类型注释来确保它是某个基类的对象。

    所以为了让这更容易理解:你有一个数据类Data,它包含数据对象的基本组件。当你想实现某个用例时,它喜欢在某个角色Role 中考虑一个Data 对象,你可以像这样准备角色:

    trait Role { self : Data => 
      def methodForOnlyThisUseCase = {...}
    }
    

    为了执行用例,您可以通过以下方式创建一个特定于该角色的对象:

    val myUseCaseObject = new Data with Role
    

    像这样,对象myUseCaseObject 仅限于它的Data 成分以及它在给定用例中的角色所需的方法。

    如果它变得更复杂,您可能必须创建类似伪角色特征的东西,它定义了多个用例共有的方法。然后用例角色​​的自类型注释将指向该伪特征,而伪特征自类型注释指向相应的数据类。

    【讨论】:

    • 是的,这是一篇关于 DCI 的重要文章,我的问题是从这种类型的设计开始的,但问题是您必须为每个角色中的角色明确编写一个特征。在我的示例中,您最终必须创建一个特征 FooInRole1、FooInRole2 和 BazInRole1、BazInRole2,甚至最终必须创建一个 BarInRole1 以便将角色类型“传递”给 Baz。 可能这已经是你能做到的最好了,但似乎应该有一种方法可以避免如此多的命名空间混乱。
    猜你喜欢
    • 2016-10-09
    • 1970-01-01
    • 1970-01-01
    • 2010-11-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-30
    相关资源
    最近更新 更多