【问题标题】:Combinatorial Subtyping (in Scala)组合子类型(在 Scala 中)
【发布时间】:2011-05-09 08:53:42
【问题描述】:

我正在寻找一种干净的面向对象的方式来建模以下内容(在 Scala 中):

一个人可以是:

  • 某公司的经理
  • 数学家
  • 世界级的网球选手
  • 业余程序员
  • 当地学校的志愿者
  • 创意画家

这表明我们引入了Person 超类和子类:

  • Manager
  • Mathematician
  • TennisPlayer
  • HobbyistProgrammer
  • Volunteer
  • Painter

Manager 类有方法如:getSalary()workLongHours()findNewJob() 等。TennisPlayer 类有方法如:getWorldRanking()playGame()strainAnkle() 等。 等等。此外,Person 类中还有方法,例如becomeSick()。生病的经理失去了工作,网球运动员在本赛季停止比赛。

此外,这些类是不可变的。也就是说,例如 strainAnkle() 返回一个新的 TennisPlayer,它的脚踝拉伤,但所有其他属性保持不变。

现在的问题是:我们如何模拟一个人既可以是Manager 又可以是TennisPlayer 的事实?

解决方案保持不变性和类型安全性很重要。

我们可以实现如下类:

  • ManagerAndMathematician
  • ManagerAndTennisPlayerAndPainter
  • ManagerAndPainter

但这会导致类的组合爆炸。

我们也可以使用特征(带状态),但是我们如何实现诸如findNewJob() 之类的方法,这需要返回一个新的人,其中混合了相同的特征,但具有Manager 的新状态特征。同样,我们如何实现becomeSick()等方法?

问题:您将如何在 Scala 中以干净的 OO 方式实现这一点?记住:不可变性和类型安全是必须的。

【问题讨论】:

  • 生病的经理失业了” 太可怕了……你们国家没有社会保障?
  • 其实有很多,就说他请病假吧:)
  • 查看 odersky mp.binaervarianz.de/icsoft2008.pdf 的这篇论文。它可能正好有你需要的东西。
  • @chang 请添加标题,这样我们就不必点击链接就发现我们已经阅读了这篇论文。
  • 这篇论文讨论了面向角色和协作的编程。它似乎接近我希望实现的目标,但存在一些问题。

标签: scala


【解决方案1】:

在我看来,这不是继承的理想案例。也许您正试图将事物强制转换为继承模式,因为处理具有不可变值的组合似乎很尴尬。这是几种方法之一。

object Example {
  abstract class Person(val name: String) {
    def occupation: Occupation
    implicit val self = this
    abstract class Occupation(implicit val practitioner: Person) {
       def title: String
       def advanceCareer: Person
    }
    class Programmer extends Occupation {
      def title = "Code Monkey"
      def advanceCareer = practitioner
    }
    class Student extends Occupation {
      def title = "Undecided"
      def advanceCareer = new Person(practitioner.name) {
        def occupation = new Programmer
      }
    }
  }

  def main(args: Array[String]) {
    val p = new Person("John Doe") { def occupation = new Student }
    val q = p.occupation.advanceCareer
    val r = q.occupation.advanceCareer
    println(p.name + " is a " + p.occupation.title)
    println(q.name + " is a " + q.occupation.title)
    println(r.name + " is a " + r.occupation.title)
    println("I am myself: " + (r eq r.occupation.practitioner))
  }
}

让我们试试吧:

scala> Example.main(Array())
John Doe is a Undecided
John Doe is a Code Monkey
John Doe is a Code Monkey
I am myself: true

所以这有点有用。

这里的诀窍是,每次一个职业(这是一个内部类)决定改变事情时,你都会为你的人创建匿名子类。它的工作是创造一个新角色完整的新人;这得益于implicit val self = thisOccupation 上的隐式构造函数,它有助于自动加载正确的人实例。

您可能需要职业列表,因此可能需要帮助方法来重新生成职业列表。类似的东西

object Example {
  abstract class Person(val name: String) {
    def occupations: List[Occupation]
    implicit val self = this
    def withOccupations(others: List[Person#Occupation]) = new Person(self.name) {
      def occupations = others.collect {
        case p: Person#Programmer => new Programmer
        case s: Person#Pirate => new Pirate
      }
    }
    abstract class Occupation(implicit val practitioner: Person) {
       def title: String
       def addCareer: Person
       override def toString = title
    }
    class Programmer extends Occupation {
      def title = "Code Monkey"
      def addCareer: Person = withOccupations( this :: self.occupations )
    }
    class Pirate extends Occupation {
      def title = "Sea Monkey"
      def addCareer: Person = withOccupations( this :: self.occupations )
    }
  }

  def main(args: Array[String]) {
    val p = new Person("John Doe") { def occupations = Nil }
    val q = (new p.Programmer).addCareer
    val r = (new q.Pirate).addCareer
    println(p.name + " has jobs " + p.occupations)
    println(q.name + " has jobs " + q.occupations)
    println(r.name + " has jobs " + r.occupations)
    println("I am myself: " + (r eq r.occupations.head.practitioner))
  }
}

【讨论】:

