Scala Generic Trait Factory - PullRequest
       23

Scala Generic Trait Factory

0 голосов
/ 15 ноября 2018

В моем проекте много событий, которые очень похожи. Вот сокращенный пример:

object Events {
  final case class UpdatedCount(id: Int, prevValue: Double, newValue: Double) 
      extends PropertyEvent[Double]
  final case class UpdatedName(id: Int, prevValue: String, newValue: String) 
      extends PropertyEvent[String]
}

Черта выглядит так:

trait PropertyEvent[A] {
  val id: Int
  val prevValue: A
  val newValue: A
}

Существует фабрика, которая используется для получения соответствующего события во время выполнения. Это вызывается другим универсальным методом, который использует частичные функции для получения preValue и newValue:

object PropertyEventFactory{
  def getEvent[A, B <: PropertyEvent[A]](id: Int, preValue: A, newValue: A, prop: B): PropertyEvent[A]= prop match{
    case UpdatedCount(_,_,_) => UpdatedCount(id, preValue, newValue)
    case UpdatedName(_,_,_) => UpdatedName(id, preValue, newValue)
  }
}

IntelliSense IntelliJ жалуется на preValue и newValue, но компилятор может это выяснить и успешно построить.

Вот базовая спецификация, показывающая, как это может быть вызвано:

"Passing UpdatedCount to the factory" should "result in UpdatedCount" in {
    val a = PropertyEventFactory.getEvent(0, 1d,2d, UpdatedCount(0,0,0))
    assert(a.id == 0)
    assert(a.prevValue == 1)
    assert(a.newValue == 2)
}

Есть ли способ достичь этого, передав UpdatedCount как тип вместо объекта? Создание временной версии UpdatedCount только для того, чтобы получить фактическое UpdatedCount Событие, имеет запах кода для меня. Я пробовал много способов, но в итоге столкнулся с другими проблемами. Есть идеи?

Редактировать 1: Добавлена ​​функция вызова getEvent и некоторый дополнительный вспомогательный код, помогающий продемонстрировать схему использования.

Вот базовый объект, который обновляется. Простите за использование vars в классе case, поскольку это значительно упрощает примеры.

final case class BoxContent(id: Int, var name: String, var count: Double, var stringProp2: String, var intProp: Int){}

Команда, использованная для запроса обновления:

object Commands {
  final case class BoxContentUpdateRequest(requestId: Long, entity: BoxContent, fields: Seq[String])
}

Вот постоянный субъект, который получает запрос на обновление BoxContent в Box. Метод, который вызывает фабрику, находится здесь в функции editContentProp:

class Box extends PersistentActor{

  override def persistenceId: String = "example"

  val contentMap: BoxContentMap = new BoxContentMap()

  val receiveCommand: Receive = {
    case request: BoxContentUpdateRequest =>
      val item = request.entity
      request.fields.foreach{
        case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, UpdatedName.apply(0,"",""))
        case "count" => editContentProp(item.id, item.count, contentMap.getCountProp, contentMap.editCountProp, UpdatedCount.apply(0,0,0))
        case "stringProp2" => /*Similar to above*/
        case "intProp" => /*Similar to above*/
        /*Many more similar cases*/
      }
  }

  val receiveRecover: Receive = {case _ => /*reload and persist content info here*/}


  private def editContentProp[A](key: Int, newValue: A, prevGet: Int => A,
                             editFunc: (Int, A) => Unit, propEvent: PropertyEvent[A]) = {
    val prevValue = prevGet(key)
    persist(PropertyEventFactory.getEvent(key, prevValue, newValue, propEvent)) { evt =>
      editFunc(key, newValue)
      context.system.eventStream.publish(evt)
    }
  }
}

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

Вот модифицированный Box класс:

class Box extends PersistentActor{

  override def persistenceId: String = "example"

  val contentMap: BoxContentMap = new BoxContentMap()

  val receiveCommand: Receive = {
    case request: BoxContentUpdateRequest =>
      val item = request.entity
      request.fields.foreach{
        case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, PropertyEventFactory.getNameEvent)
        case "count" => editContentProp(item.id, item.count, contentMap.getCountProp, contentMap.editCountProp, PropertyEventFactory.getCountEvent)
        case "stringProp2" => /*Similar to above*/
        case "intProp" => /*Similar to above*/
        /*Many more similar cases*/
      }
  }

  val receiveRecover: Receive = {case _ => /*reload and persist content info here*/}

  private def editContentProp[A](key: Int, newValue: A, prevGet: Int => A,
                                 editFunc: (Int, A) => Unit, eventFactMethod: (Int, A, A) => PropertyEvent[A]) = {
    val prevValue = prevGet(key)
    persist(eventFactMethod(key, prevValue, newValue)) { evt =>
      editFunc(key, newValue)
      context.system.eventStream.publish(evt)
    }
  }
}

А вот и модифицированный PropertyEventFactory:

object PropertyEventFactory{
  def getCountEvent(id: Int, preValue: Double, newValue: Double): UpdatedCount = UpdatedCount(id, preValue, newValue)
  def getNameEvent(id: Int, preValue: String, newValue: String): UpdatedName = UpdatedName(id, preValue, newValue)
}

Если один из комментаторов, предложивших этот подход, захочет предложить ответ с этим содержанием, я буду рад объявить его.

1 Ответ

0 голосов
/ 17 ноября 2018

Это моя попытка обобщить ответ.

Прежде всего, нет такой вещи, как универсальная фабрика для вашей черты.Ваша черта PropertyEvent указывает только три vals, которые каждый подкласс черты должен выполнять после создания .Каждый класс, который реализует эту черту, может иметь очень разные конструкторы и / или фабрики.

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

Если вы попробуете что-то вроде этого:

object PropertyEventFactory2 {
  def getEvent[A, B <: PropertyEvent[A]](id: Int, preValue: A, newValue: A, prop: Class[B]): B = prop.getName match {
    case "org.example.UpdatedCount" => UpdatedCount(id, preValue, newValue)
    case "org.example.UpdatedName" => UpdatedName(id, preValue, newValue)
  }
}

Чем это не скомпилируется.Вам нужно привести preValue и newValue к соответствующему типу, и это также вонючий код.

Вы можете создать событие перед вызовом editContentProp:

case "name" => {
    val event = UpdatedName(item.id, contentMap.getNameProp(item.id), item.name)
    editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, event)
}

Однако все ваши ветви case будут повторять одну и ту же структуру, что является своего рода дублированием кода.Вы уже узнали это, и это хорошо.

Так что ваш лучший выбор - проходить на фабрике для каждого события.И поскольку все ваши события являются классами наблюдений, для каждого класса наблюдений вы получаете бесплатный метод фабрики, сгенерированный компилятором Scala.Метод factory находится в сопутствующем объекте класса case, и его просто называют CaseClass.apply

Это приводит к окончательной форме вашей case ветви:

case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, UpdatedName.apply)

, котораяпотребляется параметром:

eventFactMethod: (Int, A, A)
...