Подтвердить что ты не робот

Как абстрагировать доменный уровень из уровня сохранения в Scala

UPDATE: Я отредактировал заголовок и добавил этот текст, чтобы лучше объяснить, чего я пытаюсь достичь: я пытаюсь создать новое приложение с нуля, но не хочу, чтобы бизнес-уровень знал о слое persistence, таким же образом не хотелось бы, чтобы бизнес-уровень знал о слое API REST. Ниже приведен пример слоя сохранения, который я хотел бы использовать. Я ищу хорошие рекомендации по интеграции с этим, то есть мне нужна помощь в разработке/архитектуре, чтобы четко разделить обязанности между бизнес-логикой и логикой сохранения. Возможно, концепция вдоль линии сортировки и разборки объектов устойчивости к объектам домена.

Из примера SLICK (a.k.a. ScalaQuery) , таким образом вы создаете отношения базы данных "многие ко многим". Это создаст 3 таблицы: a, b и a_to_b, где a_to_b поддерживает ссылки строк в таблицах a и b.

object A extends Table[(Int, String)]("a") {
  def id = column[Int]("id", O.PrimaryKey)
  def s = column[String]("s")
  def * = id ~ s
  def bs = AToB.filter(_.aId === id).flatMap(_.bFK)
}

object B extends Table[(Int, String)]("b") {
  def id = column[Int]("id", O.PrimaryKey)
  def s = column[String]("s")
  def * = id ~ s
  def as = AToB.filter(_.bId === id).flatMap(_.aFK)
}

object AToB extends Table[(Int, Int)]("a_to_b") {
  def aId = column[Int]("a")
  def bId = column[Int]("b")
  def * = aId ~ bId
  def aFK = foreignKey("a_fk", aId, A)(a => a.id)
  def bFK = foreignKey("b_fk", bId, B)(b => b.id)
}

(A.ddl ++ B.ddl ++ AToB.ddl).create
A.insertAll(1 -> "a", 2 -> "b", 3 -> "c")
B.insertAll(1 -> "x", 2 -> "y", 3 -> "z")
AToB.insertAll(1 -> 1, 1 -> 2, 2 -> 2, 2 -> 3)

val q1 = for {
  a <- A if a.id >= 2
  b <- a.bs
} yield (a.s, b.s)
q1.foreach(x => println(" "+x))
assertEquals(Set(("b","y"), ("b","z")), q1.list.toSet)

В качестве следующего шага я хотел бы сделать это на одном уровне (я все еще хочу использовать SLICK, но обернуть его красиво), для работы с объектами. Поэтому в псевдокоде было бы здорово сделать что-то вроде:

objectOfTypeA.save()
objectOfTypeB.save()
linkAtoB.save(ojectOfTypeA, objectOfTypeB)

Или что-то в этом роде. У меня есть свои идеи о том, как я могу подходить к этому в Java, но я начинаю понимать, что некоторые из моих объектно-ориентированных идей с чистых языков OO начинают меня терпеть. Может кто-нибудь, пожалуйста, дайте мне несколько указаний относительно того, как подходит эта проблема в Scala.

Например: создать простые объекты, которые просто переносят или расширяют объекты таблицы, а затем включают их (состав) в другой класс, который их управляет?

Любые идеи, рекомендации, пример (пожалуйста), которые помогут мне лучше подойти к этой проблеме, поскольку разработчик и кодер будут очень благодарны.

4b9b3361

Ответ 1

Хорошим решением для простых требований к сохранению является шаблон ActiveRecord: http://en.wikipedia.org/wiki/Active_record_pattern. Это реализовано в Ruby и Play! Framework 1.2, и вы можете легко реализовать его в Scala в автономном приложении

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

  • Общий признак ActiveRecord
  • Общий класс ActiveRecordHandler

Используя мощь implicits, вы можете получить удивительный синтаксис:

trait ActiveRecordHandler[T]{

  def save(t:T):T

  def delete[A<:Serializable](primaryKey:A):Option[T]

  def find(query:String):Traversable[T]
}

object ActiveRecordHandler {
  // Note that an implicit val inside an object with the same name as the trait 
  // is  one of the way to have the implicit in scope.
  implicit val myClassHandler = new ActiveRecordHandler[MyClass] {

    def save(myClass:MyClass) = myClass

    def delete[A <: Serializable](primaryKey: A) = None

    def find(query: String) = List(MyClass("hello"),MyClass("goodbye"))
  }
}

trait ActiveRecord[RecordType] {
  self:RecordType=>


  def save(implicit activeRecordHandler:ActiveRecordHandler[RecordType]):RecordType = activeRecordHandler.save(this)

  def delete[A<:Serializable](primaryKey:A)(implicit activeRecordHandler:ActiveRecordHandler[RecordType]):Option[RecordType] = activeRecordHandler.delete(primaryKey)
}

case class MyClass(name:String) extends ActiveRecord[MyClass] 

object MyClass {
  def main(args:Array[String]) = {
    MyClass("10").save
  }
}

При таком решении вам нужен только ваш класс, чтобы расширять ActiveRecord [T] и иметь неявный ActiveRecordHandler [T], чтобы справиться с этим.

Фактически также реализована реализация: https://github.com/aselab/scala-activerecord, которая основана на подобной идее, но вместо того, чтобы сделать ActiveRecord абстрактным, объявляет общий компаньон объект.


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

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

def persist(objectsofTypeA:Traversable[A],objectsOfTypeB:Traversable[B])

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

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

Ответ 2

Лучшей идеей было бы реализовать что-то вроде шаблона отображения данных. Что, в отличие от активной записи, не будет нарушать SRP.

Поскольку я не разработчик Scala, я не буду показывать никакого кода.

Идея следующая:

  • создать экземпляр объекта домена
  • установить условия для элемента (например, setId(42), если вы ищете элемент по ID)
  • создать экземпляр карты данных
  • выполнить fetch() метод на mapper, передав в домене объект как параметр

Картограф будет искать текущие параметры предоставленного объекта домена и, основываясь на этих параметрах, извлекать информацию из хранилища (это может быть база данных SQL или файл JSON или, возможно, удаленный REST API). Если информация получена, она присваивает значения объекту домена.

Кроме того, я должен отметить, что создатели данных созданы для работы с определенным интерфейсом объектов домена, но информация, которую они передают от объекта домена к хранилищу и обратно, может быть сопоставлена ​​с несколькими таблицами SQL или несколькими ресурсами REST.

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

Объект домена (или, в некоторых случаях, коллекция объектов домена) полностью не знает, будет ли он сохранен или восстановлен. Это будет зависеть от карт данных.

Если это все в контексте MVC, то, чтобы полностью реализовать это, вам понадобится другая группа структур на уровне модели. Я называю их "сервисами" (пожалуйста, поделитесь, у вас появилось лучшее имя). Они отвечают за то, что они содержат взаимодействие между данными и объектами домена. Таким образом, вы можете предотвратить утечку бизнес-логики на уровне презентации (точнее, контроллеры), и эти службы создают естественный интерфейс для взаимодействия между бизнес-словом (также как модель) и уровнем представления.

P.S. Еще раз, жаль, что я не могу представить примеры кода, потому что я разработчик PHP и понятия не имею, как писать код в Scala.

P.P.S. Если вы используете шаблон карты данных, лучшим вариантом является запись картографов вручную, а не использование сторонней ORM, которая заявляет о ее реализации. Это даст вам больше контроля над кодовой базой и избежит бессмысленного технического долга [1] [2].