Комбинаторный подтипирование (в 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
, чтобы пойти с ним.