-
Notifications
You must be signed in to change notification settings - Fork 0
KernelReferencesUseCases
This lesson presents a couple of kernel references use cases along with some new stuff.
Kernel references open new ways to separate abstract parts of an application from concrete ones. One of the popular design patterns having such a separation in its heart, is the abstract factory. This example presents an adaptation of this design pattern to Morpheus.
In order to design such a factory properly, we have to find the division line between the abstract and concrete stuff. In our chat application it means to specify an abstract morph model describing well the behaviour of the modelled system, i.e. the chat contact, while being general enough at the same time, so as to give freedom for providing a rich variety of implementations.
Let us define the minimum behavior of the contact as follows. The morph model must allow
- Switching the contact status between offline and online modes
- Printing the contact
These behavioral requirements can be satisfied by this morph model type:
(OfflineContact or OnlineContact) with ContactPrinter
Since the contact's members are immutable, the factory must allow passing their values to its abstract factory method.
Now we can specify the factory's interface.
trait ContactKernelFactory {
def makeContactKernel(firstName: String,
lastName: String,
male: Boolean,
email: String,
nationality: Locale): &[(OfflineContact or OnlineContact) with ContactPrinter]
}
The default implementation of this factory will essentially wrap up the code from the previous lessons up to some exceptions.
object DefaultContactKernelFactory extends ContactKernelFactory {
var printerCoord: Int = 0
var channelCoord: Int = 0
def makeContactKernel(firstName: String,
lastName: String,
male: Boolean,
email: String,
nationality: Locale
): &[(OfflineContact or OnlineContact) with ContactPrinter] = {
val contactData = ContactData(firstName, lastName, male,email, nationality)
implicit val offlineContactFrag = single[OfflineContact, Contact](contactData)
implicit val onlineContactFrag = single[OnlineContact, Contact](contactData)
// Use the parse macro to parse the morph model
val contactModel = parse[(OfflineContact or OnlineContact) with
(ContactRawPrinter or ContactPrettyPrinter) with
(StandardOutputChannel or MemoryOutputChannel)](true)
// Prepare the default strategy of the kernel. The strategy deals with two dimensions only:
// ContactPrinter and OutputChannel. The Contact dimensions is left free for the sake of the consumer.
val rootStr = rootStrategy(contactModel)
val printerDimStr = promote[ContactRawPrinter or ContactPrettyPrinter](rootStr, printerCoord)
val channelDimStr = promote[StandardOutputChannel or MemoryOutputChannel](printerDimStr, channelCoord)
// Create the kernel by the overloaded version of the singleton macro allowing specifying
// the default strategy.
val contactKernel = singleton(contactModel, channelDimStr)
contactKernel
}
}
We have to stop here for a moment since there are several new things that deserve some explanation.
Firstly, instead of the one-step instantiation of the kernel by means of the no-argument singleton
macro, we are using the two-step kernel instantiation. The reason is that this two step process allows us to specify the default morphing strategy of the kernel. A morphing strategy needs to know the morph model in order to work properly. Therefore we parse the morph model first, then we use it to construct the strategy and finally we can create the kernel passing both the model and the strategy.
Secondly, the parse
macro's single argument indicates whether the parsed model will be subject of the dependency check. Sometimes it can be useful to get around this check, but it is not our case now.
Thirdly, although the model has three dimensions, the default strategy handles only two: ContactPrinter
and OutputChannel
. The Contact
dimensions is left free for the sake of the consumer of the kernel. The number of degrees of freedom is thus reduced from 3 to 1. The consumer does not see the fragments from dimensions OutputChannel
and ContactPrinter
. They are hidden for it.
Fourthly, the two dimensions are governed by the two public integer fields printerCoord
and channelCoord
. It is assumed that these two fields are primarily manipulated with another part of the application than that of the consumer of the kernel.
Now the factory is finished and we can use it.
val contactKernelRef = DefaultContactKernelFactory.makeContactKernel("Pepa", "Novák", male = true, email="[email protected]", Locale.CANADA)
val contactKernel = *(contactKernelRef)
val contact = contactKernel.~
Let us print the active alternative of the contact morph.
// Print both the visible and hidden fragments
println(contact.myAlternative)
Since the contact coordinate is 0, i.e. OfflineContact
, as well as the two hidden coordinates for the printer and channel are 0 (i.e. ContactRawPrinter
and StandardOutputChannel
), the output should look like this.
List(org.cloudio.morpheus.tutor.chat.frag.step7.OfflineContact, org.cloudio.morpheus.tutor.chat.frag.step7.ContactRawPrinter, org.cloudio.morpheus.tutor.chat.frag.step7.StandardOutputChannel)
We can change the contact coordinate and see its effect after the remorphing.
// Altering the visible dimension should not affect the hidden dimensions
var contactCoord: Int = 1
val contactDimStr = promote[OfflineContact or OnlineContact](contactKernel.defaultStrategy, contactCoord)
contact.remorph(contactDimStr)
println(contact.myAlternative)
The following code is actually a little hack, since it attempts to change the hidden coordinates by accessing the fields in DefaultContactKernelFactory
. Nevertheless, it works.
// Controlling the hidden dimensions
DefaultContactKernelFactory.printerCoord = 1
DefaultContactKernelFactory.channelCoord = 0
contact.remorph(contactDimStr)
println(contact.myAlternative)
Although the visitor pattern is rarely used in Scala, it may be a good instrument for the illustration of another usage of kernel references.
Let us suppose that we need to handle the two states of the contact by one object (the so-called object switch). The following visitor trait represents such a switch object.
trait ContactStatusVisitor[T] {
def visitOfflineContact(contact: OfflineContact): T
def visitOnlineContact(contact: OnlineContact): T
}
Next, we need another object that will recognise the state of the contact and will invoke the corresponding method on the visitor. Let us call it ContactStatusAcceptor
.
class ContactStatusAcceptor(contactStatusRef: &[(OfflineContact or OnlineContact)]) {
private val contactStatus = *(contactStatusRef).~
def acceptVisitor[T](vis: ContactStatusVisitor[T]): T = {
contactStatus.remorph() match {
case c: OfflineContact => vis.visitOfflineContact(c)
case c: OnlineContact => vis.visitOnlineContact(c)
case _ => sys.error("Unexpected status")
}
}
}
The constructor's single parameter is used to pass a kernel reference to the OfflineContact or OnlineContact
morph type, which is the minimum one to allow the acceptor to recognise the contact's status.
The contactStatus
value contains the default morph of the referenced kernel. This morph is used to query the contact's status in the acceptVisitor
method.
The acceptVisitor
method has one argument for passing a visitor. It calls remorph
on contactStatus
in order to sync the morph`s status. Then it matches the result and invokes the corresponding method on the visitor.
The following code creates a new instance of the acceptor and passes the kernel's default morph (~
). One may ask why we do not pass contactKernel
directly when it perfectly matches ContactStatusAcceptor
's constructor parameter. The answer is that we could but the acceptor would not see subsequent re-morphings invoked on the default mutable morph. Instead, passing contactKernel.~
with an implicit conversion acting behind the scenes we 'magically' establish the connection between the default kernel's morph and the acceptor's morph so that the acceptor can sync the state. For more information see this.
val contactAcceptor = new ContactStatusAcceptor(contactKernel.~) // using contactKernel.~ instead contactKernel links the reference with the source morph via its current alternatives
val contactVisitor = new ContactStatusVisitor[Unit] {
override def visitOfflineContact(contact: OfflineContact): Unit = {
println(s"${contact.lastName} is offline")
}
override def visitOnlineContact(contact: OnlineContact): Unit = {
println(s"${contact.lastName} is online")
}
}
Now we can play with the contact morph and verify that the proper visitor's method is invoked when passed to the acceptor.
contactAcceptor.acceptVisitor(contactVisitor)
contactCoord = 1
contactKernel.~.remorph()
contactAcceptor.acceptVisitor(contactVisitor)
The next example will show how to initiate remorphing through a kernel reference.
The ContactStatusController
is able to switch between the two contact states via method setStatus
. The class has one argument for passing a kernel reference compatible with the minimum morph model OfflineContact or OnlineContact
.
class ContactStatusController(contactStatusRef: &[OfflineContact or OnlineContact]) {
def setStatus(active: Boolean): Unit = {
remorph(contactStatusRef, if (active) 1 else 0)
}
}
The setStatus
invokes the remorph
macro to reshape the default mutable morph of the referenced kernel. The first argument of this macro is the kernel reference and the second argument is the number of the alternative from the kernel reference's model (i.e. the target model) to be promoted in the kernel's default mutable morph.
val controller: ContactStatusController = new ContactStatusController(contactKernel)
controller.setStatus(false)
contactAcceptor.acceptVisitor(contactVisitor)
controller.setStatus(true)
contactAcceptor.acceptVisitor(contactVisitor)
####Morpheus Tutorial####
- Modelling simple entity
- Adding some behavior
- Abstracting fragment
- Reusing fragment
- Using fragment in Java
- Multidimensional morphs
- Morphing strategies
- Mutable morphs
- Delegating and sharing
- Dimension wrappers
- Fragment wrappers
- Using a morph as a fragment
- Kernel references
- Kernel references use cases
- Promoting, masking and rating