Комбинаторный подтипирование (в Scala)

Я ищу чистый объектно-ориентированный способ моделирования следующего (в Scala):

Человек может быть:

  • Менеджер в какой-то фирме
  • Математик
  • Теннис в мире мирового класса
  • Программист-программист
  • Волонтер в местной школе
  • Креативный художник

Это говорит о том, что мы вводим суперкласс и подклассы Person:

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

Класс Manager имеет такие методы, как: getSalary(), workLongHours(), findNewJob() и т.д. Класс TennisPlayer имеет такие методы, как: getWorldRanking(), playGame(), strainAnkle() и т.д.. И так далее. Кроме того, существуют методы в классе Person, такие как becomeSick(). Больной менеджер теряет работу, и теннисист перестает играть в сезоне.

Более того, классы неизменяемы. То есть, например, strainAnkle() возвращает новый TennisPlayer, который имеет напряженную лодыжку, но где все остальные свойства остаются неизменными.

Теперь вопрос: как мы моделируем тот факт, что человек может быть как Manager, так и TennisPlayer?

Важно, чтобы решение сохраняло как неизменность, так и безопасность типов.

Мы могли бы реализовать такие классы, как:

  • ManagerAndMathematician
  • ManagerAndTennisPlayerAndPainter
  • ManagerAndPainter

но это приводит к комбинаторному взрыву классов.

Мы могли бы также использовать черты (с состоянием), но тогда как мы реализуем такие методы, как findNewJob(), которые должны возвращать нового человека с теми же чертами, смешанными, но с новым состоянием Manager черта. Аналогично, как мы можем реализовать такие методы, как becomeSick()?

Вопрос: Как бы вы реализовали это в чистом OO-модуле в 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 = this и неявный конструктор на Occupation, который автоматически загружает правильный экземпляр человека.

Вероятно, вам понадобится список профессий, и, вероятно, вам понадобятся вспомогательные методы, которые будут обновлять список профессий. Что-то вроде

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

Ответ 2

Чистый объектно-ориентированный способ решения этого не должен быть Scala -специфичным. Можно было бы придерживаться общего объектно-ориентированного принципа построения предпочтительного состава над наследованием и использовать что-то вроде шаблон стратегии, который является стандартным способом избежать класса взрыв.

Ответ 3

Я думаю, что это можно решить так же, как типа безопасных сборщиков.

Основная идея - представлять "состояние" через параметры типа и использовать implicits для методов управления. Например:

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
}

Чтобы получить более качественные сообщения об ошибках, вы должны использовать специализированные версии TBoolean (то есть HasManagerJob, PlaysTennis и т.д.) и аннотация implicitNotFound, чтобы пойти с ним.