Шаблон адаптера Scala - автоматически разрешает утиный ввод для классов с теми же методами

Допустим, класс A используется в каком-то коде, и я хочу использовать вместо него класс B, который имеет те же методы, что и класс A, без B расширения A. Что было бы проще всего сделать? Другими словами, я ищу простую, готовую к использованию и общую реализацию adaptBtoA (она должна работать для любых двух классов, имеющих одинаковую структуру / методы). .

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

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

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

bar(adaptBtoA(new B()))

Если вы знакомы с интерфейсами Go с утиной печатью, я стремлюсь к такой семантике.


ИЗМЕНИТЬ

Я думаю, что обобщенное решение может быть невозможно из-за стирания типа, хотя я не уверен. Вот моя попытка использовать библиотеку mockito:

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

к сожалению, это не работает, так как я получаю следующую ошибку компилятора:

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

Однако я могу использовать жестко запрограммированные типы для конкретных случаев использования:

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"

Если получение информации о типе класса из параметра шаблона во время выполнения невозможно, возможно, я смогу использовать макросы для генерации всех adapt функций, которые мне нужны во время компиляции? Тогда код будет выглядеть примерно так:

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

Но я еще недостаточно знаю о макросах Scala, чтобы реализовать это, или если это возможно.


person solyd    schedule 28.01.2020    source источник
comment
В Scala вы можете использовать структурную типизацию, которая будет работать как утиная типизация. Однако обычно предпочтительнее использовать класс типов. здесь вы можете найти обсуждение этого (отказ от ответственности я являюсь автором) .   -  person Luis Miguel Mejía Suárez    schedule 28.01.2020
comment
Мне известно о структурной типизации в scala, и, к сожалению, это не решает мою проблему. Тем не менее, хороший пост в блоге.   -  person solyd    schedule 28.01.2020
comment
А как насчет классов типов? Мне потребовалось бы немного рефакторинга, но это лучшее решение этой проблемы.   -  person Luis Miguel Mejía Suárez    schedule 28.01.2020
comment
Другой вариант - предоставить неявное преобразование из A в B или использовать магнитный шаблон Но, ИМХО, лучше всего неявные преобразования класса типов очень хрупкие.   -  person Luis Miguel Mejía Suárez    schedule 28.01.2020
comment
A и B не являются классами типов.   -  person solyd    schedule 28.01.2020
comment
универсальной реализацией структурно эквивалентных преобразований типов может быть сериализация и десериализация, если классы правильно сериализуемы. добавьте и неявное преобразование, и вы получите нужную функцию.   -  person winson    schedule 28.01.2020
comment
отличная идея @winson   -  person solyd    schedule 28.01.2020
comment
@solyd Я думаю, вы не понимаете классы типов, вы должны создать только один (который определяет общую функциональность). И вы бы реализовали это как для A, так и B, тогда вы бы написали свою функцию в терминах класса типов, а не какого-то конкретного типа.   -  person Luis Miguel Mejía Suárez    schedule 28.01.2020
comment
@ LuisMiguelMejíaSuárez, взгляни на мою правку, может быть, ты лучше поймешь, чем я хочу заниматься сейчас.   -  person solyd    schedule 28.01.2020
comment
кстати: вы рассматривали структурную типизацию? например. type Closable = { def close(): Unit }. теперь ваш код может принимать этот тип, и вы можете предоставить оба структурно эквивалентных типа (A и B).   -  person winson    schedule 30.01.2020


Ответы (2)


Вы просто упустили несколько вещей, пытаясь применить метод adapt. Компилятор говорит: ему нужно T для расширения AnyRef и ClassTag[T]. Вам также понадобится ClassTag[F], поскольку вы вызываете метод на F, а не на 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"
person Worakarn Isaratham    schedule 29.01.2020
comment
Потрясающий мужик, огромное спасибо! Просто примечание о поиске метода: у меня был случай, когда стирание типа произошло с некоторыми параметрами: я передавал (String, String, Baz), а InvocationOnMock вернул (Object, Object, Baz) на inv.getMethod.getParameterTypes. Хотя я делал что-то странное, издевался над нестатическим внутренним классом какой-то библиотеки. Никогда не разбирался в сути проблемы, но просто сказал - вы можете развернуть свою собственную логику метода в качестве резервной копии для getDeclaredMethod, приложив все усилия, чтобы найти подходящий метод с учетом стирания типа. - person solyd; 29.01.2020

