Designer's commitments
Before going a bit deeper into the low-level stuff, here are some key sentences we should better often refer to:
- I shall collaborate with domain experts;
- I shall focus on the ubiquitous language;
- I shall not care about technical stuff or framework, but about modeling the Domain;
- I shall make the implicit explicit;
- I shall use end-user scenarios to get real and concrete;
- I shall not be afraid of defining one model per context;
- I shall focus on my Core Domain;
- I shall let my Domain code uncoupled to any external influence;
- I shall separate values and time in state;
- I shall reduce statefulness to the only necessary;
- I shall always adapt my model as soon as possible, once it appears inadequate.
As a consequence, you will find in mORMot no magic powder to build your DDD, but all the tools you need to focus on your business, without loosing time in re-inventing the wheel, or fixing technical details.
Defining objects in Delphi
How to implement all those DDD concepts in an object-oriented language like
Delphi?
Let's go back to the basics. Objects are defined by a state, a behavior and an
identity. A factory helps creating objects with the same state and
behavior.
In Delphi and most Object-Oriented languages (OOP - including C# or Java)
each class
instance (always inheriting from TObject):
- State is defined by all its property / member values;
- Behavior are defined by all its methods;
- Identity is defined by reference, i.e.
a=b
is true only ifa
andb
refers to the same object; - Factory is in fact the
class
type definition itself, which will force each instance to have the same members and methods.
In Delphi, the record
type (and deprecated object
type for older versions of the compiler) has an alternative behavior:
- State is also defined by all its property / member values;
- Behavior are also defined by all its methods;
- But identity is defined by content, i.e.
RecordEquals(a,b)
is true only ifa
andb
have the same exact property values; - Factory is in fact the
record
/object
type definition itself, which will force each instance to have the same members and methods.
We propose to use either one of the two kinds of object types, depending on the behavior expected by DDD patterns.
Defining DDD objects in mORMot
DDD's Value Objects are probably meant to be defined as
record
, with methods (i.e. in this case as object
for
older versions of Delphi). You may also use TComponent
or
TSQLRecord
classes, ensuring the published
properties
do not have setters but just read F...
definition, to make them
read-only, and, at the same time, directly serializable.
If you use record
/ object
types, you may need to
customize the JSON
serialization when targeting AJAX clients (by default, record
s
are serialized as binary + Base64 encoding, but you can define easily the
record serialization e.g. from text). Note that since record
/
object
defines in Delphi by-value types (whereas
class
defines by-reference types - see previous
paragraph), they are probably the cleanest way of defining Value
Objects.
DDD's Entity objects could be either regular Delphi classes, or
inherit from TSQLRecord
:
- Using PODOs (Plain Old Delphi Object - see so-called POJO or POCO
for Java or C#) has some advantages. Since your domain has to be uncoupled from
the rest of your code, using plain
class
helps keeping your code clean and maintainable. - Inheriting from
TSQLRecord
will give it access to a whole set of methods supplied by mORMot. It will implement the "Layer Supertype" pattern, as explained by Martin Fowler.
DDD's Aggregates may either benefit of using mORMot's 3, or you can use a repository service.
- In the first case, your aggregate roots will be defined as
TSQLRecord
, and you will benefit of all CRUD methods made available by the framework; - Otherwise, you should define a dedicated persistence service, then use
plain DTO (like Delphi
record
) or even publish theTSQLRecord
types, and benefit of their automated serialization.
In all cases, when defining domain objects, we should always make the
implicit explicit, i.e. defining one type (either
record/object
or class
) per reality in the
model.
Thanks to Delphi's strong typing, it will ensure that the Domain Ubiquitous
language will appear in the code.
DDD's DTO may also be defined as record
, and directly
serialized as JSON via text-based serialization. Don't be afraid of writing
some translation layers between TSQLRecord
and DTO records or,
more generally, between your Application layer and your
Presentation layer. It will be very fast, on the server side. If your
service interfaces are cleaner, do not hesitate. But if it tends to enforce you
writing a lot of wrapping code, forget about it, and expose your Value
Objects or even your Entities, as stated above. Or automate the
wrapper coding, using RTTI and code generators. You have to weight the PROs and
the CONs, like always...
DDD's Events should be defined also as record
, just
like regular DTOs. Note that in the close future, it is planned that
mORMot will allow such events to be defined as interface
,
in a KISS implementation.
mORMot's
BATCH support is a convenient implementation of the Unit of Work
pattern (i.e. regrouping all update / delete / insert operations in a single
stream, with global Commit
and Rollback
methods).
Note that the current implementation of Batch*
methods in
mORMot, which focuses on Client side, should be enhanced to be more
convenient and available on the server side, i.e. in the Application
Layer.
Defining services
In practice, mORMot's Client-Server architecture may be used as such:
-
Services via methods can be used to publish methods corresponding to
your aggregate roots defined as
TSQLRecord
.
This will make it pretty RESTful compatible. - Services via
interfaces can be used to publish all your processes.
Dedicated factories can be used on both Client and Server side, to define your repositories and/or domain operations.
Both methods will allow proper customization, and, especially for the second, offer both integrated and automated process, e.g. RESTful access, JSON marshalling, session, security, logging, multi-threading.
Building a Clean architecture
A common DDD architecture is expressed as in the following model, which may look like a regular multi-Tier design at first, but should be implemented as a Clean Architecture.
Layer | Description |
Presentation | MVC UI generation and reporting |
Application | Services and high-level adapters |
Domain Model | Where business logic remains |
Data persistence | ORM and external services |
Cross-Cutting | Horizontal aspects shared by other layers |
Physically, it involves a common n-Tier representation
splitting the classical Logic Tier into two layers, i.e.
Application layer and Domain Model layer. At logical
level, DDD will try to uncouple the Domain Model layer from other
layers, so the code itself will rely on interface
s and
dependency injection to let the core Domain focus on the
business logic, not on implementation details (e.g. persistence or
communication).
This is what we called a Clean Architecture, defined as such:
The RESTful SOA components of our Synopse mORMot framework can therefore define such an Architecture:
As we already stated, the main point of this Clean Architecture is
to control coupling, and isolate the Domain core from the outer
layers. In Delphi, unit dependencies (as displayed e.g. by our
SynProject tool) will be a good testimony of proper objects
uncoupling: in the units defining your domain, you may split it between
Domain Model and Domain Services (the 2nd using the first,
and not vice-versa), and you should never have any dependency to a
particular DB unit, just to the framework's core units, i.e.
SynCommons.pas
and mORMot.pas
. Inversion of Control - via
interface-based
services or at ORM initialization level - will ensure that your code is
uncoupled from any low-level technical dependency. It will also allow proper
testing of your application workflows, e.g. stubbing the database if
necessary.
In fact, since SOA tends to ensure that services comprise unassociated,
loosely coupled units of functionality that have no calls to each other
embedded in them, we may define two levels of services, implemented by two
interface
factories, using their own hosting and
communication:
- One set of services at Application layer, to define the uncoupled contracts available from Client applications;
- One set of services at Domain Model layer, which will allow all involved domains to communicate with each other, without exposing it to the remote clients.
In order to provide the better scaling of the server side, cache can be easily implemented at every level, and hosting can be tuned in order to provide the best response time possible: one central server, several dedicated servers for application, domain and persistence layers...
Due to the SOLID design of mORMot you can use as many Client-Server services layers as needed in the same architecture (i.e. a Server can be a Client of other processes), in order to fit your project needs, and let it evolve from the simplest architecture to a full scalable Domain-Driven design.