Using service and callback interfaces
For instance, you may define the following generic service and callback to
retrieve a picture from a remote camera, using mORMot's
interface
-based approach:
type // define some custom types to make the implicit explicit TCameraID = RawUTF8; TPictureID = RawUTF8; // mORMot notifications using a callback interface definition IMyCameraCallback = interface(IInvokable) ['{445F967F-79C0-4735-A972-0BED6CC63D1D}'] procedure Started(const Camera: TCameraID; const Picture: TPictureID); procedure Progressed(const Camera: TCameraID; const Picture: TPictureID; CurrentSize,TotalSize: cardinal); procedure Finished(const Camera: TCameraID; const Picture: TPictureID; const PublicURI: RawUTF8; TotalSize: cardinal); procedure ErrorOccured(const Camera: TCameraID; const Picture: TPictureID; const MessageText: RawUTF8); end; // mORMot main service, also defined as an interface IMyCameraService = interface(IInvokable) ['{3CE61E74-A01D-41F5-A414-94F204F140E1}'] function TakePicture(const Camera: TCameraID; const Callback: IMyCameraCallback): TPictureID; end;
In a single look, I guess you did get the expectation of the "Camera
Service".
Take a deep breath, and keep in mind those two type definitions as
reference.
We will now compare with a classical message-based pattern.
Classical message(s) event
With a class
-based message kind of implementation, you may
either have to define a single class, containing all potential information:
type // a single class message would need a status TMyCameraCallbackState = ( ccsStarted, ccsProgressed, ccsFinished, ccsErrorOccured); // the single class message TMyCameraCallbackMessage = class private fCamera: TCameraID; fPicture: TPictureID; fTotalSize: cardinal; fMessageText: RawUTF8; fState: TMyCameraCallbackState; published property State: TMyCameraCallbackState read fState write fState; property Camera: TCameraID read fCamera write fCamera; property Picture: TPictureID read fPicture write fPicture; property TotalSize: cardinal read fTotalSize write fTotalSize; property MessageText: RawUTF8 read fMessageText write fMessageText; end;
This single class is easy to write, but makes it a bit confusing to consume
the notification. Which field comes with which state? The client-side code
would eventually consist of a huge case aMessage.State of
...
block, with potential issues. The business logic does not appear in this type
definition. Easy to write, difficult to read - and maintain...
In order to have an implementation closer to SOLID design principles, you may define a set of classes, as such:
type // all classes would inherit from this one, to have common properties TMyCameraCallbackAbstract = class private fCamera: TCameraID; fPicture: TPictureID; published property Camera: TCameraID read fCamera write fCamera; property Picture: TPictureID read fPicture write fPicture; end; // message class when the picture acquisition starts TMyCameraCallbackStarted = class(TMyCameraCallbackAbstract); // message class when the picture is acquired TMyCameraCallbackFinished = class(TMyCameraCallbackAbstract) private fPublicURI: RawUTF8; fTotalSize: cardinal; published property TotalSize: cardinal read fTotalSize write fTotalSize; property PublicURI: RawUTF8 read fPublicURI write fPublicURI; end; // message during picture download TMyCameraCallbackProgressed = class(TMyCameraCallbackFinished) private fCurrentSize: cardinal; published property CurrentSize: cardinal read fCurrentSize write fCurrentSize; end; // error message TMyCameraCallbackErrorOccured = class(TMyCameraCallbackAbstract) private fMessageText: RawUTF8; published property MessageText: RawUTF8 read fMessageText write fMessageText; end;
Inheritance makes this class hierarchy not as verbose as it may have been
with plain "flat" classes, but it is still much less readable than the
IMyCameraCallback
type definition.
In both cases, such class
definitions make it difficult to
guess which message does match with a given service. You must be very careful
and consistent about your naming conventions, and uncouple your service
definitions in clear name spaces.
When implementing SOA services, DDD's
Ubiquitous Language tends to be polluted by the class
definition (getters and setters), and implementation details of the
messages-based notification: your Domain code would be tied to the
message oriented nature of the Infrastructure layer.
interface
callbacks would therefore help implementing DDD's
Event-Driven pattern, in a cleaner way.
Workflow adaptation
Sometimes, it may be necessary to react to some unexpected event. The consumer may be able to change the workflow of the producer, depending on some business rules, or user expectations. By definition, all message-based implementation are asynchronous: as a result, implementing "reverse" messaging tends to be difficult to write and debug.
A common implementation is to have a dedicated set of "answer" messages, to notify the service providers of a state change - it comes with potential race conditions, or unexpected rebound phenomenons, for instance when you add a node to an existing event-driven system.
Another solution may be to define explicit rules for service providers, e.g. when the service is called. You may define a set of workflows, injected to the provider service at runtime. It will definitively tend to break the Single Responsibility Principle.
On the other hand, since mORMot's callbacks are true
interface
methods, they may return some values (as a
function
result or a var/out
parameter). On the
server side, such callbacks would block and wait for the client end to
respond.
So by writing an additional method like:
IMyCameraCallback = interface(IInvokable) ... function ShouldRetryIfBusy(const Camera: TCameraID; const Picture: TPictureID): boolean; ...
... you would be able to implement any needed complex workflow adaptation,
in real time.
The server side code would still be very readable and efficient, with no
complex plumbing, wait queue or state machine to set up.
From interfaces come abstraction and ease
As an additional benefit, integration with the Delphi language is clearly
implementation agnostic: you are not even tied to use the framework, when
working with such interface
type definitions. In fact, this is a
good way of implementing callbacks conforming to SOLID design
principles on the server side, and let the mORMot framework
publish this mechanism in a client/server way, by using WebSockets,
only if necessary.
The very same code could be used on the server side, with no transmission
nor marshalling overhead (via direct interface
instance calls),
and over a network, with optimized use of resource and bandwidth (via "fake"
interface
calls, and binary/JSON marshalling over TCP/IP).
On the server side, your code - especially your Domain code - may
interact directly with the lower level services, defined in the Domain
as interface
types, and implemented in the infrastructure
layer. You may host both Domain and Infrastructure code in a
single server executable, with direct assignment of local class
instance as callbacks. This will minimize the program resources, in both CPU
and memory terms - which is always a very valuable goal, for any business
system.
Last but not least, using an interface
would help implementing
the whole callback mechanism using
Stubs and mocks, e.g. for easy unit testing via Calls
tracing.
You may also write your unit tests with real local callback class
instances, which would be much easier to debug than over the whole
client/server stack. Once you identified a scenario which fails the system, you
could reproduce it with a dedicated test, even in an aggressive multi-threaded
way, then use the debugger to trace the execution and identify the root cause
of the issue.