Модульный дизайн Scala: как избежать выталкивания конструктора?

Возможно, эксперт по Scala с хорошим чувством стиля и элегантности сможет помочь мне найти лучший способ структурировать следующий код, в котором есть проблема «выталкивания» конструктора.

Начнем с простого базового класса:

class Foo(val i: Int, val d: Double, val s: String) {

  def add(f: Foo) = new Foo(i + f.i, d + f.d, s + f.s)
  override def toString = "Foo(%d,%f,%s)".format(i,d,s)

}

Для проверки типов в сложном приложении мне нужен подкласс без какого-либо дополнительного состояния:

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) {

  override def toString = "Bar(%d,%f,%s)".format(i,d,s)

}

В нынешнем виде, когда я добавляю два Bar, я получаю только Foo:

val x = new Bar(1,2.3,"x")
val y = new Bar(4,5.6,"y")
val xy = x.add(y)

со следующим ответом в REPL:

x  : Bar = Bar(1,2.300000,x)
y  : Bar = Bar(4,5.600000,y)
xy : Foo = Foo(5,7.900000,xy)

Как сделать так, чтобы два Bars складывались вместе, чтобы сформировать еще один Bar (а не Foo) элегантным способом, без необходимости копировать и вставлять метод add Foo, как показано ниже?

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) {

  // ugly copy-and-paste from Foo:
  def add(b: Bar) = new Bar(i + b.i, d + b.d, s + b.s)
  override def toString = "Bar(%d,%f,%s)".format(i,d,s)

}

У меня много таких Баров (все по сути копии Foo, но очень важны для проверки типов), решение без вырезания и вставки принесет дивиденды.

Спасибо!


person Perfect Tiling    schedule 04.09.2012    source источник


Ответы (3)


Я стараюсь избегать наследования, насколько это возможно. Итак, вот альтернативный подход.

Класс Bar имеет точно такой же конструктор, что и Foo, и оба не имеют состояния. Если вы хотите иметь несколько подтипов, просто для передачи дополнительной информации, вы можете использовать общий параметр в качестве «метки». Например:

trait Kind
trait Bar extends Kind

class Foo[T<:Kind](val i: Int, val d: Double, val s: String) {
   def add(f: Foo[T]) = new Foo[T](i + f.i, d + f.d, s + f.s)
   override def toString = "Foo(%d,%f,%s)".format(i,d,s)
}


scala> val b1 = new Foo[Bar](2,3.0,"hello")
b1: Foo[Bar] = Foo(2,3.000000,hello)

scala> val b2 = new Foo[Bar](3,1.0," world")
b2: Foo[Bar] = Foo(3,1.000000, world)

scala> b1 add b2
res1: Foo[Bar] = Foo(5,4.000000,hello world)

Теперь add безопасен для типов. Затем вы можете использовать класс типов, чтобы получить toString для отображения Kind.

person paradigmatic    schedule 04.09.2012
comment
Мне нравится параметр как метка для безопасности типов. Благодарю вас! - person Perfect Tiling; 05.09.2012

Расширяя ответ @paradigmatic, если вы хотите иметь возможность поддерживать операции, специфичные для каждого Bar (например, разные toString), вы можете сделать еще один шаг и сделать Kind классом типов.

trait Kind[T] { def name : String }
trait Bar
implicit object BarHasKind extends Kind[Bar] { val name = "Bar" }

class Foo[T : Kind](val i : Int, val d : Double, val s : String) {
  def add(f : Foo[T]) = new Foo[T](i + f.i, d + f.d, s + f.s)
  override def toString = implicitly[Kind[T]].name + "(%d,%f,%s)".format(i,d,s)
}

scala> val b1 = new Foo[Bar](2, 3.0, "hello")
b1: Foo[Bar] = Bar(2,3.000000,hello)

trait Biz
implicit object BizHasKind extends Kind[Biz] { val name = "Biz" }

scala> val b2 = new Foo[Biz](1, 1.0, "One")

Это так же безопасно для типов, как и раньше:

scala> b1 add b2
<console>:16: error: type mismatch;
  found   : Foo[Biz]
  required: Foo[Bar]

scala> b2 add b2
resN: Foo[Biz] = Biz(2,2.000000,OneOne)

Для любого свойства, которое вы хотите, чтобы оно зависело от тега, объявите их абстрактно в Kind и предоставьте реализации в неявных объектах.

person Philippe    schedule 04.09.2012
comment
Интересный. В чем преимущество вашего неявного подхода по сравнению с объявлением метода 'def name = Foo' внутри Foo и его переопределением Bar и Biz? - person Perfect Tiling; 05.09.2012
comment
На самом деле это не преимущество, я просто пытался показать, что вы можете добиться именно этого даже в условиях без наследования, подобных тем, которые предложены в принятом ответе. - person Philippe; 05.09.2012

Подход с параметризацией типов имеет ограничение, заключающееся в том, что он не позволяет вам расширять функциональность естественным образом, то есть наследованием. Таким образом, альтернативным подходом может быть переопределяемый фабричный метод (который является просто делегатом конструктора):

class Foo(val i: Int, val d: Double, val s: String) {

  protected def create(i: Int, d: Double, s: String) = new Foo(i, d, s)

  def add[A <: Foo](f: A) = create(i + f.i, d + f.d, s + f.s)

  override def toString = "Foo(%d,%f,%s)".format(i,d,s)
}

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) {

  protected override def create(i: Int, d: Double, s: String) = new Bar(i, d, s)

  override def toString = "Bar(%d,%f,%s)".format(i,d,s)

  // additional methods...

}

println( new Foo(10, 10.0, "10") add new Bar(10, 10.0, "10") )
println( new Bar(10, 10.0, "10") add new Foo(10, 10.0, "10") )
person Vlad    schedule 07.09.2012