¿Cómo puedo abstraer la capa de dominio de la capa de persistencia en Scala?
architecture scalaquery (2)
ACTUALIZACIÓN: edité el título y agregué este texto para explicar mejor lo que estoy tratando de lograr: estoy tratando de crear una nueva aplicación desde cero, pero no quiero que la capa empresarial sepa acerca de la persistencia. capa, de la misma manera, uno no querría que la capa empresarial supiera acerca de una capa API REST. A continuación se muestra un ejemplo de una capa de persistencia que me gustaría usar. Estoy buscando buenos consejos para integrarme con esto, es decir, necesito ayuda con el diseño / arquitectura para dividir limpiamente las responsabilidades entre la lógica empresarial y la lógica de persistencia. Tal vez un concepto en la línea de clasificación y desvinculación de objetos de persistencia a objetos de dominio.
Desde un ejemplo de prueba SLICK (también conocido como ScalaQuery), así es como se crea una relación de base de datos de muchos a muchos. Esto creará 3 tablas: a, b y a_to_b, donde a_to_b conserva los enlaces de las filas en la tabla a y 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)
Como mi siguiente paso, me gustaría llevar esto a un nivel (todavía quiero usar SLICK pero envolverlo bien), para trabajar con objetos. Así que en pseudo código sería genial hacer algo como:
objectOfTypeA.save()
objectOfTypeB.save()
linkAtoB.save(ojectOfTypeA, objectOfTypeB)
O algo así. Tengo mis ideas sobre cómo abordar esto en Java, pero estoy empezando a darme cuenta de que algunas de mis ideas orientadas a objetos de lenguajes OO puros están empezando a fallarme. ¿Puede alguien darme algunos consejos sobre cómo abordar este problema en Scala?
Por ejemplo: ¿Creo objetos simples que simplemente envuelven o extienden los objetos de la tabla, y luego los incluyen (composición) en otra clase que los maneja?
Cualquier idea, guía, ejemplo (por favor) que me ayude a abordar mejor este problema como diseñador y codificador será muy apreciado.
Una buena solución para requisitos de persistencia simples es el patrón ActiveRecord: http://en.wikipedia.org/wiki/Active_record_pattern . Esto se implementa en Ruby y en Play! Framework 1.2, y puede implementarlo fácilmente en Scala en una aplicación independiente
El único requisito es tener un DB singleton o un servicio singleton para obtener una referencia a la base de datos que necesita. Yo personalmente iría por una implementación basada en lo siguiente:
- Un rasgo genérico ActiveRecord
- Una clase genérica ActiveRecordHandler
Explotando el poder de las implicidades, puedes obtener una sintaxis asombrosa:
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
}
}
Con esta solución, solo necesita que su clase amplíe ActiveRecord [T] y tenga un ActiveRecordHandler implícito [T] para manejar esto.
En realidad, también hay una implementación: https://github.com/aselab/scala-activerecord, que se basa en una idea similar, pero en lugar de hacer que ActiveRecord tenga un tipo abstracto, declara un objeto complementario genérico.
Un comentario general pero muy importante sobre el patrón de ActiveRecord es que ayuda a cumplir requisitos simples en términos de persistencia, pero no puede hacer frente a requisitos más complejos: por ejemplo, es cuando desea persistir múltiples objetos en la misma transacción.
Si su aplicación requiere una lógica de persistencia más compleja, el mejor enfoque es introducir un servicio de persistencia que exponga solo un conjunto limitado de funciones a las clases del cliente, por ejemplo
def persist(objectsofTypeA:Traversable[A],objectsOfTypeB:Traversable[B])
Tenga en cuenta también que, de acuerdo con la complejidad de su aplicación, es posible que desee exponer esta lógica en diferentes modas:
- como un objeto singleton en el caso de que su aplicación sea simple, y no quiere que su lógica de persistencia sea conectable
- a través de un objeto singleton que actúa como una especie de "contexto de la aplicación", para que en la aplicación al inicio pueda decidir qué lógica de persistencia desea usar.
- con algún tipo de patrón de servicio de búsqueda, si su aplicación se distribuye.
La mejor idea sería implementar algo así como el patrón del mapeador de datos . Lo cual, a diferencia del registro activo, no violará a SRP.
Como no soy un desarrollador de Scala, no mostraré ningún código.
La idea es siguiente:
- crear instancia de objeto de dominio
- establecer condiciones en el elemento (por ejemplo
setId(42)
, si está buscando elemento por ID) - crear instancia del mapeador de datos
-
fetch()
métodofetch()
en el asignador al pasar el objeto de dominio como parámetro
El mapeador buscará los parámetros actuales del objeto de dominio proporcionado y, en función de esos parámetros, recuperará la información del almacenamiento (que podría ser una base de datos SQL, un archivo JSON o una API REST remota). Si se recupera información, asigna los valores al objeto de dominio.
Además, debo señalar que los correlacionadores de datos se crean para trabajar con la interfaz específica del objeto de dominio, pero la información, que pasan del objeto de dominio al almacenamiento y viceversa, se puede asignar a varias tablas SQL o a múltiples recursos REST.
De esta forma, puede reemplazar fácilmente el asignador, cuando cambia a un medio de almacenamiento diferente, o incluso prueba la lógica en objetos de dominio sin tocar el almacenamiento real. Además, si decide agregar el almacenamiento en caché en algún momento, eso sería simplemente otro asignador, que intentó obtener información de la memoria caché y, si falla, iniciará el mapeador para el almacenamiento persistente.
El objeto de dominio (o, en algunos casos, una colección de objetos de dominio) sería completamente inconsciente de si se almacena o recupera. Esa sería la responsabilidad de los cartógrafos de datos.
Si todo esto está en contexto MVC, entonces, para implementarlo completamente, necesitaría otro grupo de estructuras en la capa del modelo. Los llamo "servicios" (por favor compártanlo, de ustedes se les ocurre un mejor nombre). Son responsables de contener la interacción entre los mapeadores de datos y los objetos de dominio. De esta forma puede evitar que la lógica comercial se filtre en la capa de presentación (controladores, para ser exactos), y estos servicios crean una interfaz natural para la interacción entre la capa de negocios (también conocida como modelo) y la capa de presentación.
PD Una vez más, siento no poder proporcionar ningún ejemplo de código, porque soy un desarrollador de PHP y no tengo idea de cómo escribir código en Scala.
PPS Si está utilizando un patrón de correlacionador de datos, la mejor opción es escribir mapeadores manualmente y no usar ningún ORM de terceros que afirme implementarlo. Le daría más control sobre la base de código y evitaría la deuda técnica sin sentido [1] [2] .