Reputation: 550
I'm building a repository that I would like to have a consistent interface across multiple implementations. This repository needs three pieces so it know what to do: an Id, a Model and a Event. I define these as traits, then group them together as a Protocol.
trait AbstractId
trait AbstractModel
trait AbstractEvent
abstract class Protocol
Here are a couple of Protocols:
object BusinessProtocol extends Protocol {
final case class Id(id: Int) extends AbstractId
final case class Model(id: Id, name: String) extends AbstractModel
sealed abstract class Event extends AbstractEvent
final case class Create(id: Id, name: String) extends Event
}
object Order {
final case class Id(id: Uid) extends AbstractId
final case class Model(id: Id, cost: BigDecimal) extends AbstractModel
sealed abstract class Event extends AbstractEvent
final case class Create(id: Id, cost: BigDecimal) extends Event
final case class Close(id: Id) extends Event
}
Now I define my Repository interface. It has to take in a Model, Event and Id separately.
trait Repository[M <: AbstractModel, E <: AbstractEvent, I <: AbstractId] {
def hydrate(id: I): M
def persist(id: I, event: E)
}
And for completness, here's what a implemented Repository looks like:
object BusinessRepo extends Repository[BusinessProtocol.Model, BusinessProtocol.Event, BusinessProtocol.Id] {
override def hydrate(id: Id): Model = ???
override def persist(id: Id, event: BusinessProtocol.Event): Unit = ???
}
This all works, but I'd like to have a way to force two things:
1) Make Protocols provide definitions of Id, Model and Event.
2) Make the Repository just take a Protocol, and be able to pull the Id, Model and Event from the Protocol. Something like:
trait Repository[P <: Protocol] {
def hydrate(id: P.Id): P.Model
def persist(id: P.Id, event: P.Event)
}
This way, I can force Repository implementations to be always work on the three types that related. Is this possible?
Upvotes: 2
Views: 98
Reputation: 6132
Let's try this with some type-level programming:
trait Protocol {
type E <: AbstractEvent // define and Event that extends AbstractEvent
type I <: AbstractId //....
type M <: AbstractModel
}
An implementation from this would be:
object BusinessProtocol {
final case class Id(id: Int) extends AbstractId
final case class Model(id: Id, name: String) extends AbstractModel
sealed abstract class Event extends AbstractEvent
final case class Create(id: Id, name: String) extends Event
}
class BusinessProtocol extends Protocol{
import BusinessProtocol._
//here they're assigned to the type variables
type E = Event
type I = Id
type M = Model
}
And then the Repo:
trait Repository {
type P <: Protocol
def hydrate(id: P#I): P#M
def persist(id: P#I, event : P#E)
}
A BusinessRepo would then be:
class BusinessRepo extends Repository{
type P = BusinessProtocol
import BusinesProtocol._
def hydrate(id: Id) : Model = {...}
def persist(id:Id, event: Event) = {...}
}
You can find the science behind this on this question
EDIT : I experimented a little bit more, and you could do the Repo like this too:
trait Repository[P <: Protocol]{
def hydrate(id: P#I): P#M
def persist(id: P#I, event : P#E)
}
class BusinessRepo extends Repository[BusinessProtocol]{
....
}
Same effect, but his one makes it the signature of Repository
more explicit.
Upvotes: 3