Вот запись в блоге, в которой рассматриваются различные подходы CRUD в вашем коде Scala. Я покажу вам три разных подхода, подчеркнув достоинства и недостатки каждого из них. Основной мотивацией стали наши эксперименты с проектом Scala 2.10 без каски.
Подход 1
Первый подход состоит в том, чтобы определить Crud[T]
для некоторого типа T
и требовать, чтобы MongoSerializer
и IdentityQueryBuilder
для этого T
были смешаны.
trait Crud[T] { this: MongoSerializer[T] with IdentityQueryBuilder[T] => def collection: DBCollection def updateFirst(entity: T): Option[T] = { val id = createIdQuery(entity) if (collection.findAndModify(id, serialize(entity)) != null) Some(entity) else None } }
Это позволяет нам создавать тривиальные компоненты CRUD:
val userCrud = new Crud[User] with MongoSerializer[User] with IdentityQueryBuilder[User] { def collection: DBCollection = // set the collection where the Users will end up def serialize: ... def deserialize: ... def createIdQuery: ... }
Мы хотели бы сохранить наши печатать и иметь MongoSerializer
и IdentityQueryBuilder
компоненты заранее подготовленные и , возможно , обобщаются быть применимы к различным типам. Это IdentityQueryBuilder
было бы легко определить, давая нам:
trait IdUuidIdentityQueryBuilder[A <: {def id: UUID}] extends IdentityQueryBuilder[A] { ... }
Это MongoSerializer
даст нам больше головной боли. Мы хотели бы использовать существующие сериализаторы JSON, но они неизбежно требуют больше информации о типах, чем мы можем предоставить MongoSerializer[A]
. (Самый простой способ думать об этом — это то, что параметр типа A
должен быть изменен **.)
Подход 2
Во втором подходе нам нужен доступ к полным параметрам типа. Это означает, что наша Crud[A]
больше не может быть чертой. Это должно стать абстрактным классом:
abstract class Crud[A : MongoSerializer : IdentityQueryBuilder] { val serializer = implicitly[MongoSerializer[T]] val identityQueryBuilder = implicitly[IdentityQueryBuilder[T]] def collection: DBCollection def updateFirst(entity: T): Option[T] = { val id = identityQueryBuilder.createIdQuery(entity) if (collection.findAndModify(id, serializer.serialize(entity)) != null) Some(entity) else None } }
Этот подход не отбрасывает методы из наследования собственного типа в методы в Crud[A]
. Однако требуется, чтобы уникальный экземпляр типа MongoSerializer
и IdentityQueryBuilder
для типа A
был доступен в области, в которой создается экземпляр Crud[A]
.
implicit def productMongoSerializer[A <: Product] = new MongoSerializer[A] { def serialize(entity: A) = ??? def deserialize(dbObject: DBObject) = ??? } implicit def UUIDIdIdentityFieldExtractor[A <: {def id: Long}] = new IdentityQueryBuilder[A] { def createIdQuery(entity: A) = ??? } val crud = new ImplicitCrud[SomeEntity] { def collection = db.getCollection("some") }
Причина этого в том, что Scala генерирует конструктор для вышеуказанного класса как
abstract class Crud[A]( implicit ev1: MongoSerializer[A], ev2: IdentityQueryBuilder[A])
Таким образом, по сути, уточняются параметры типа. Хорошей новостью является то, что вам могут потребоваться дополнительные классы типов. Например, было бы вполне разумно требовать, чтобы экземпляр JsonWriter
был доступен для нашего MongoSerializer
из A
:
implicit def productMongoSerializer[A <: Product : JsonWriter] = new MongoSerializer[A] { val writer = implicitly[JsonWriter[A]] def serialize(entity: A) = ??? def deserialize(dbObject: DBObject) = ??? }
Это даст нам больше свободы, но это означает, что сериализаторы / конструкторы запросов будут доступны в области, в которой мы создаем экземпляры CRUD. Это также означает, что CRUD — это одна единица, и что отделение C от R от U от D означало бы ломать Crud[A]
черту.
Подход 3
Что приводит нас к последнему подходу, где мы оставляем Crud
черту как черту, но делаем ее методы полиморфными, требуя наличия классов типов. Это освобождает Crud
от типов объектов, которые он обрабатывает. Это, однако, означает, что место, в DBCollection
котором находятся объекты, должно быть указано дополнительно. Как обычно, у нас есть несколько вариантов:
3,1
Первый выбор состоит в том, чтобы просто потребовать, DBCollection
чтобы он был указан как параметр.
trait Crud { def updateFirst[T : MongoSerializer : IdentityQueryBuilder] (collection: DBCollection, entity: T): Option[T] = { val serializer = implicitly[MongoSerializer[T]] val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity) if (collection.findAndModify(id, serializer.serialize(entity)) != null) Some(entity) else None } }
3,2
Во втором варианте мы карри все функции Crud
:
trait Crud { def updateFirst[T : MongoSerializer : IdentityQueryBuilder] (collection: DBCollection)(entity: T): Option[T] = { val serializer = implicitly[MongoSerializer[T]] val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity) if (collection.findAndModify(id, serializer.serialize(entity)) != null) Some(entity) else None } }
3,3
И третий выбор вводит CollectionProvier[A]
, который дает в DBCollection
зависимости от типа A
.
trait Crud { def updateFirst[T : MongoSerializer : IdentityQueryBuilder : CollectionProvider](entity: T): Option[T] = { val collection = implicitly[CollectionProvider[T]].getCollection val serializer = implicitly[MongoSerializer[T]] val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity) if (collection.findAndModify(id, serializer.serialize(entity)) != null) Some(entity) else None } }
Различия в коде очень тонкие, но последствия довольно велики.
// assuming the required typecalss instances are in scope val crud31 = new Crud {} val crud32 = new Crud {} val crud33 = new Crud {} val users: DBCollection = ... val userUpdateFirst = crud32.updateFirst[User](users) crud31.updateFirst(users, User(...)) crud32.updateFirst(users)(User(...)) /* or */ userUpdateFirst(User(...)) crud33.updateFirst(User(...))
С помощью crud31
нам нужно перепрыгивать через обручи и указывать коллекцию при каждом вызове. Это может быть подвержено ошибкам. С помощью crud31
мы можем применить updateFirst
функцию только к первому списку параметров, предоставив нам функцию, которая выполняет операцию обновления users
коллекции. Это все еще оставляет нас с некоторой вероятностью ошибок. Наконец, с crud33
типом сущности управляет коллекцией, в которой он собирается оказаться. (Это именно то, что нам нужно, за исключением случаев, когда мы хотели бы иметь возможность изменить коллекцию.)
*** F # делает различие намного яснее: если вы используете
'a
, вы определяете общий элемент, если вы используете^a
, вы определяете полиморфный элемент по отношению к типуa
. И, поскольку код заканчивается как код CLR, параметры уровня типа могут быть только общими. Так же, как Скала.