开发者

Combinatorial Subtyping (in Scala)

I am looking for a clean object-orientated way to model the following (in Scala):

A person can be:

  • A manager at some firm
  • A mathematician
  • A world-class tennis player
  • A hobbyist programmer
  • A volunteer at a local school
  • A creative painter

This suggests that we introduce a Person super-class and sub-classes:

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

The Manager class has methods such as: getSalary(), workLongHours(), findNewJob(), etc. The TennisPlayer class has methods such as: getWorldRanking(), playGame(), strainAnkle(), etc. And so on. In addition there are methods in class Person such as becomeSick(). A sick manager loses his job and tennis player stops playing in the season.

Futhermore the classes are immutable. That is, for instance strainAnkle() returns a new TennisPlayer that t开发者_开发技巧hat has a strained ankle, but where all other properties remain the same.

The question is now: How do we model the fact that a person can be both a Manager and a TennisPlayer?

It's important that the solution preserves both immutability and type-safety.

We could implement classes such as:

  • ManagerAndMathematician
  • ManagerAndTennisPlayerAndPainter
  • ManagerAndPainter

but this leads to a combinatorial explosion of classes.

We could also use traits (with state), but then how do we implement methods such as findNewJob(), which needs to return a new person with the same traits mixed in, but with a new state of the Manager trait. Similarly, how can we implement methods such as becomeSick()?

Question: How would you implement this in a clean OO-fashion in Scala? Remember: Immutability and type-safety are a must.


This does not look to me like an ideal case for inheritance. Maybe you're trying to force things into an inheritance pattern because it seems awkward to handle composition with immutable values. Here's one of several ways to do it.

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

Let's try it out:

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

So this works in a somewhat useful way.

The trick here is that you create anonymous subclasses of your person each time an occupation (which is an inner class) decides to change things up. Its job is to create a new person with the new roles intact; this is helped out by the implicit val self = this and the implicit constructor on Occupation which helpfully automatically loads the correct instance of the person.

You will probably want a list of occupations, and thus will probably want helper methods that will regenerate the list of professions. Something like

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


A clean object-oriented way of solving this does not have to be Scala-specific. One could adhere to the general object-oriented design principle of favoring composition over inheritance and use something like Strategy pattern, which is a standard way of avoiding class explosion.


I think this can be solved in a manner similar to type-safe builders.

The basic idea is to represent "state" through type parameters, and use implicits to control methods. For example:

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

It can get very wordy, and if things get complex enough, you may need something like an HList. Here's another implementation, that separates concerns better:

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
}

To get better error messages you should use specialized versions of TBoolean (ie, HasManagerJob, PlaysTennis, etc), and the annotation implicitNotFound to go with it.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