具有类型上限的 Scala 域建模

Scala domain modeling with upper type bounds

这是对类型上限的合理使用吗?我想要一个由不同员工组成的团队,或者一个只有一种子类型的团队。以这种方式建模有缺点吗?

sealed abstract class Employee {def salary: Double}
object Employee {
  case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
  case class Manager(salary: Double, reports: List[Employee]) extends Employee
}
case class Team[T <: Employee](name: String, members: List[T])

我的理由是我可能在某些地方使用任何类型的员工团队,而在其他地方我需要特定类型的员工:

def teamSalaries(team: Team[Employee]) = team.members.map(_.salary).sum
def teamLinesOfCode(team: Team[IndividualContributor]) = team.members.map(_.linesOfCode).sum

如果我可以在这里分享我的经验:

你或许可以不绑定类型。虽然可以有 Team[Int],但没有人会这样做,并且边界可能会使许多与 Team 相关的其他类型签名复杂化,例如方法参数,...

但是,正如 Luis Miguel 评论的那样,我建议您使 Team 协变。

一方面相关说明:根据您的域,您可能会在 Team 上将 teamSalaries 实现为 totalSalary 方法。如果您的领域专家在这种情况下谈论“团队的总薪水”,那可能是有道理的。 DDD 是关于建模和表现力(无处不在的语言)

我有点反对@winson的回答:)

You could probably do without the type bound. While it is possible to have a Team[Int], nobody would do that and the bounds might complicate a lot of other type signatures that are related to a Team as e.g. method parameter, ...

值得注意的是,在这种情况下,类型界限不仅仅是排除外部的东西,比如 Int。如果您决定重构您的代码,可以在案例 class Team

中移动方法 teamSalariesteamLinesOfCode
case class Team[T <: Employee](name: String, members: List[T]) {
  def salaries = members.map(_.salary).sum
  def linesOfCode(implicit ev: T <:< IndividualContributor) = 
    members.map(_.linesOfCode).sum
}

或将它们定义为扩展方法

implicit class TeamOps[T <: Employee](val team: Team[T]) extends AnyVal {
  def salaries = team.members.map(_.salary).sum
  def linesOfCode(implicit ev: T <:< IndividualContributor) = 
    team.members.map(_.linesOfCode).sum
}

如果没有上限,代码将无法编译。

因此,为了补充这些,还有其他建模方法...但是,剧透,我仍然认为类型绑定仍然是这种情况下的最佳解决方案。

正常重载

final case class Team[+T](name: String, members: List[T])

def teamSalaries(team: Team[Employee]): Double = ???
def teamLinesOfCode(team: Team[IndividualContributor]): Double = ???

然而,这允许创建不是员工的团队,这是无稽之谈和恕我直言,使用静态类型语言的想法 (笑话) 是让编译器为您捕获这些错误。

隐含证据

final case class Team[T](name: String, members: List[T])

def teamSalaries[T](team: Team[T])(implicit ev: T <:< Employee): Double = ???
def teamLinesOfCode[T](team: Team[T])(implicit ev: T <:< IndividualContributor): Double = ???

在这种情况下,这与上一个相同。但是,有时这是建模某些东西的唯一/最佳方法 (有关此类情况的示例,请参阅@Dmytro 的回答).

和ADT

sealed trait Employee {
  def salary: Double
}
object Employee {
  final case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
  final case class Manager(salary: Double, reports: List[Employee]) extends Employee
}

sealed trait Team {
  def name: String
}

object Team {
  final case class FullTeam(name: String, members: List[Employee]) extends Team
  final case class OnlyDevelopersTeam(name: String, members: List[IndividualContributor]) extends Team
}

def teamSalaries(team: Team): Double = team match {
  case Team.FullTeam => ???
  case Team.OnlyDevelopersTeam => ???
}

def teamLinesOfCode(team: Team): Option[Double] = team match {
  case Team.OnlyDevelopersTeam => Some(???)
  case _ => None
}

但这只是将编译器可以检查的内容移入运行时,并强制管理不必要的 Option。但是,如果数据是动态的并且您无法在编译时知道您拥有哪个团队,那么这是一个很好的 (最好?) 选项。

类型类

trait HasSalary[T] {
  def salary(t: T): Double
}

trait HasLinesOfCode[T] {
  def linesOfCode(t: T): Double
}

sealed trait Employee {
  def salary: Double
}
object Employee {
  final case class IndividualContributor(salary: Double, linesOfCode: Int) extends Employee
  final case class Manager(salary: Double, reports: List[Employee]) extends Employee

  implicit EmployeeHasSalary: HasSalary[Employee] = (employee: Employee) => employee.salary
  implicit IndividualContributorHasLinesOfCode: HasLinesOfCode[IndividualContributor] = (employee: IndividualContributor) => employee.linesOfCode
}

final case class Team[+T](name: String, members: List[T])

def teamSalaries[T : HasSalary](team: Team[T]): Double = ???
def teamLinesOfCode[T : HasLinesOfCode](team: Team[T]): Double = ???

但这可能是太多的样板文件了。然而,如果你的抽象足够通用,为许多不相关的类型实现并且数据的类型可以在编译时确定,那么没有什么比 flexibility & safety 类型类.