Вот предлагаемое решение класса типов.

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")
}

Что вы можете использовать так:

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

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

Это решение требует довольно небольшого рефакторинга, но его преимущество состоит в том, что оно работает, расширяемо, более гибко и более типично, чем предлагаемое адаптируемое решение.

person Luis Miguel Mejía Suárez    schedule 28.01.2020
comment
Это классно. Теперь сделайте это для 5 разных классов (A в нашем примере), которые вы не можете расширять и не можете редактировать (изменять код), и каждый из которых имеет ~ 15 методов. - person solyd; 28.01.2020
comment
@solyd вы создаете класс типов для каждой общей функции и предоставляете экземпляр для каждого класса типов для каждого типа, который может быть реализован. И что замечательно в этом, так это то, что вам не нужно редактировать какой-либо существующий класс, вы можете создавать экземпляры класса типов для внешнего типа. Вам нужно только указать последствия использования сайта. Я бы порекомендовал вам узнать больше по этой теме. Единственный код, который вам нужно изменить (не добавлять), - это функции, которые используют ваш As как общий и вместо этого используют класс типов. - person Luis Miguel Mejía Suárez; 28.01.2020
comment
Я вижу, вам нравится печатать и применять шаблоны проектирования. Пожалуйста, уделите секунду, чтобы сравнить ваше решение с моим отражающим решением, которое состоит из 4 строк, и будет работать для любых двух типов (только текущий гандикап, который вам нужно указать, и вы не можете использовать параметры шаблона), и будет продолжать работать независимо от того, что методы добавляются / изменяются - пока A и B остаются неизменными по своей структуре. - person solyd; 28.01.2020
comment
@solyd 4 строки кода, которые медленнее, хрупче, жестче и небезопаснее. классы типов дадут вам больше контроля над тем, что можно, а что нельзя использовать, они более гибкие, поскольку методы в каждом классе не должны иметь одинаковые имена и могут иметь разные параметры, будут быстрее в время выполнения (но медленнее во время компиляции, если вы интенсивно используете автоматическое создание). Но, если вы предпочитаете, чтобы ваше решение использовалось, я просто думаю, что стоило показать вам, как вы можете использовать этот шаблон. - person Luis Miguel Mejía Suárez; 28.01.2020
comment
Отражение может быть медленнее, но я не оптимизирую банкомат, и в любом случае это для целей тестирования. Не уверен, что вы имеете в виду под «хрупким», «небезопасным» или «жестким» (во всяком случае, решение typeclasses является жестким, поскольку требует рефакторинга любых изменений, внесенных в базовый классы). классы типов хоть и хороши, но являются неадекватным инструментом для решения проблемы. Дорога в ад монад вымощена хорошими классами типов :) - person solyd; 29.01.2020
comment
@solyd ах, вы могли сказать, что это было для тестирования до (если вы извинитесь за то, что не заметили этого) Тогда я согласен, что, вероятно, всего оборудования будет слишком много (в этом случае для продакшн кода ИМХО стоимость более чем разумная). Просто чтобы ответить, хрупкий в том смысле, что у вас нет контроля, может быть, для какого-то класса вы не хотите разрешать адаптацию или, может быть, он предоставляет какой-то метод, который вы не хотите раскрывать. Жесткий. В этом случае методы должны называться одинаковыми и иметь одинаковые параметры с одними и теми же типами. И небезопасно в том, что отражение по природе небезопасно, теряешь все гарантии. - person Luis Miguel Mejía Suárez; 29.01.2020