简体   繁体   中英

Scala Adapter pattern - autommagically allow "duck typing" for classes with same methods

Let's say class A is being used in some code, and that I want to use instead class B , which has exactly the same methods as class A - without having B extend A . What would be the easiest way to go about this? In other words, I'm looking for simple ready-to-use and generic implementation of adaptBtoA (it should work for any two classes that have the same structure/methods).

class A {
  def foo(x: String) = "A_" + x
}

class B {
  def foo(x: String) = "B_" + x
}

def bar(a: A) = {
  // ...
}

bar(adaptBtoA(new B()))

If you're familiar with duck typing interfaces of Go, that's sort of the semantics I'm aiming for.


EDIT

I think a generalized solution might be impossible due to type erasure, although I'm not sure. Here's my attempt using the mockito library:

def adapt[F, T](impl: F): T = mock[T](new Answer[Any]() {
  override def answer(inv: InvocationOnMock): Any =
    classOf[T]
      .getDeclaredMethod(inv.getMethod.getName, inv.getMethod.getParameterTypes:_*)
      .invoke(impl, inv.getArguments:_*)
})

val a: A = adapt[B, A](new B()) 
val res = a.foo("test") // should be "B_test" but errors in compilation

unfortunately this does not work as I get the following compiler error:

type arguments [T] conform to the bounds of none of the overloaded alternatives of
value mock: [T <: AnyRef](name: String)(implicit classTag: scala.reflect.ClassTag[T])T <and> [T <: AnyRef](mockSettings: org.mockito.MockSettings)(implicit classTag: scala.reflect.ClassTag[T])T <and> [T <: AnyRef](defaultAnswer: org.mockito.stubbing.Answer[_])(implicit classTag: scala.reflect.ClassTag[T])T <and> [T <: AnyRef](implicit classTag: scala.reflect.ClassTag[T])T

I can, however, use hardcoded types for specific use-cases:

def adaptBtoA(b: B): A = mock[A](new Answer[Any]() {
  override def answer(inv: InvocationOnMock): Any =
    classOf[B]
      .getDeclaredMethod(inv.getMethod.getName, inv.getMethod.getParameterTypes:_*)
      .invoke(b, inv.getArguments:_*)
})

val a: A = adaptBtoA(new B()) 
val res = a.foo("test")  // res == "B_test"

If getting class type information from template parameter at runtime is impossible, perhaps I can use macros to generate all of the adapt functions I need at compile time? Then the code will look something like:

genAdapt[B, A]()
genAdapt[D, C]()
// etc...

But I don't know enough about scala macros yet to implement this, or if this is possible.

You just missed a few things in your attempt at the adapt method. The compiler says: it needs T to extends AnyRef , and a ClassTag[T] . You'd also need a ClassTag[F] since you'd invoke the method on F , not T .

def adapt[F: ClassTag, T <: AnyRef : ClassTag](impl: F): T = {
  mock[T](new Answer[Any]() {
    override def answer(inv: InvocationOnMock): Any =
      implicitly[ClassTag[F]].runtimeClass
        .getDeclaredMethod(inv.getMethod.getName, inv.getMethod.getParameterTypes: _*)
        .invoke(impl, inv.getArguments: _*)
  })
}

adapt[B, A](new B()).foo("test") // "B_test"
adapt[A, B](new A()).foo("test") // "A_test"

Here is the proposed typeclass solution.

trait Foo[T] {
  def foo(t: T)(x: String): String
}

object Foo {
  object syntax {
    implicit class FooOps[T](private val t: T) extends AnyVal {
      @inline
      final def foo(x: String)(implicit ev: Foo[T]): String =
        ev.foo(t)(x)
    }
  }
}

final class A {
  def foo(x: String) = s"A_${x}"
}

object A {
  implicit final val AFoo: Foo[A] =
    new Foo[A] {
      override def foo(a: A)(x: String): String =
        a.foo(x)
    }
}

final class B {
  def foo(x: String) = s"B_${x}"
}

object B {
  implicit final val BFoo: Foo[B] =
    new Foo[B] {
      override def foo(b: B)(x: String): String =
        b.foo(x)
    }
}

def bar[T : Foo](t: T): String = {
  import Foo.syntax._
  t.foo("test")
}

Which you can use like this:

bar(new A()) 
// res: String = "A_test"

bar(new B()) 
// res: String = "B_test"

Now, this solution requires quite a bit of refactoring, but it has the advantage that it works, it is extensible and it is more flexible and more typesafe than the proposed adapt solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM