Herança de classe de caso Scala

91

Tenho um aplicativo baseado no Squeryl. Eu defino meus modelos como classes de caso, principalmente porque acho conveniente ter métodos de cópia.

Tenho dois modelos estritamente relacionados. Os campos são os mesmos, muitas operações são comuns e devem ser armazenadas na mesma tabela de banco de dados. Mas há algum comportamento que só faz sentido em um dos dois casos, ou que faz sentido nos dois casos, mas é diferente.

Até agora, usei apenas uma única classe de caso, com um sinalizador que distingue o tipo do modelo, e todos os métodos que diferem com base no tipo do modelo começam com um if. Isso é irritante e não é totalmente seguro para o tipo.

O que eu gostaria de fazer é fatorar o comportamento comum e os campos em uma classe de caso ancestral e ter os dois modelos reais herdados dela. Mas, até onde eu entendo, herdar de classes de caso é desaprovado em Scala e é até proibido se a própria subclasse for uma classe de caso (não é o meu caso).

Quais são os problemas e armadilhas que devo estar ciente ao herdar de uma classe de caso? Isso faz sentido no meu caso?

Andrea
fonte
1
Você não poderia herdar de uma classe sem caso ou estender um traço comum?
Eduardo
Não tenho certeza. Os campos são definidos no ancestral. Eu quero obter métodos de cópia, igualdade e assim por diante com base nesses campos. Se eu declarar o pai como uma classe abstrata e os filhos como uma classe de caso, isso levará em parâmetros de conta definidos no pai?
Andrea,
Eu acho que não, você tem que definir adereços tanto no pai abstrato (ou traço) quanto na classe de caso alvo. No final das contas, não é nada padrão, mas pelo menos digite seguro
virtualeyes

Respostas:

121

Minha maneira preferida de evitar a herança de classe de caso sem duplicação de código é um tanto óbvia: crie uma classe base comum (abstrata):

abstract class Person {
  def name: String
  def age: Int
  // address and other properties
  // methods (ideally only accessors since it is a case class)
}

case class Employer(val name: String, val age: Int, val taxno: Int)
    extends Person

case class Employee(val name: String, val age: Int, val salary: Int)
    extends Person


Se você quiser ser mais refinado, agrupe as propriedades em características individuais:

trait Identifiable { def name: String }
trait Locatable { def address: String }
// trait Ages { def age: Int }

case class Employer(val name: String, val address: String, val taxno: Int)
    extends Identifiable
    with    Locatable

case class Employee(val name: String, val address: String, val salary: Int)
    extends Identifiable
    with    Locatable
Malte Schwerhoff
fonte
84
onde está esse "sem duplicação de código" de que você fala? Sim, um contrato é definido entre a classe de caso e seu (s) pai (s), mas você ainda está digitando props X2
virtualeyes
6
@virtualeyes Verdade, você ainda precisa repetir as propriedades. No entanto, você não precisa repetir métodos, o que geralmente significa mais código do que as propriedades.
Malte Schwerhoff,
1
sim, estava apenas esperando contornar a duplicação de propriedades - outra resposta sugere classes de tipo como uma possível solução alternativa; não tenho certeza de como, no entanto, parece mais voltado para misturar comportamentos, como traços, mas mais flexível. Apenas clichês re: classes de caso, pode viver com isso, seria incrível se fosse de outra forma, poderia realmente hackear grandes faixas de definições de propriedade
virtualeyes
1
@virtualeyes Concordo plenamente que seria ótimo se a repetição de propriedade pudesse ser evitada de uma maneira fácil. Um plug-in do compilador certamente poderia fazer o truque, mas eu não chamaria isso de uma maneira fácil.
Malte Schwerhoff
13
@virtualeyes Acho que evitar a duplicação de código não é apenas escrever menos. Para mim, é mais sobre não ter o mesmo pedaço de código em diferentes partes do seu aplicativo sem qualquer conexão entre eles. Com esta solução, todas as subclasses estão vinculadas a um contrato, portanto, se a classe pai mudar, o IDE poderá ajudá-lo a identificar as partes do código que você precisa corrigir.
Daniel
40

Uma vez que este é um tópico interessante para muitos, deixe-me lançar algumas luzes aqui.

Você poderia escolher a seguinte abordagem:

// You can mark it as 'sealed'. Explained later.
sealed trait Person {
  def name: String
}

case class Employee(
  override val name: String,
  salary: Int
) extends Person

case class Tourist(
  override val name: String,
  bored: Boolean
) extends Person

Sim, você deve duplicar os campos. Do contrário, simplesmente não seria possível implementar a igualdade correta entre outros problemas .