  • 这似乎是一个很有前途的方法。我需要进一步研究它,但在我的头顶上:你将如何实施说Person.retire(),它从职业列表中删除Programmer,而不是Pirate?其次,你将如何实现Person.getSalary(假设程序员是唯一有薪水的人。)
  • 我可能会添加一个名为 withOccupations(self.occupations.flatMap { case p: Programmer => None; case _ => Some(p) })dropCareer 方法,如果我不想让 Occupation 默认定义 salary: Option[Double] = None 并覆盖它,我会有一个WageEarner 特征混合了一个 salary: Double 特征(我想在创建职业时被一个值覆盖)。然后该人使用occupations.flatMap(_.salary).sumoccupations.collect{ case w: WageEarner => w.salary }.sum 获得薪水,具体取决于方法。
【解决方案2】:

解决这个问题的干净的面向对象的方法不必是 Scala 特定的。人们可以坚持倾向于组合而不是继承的一般面向对象设计原则,并使用类似Strategy pattern 的东西,这是一种避免类爆炸的标准方法。

【讨论】:

  • 是的,但不幸的是,这需要一个人拥有诸如 manager、tennisPlayer 等字段,而其中许多都是空的。
  • @magnus-madsen 好吧,不完全是。在这种情况下,沿着 Strategy 模式行将意味着创建一个名为 Role 的类,并使用 ManagerTennisPlayer 等对其进行子类化。Person 类将包含一个角色列表。为了满足您的其他要求,您可以在每个角色中添加对人员的引用,甚至在需要时实现角色之间的一些交互逻辑。
【解决方案3】:

我认为这可以通过类似于type-safe builders的方式来解决。

基本思想是通过类型参数来表示“状态”,并使用隐式来控制方法。例如:

sealed trait TBoolean
final class TTrue extends TBoolean
final class TFalse extends TBoolean

class Person[IsManager <: TBoolean, IsTennisPlayer <: TBoolean, IsSick <: TBoolean] private (val name: String) {
  // Factories
  def becomeSick = new Person[TFalse, IsTennisPlayer, TTrue](name)
  def getBetter = new Person[IsManager, IsTennisPlayer, TFalse](name)
  def getManagerJob(initialSalary: Int)(implicit restriction: IsSick =:= TFalse) = new Person[TTrue, IsTennisPlayer, IsSick](name) {
    protected override val salary = initialSalary
  }
  def learnTennis = new Person[IsManager, TTrue, IsSick](name)

  // Other methods
  def playGame(implicit restriction: IsTennisPlayer =:= TTrue) { println("Playing game") } 
  def playSeason(implicit restriction1: IsSick =:= TFalse, restriction2: IsTennisPlayer =:= TTrue) { println("Playing season") }
  def getSalary(implicit restriction: IsManager =:= TTrue) = salary

  // Other stuff
  protected val salary = 0
}

object Person {
  def apply(name: String) = new Person[TFalse, TFalse, TFalse](name)
}

它可能会变得非常冗长,如果事情变得足够复杂,您可能需要 HList 之类的东西。这是另一种实现,可以更好地分离关注点:

class Person[IsManager <: TBoolean, IsTennisPlayer <: TBoolean, IsSick <: TBoolean] private (val name: String) {
  // Factories
  def becomeSick = new Person[TFalse, IsTennisPlayer, TTrue](name)
  def getBetter = new Person[IsManager, IsTennisPlayer, TFalse](name)
  def getManagerJob(initialSalary: Int)(implicit restriction: IsSick =:= TFalse) = new Person[TTrue, IsTennisPlayer, IsSick](name) {
      protected override val salary = initialSalary
  }
  def learnTennis = new Person[IsManager, TTrue, IsSick](name)

  // Other stuff
  protected val salary = 0
}

object Person {
  def apply(name: String) = new Person[TFalse, TFalse, TFalse](name)

  // Helper types
  type PTennisPlayer[IsSick <: TBoolean] = Person[_, TTrue, IsSick]
  type PManager = Person[TTrue, _, _]

  // Implicit conversions
  implicit def toTennisPlayer[IsSick <: TBoolean](person: PTennisPlayer[IsSick]) = new TennisPlayer[IsSick]
  implicit def toManager(person: PManager) = new Manager(person.salary)
}

class TennisPlayer[IsSick <: TBoolean] {
  def playGame { println("Playing Game") }
  def playSeason(implicit restriction: IsSick =:= TFalse) { println("Playing Season") }
}

class Manager(salary: Int) {
  def getSalary = salary
}

要获得更好的错误消息,您应该使用专用版本的 TBolean(即 HasManagerJob、PlaysTennis 等)和注释 implicitNotFound

【讨论】:

  • 使用类型作为所有可能的人类职业和状态的位集?哎哟。这不能很好地扩展——你有 N 个职业的代码中需要 O(N^2) 类型的注释。
  • @Rex 我不确定你从哪里得到O(N^2) 类型注释。在那里,我有 2 个职业的 2 个注释。但是,可以肯定的是,如果一组职业可能任意长,那么就不得不求助于某种HList 来处理事情。
  • 如果有N 可能的职业,您的类将采用N 类型参数,这些参数必须在N 隐式转换中重复。因此您需要O(N^2) 条目。
  • @Rex 哦,我明白你的意思了。但这仅适用于使用隐式转换的版本!其他版本没有这样的限制。此外,即使在带有类型参数的版本中也可以改进它,例如我现在所做的。它仍然是二次的,但使用通配符使它更短。
  • @Rex 虽然现在你提到它,但我看到二次问题将适用于构造函数,这实际上使该版本更加冗长。但是,如果您有大量此类课程,则最好使用基于 HList 的内容,我认为它仍然适合表示此类限制。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-06
  • 1970-01-01
  • 2017-07-30
相关资源
最近更新 更多