Architecture Module
This chapter will delve into the implementation of the Architecture Module within the HexArc framework.
Table of Contents
Implementation
The Architecture Module is the core module of the HexArc architecture, defining and implementing all the concepts required to describe, configure and deploy a service, following the best practices of the Hexagonal Architecture.
Below is the complete class diagram of the Architecture Module.
As shown in the diagram, the Architecture Module is made of two main submodules:
- Components Submodule: defines the concepts required to describe a service;
- VertxDSL Submodule: provides a DSL for configuring and deploying services in a declarative way.
As of now, HexArc supports only Vertx as the underlying framework for deploying services, but this isn’t a binding choice. In fact, some effort could be put in order to support other technologies (e.g. Akka…) in new modules for HexArc, abstracting the concepts shared among different modules.
Components Submodule
The Components Submodule is the part of the Architecture Module that defines the concepts required to describe a service, following the best practices of the Hexagonal Architecture.
Below is the class diagram of the Components Submodule.
The Components Submodule divides a service into components, called ServiceComponent
s, each taking care of providing or supporting the provision of part of the affordances of the service.
In order to define a ServiceComponent
, you need to specify how it will be configured when initialized. In fact, the simplest ServiceComponent
is just a function specifying its configuration.
The main ServiceComponent
s in a service are the following:
Port
s: defines what are the affordances of the service with respect to a specific use case.Port
s are parts of the contract of a service, as such they should be represented as traits. To address the concrete implementation of one or morePort
s, HexArc adopts the term model.Adapter
s: typically defines how the affordances of the service are exposed to its users, enabling technologies for communicating with one or morePort
s of the service. This is the case of inboundAdapter
s, which are the most common types. However, anAdapter
could also be used to monitor the service and to communicate its events to other services, which is the case of outboundAdapter
s.VertxService
: it’s the service itself, or at least the part of the service holding together itsServiceComponent
s. In fact, thisServiceComponent
is used internally for integrating all theServiceComponent
s of the service into a singleVerticle
that can be deployed within Vertx. Likely, it won’t be used by the end user, unless he requires for some reason to personalize how the integration between theServiceComponent
s happens.
Upon the deployment of a service, proper execution contexts, called ServiceComponentContext
s, must be provided for all of its ServiceComponent
s. Then, each ServiceComponent
is initialized consuming its corresponding ServiceComponentContext
.
A ServiceComponentContext
may contain all sorts of useful information for initializing a ServiceComponent
. This information depends on the type of the ServiceComponent
to initialize, but all ServiceComponentContext
s provide:
name
: the name of the service containing theServiceComponent
;vertx
: theVertx
instance on which the service is deployed;log
: an Slf4jLogger
specific to theServiceComponent
.
More in detail, there are three types of ServiceComponentContext
:
ServiceContext
: theServiceComponentContext
provided when aVertxService
is initialized, that is when its correspondingVerticle
is deployed.PortContext
: theServiceComponentContext
provided when aPort
is initialized. It also contains theServiceContext
of the service who owns thatPort
.AdapterContext
: theServiceComponentContext
provided when anAdapter
is initialized. It also contains thePort
exposed by thatAdapter
and itsPortContext
.
The Components Submodule provides the end user with all the means for instantiating a service, letting him personalize the integration of its ServiceComponent
s and the deployment of the service. However, since these processes can be complex, HexArc provides a DSL to make things easier, less imperative and more explicit.
VertxDSL Submodule
The VertxDSL Submodule is the part of the Architecture Module that defines the DSL for configuring and deploying services in a declarative way, without the hassle of manually integrating their ServiceComponent
s.
Below is the class diagram of the VertxDSL Submodule.
The entrypoint of the module is the DSL, modelled by the object VertxDSL
. The VertxDSL
consists in a set of exports defining the vocabulary of the DSL and enriching its syntax by means of different extensions.
In particular, the vocabulary of the DSL is defined by the VertxDSL.Vocabulary
, which exports the different contexts of the DSL, called DSLContext
s, as keywords of the DSL.
A DSLContext
defines which keywords of a DSL are or aren’t allowed inside the portion of code within the DSLContext
. In fact, a DSLContext
may be a DSLContext.Root
, meaning that its corresponding keyword can be used anywhere, or a DSLContext.Child
, meaning that its corresponding keyword can only be used within its parent DSLContext
.
For example, referring to the User Documentation, an Adapter
may be defined only within a Port
and it cannot be defined as a direct child of a Service
. In order to explicit when a context is closed, the example also reports the end new
scala syntax, which is completely optional.
new DeploymentGroup(Vertx.vertx()): // opening "DeploymentGroup" DSLContext (Root)
new Service: // opening "Service" DSLContext (Child)
name = "ColoredLampService"
new Port[LampSwitchPort]: // opening "Port" DSLContext (Child)
name = "SwitchPort"
model = ColoredLampModel()
// Here the keyword `Adapter` exists
new Adapter(LampSwitchHttpAdapter()): // opening "Adapter" DSLContext (Child)
name = "Http"
end new // closing "Adapter" DSLContext
end new // closing "Port" DSLContext
// Here the keyword `Adapter` does not exist
new Adapter(LampSwitchHttpAdapter()): // ERROR: keyword `Adapter` is not inside a `Port`
name = "Http"
end new
end new // closing "Service" DSLContext
end new // closing "DeploymentGroup" DSLContext
Note: here
DeploymentGroup
,Service
,Port
,Adapter
,name
andmodel
are the keywords of the DSL. In particular, the keywordsPort
andAdapter
are not the same classes as their homonyms in the Components Module.
As introduced by the example above, HexArc defines four main types of DSLContext
:
-
DeploymentGroupDSLContext
: aDSLContext.Root
describing the configuration for the deployment of a group of services. Such configuration consists of a list of the services that should be deployed. These services can then be deployed by callingdeploy
on theDeploymentGroupDSLContext
.The
VertxDSL.Vocabulary
exposes this type ofDSLContext
as the global keywordDeploymentGroup
. While aDeploymentGroupDSLContext
exposes the followingServiceDSLContext
as the scoped keywordService
. The actual implementation relies on the definition of a type member calledService
.Note: in HexArc, a global keyword is a keyword that can be used everywhere in the code, therefore it should be made available everywhere in the code; while a scoped keyword is a keyword that requires positioning inside a specific scope, therefore it should be made available only in the scope where it is allowed to use it.
-
ServiceDSLContext
: aDSLContext.Child
ofDeploymentGroupDSLContext
describing the configuration for the instance of a service to be deployed. Such configuration consists of a list of thePort
s forming the contract of the service.When a
ServiceDSLContext
is opened (i.e. created) within aDeploymentGroupDSLContext
, it automatically adds itself to the services that should be deployed by thatDeploymentGroupDSLContext
.The
VertxDSL.Vocabulary
exposes this type ofDSLContext
as the global keywordService
, so that it could be lazily configured outside aDeploymentGroupDSLContext
. While aServiceDSLContext
exposes the followingPortDSLContext
as the scoped keywordPort
. -
PortDSLContext
: aDSLContext.Child
ofServiceDSLContext
describing the configuration of aPort
of a service. Such configuration includes the contract exposed by thePort
(defined as type parameter), the actual implementation of thePort
and a lists of theAdapter
s installed on thePort
.When a
PortDSLContext
is opened within aServiceDSLContext
, it automatically adds itself to thePort
s of the service configured by thatServiceDSLContext
.The
VertxDSL.Vocabulary
exposes this type ofDSLContext
as the global keywordPort
(not to be confused with the homonym component), so that it could be lazily configured outside aServiceDSLContext
. While aPortDSLContext
exposes the followingAdapterDSLContext
as the scoped keywordAdapter
(still, not to be confused with the homonym component). Moveover, it exposes the scoped keywordmodel
for configuring its actual implementation. -
AdapterDSLContext
: aDSLContext.Child
ofPortDSLContext
describing the configuration of anAdapter
of aPort
of a service. Such configuration consists of the implementation of theAdapter
.When an
AdapterDSLContext
is opened within aPortDSLContext
, it automatically adds itself to theAdapter
s of thePort
configured by thatPortDSLContext
.The
VertxDSL.Vocabulary
exposes this type ofDSLContext
as the global keywordAdapter
(not to be confused with the homonym component), so that it could be lazily configured outside aPortDSLContext
, provided the type ofPort
on which it can be installed.
These last three DSLContext
s are extended using a mixin called NamedDSLContext
, which exposes a new scoped keyword for each them, called name
, for configuring the name of each ServiceComponent
(used for creating their Logger
s).
As the DSLContext
s of the VertxDSL
are opened, a tree-like structure is generated starting from the root, which is always a DeploymentGroupDSLContext
.
Internally, each of the four types of DSLContext
provide a close
method, which is used to finalize their configuration. In particular, when deploy
is called on a DeploymentGroupDSLContext
, all of the DSLContext
s belonging to its tree are closed bottom-up, configuring the corresponding ServiceComponent
s. Finally, the DeploymentGroupDSLContext
closes itself, deploying the configured services.
The deployment of the services is delegated to a support class called Deployment
. Its companion object provides methods for deploying services and obtaining their corresponding Deployment
s, while an instance of the Deployment
class itself allows to undeploy the corresponding service.
In addition to the DSLContext
s, another way the VertxDSL
enriches its syntax is by means of extensions. In particular, it exports all the extension methods provided by the VertxDSLExtensions
object, which include methods for manipulating Future
s (e.g. awaiting the deployment or un-deployment of a service…).
To summarize, the VertxDSL
is defined through keywords, where a global keyword can be either:
- a
DSLContext
, exposing new scoped keywords in the form of:- public or protected methods;
- public or protected type members.
- an extension method provided by some extension of the DSL.
From Functional DSL to YAML-like DSL
Initially, HexArc provided a functional DSL (where keywords were pure functions), instead of the current YAML-like DSL (mostly based on anonymous classes).
One of the reasons why HexArc migrated from its original functional syntax to a YAML-like syntax is type inference. For example, inside a
Port
keyword, theAdapter
scoped keywords automatically refer to the proper type ofAdapter
for thatPort
, without requiring the user to explicit that type for eachAdapter
.// Original functional syntax deploy(Vertx.vertx()){ service("CustomLamp"){ port[DimmableLampPort](DimmableLampModel()){ // Here `[DimmableLampPort]` couldn't be omitted adapter[DimmableLampPort](DimmableLampLocalAdapter()) adapter[DimmableLampPort](DimmableLampHttpAdapter()) adapter[DimmableLampPort](DimmableLampMqttAdapter()) } port[ColoredLampPort](ColoredLampModel()){ // Here `[ColoredLampPort]` couldn't be omitted adapter[ColoredLampPort](ColoredLampLocalAdapter()) adapter[ColoredLampPort](ColoredLampHttpAdapter()) adapter[ColoredLampPort](ColoredLampMqttAdapter()) } } }
// Current YAML-like syntax new DeploymentGroup(Vertx.vertx()): new Service: name = "CustomLamp" new Port[DimmableLampPort]: model = DimmableLampModel() // Here `Adapter` can only mean `Adapter[DimmableLampPort]` new Adapter(DimmableLampLocalAdapter()) new Adapter(DimmableLampHttpAdapter()) new Adapter(DimmableLampMqttAdapter()) new Port[ColoredLampPort]: model = ColoredLampModel() // Here `Adapter` can only mean `Adapter[ColoredLampPort]` new Adapter(ColoredLampLocalAdapter()) new Adapter(ColoredLampHttpAdapter()) new Adapter(ColoredLampMqttAdapter()) }
Other reasons of the migration include the following:
- A YAML-like syntax feels more proper for representing a data structure, such as the configuration of the deployment of a group of services. In that sense, it is also more direct to extend the DSL without reducing its readability (e.g. just add a method for configuring a field in a
DSLContext
and it won’t affect how the DSL visually appears…).- A YAML-like syntax provides support for scoped keywords. All the keywords of a functional DSL are available everywhere in the code, even though some require positioning within a specific scope. By defining keywords as type members instead of functions, it is possible to make keywords available only in the scopes where they can actually be used.
Of course the YAML-like syntax comes with its own downsides, the most noticeable being that the functional syntax still appears cleaner, as it does not require the boilerplate code that a YAML-like syntax does require (e.g. as of now, the
new
keyword is unfortunately mandatory for creating anonymous classes in Scala 3…).