No entanto, você não precisa duplicar métodos / funções.

Se a duplicação de algumas propriedades é tão importante para você, use classes regulares, mas lembre-se de que elas não se encaixam bem no PF.

Como alternativa, você pode usar composição em vez de herança:

case class Employee(
  person: Person,
  salary: Int
)

// In code:
val employee = ...
println(employee.person.name)

A composição é uma estratégia válida e sólida que você também deve considerar.

E caso você esteja se perguntando o que significa um traço selado - é algo que pode ser estendido apenas no mesmo arquivo. Ou seja, as duas classes de caso acima devem estar no mesmo arquivo. Isso permite verificações exaustivas do compilador:

val x = Employee(name = "Jack", salary = 50000)

x match {
  case Employee(name) => println(s"I'm $name!")
}

Dá um erro:

warning: match is not exhaustive!
missing combination            Tourist

O que é muito útil. Agora você não vai esquecer de lidar com os outros tipos de Persons (pessoas). Isso é essencialmente o que a Optionclasse em Scala faz.

Se isso não importa para você, pode torná-lo não lacrado e colocar as classes de caso em seus próprios arquivos. E talvez vá com composição.

Kai Sellgren
fonte
1
Eu acho que def nameo traço precisa ser val name. Meu compilador estava me dando avisos de código inacessível com o anterior.
BAR
13

classes de caso são perfeitas para objetos de valor, ou seja, objetos que não alteram nenhuma propriedade e podem ser comparados com iguais.

Mas implementar igual na presença de herança é bastante complicado. Considere duas classes:

class Point(x : Int, y : Int)

e

class ColoredPoint( x : Int, y : Int, c : Color) extends Point

Então de acordo com a definição o ColorPoint (1,4, red) deve ser igual ao Point (1,4) eles são o mesmo Point afinal. Então ColorPoint (1,4, blue) também deve ser igual a Point (1,4), certo? Mas é claro que ColorPoint (1,4, vermelho) não deve ser igual a ColorPoint (1,4, azul), porque eles têm cores diferentes. Aí está, uma propriedade básica da relação de igualdade foi quebrada.

atualizar

Você pode usar a herança de características resolvendo muitos problemas, conforme descrito em outra resposta. Uma alternativa ainda mais flexível é usar classes de tipo. Consulte Para que servem as classes de tipo em Scala? ou http://www.youtube.com/watch?v=sVMES4RZF-8

Jens Schauder
fonte
Eu entendo e concordo com isso. Então, o que você sugere que deve ser feito quando você tem um aplicativo que lida com, digamos, empregadores e funcionários. Suponha que eles compartilhem todos os campos (nome, endereço e assim por diante), a única diferença sendo em alguns métodos - por exemplo, pode-se querer definir, Employer.fire(e: Emplooyee)mas não o contrário. Eu gostaria de fazer duas classes diferentes, pois na verdade elas representam objetos diferentes, mas também não gosto da repetição que surge.
Andrea,
obteve um exemplo de abordagem de classe de tipo com a pergunta aqui? ou seja, em relação às classes de caso
virtualeyes
@virtualeyes Pode-se ter tipos completamente independentes para os vários tipos de Entidades e fornecer Classes de Tipo para fornecer o comportamento. Essas classes de tipo podem usar herança tanto quanto úteis, uma vez que não são vinculadas ao contrato semântico das classes de caso. Seria útil nesta pergunta? Não sei, a pergunta não é específica o suficiente para ser explicada.
Jens Schauder,
@JensSchauder, parece que as características fornecem a mesma coisa em termos de comportamento, apenas menos flexível do que as classes de tipo; Estou chegando à não duplicação das propriedades da classe de caso, algo que características ou classes abstratas normalmente ajudariam a evitar.
virtualeyes
7

Nessas situações, tendo a usar composição em vez de herança, ou seja,

sealed trait IVehicle // tagging trait

case class Vehicle(color: String) extends IVehicle

case class Car(vehicle: Vehicle, doors: Int) extends IVehicle

val vehicle: IVehicle = ...

vehicle match {
  case Car(Vehicle(color), doors) => println(s"$color car with $doors doors")
  case Vehicle(color) => println(s"$color vehicle")
}

Obviamente, você pode usar uma hierarquia e correspondências mais sofisticadas, mas espero que isso lhe dê uma ideia. A chave é tirar vantagem dos extratores aninhados que as classes de caso fornecem


fonte
3
Esta parece ser a única resposta aqui que realmente não tem campos duplicados
Alan Thomas