source upload

This commit is contained in:
Razor12911
2022-01-17 22:16:47 +02:00
parent 12936d065b
commit 098e8c48de
1778 changed files with 1206749 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.

View File

@@ -0,0 +1,21 @@
/// Conference Domain dependencies interface definition
unit DomConferenceDepend;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
]);
end.

View File

@@ -0,0 +1,21 @@
/// Conference Domain services interfaces definition
unit DomConferenceInterfaces;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
]);
end.

View File

@@ -0,0 +1,18 @@
/// Conference Domain services implementation
unit DomConferenceServices;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces;
implementation
initialization
end.

View File

@@ -0,0 +1,40 @@
/// Conference Domain unit tests
unit DomConferenceTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices;
type
TTestConference = class(TSynTestCase)
protected
published
procedure DomainTypes;
procedure DomainBooking;
end;
implementation
{ TConferenceTest }
procedure TTestConference.DomainTypes;
begin
end;
procedure TTestConference.DomainBooking;
begin
end;
initialization
end.

View File

@@ -0,0 +1,18 @@
/// entities, values, aggregates for the Conference domain
unit DomConferenceTypes;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
end.

View File

@@ -0,0 +1,19 @@
/// Conference Repository implementation
unit InfraConferenceRepository;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceDepend;
implementation
initialization
// TDDDRepositoryRestFactory.ComputeSQLRecord(); from mORMotDDD
end.

View File

@@ -0,0 +1,60 @@
program ServBook;
{$APPTYPE CONSOLE}
uses
{$I SynDprUses.inc} // includes FastMM4
SysUtils,
SynLog,
mORMot,
SynSQLite3Static,
mORMotSQLite3,
mORMotService, // cross-platform service/daemon skeleton with settings
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas';
type
TBookSettings = class(TSynDaemonSettings)
private
fProcess: TBookProcessSettings;
published
property Process: TBookProcessSettings read fProcess;
end;
TBookDaemon = class(TSynDaemon)
protected
fProcess: TBookProcess;
public
procedure Start; override;
procedure Stop; override;
end;
{ TBookDaemon }
procedure TBookDaemon.Start;
begin
if fProcess = nil then
fProcess := TBookProcess.Create((fSettings as TBookSettings).Process);
end;
procedure TBookDaemon.Stop;
begin
FreeAndNil(fProcess);
end;
begin
with TBookDaemon.Create(TBookSettings, '', '', '') do
try
CommandLine(true);
finally
Free;
end;
end.

View File

@@ -0,0 +1,40 @@
/// Booking server implementation
unit ServBookMain;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository;
type
TBookProcessSettings = class(TSynAutoCreateFields)
published
end;
TBookProcess = class(TSynPersistent)
protected
fSettings: TBookProcessSettings;
public
constructor Create(aSettings: TBookProcessSettings); reintroduce;
property Settings: TBookProcessSettings read fSettings;
end;
implementation
{ TBookProcess }
constructor TBookProcess.Create(aSettings: TBookProcessSettings);
begin
inherited Create;
fSettings := aSettings;
end;
initialization
end.

View File

@@ -0,0 +1,46 @@
/// unit tests for the Booking server
unit ServBookTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository,
ServBookMain;
type
TTestBookingApplication = class(TSynTestCase)
published
procedure RunService;
procedure ApplicationTest;
procedure ShutdownService;
end;
implementation
{ TTestBookingApplication }
procedure TTestBookingApplication.RunService;
begin
end;
procedure TTestBookingApplication.ApplicationTest;
begin
end;
procedure TTestBookingApplication.ShutdownService;
begin
end;
initialization
end.

View File

@@ -0,0 +1,28 @@
program TestAll;
uses
{$I SynDprUses.inc} // cross-platform, cross-compiler (includes FastMM4)
SynCommons, // shared types for the whole framework
SynLog, // logging
SynTests, // unitary tests
mORMot, // ORM + SOA
SynSQLite3Static, // statically linked SQLite3 engine
mORMotSQLite3, // use SQlite3 as ORM core
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
DomConferenceTest in '..\dom\DomConferenceTest.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas',
ServBookTest in '..\serv\ServBookTest.pas',
// tests
TestAllMain in 'TestAllMain.pas';
begin
TSynLogTestLog := TSQLLog; // share the same log file with the whole mORMot
TTestEKON.RunAsConsole('EKON Automated Tests', LOG_VERBOSE);
end.

View File

@@ -0,0 +1,38 @@
unit TestAllMain;
interface
uses
SynCommons,
SynTests,
DomConferenceTest,
ServBookTest;
type
TTestEkon = class(TSynTestsLogged)
published
procedure Infrastructure;
procedure Domain;
procedure Applications;
end;
implementation
{ TTestEkon }
procedure TTestEkon.Infrastructure;
begin
end;
procedure TTestEkon.Domain;
begin
AddCase([TTestConference]);
end;
procedure TTestEkon.Applications;
begin
AddCase([TTestBookingApplication]);
end;
end.

View File

@@ -0,0 +1,32 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 Iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.
02 Iteration
------------
We added some Domain objects, and a basic booking service.
This has been implemented via the participation of all attendees to the EKON 21 Conference, just after the slides.
Nice first attempt, even it may be really mind-breaking from a classical DB-centric approach! ;)
Introduces a Repository dependency contract, which will be implemented in the test with a `TSynStub`, to let the test pass.

View File

@@ -0,0 +1,35 @@
/// Conference Domain dependencies interface definition
unit DomConferenceDepend;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
type
TBookingRepositoryError = (brSuccess, brWriteFailure);
IBookingRepository = interface(IInvokable)
['{8E121C97-7E53-4208-BE05-1660EAD8AB43}']
function SaveNewRegistration(const Attendee: TAttendee;
out RegistrationNumber: TAttendeeRegistrationNumber): TBookingRepositoryError;
end;
{
TAPIError = (apiSuccess, apiError);
const
DomainToAPI: array[TBookingRepositoryError] of TAPIError = (apiSuccess, apiError);
}
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
]);
end.

View File

@@ -0,0 +1,33 @@
/// Conference Domain services interfaces definition
unit DomConferenceInterfaces;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
{ Conference Domain Services }
type
TRegisterAttendee = (raSuccess, raMissingField, raInvalidField,
raAlreadyRegistered, raPersistenceError);
IConferenceBooking = interface(IInvokable)
['{0A128982-38E3-406E-B7B4-7FE212552BBF}']
function RegisterAttendee(var Attendee: TAttendee;
const Days: TSessionDays): TRegisterAttendee;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
]);
end.

View File

@@ -0,0 +1,57 @@
/// Conference Domain services implementation
unit DomConferenceServices;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceDepend;
type
TConferenceBooking = class(TInterfacedObject, IConferenceBooking)
protected
fRepository: IBookingRepository;
public
constructor Create(aRepository: IBookingRepository); reintroduce;
// IConferenceBooking methods below
function RegisterAttendee(var Attendee: TAttendee;
const Days: TSessionDays): TRegisterAttendee;
end;
implementation
{ TConferenceBooking }
constructor TConferenceBooking.Create(aRepository: IBookingRepository);
begin
inherited Create;
fRepository := aRepository;
end;
function TConferenceBooking.RegisterAttendee(var Attendee: TAttendee;
const Days: TSessionDays): TRegisterAttendee;
var
number: TAttendeeRegistrationNumber;
res: TBookingRepositoryError;
begin
Attendee.CleanupName;
if (Attendee.Name = '') or (Attendee.FirstName = '') then
exit(raMissingField);
//if length(Days) = 0 then
// exit(raMissingField);
res := fRepository.SaveNewRegistration(Attendee, number);
if res = brSuccess then begin
Attendee.RegistrationNumber := number;
result := raSuccess;
end
else
result := raPersistenceError;
end;
initialization
end.

View File

@@ -0,0 +1,74 @@
/// Conference Domain unit tests
unit DomConferenceTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend;
type
TTestConference = class(TSynTestCase)
protected
published
procedure DomainTypes;
procedure DomainBooking;
end;
implementation
{ TTestConference }
procedure TTestConference.DomainTypes;
var
a: TAttendee;
begin
a := TAttendee.Create;
try
a.FirstName := ' abc';
a.Name := 'def ';
a.CleanupName;
Check(a.FirstName = 'abc');
Check(a.Name = 'def');
finally
a.Free;
end;
end;
procedure TTestConference.DomainBooking;
var
book: IConferenceBooking;
a: TAttendee;
res: TRegisterAttendee;
repo: IBookingRepository;
begin
TInterfaceStub.Create(TypeInfo(IBookingRepository), repo);
book := TConferenceBooking.Create(repo);
a := TAttendee.Create;
try
res := book.RegisterAttendee(a, nil);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
a.FirstName := 'abc';
a.Name := ' def';
res := book.RegisterAttendee(a, nil);
check(res = raSuccess);
finally
a.Free;
end;
end;
initialization
end.

View File

@@ -0,0 +1,59 @@
/// entities, values, aggregates for the Conference domain
unit DomConferenceTypes;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot;
{ Conference Domain Objects }
type
TAttendeeName = type RawUTF8;
TAttendeeFirstName = type RawUTF8;
TAttendeeRegistrationNumber = type cardinal;
TSessionDate = type TDateTime;
TAttendee = class(TPersistent)
private
fName: TAttendeeName;
fFirstName: TAttendeeFirstName;
fRegistrationNumber: TAttendeeRegistrationNumber;
public
procedure CleanupName;
published
property RegistrationNumber: TAttendeeRegistrationNumber read fRegistrationNumber
write fRegistrationNumber;
property Name: TAttendeeName read fName write fName;
property FirstName: TAttendeeFirstName read fFirstName write fFirstName;
end;
TSessionDay = class(TPersistent)
private
fDay: TSessionDate;
published
property Day: TSessionDate read fDay write fDay;
end;
TSessionDays = array of TSessionDay;
implementation
{ TAttendee }
procedure TAttendee.CleanupName;
begin
fName := Trim(fName);
fFirstName := Trim(fFirstName);
end;
initialization
TJSONSerializer.RegisterObjArrayForJSON([
TypeInfo(TSessionDays), TSessionDay]);
end.

View File

@@ -0,0 +1,19 @@
/// Conference Repository implementation
unit InfraConferenceRepository;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceDepend;
implementation
initialization
// TDDDRepositoryRestFactory.ComputeSQLRecord(); from mORMotDDD
end.

View File

@@ -0,0 +1,70 @@
program ServBook;
{$APPTYPE CONSOLE}
uses
{$I SynDprUses.inc} // includes FastMM4
SysUtils,
SynLog,
mORMot,
SynSQLite3Static,
mORMotSQLite3,
mORMotService, // cross-platform service/daemon skeleton with settings
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas';
type
TBookSettings = class(TSynDaemonSettings)
private
fProcess: TBookProcessSettings;
public
constructor Create; override;
published
property Process: TBookProcessSettings read fProcess;
end;
TBookDaemon = class(TSynDaemon)
protected
fProcess: TBookProcess;
public
procedure Start; override;
procedure Stop; override;
end;
{ TBookDaemon }
procedure TBookDaemon.Start;
begin
if fProcess = nil then
fProcess := TBookProcess.Create((fSettings as TBookSettings).Process);
end;
procedure TBookDaemon.Stop;
begin
FreeAndNil(fProcess);
end;
{ TBookSettings }
constructor TBookSettings.Create;
begin
inherited;
fServiceDisplayName := 'My Long Name for Service';
end;
begin
with TBookDaemon.Create(TBookSettings, '', '', '') do
try
CommandLine(true);
finally
Free;
end;
end.

View File

@@ -0,0 +1,40 @@
/// Booking server implementation
unit ServBookMain;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository;
type
TBookProcessSettings = class(TSynAutoCreateFields)
published
end;
TBookProcess = class(TSynPersistent)
protected
fSettings: TBookProcessSettings;
public
constructor Create(aSettings: TBookProcessSettings); reintroduce;
property Settings: TBookProcessSettings read fSettings;
end;
implementation
{ TBookProcess }
constructor TBookProcess.Create(aSettings: TBookProcessSettings);
begin
inherited Create;
fSettings := aSettings;
end;
initialization
end.

View File

@@ -0,0 +1,46 @@
/// unit tests for the Booking server
unit ServBookTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository,
ServBookMain;
type
TTestBookingApplication = class(TSynTestCase)
published
procedure RunService;
procedure ApplicationTest;
procedure ShutdownService;
end;
implementation
{ TTestBookingApplication }
procedure TTestBookingApplication.RunService;
begin
end;
procedure TTestBookingApplication.ApplicationTest;
begin
end;
procedure TTestBookingApplication.ShutdownService;
begin
end;
initialization
end.

View File

@@ -0,0 +1,28 @@
program TestAll;
uses
{$I SynDprUses.inc} // cross-platform, cross-compiler (includes FastMM4)
SynCommons, // shared types for the whole framework
SynLog, // logging
SynTests, // unitary tests
mORMot, // ORM + SOA
SynSQLite3Static, // statically linked SQLite3 engine
mORMotSQLite3, // use SQlite3 as ORM core
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
DomConferenceTest in '..\dom\DomConferenceTest.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas',
ServBookTest in '..\serv\ServBookTest.pas',
// tests
TestAllMain in 'TestAllMain.pas';
begin
TSynLogTestLog := TSQLLog; // share the same log file with the whole mORMot
TTestEKON.RunAsConsole('EKON Automated Tests', LOG_VERBOSE);
end.

View File

@@ -0,0 +1,38 @@
unit TestAllMain;
interface
uses
SynCommons,
SynTests,
DomConferenceTest,
ServBookTest;
type
TTestEkon = class(TSynTestsLogged)
published
procedure Infrastructure;
procedure Domain;
procedure Applications;
end;
implementation
{ TTestEkon }
procedure TTestEkon.Infrastructure;
begin
end;
procedure TTestEkon.Domain;
begin
AddCase([TTestConference]);
end;
procedure TTestEkon.Applications;
begin
AddCase([TTestBookingApplication]);
end;
end.

View File

@@ -0,0 +1,42 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 Iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.
02 Iteration
------------
We added some Domain objects, and a basic booking service.
This has been implemented via the participation of all attendees to the EKON 21 Conference, just after the slides.
Nice first attempt, even it may be really mind-breaking from a classical DB-centric approach! ;)
Introduces a Repository dependency contract, which will be implemented in the test with a `TSynStub`, to let the test pass.
03 Iteration
------------
Minor refactoring:
* Cleaning the code to use mORMot registration for interface TypeInfo();
* Refactored IConferenceBooking for a more realistic use of parameters;
* Include session days to the regression tests.

View File

@@ -0,0 +1,31 @@
/// Conference Domain dependencies interface definition
unit DomConferenceDepend;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
type
TBookingRepositoryError = (
brSuccess, brDuplicatedInfo, brWriteFailure);
IBookingRepository = interface(IInvokable)
['{8E121C97-7E53-4208-BE05-1660EAD8AB43}']
function SaveNewRegistration(const Attendee: TAttendee;
out RegistrationNumber: TAttendeeRegistrationNumber): TBookingRepositoryError;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IBookingRepository)
]);
end.

View File

@@ -0,0 +1,35 @@
/// Conference Domain services interfaces definition
unit DomConferenceInterfaces;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
{ Conference Domain Services }
type
TRegisterAttendee = (raSuccess, raMissingField, raInvalidField,
raAlreadyRegistered, raPersistenceError);
IConferenceBooking = interface(IInvokable)
['{0A128982-38E3-406E-B7B4-7FE212552BBF}']
function RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IConferenceBooking)
]);
end.

View File

@@ -0,0 +1,65 @@
/// Conference Domain services implementation
unit DomConferenceServices;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceDepend;
type
TConferenceBooking = class(TInterfacedObject, IConferenceBooking)
protected
fRepository: IBookingRepository;
public
constructor Create(aRepository: IBookingRepository); reintroduce;
// IConferenceBooking methods below
function RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
end;
implementation
{ TConferenceBooking }
constructor TConferenceBooking.Create(aRepository: IBookingRepository);
begin
inherited Create;
fRepository := aRepository;
end;
function TConferenceBooking.RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
const
// this kind of structures won't compile any more if you add items to
// TBookingRepositoryError: so it is a safe way to enforce error coverage
RepoToDomain: array[TBookingRepositoryError] of TRegisterAttendee =
// brSuccess, brDuplicatedInfo, brWriteFailure
(raSuccess, raAlreadyRegistered, raPersistenceError);
var
number: TAttendeeRegistrationNumber;
res: TBookingRepositoryError;
begin
result := raMissingField;
if Days = nil then
exit;
Attendee.Name := Name;
Attendee.FirstName := FirstName;
Attendee.CleanupName;
if (Attendee.Name = '') or (Attendee.FirstName = '') then
exit;
res := fRepository.SaveNewRegistration(Attendee, number);
result := RepoToDomain[res];
if result = raSuccess then
Attendee.RegistrationNumber := number;
end;
initialization
end.

View File

@@ -0,0 +1,84 @@
/// Conference Domain unit tests
unit DomConferenceTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend;
type
TTestConference = class(TSynTestCase)
protected
published
procedure DomainTypes;
procedure DomainBooking;
end;
implementation
{ TTestConference }
procedure TTestConference.DomainTypes;
var
a: TAttendee;
begin
a := TAttendee.Create;
try
a.FirstName := ' abc';
a.Name := 'def ';
a.CleanupName;
Check(a.FirstName = 'abc');
Check(a.Name = 'def');
finally
a.Free;
end;
end;
procedure TTestConference.DomainBooking;
var
book: IConferenceBooking;
a: TAttendee;
days: TSessionDays;
res: TRegisterAttendee;
repo: IBookingRepository;
begin
TInterfaceStub.Create(IBookingRepository, repo);
book := TConferenceBooking.Create(repo);
a := TAttendee.Create;
try
res := book.RegisterAttendee('', '', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
res := book.RegisterAttendee('abc', ' def', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
days := TSessionDay.From([0, 1, 2]);
res := book.RegisterAttendee('abc', ' def', days, a);
check(res = raSuccess);
check(a.Name = 'abc');
check(a.FirstName = 'def');
finally
a.Free;
ObjArrayClear(days);
end;
end;
initialization
end.

View File

@@ -0,0 +1,82 @@
/// entities, values, aggregates for the Conference domain
unit DomConferenceTypes;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot;
{ Conference Domain Objects }
type
TAttendeeName = type RawUTF8;
TAttendeeFirstName = type RawUTF8;
TAttendeeRegistrationNumber = type cardinal;
TSessionDate = type TDateTime;
TAttendee = class(TPersistent)
private
fName: TAttendeeName;
fFirstName: TAttendeeFirstName;
fRegistrationNumber: TAttendeeRegistrationNumber;
public
procedure CleanupName;
published
property RegistrationNumber: TAttendeeRegistrationNumber
read fRegistrationNumber write fRegistrationNumber;
property Name: TAttendeeName read fName write fName;
property FirstName: TAttendeeFirstName read fFirstName write fFirstName;
end;
TSessionDay = class;
TSessionDays = array of TSessionDay;
TSessionDay = class(TPersistent)
private
fDay: TSessionDate;
public
constructor Create(aDay: TSessionDate); overload; virtual;
class function From(const Days: array of TSessionDate): TSessionDays;
published
property Day: TSessionDate read fDay write fDay;
end;
implementation
{ TAttendee }
procedure TAttendee.CleanupName;
begin
fName := Trim(fName);
fFirstName := Trim(fFirstName);
end;
{ TSessionDay }
constructor TSessionDay.Create(aDay: TSessionDate);
begin
inherited Create;
fDay := aDay;
end;
class function TSessionDay.From(
const Days: array of TSessionDate): TSessionDays;
var
i, n: integer;
begin
n := length(Days);
SetLength(result, n);
for i := 0 to n - 1 do
result[i] := TSessionDay.Create(Days[i]);
end;
initialization
TJSONSerializer.RegisterObjArrayForJSON([
TypeInfo(TSessionDays), TSessionDay]);
end.

View File

@@ -0,0 +1,19 @@
/// Conference Repository implementation
unit InfraConferenceRepository;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceDepend;
implementation
initialization
// TDDDRepositoryRestFactory.ComputeSQLRecord(); from mORMotDDD
end.

View File

@@ -0,0 +1,70 @@
program ServBook;
{$APPTYPE CONSOLE}
uses
{$I SynDprUses.inc} // includes FastMM4
SysUtils,
SynLog,
mORMot,
SynSQLite3Static,
mORMotSQLite3,
mORMotService, // cross-platform service/daemon skeleton with settings
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas';
type
TBookSettings = class(TSynDaemonSettings)
private
fProcess: TBookProcessSettings;
public
constructor Create; override;
published
property Process: TBookProcessSettings read fProcess;
end;
TBookDaemon = class(TSynDaemon)
protected
fProcess: TBookProcess;
public
procedure Start; override;
procedure Stop; override;
end;
{ TBookDaemon }
procedure TBookDaemon.Start;
begin
if fProcess = nil then
fProcess := TBookProcess.Create((fSettings as TBookSettings).Process);
end;
procedure TBookDaemon.Stop;
begin
FreeAndNil(fProcess);
end;
{ TBookSettings }
constructor TBookSettings.Create;
begin
inherited;
fServiceDisplayName := 'My Long Name for Service';
end;
begin
with TBookDaemon.Create(TBookSettings, '', '', '') do
try
CommandLine(true);
finally
Free;
end;
end.

View File

@@ -0,0 +1,40 @@
/// Booking server implementation
unit ServBookMain;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository;
type
TBookProcessSettings = class(TSynAutoCreateFields)
published
end;
TBookProcess = class(TSynPersistent)
protected
fSettings: TBookProcessSettings;
public
constructor Create(aSettings: TBookProcessSettings); reintroduce;
property Settings: TBookProcessSettings read fSettings;
end;
implementation
{ TBookProcess }
constructor TBookProcess.Create(aSettings: TBookProcessSettings);
begin
inherited Create;
fSettings := aSettings;
end;
initialization
end.

View File

@@ -0,0 +1,46 @@
/// unit tests for the Booking server
unit ServBookTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository,
ServBookMain;
type
TTestBookingApplication = class(TSynTestCase)
published
procedure RunService;
procedure ApplicationTest;
procedure ShutdownService;
end;
implementation
{ TTestBookingApplication }
procedure TTestBookingApplication.RunService;
begin
end;
procedure TTestBookingApplication.ApplicationTest;
begin
end;
procedure TTestBookingApplication.ShutdownService;
begin
end;
initialization
end.

View File

@@ -0,0 +1,28 @@
program TestAll;
uses
{$I SynDprUses.inc} // cross-platform, cross-compiler (includes FastMM4)
SynCommons, // shared types for the whole framework
SynLog, // logging
SynTests, // unitary tests
mORMot, // ORM + SOA
SynSQLite3Static, // statically linked SQLite3 engine
mORMotSQLite3, // use SQlite3 as ORM core
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
DomConferenceTest in '..\dom\DomConferenceTest.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas',
ServBookTest in '..\serv\ServBookTest.pas',
// tests
TestAllMain in 'TestAllMain.pas';
begin
TSynLogTestLog := TSQLLog; // share the same log file with the whole mORMot
TTestEKON.RunAsConsole('EKON Automated Tests', LOG_VERBOSE);
end.

View File

@@ -0,0 +1,38 @@
unit TestAllMain;
interface
uses
SynCommons,
SynTests,
DomConferenceTest,
ServBookTest;
type
TTestEkon = class(TSynTestsLogged)
published
procedure Infrastructure;
procedure Domain;
procedure Applications;
end;
implementation
{ TTestEkon }
procedure TTestEkon.Infrastructure;
begin
end;
procedure TTestEkon.Domain;
begin
AddCase([TTestConference]);
end;
procedure TTestEkon.Applications;
begin
AddCase([TTestBookingApplication]);
end;
end.

View File

@@ -0,0 +1,48 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 Iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.
02 Iteration
------------
We added some Domain objects, and a basic booking service.
This has been implemented via the participation of all attendees to the EKON 21 Conference, just after the slides.
Nice first attempt, even it may be really mind-breaking from a classical DB-centric approach! ;)
Introduces a Repository dependency contract, which will be implemented in the test with a `TSynStub`, to let the test pass.
03 Iteration
------------
Minor refactoring:
* Cleaning the code to use mORMot registration for interface TypeInfo();
* Refactored IConferenceBooking for a more realistic use of parameters;
* Include session days to the regression tests.
04 Iteration
------------
Adding ORM persistence, as used in ServBook process.
With booking service unit test, of course.

View File

@@ -0,0 +1,31 @@
/// Conference Domain dependencies interface definition
unit DomConferenceDepend;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
type
TBookingRepositoryError = (
brSuccess, brDuplicatedInfo, brWriteFailure);
IBookingRepository = interface(IInvokable)
['{8E121C97-7E53-4208-BE05-1660EAD8AB43}']
function SaveNewRegistration(const Attendee: TAttendee; const Days: TSessionDays;
out RegistrationNumber: TAttendeeRegistrationNumber): TBookingRepositoryError;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IBookingRepository)
]);
end.

View File

@@ -0,0 +1,35 @@
/// Conference Domain services interfaces definition
unit DomConferenceInterfaces;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
{ Conference Domain Services }
type
TRegisterAttendee = (raSuccess, raMissingField, raInvalidField,
raAlreadyRegistered, raPersistenceError);
IConferenceBooking = interface(IInvokable)
['{0A128982-38E3-406E-B7B4-7FE212552BBF}']
function RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IConferenceBooking)
]);
end.

View File

@@ -0,0 +1,65 @@
/// Conference Domain services implementation
unit DomConferenceServices;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceDepend;
type
TConferenceBooking = class(TInterfacedObject, IConferenceBooking)
protected
fRepository: IBookingRepository;
public
constructor Create(aRepository: IBookingRepository); reintroduce;
// IConferenceBooking methods below
function RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
end;
implementation
{ TConferenceBooking }
constructor TConferenceBooking.Create(aRepository: IBookingRepository);
begin
inherited Create;
fRepository := aRepository;
end;
function TConferenceBooking.RegisterAttendee(const Name: TAttendeeName;
FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
const
// this kind of structures won't compile any more if you add items to
// TBookingRepositoryError: so it is a safe way to enforce error coverage
RepoToDomain: array[TBookingRepositoryError] of TRegisterAttendee =
// brSuccess, brDuplicatedInfo, brWriteFailure
(raSuccess, raAlreadyRegistered, raPersistenceError);
var
number: TAttendeeRegistrationNumber;
res: TBookingRepositoryError;
begin
result := raMissingField;
if Days = nil then
exit;
Attendee.Name := Name;
Attendee.FirstName := FirstName;
Attendee.CleanupName;
if (Attendee.Name = '') or (Attendee.FirstName = '') then
exit;
res := fRepository.SaveNewRegistration(Attendee, Days, number);
result := RepoToDomain[res];
if result = raSuccess then
Attendee.RegistrationNumber := number;
end;
initialization
end.

View File

@@ -0,0 +1,84 @@
/// Conference Domain unit tests
unit DomConferenceTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend;
type
TTestConference = class(TSynTestCase)
protected
published
procedure DomainTypes;
procedure DomainBooking;
end;
implementation
{ TTestConference }
procedure TTestConference.DomainTypes;
var
a: TAttendee;
begin
a := TAttendee.Create;
try
a.FirstName := ' abc';
a.Name := 'def ';
a.CleanupName;
Check(a.FirstName = 'abc');
Check(a.Name = 'def');
finally
a.Free;
end;
end;
procedure TTestConference.DomainBooking;
var
book: IConferenceBooking;
a: TAttendee;
days: TSessionDays;
res: TRegisterAttendee;
repo: IBookingRepository;
begin
TInterfaceStub.Create(IBookingRepository, repo);
book := TConferenceBooking.Create(repo);
a := TAttendee.Create;
try
res := book.RegisterAttendee('', '', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
res := book.RegisterAttendee('abc', ' def', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
days := TSessionDay.From([0, 1, 2]);
res := book.RegisterAttendee('abc', ' def', days, a);
check(res = raSuccess);
check(a.Name = 'abc');
check(a.FirstName = 'def');
finally
a.Free;
ObjArrayClear(days);
end;
end;
initialization
end.

View File

@@ -0,0 +1,82 @@
/// entities, values, aggregates for the Conference domain
unit DomConferenceTypes;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot;
{ Conference Domain Objects }
type
TAttendeeName = type RawUTF8;
TAttendeeFirstName = type RawUTF8;
TAttendeeRegistrationNumber = type cardinal;
TSessionDate = type TDateTime;
TAttendee = class(TPersistent)
private
fName: TAttendeeName;
fFirstName: TAttendeeFirstName;
fRegistrationNumber: TAttendeeRegistrationNumber;
public
procedure CleanupName;
published
property RegistrationNumber: TAttendeeRegistrationNumber
read fRegistrationNumber write fRegistrationNumber;
property Name: TAttendeeName read fName write fName;
property FirstName: TAttendeeFirstName read fFirstName write fFirstName;
end;
TSessionDay = class;
TSessionDays = array of TSessionDay;
TSessionDay = class(TPersistent)
private
fDay: TSessionDate;
public
constructor Create(aDay: TSessionDate); overload; virtual;
class function From(const Days: array of TSessionDate): TSessionDays;
published
property Day: TSessionDate read fDay write fDay;
end;
implementation
{ TAttendee }
procedure TAttendee.CleanupName;
begin
fName := Trim(fName);
fFirstName := Trim(fFirstName);
end;
{ TSessionDay }
constructor TSessionDay.Create(aDay: TSessionDate);
begin
inherited Create;
fDay := aDay;
end;
class function TSessionDay.From(
const Days: array of TSessionDate): TSessionDays;
var
i, n: integer;
begin
n := length(Days);
SetLength(result, n);
for i := 0 to n - 1 do
result[i] := TSessionDay.Create(Days[i]);
end;
initialization
TJSONSerializer.RegisterObjArrayForJSON([
TypeInfo(TSessionDays), TSessionDay]);
end.

View File

@@ -0,0 +1,77 @@
/// Conference Repository implementation
unit InfraConferenceRepository;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceDepend;
type
/// defines the table in the DB
// - ID primary key is the TAttendeeRegistrationNumber
TSQLBooking = class(TSQLRecord)
private
fName: RawUTF8;
fFirstName: RawUTF8;
fSessions: variant;
published
property Name: RawUTF8 read fName write fName;
property FirstName: RawUTF8 read fFirstName write fFirstName;
property Sessions: variant read fSessions write fSessions;
end;
/// the repository implementation using mORMot REST ORM
TORMBookingRepository = class(TInterfacedObject, IBookingRepository)
protected
fRest: TSQLRest;
public
constructor Create(ORM: TSQLRest); reintroduce;
// IBookingRepository methods
function SaveNewRegistration(const Attendee: TAttendee; const Days: TSessionDays;
out RegNum: TAttendeeRegistrationNumber): TBookingRepositoryError;
end;
implementation
{ TORMBookingRepository }
constructor TORMBookingRepository.Create(ORM: TSQLRest);
begin
inherited Create;
ORM.Model.GetTableIndexExisting(TSQLBooking); // ensure part of the ORM model
fRest := ORM;
end;
function TORMBookingRepository.SaveNewRegistration(const Attendee: TAttendee;
const Days: TSessionDays; out RegNum: TAttendeeRegistrationNumber): TBookingRepositoryError;
var
rec: TSQLBooking;
begin
rec := TSQLBooking.Create(fRest, 'Name like ? and FirstName like ?',
[Attendee.Name, Attendee.FirstName]);
try
result := brDuplicatedInfo;
if rec.IDValue <> 0 then
exit;
rec.Name := Attendee.Name;
rec.FirstName := Attendee.FirstName;
if Days <> nil then
rec.Sessions := _Json(ObjArrayToJSON(Days), JSON_OPTIONS_FAST_EXTENDED);
RegNum := fRest.Add(rec, true);
result := brWriteFailure;
if RegNum = 0 then
exit;
result := brSuccess;
finally
rec.Free;
end;
end;
end.

View File

@@ -0,0 +1,70 @@
program ServBook;
{$APPTYPE CONSOLE}
uses
{$I SynDprUses.inc} // includes FastMM4
SysUtils,
SynLog,
mORMot,
SynSQLite3Static,
mORMotSQLite3,
mORMotService, // cross-platform service/daemon skeleton with settings
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas';
type
TBookSettings = class(TSynDaemonSettings)
private
fProcess: TBookProcessSettings;
public
constructor Create; override;
published
property Process: TBookProcessSettings read fProcess;
end;
TBookDaemon = class(TSynDaemon)
protected
fProcess: TBookProcess;
public
procedure Start; override;
procedure Stop; override;
end;
{ TBookDaemon }
procedure TBookDaemon.Start;
begin
if fProcess = nil then
fProcess := TBookProcess.Create((fSettings as TBookSettings).Process);
end;
procedure TBookDaemon.Stop;
begin
FreeAndNil(fProcess);
end;
{ TBookSettings }
constructor TBookSettings.Create;
begin
inherited;
fServiceDisplayName := 'My Long Name for Service';
end;
begin
with TBookDaemon.Create(TBookSettings, '', '', '') do
try
CommandLine(true);
finally
Free;
end;
end.

View File

@@ -0,0 +1,83 @@
/// Booking server implementation
unit ServBookMain;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
SynSQLite3,
mORMotSQLite3,
mORMotDB,
dddInfraApps,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend,
InfraConferenceRepository;
type
TBookProcessSettings = class(TSynAutoCreateFields)
private
fStore: TSynConnectionDefinition;
public
constructor Create; override;
published
property Store: TSynConnectionDefinition read fStore;
end;
TBookProcess = class(TSynPersistent)
protected
fSettings: TBookProcessSettings;
fRest: TSQLRest;
fBooking: IConferenceBooking;
public
constructor Create(aSettings: TBookProcessSettings); reintroduce;
destructor Destroy; override;
property Booking: IConferenceBooking read fBooking;
property Settings: TBookProcessSettings read fSettings;
end;
implementation
{ TBookProcessSettings }
constructor TBookProcessSettings.Create;
begin
inherited;
// use a local SQlite3 database file by default
fStore.Kind := 'TSQLRestServerDB'; // change Kind to switch to another engine
fStore.ServerName := ChangeFileExt(ExeVersion.ProgramFileName, '.db');
end;
{ TBookProcess }
constructor TBookProcess.Create(aSettings: TBookProcessSettings);
begin
inherited Create;
fSettings := aSettings;
fRest := TSQLRestExternalDBCreate(
TSQLModel.Create([TSQLBooking], 'book'), fSettings.Store, false, []);
fRest.Model.Owner := fRest;
if fRest is TSQLRestServerDB then
with TSQLRestServerDB(fRest) do begin // may be a client in settings :)
DB.Synchronous := smOff; // faster exclusive access to the file
DB.LockingMode := lmExclusive;
CreateMissingTables; // will create the Booking table, if necessary
end;
fBooking := TConferenceBooking.Create(TORMBookingRepository.Create(fRest));
end;
destructor TBookProcess.Destroy;
begin
inherited;
fBooking := nil; // before fRest
fRest.Free;
end;
initialization
end.

View File

@@ -0,0 +1,67 @@
/// unit tests for the Booking server
unit ServBookTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository,
ServBookMain;
type
TTestBookingApplication = class(TSynTestCase)
protected
fSettings: TBookProcessSettings;
fProcess: TBookProcess;
published
procedure RunService;
procedure ApplicationTest;
procedure ShutdownService;
end;
implementation
{ TTestBookingApplication }
procedure TTestBookingApplication.RunService;
begin
fSettings := TBookProcessSettings.Create;
fProcess := TBookProcess.Create(fSettings);
end;
procedure TTestBookingApplication.ApplicationTest;
var
a: TAttendee;
days: TSessionDays;
res: TRegisterAttendee;
begin
a := TAttendee.Create;
try
days := TSessionDay.From([0, 1, 2]);
res := fProcess.Booking.RegisterAttendee('abc', ' def', days, a);
if res = raAlreadyRegistered then // would works only first time
res := fProcess.Booking.RegisterAttendee(CardinalToHex(UnixTimeUTC),
RandomIdentifier(10), days, a);
check(res = raSuccess);
finally
a.Free;
ObjArrayClear(days);
end;
end;
procedure TTestBookingApplication.ShutdownService;
begin
FreeAndNil(fProcess);
FreeAndNil(fSettings);
end;
initialization
end.

View File

@@ -0,0 +1,28 @@
program TestAll;
uses
{$I SynDprUses.inc} // cross-platform, cross-compiler (includes FastMM4)
SynCommons, // shared types for the whole framework
SynLog, // logging
SynTests, // unitary tests
mORMot, // ORM + SOA
SynSQLite3Static, // statically linked SQLite3 engine
mORMotSQLite3, // use SQlite3 as ORM core
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
DomConferenceTest in '..\dom\DomConferenceTest.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas',
ServBookTest in '..\serv\ServBookTest.pas',
// tests
TestAllMain in 'TestAllMain.pas';
begin
TSynLogTestLog := TSQLLog; // share the same log file with the whole mORMot
TTestEKON.RunAsConsole('EKON Automated Tests', LOG_VERBOSE);
end.

View File

@@ -0,0 +1,38 @@
unit TestAllMain;
interface
uses
SynCommons,
SynTests,
DomConferenceTest,
ServBookTest;
type
TTestEkon = class(TSynTestsLogged)
published
procedure Infrastructure;
procedure Domain;
procedure Applications;
end;
implementation
{ TTestEkon }
procedure TTestEkon.Infrastructure;
begin
end;
procedure TTestEkon.Domain;
begin
AddCase([TTestConference]);
end;
procedure TTestEkon.Applications;
begin
AddCase([TTestBookingApplication]);
end;
end.

View File

@@ -0,0 +1,54 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 Iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.
02 Iteration
------------
We added some Domain objects, and a basic booking service.
This has been implemented via the participation of all attendees to the EKON 21 Conference, just after the slides.
Nice first attempt, even it may be really mind-breaking from a classical DB-centric approach! ;)
Introduces a Repository dependency contract, which will be implemented in the test with a `TSynStub`, to let the test pass.
03 Iteration
------------
Minor refactoring:
* Cleaning the code to use mORMot registration for interface TypeInfo();
* Refactored IConferenceBooking for a more realistic use of parameters;
* Include session days to the regression tests.
04 Iteration
------------
Adding ORM persistence, as used in ServBook process.
With booking service unit test, of course.
05 Iteration
------------
Enhanced booking service to search for a registration, via the repository service.

View File

@@ -0,0 +1,34 @@
/// Conference Domain dependencies interface definition
unit DomConferenceDepend;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
type
TBookingRepositoryError = (
brSuccess, brDuplicatedInfo, brWriteFailure);
IBookingRepository = interface(IInvokable)
['{8E121C97-7E53-4208-BE05-1660EAD8AB43}']
function SaveNewRegistration(const Attendee: TAttendee; const Days: TSessionDays;
out RegistrationNumber: TAttendeeRegistrationNumber): TBookingRepositoryError;
function RetrieveRegistration(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; out Days: TSessionDays;
out Attendee: TAttendee): boolean;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IBookingRepository)
]);
end.

View File

@@ -0,0 +1,39 @@
/// Conference Domain services interfaces definition
unit DomConferenceInterfaces;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes;
{ Conference Domain Services }
type
TRegisterAttendee = (raSuccess, raMissingField, raInvalidField,
raAlreadyRegistered, raPersistenceError);
TSearchRegistration = (srNotFound, srFound);
IConferenceBooking = interface(IInvokable)
['{0A128982-38E3-406E-B7B4-7FE212552BBF}']
function RegisterAttendee(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
function SearchRegistration(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; out Days: TSessionDays;
out Attendee: TAttendee): TSearchRegistration;
end;
implementation
initialization
TJSONSerializer.RegisterObjArrayForJSON([
]);
TInterfaceFactory.RegisterInterfaces([
TypeInfo(IConferenceBooking)
]);
end.

View File

@@ -0,0 +1,78 @@
/// Conference Domain services implementation
unit DomConferenceServices;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceDepend;
type
TConferenceBooking = class(TInterfacedObject, IConferenceBooking)
protected
fRepository: IBookingRepository;
public
constructor Create(aRepository: IBookingRepository); reintroduce;
// IConferenceBooking methods below
function RegisterAttendee(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
function SearchRegistration(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; out Days: TSessionDays;
out Attendee: TAttendee): TSearchRegistration;
end;
implementation
{ TConferenceBooking }
constructor TConferenceBooking.Create(aRepository: IBookingRepository);
begin
inherited Create;
fRepository := aRepository;
end;
function TConferenceBooking.RegisterAttendee(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; const Days: TSessionDays;
out Attendee: TAttendee): TRegisterAttendee;
const
// this kind of structures won't compile any more if you add items to
// TBookingRepositoryError: so it is a safe way to enforce error coverage
RepoToDomain: array[TBookingRepositoryError] of TRegisterAttendee =
// brSuccess, brDuplicatedInfo, brWriteFailure
(raSuccess, raAlreadyRegistered, raPersistenceError);
var
number: TAttendeeRegistrationNumber;
res: TBookingRepositoryError;
begin
result := raMissingField;
if Days = nil then
exit;
Attendee.Name := Name;
Attendee.FirstName := FirstName;
Attendee.CleanupName;
if (Attendee.Name = '') or (Attendee.FirstName = '') then
exit;
res := fRepository.SaveNewRegistration(Attendee, Days, number);
result := RepoToDomain[res];
if result = raSuccess then
Attendee.RegistrationNumber := number;
end;
function TConferenceBooking.SearchRegistration(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; out Days: TSessionDays;
out Attendee: TAttendee): TSearchRegistration;
begin
if fRepository.RetrieveRegistration(Trim(Name), Trim(FirstName), Days, Attendee) then
result := srFound
else
result := srNotFound;
end;
initialization
end.

View File

@@ -0,0 +1,91 @@
/// Conference Domain unit tests
unit DomConferenceTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend;
type
TTestConference = class(TSynTestCase)
protected
published
procedure DomainTypes;
procedure DomainBooking;
end;
implementation
{ TTestConference }
procedure TTestConference.DomainTypes;
var
a: TAttendee;
begin
a := TAttendee.Create;
try
a.FirstName := ' abc';
a.Name := 'def ';
a.CleanupName;
Check(a.FirstName = 'abc');
Check(a.Name = 'def');
finally
a.Free;
end;
end;
procedure TTestConference.DomainBooking;
var
book: IConferenceBooking;
a: TAttendee;
days: TSessionDays;
res: TRegisterAttendee;
repo: IBookingRepository;
begin
TInterfaceStub.Create(IBookingRepository, repo);
book := TConferenceBooking.Create(repo);
a := TAttendee.Create;
try
res := book.RegisterAttendee('', '', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
res := book.RegisterAttendee('abc', ' def', nil, a);
check(res = raMissingField);
finally
a.Free;
end;
a := TAttendee.Create;
try
days := TSessionDay.From([0, 1, 2]);
res := book.RegisterAttendee('abc', ' def', days, a);
check(res = raSuccess);
check(a.Name = 'abc');
check(a.FirstName = 'def');
finally
a.Free;
ObjArrayClear(days);
end;
a := TAttendee.Create;
try // IBookingRepository.RetrieveRegistration stub will return default false
check(book.SearchRegistration('abc', 'def', days, a) = srNotFound);
finally
a.Free;
ObjArrayClear(days);
end;
end;
initialization
end.

View File

@@ -0,0 +1,81 @@
/// entities, values, aggregates for the Conference domain
unit DomConferenceTypes;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot;
{ Conference Domain Objects }
type
TAttendeeName = type RawUTF8;
TAttendeeFirstName = type RawUTF8;
TAttendeeRegistrationNumber = type cardinal;
TSessionDate = type TDateTime;
TAttendee = class(TPersistent)
private
fName: TAttendeeName;
fFirstName: TAttendeeFirstName;
fRegistrationNumber: TAttendeeRegistrationNumber;
public
procedure CleanupName;
published
property RegistrationNumber: TAttendeeRegistrationNumber
read fRegistrationNumber write fRegistrationNumber;
property Name: TAttendeeName read fName write fName;
property FirstName: TAttendeeFirstName read fFirstName write fFirstName;
end;
TSessionDay = class;
TSessionDays = array of TSessionDay;
TSessionDay = class(TPersistent)
private
fDay: TSessionDate;
public
constructor Create(aDay: TSessionDate); overload; virtual;
class function From(const Days: array of TSessionDate): TSessionDays;
published
property Day: TSessionDate read fDay write fDay;
end;
implementation
{ TAttendee }
procedure TAttendee.CleanupName;
begin
fName := Trim(fName);
fFirstName := Trim(fFirstName);
end;
{ TSessionDay }
constructor TSessionDay.Create(aDay: TSessionDate);
begin
inherited Create;
fDay := aDay;
end;
class function TSessionDay.From(
const Days: array of TSessionDate): TSessionDays;
var
i, n: integer;
begin
n := length(Days);
SetLength(result, n);
for i := 0 to n - 1 do
result[i] := TSessionDay.Create(Days[i]);
end;
initialization
TJSONSerializer.RegisterObjArrayForJSON([
TypeInfo(TSessionDays), TSessionDay]);
end.

View File

@@ -0,0 +1,111 @@
/// Conference Repository implementation
unit InfraConferenceRepository;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
DomConferenceTypes,
DomConferenceDepend;
type
/// defines the table in the DB
// - ID primary key is the TAttendeeRegistrationNumber
TSQLBooking = class(TSQLRecord)
private
fName: RawUTF8;
fFirstName: RawUTF8;
fSessions: variant;
published
property Name: RawUTF8 read fName write fName;
property FirstName: RawUTF8 read fFirstName write fFirstName;
property Sessions: variant read fSessions write fSessions;
end;
/// the repository implementation using mORMot REST ORM
TORMBookingRepository = class(TInterfacedObject, IBookingRepository)
protected
fRest: TSQLRest;
function GetBooking(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName): TSQLBooking;
public
constructor Create(ORM: TSQLRest); reintroduce;
// IBookingRepository methods
function SaveNewRegistration(const Attendee: TAttendee; const Days: TSessionDays;
out RegNum: TAttendeeRegistrationNumber): TBookingRepositoryError;
function RetrieveRegistration(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName; out Days: TSessionDays;
out Attendee: TAttendee): boolean;
end;
implementation
uses Variants;
{ TORMBookingRepository }
constructor TORMBookingRepository.Create(ORM: TSQLRest);
begin
inherited Create;
ORM.Model.GetTableIndexExisting(TSQLBooking); // ensure part of the ORM model
fRest := ORM;
end;
function TORMBookingRepository.GetBooking(const Name: TAttendeeName;
const FirstName: TAttendeeFirstName): TSQLBooking;
begin
result := TSQLBooking.Create(fRest, 'Name like ? and FirstName like ?',
[Name, FirstName]);
end;
function TORMBookingRepository.RetrieveRegistration(
const Name: TAttendeeName; const FirstName: TAttendeeFirstName;
out Days: TSessionDays; out Attendee: TAttendee): boolean;
var
rec: TSQLBooking;
begin
result := false;
rec := GetBooking(Name, FirstName);
try
if rec.IDValue = 0 then
exit;
Attendee.Name := rec.Name;
Attendee.FirstName := rec.FirstName;
Attendee.RegistrationNumber := rec.IDValue;
DynArray(TypeInfo(TSessionDays), Days).LoadFromVariant(rec.Sessions);
result := true;
finally
rec.Free;
end;
end;
function TORMBookingRepository.SaveNewRegistration(const Attendee: TAttendee;
const Days: TSessionDays; out RegNum: TAttendeeRegistrationNumber): TBookingRepositoryError;
var
rec: TSQLBooking;
begin
rec := GetBooking(Attendee.Name, Attendee.FirstName);
try
result := brDuplicatedInfo;
if rec.IDValue <> 0 then
exit;
rec.Name := Attendee.Name;
rec.FirstName := Attendee.FirstName;
if Days <> nil then
rec.Sessions := _Json(ObjArrayToJSON(Days), JSON_OPTIONS_FAST_EXTENDED);
RegNum := fRest.Add(rec, true);
result := brWriteFailure;
if RegNum = 0 then
exit;
result := brSuccess;
finally
rec.Free;
end;
end;
end.

View File

@@ -0,0 +1,70 @@
program ServBook;
{$APPTYPE CONSOLE}
uses
{$I SynDprUses.inc} // includes FastMM4
SysUtils,
SynLog,
mORMot,
SynSQLite3Static,
mORMotSQLite3,
mORMotService, // cross-platform service/daemon skeleton with settings
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas';
type
TBookSettings = class(TSynDaemonSettings)
private
fProcess: TBookProcessSettings;
public
constructor Create; override;
published
property Process: TBookProcessSettings read fProcess;
end;
TBookDaemon = class(TSynDaemon)
protected
fProcess: TBookProcess;
public
procedure Start; override;
procedure Stop; override;
end;
{ TBookDaemon }
procedure TBookDaemon.Start;
begin
if fProcess = nil then
fProcess := TBookProcess.Create((fSettings as TBookSettings).Process);
end;
procedure TBookDaemon.Stop;
begin
FreeAndNil(fProcess);
end;
{ TBookSettings }
constructor TBookSettings.Create;
begin
inherited;
fServiceDisplayName := 'My Long Name for Service';
end;
begin
with TBookDaemon.Create(TBookSettings, '', '', '') do
try
CommandLine(true);
finally
Free;
end;
end.

View File

@@ -0,0 +1,127 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{D080C2AC-E8DE-4854-B903-873FB4745218}</ProjectGuid>
<MainSource>ServBook.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Console</AppType>
<FrameworkType>None</FrameworkType>
<ProjectVersion>16.0</ProjectVersion>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
<Base_Win64>true</Base_Win64>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win32)'!=''">
<Cfg_2_Win32>true</Cfg_2_Win32>
<CfgParent>Cfg_2</CfgParent>
<Cfg_2>true</Cfg_2>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<Manifest_File>None</Manifest_File>
<DCC_ImageBase>00400000</DCC_ImageBase>
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_K>false</DCC_K>
<DCC_S>false</DCC_S>
<DCC_N>false</DCC_N>
<SanitizedProjectName>ServBook</SanitizedProjectName>
<DCC_DcuOutput>dcu</DCC_DcuOutput>
<DCC_ExeOutput>exe</DCC_ExeOutput>
<DCC_E>false</DCC_E>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName=</VerInfo_Keys>
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace)</DCC_Namespace>
<DCC_F>false</DCC_F>
<Icon_MainIcon>ServBook_Icon.ico</Icon_MainIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
<Icon_MainIcon>ServBook_Icon.ico</Icon_MainIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<Icon_MainIcon>ServBook_Icon.ico</Icon_MainIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_DebugInformation>0</DCC_DebugInformation>
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
<Manifest_File>None</Manifest_File>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="..\dom\DomConferenceTypes.pas"/>
<DCCReference Include="..\dom\DomConferenceInterfaces.pas"/>
<DCCReference Include="..\dom\DomConferenceDepend.pas"/>
<DCCReference Include="..\dom\DomConferenceServices.pas"/>
<DCCReference Include="..\infra\InfraConferenceRepository.pas"/>
<DCCReference Include="ServBookMain.pas"/>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">ServBook.dpr</Source>
</Source>
<Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k210.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp210.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k210.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dclofficexp210.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
<Platforms>
<Platform value="OSX32">False</Platform>
<Platform value="Win32">True</Platform>
<Platform value="Win64">False</Platform>
</Platforms>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
</Project>

View File

@@ -0,0 +1,84 @@
/// Booking server implementation
unit ServBookMain;
interface
uses
SysUtils,
Classes,
SynCommons,
mORMot,
SynSQLite3,
SynTable,
mORMotSQLite3,
mORMotDB,
dddInfraApps,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
DomConferenceDepend,
InfraConferenceRepository;
type
TBookProcessSettings = class(TSynAutoCreateFields)
private
fStore: TSynConnectionDefinition;
public
constructor Create; override;
published
property Store: TSynConnectionDefinition read fStore;
end;
TBookProcess = class(TSynPersistent)
protected
fSettings: TBookProcessSettings;
fRest: TSQLRest;
fBooking: IConferenceBooking;
public
constructor Create(aSettings: TBookProcessSettings); reintroduce;
destructor Destroy; override;
property Booking: IConferenceBooking read fBooking;
property Settings: TBookProcessSettings read fSettings;
end;
implementation
{ TBookProcessSettings }
constructor TBookProcessSettings.Create;
begin
inherited;
// use a local SQlite3 database file by default
fStore.Kind := 'TSQLRestServerDB'; // change Kind to switch to another engine
fStore.ServerName := ChangeFileExt(ExeVersion.ProgramFileName, '.db');
end;
{ TBookProcess }
constructor TBookProcess.Create(aSettings: TBookProcessSettings);
begin
inherited Create;
fSettings := aSettings;
fRest := TSQLRestExternalDBCreate(
TSQLModel.Create([TSQLBooking], 'book'), fSettings.Store, false, []);
fRest.Model.Owner := fRest;
if fRest is TSQLRestServerDB then
with TSQLRestServerDB(fRest) do begin // may be a client in settings :)
DB.Synchronous := smOff; // faster exclusive access to the file
DB.LockingMode := lmExclusive;
CreateMissingTables; // will create the Booking table, if necessary
end;
fBooking := TConferenceBooking.Create(TORMBookingRepository.Create(fRest));
end;
destructor TBookProcess.Destroy;
begin
inherited;
fBooking := nil; // before fRest
fRest.Free;
end;
initialization
end.

View File

@@ -0,0 +1,76 @@
/// unit tests for the Booking server
unit ServBookTest;
interface
uses
SysUtils,
Classes,
SynCommons,
SynTests,
mORMot,
DomConferenceTypes,
DomConferenceInterfaces,
DomConferenceServices,
InfraConferenceRepository,
ServBookMain;
type
TTestBookingApplication = class(TSynTestCase)
protected
fSettings: TBookProcessSettings;
fProcess: TBookProcess;
published
procedure RunService;
procedure ApplicationTest;
procedure ShutdownService;
end;
implementation
{ TTestBookingApplication }
procedure TTestBookingApplication.RunService;
begin
fSettings := TBookProcessSettings.Create;
fProcess := TBookProcess.Create(fSettings);
end;
procedure TTestBookingApplication.ApplicationTest;
var
a, b: TAttendee;
days1, days2: TSessionDays;
res: TRegisterAttendee;
begin
a := TAttendee.Create;
b := TAttendee.Create;
try
days1 := TSessionDay.From([0, 1, 2]);
res := fProcess.Booking.RegisterAttendee('abc', ' def', days1, a);
if res = raAlreadyRegistered then // works only first time (blank DB)
res := fProcess.Booking.RegisterAttendee(CardinalToHex(UnixTimeUTC),
RandomIdentifier(10), days1, a);
check(res = raSuccess);
check(a.Name <> '');
check(length(a.FirstName) = 10);
check(fProcess.Booking.SearchRegistration(a.Name, a.FirstName, days2, b) = srFound);
check(a.Name = b.Name);
check(a.RegistrationNumber = b.RegistrationNumber);
check(DynArrayEquals(TypeInfo(TSessionDays), days1, days2));
finally
a.Free;
b.Free;
ObjArrayClear(days1);
ObjArrayClear(days2);
end;
end;
procedure TTestBookingApplication.ShutdownService;
begin
FreeAndNil(fProcess);
FreeAndNil(fSettings);
end;
initialization
end.

View File

@@ -0,0 +1,28 @@
program TestAll;
uses
{$I SynDprUses.inc} // cross-platform, cross-compiler (includes FastMM4)
SynCommons, // shared types for the whole framework
SynLog, // logging
SynTests, // unitary tests
mORMot, // ORM + SOA
SynSQLite3Static, // statically linked SQLite3 engine
mORMotSQLite3, // use SQlite3 as ORM core
// domain
DomConferenceTypes in '..\dom\DomConferenceTypes.pas',
DomConferenceInterfaces in '..\dom\DomConferenceInterfaces.pas',
DomConferenceDepend in '..\dom\DomConferenceDepend.pas',
DomConferenceServices in '..\dom\DomConferenceServices.pas',
DomConferenceTest in '..\dom\DomConferenceTest.pas',
// infrastructure
InfraConferenceRepository in '..\infra\InfraConferenceRepository.pas',
// servers
ServBookMain in '..\serv\ServBookMain.pas',
ServBookTest in '..\serv\ServBookTest.pas',
// tests
TestAllMain in 'TestAllMain.pas';
begin
TSynLogTestLog := TSQLLog; // share the same log file with the whole mORMot
TTestEKON.RunAsConsole('EKON Automated Tests', LOG_VERBOSE);
end.

View File

@@ -0,0 +1,130 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{04B59FF0-C44F-4B89-BF30-89F8FA09C105}</ProjectGuid>
<MainSource>TestAll.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Console</AppType>
<FrameworkType>None</FrameworkType>
<ProjectVersion>16.0</ProjectVersion>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
<Base_Win64>true</Base_Win64>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win32)'!=''">
<Cfg_2_Win32>true</Cfg_2_Win32>
<CfgParent>Cfg_2</CfgParent>
<Cfg_2>true</Cfg_2>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_F>false</DCC_F>
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_ExeOutput>exe</DCC_ExeOutput>
<DCC_S>false</DCC_S>
<DCC_E>false</DCC_E>
<DCC_K>false</DCC_K>
<DCC_N>false</DCC_N>
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace)</DCC_Namespace>
<Icon_MainIcon>TestAll_Icon.ico</Icon_MainIcon>
<SanitizedProjectName>TestAll</SanitizedProjectName>
<DCC_ImageBase>00400000</DCC_ImageBase>
<DCC_DcuOutput>dcu</DCC_DcuOutput>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName=</VerInfo_Keys>
<Manifest_File>None</Manifest_File>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
<VerInfo_Locale>1033</VerInfo_Locale>
<Icon_MainIcon>TestAll_Icon.ico</Icon_MainIcon>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<Icon_MainIcon>TestAll_Icon.ico</Icon_MainIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_DebugInformation>0</DCC_DebugInformation>
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
<Manifest_File>None</Manifest_File>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="..\dom\DomConferenceTypes.pas"/>
<DCCReference Include="..\dom\DomConferenceInterfaces.pas"/>
<DCCReference Include="..\dom\DomConferenceDepend.pas"/>
<DCCReference Include="..\dom\DomConferenceServices.pas"/>
<DCCReference Include="..\dom\DomConferenceTest.pas"/>
<DCCReference Include="..\infra\InfraConferenceRepository.pas"/>
<DCCReference Include="..\serv\ServBookMain.pas"/>
<DCCReference Include="..\serv\ServBookTest.pas"/>
<DCCReference Include="TestAllMain.pas"/>
<BuildConfiguration Include="Debug">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Release">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">TestAll.dpr</Source>
</Source>
<Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k210.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp210.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k210.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dclofficexp210.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality>
<Platforms>
<Platform value="OSX32">False</Platform>
<Platform value="Win32">True</Platform>
<Platform value="Win64">False</Platform>
</Platforms>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
</Project>

View File

@@ -0,0 +1,38 @@
unit TestAllMain;
interface
uses
SynCommons,
SynTests,
DomConferenceTest,
ServBookTest;
type
TTestEkon = class(TSynTestsLogged)
published
procedure Infrastructure;
procedure Domain;
procedure Applications;
end;
implementation
{ TTestEkon }
procedure TTestEkon.Infrastructure;
begin
end;
procedure TTestEkon.Domain;
begin
AddCase([TTestConference]);
end;
procedure TTestEkon.Applications;
begin
AddCase([TTestBookingApplication]);
end;
end.

View File

@@ -0,0 +1,54 @@
Practical Domain Driven Design
================================
Welcome to the Progressive samples folders!
In the sub-folders, we will write a almost-complete DDD sample, using TDD.
Folders are enumerated to follow the iterations on the project: 01, 02, 03...
Since it was started during EKON 21 conferences, it will modelize a conference booking system.
Don't forget to check out the associated Slides from https://synopse.info/files/ekon21
01 Iteration
------------
Contains the core units, following the "Clean Architecture" patterns.
Implements
* TestAll.dpr to run the regression tests;
* ServBook.dpr to run a Booking service, using the Conference Domain objects.
02 Iteration
------------
We added some Domain objects, and a basic booking service.
This has been implemented via the participation of all attendees to the EKON 21 Conference, just after the slides.
Nice first attempt, even it may be really mind-breaking from a classical DB-centric approach! ;)
Introduces a Repository dependency contract, which will be implemented in the test with a `TSynStub`, to let the test pass.
03 Iteration
------------
Minor refactoring:
* Cleaning the code to use mORMot registration for interface TypeInfo();
* Refactored IConferenceBooking for a more realistic use of parameters;
* Include session days to the regression tests.
04 Iteration
------------
Adding ORM persistence, as used in ServBook process.
With booking service unit test, of course.
05 Iteration
------------
Enhanced booking service to search for a registration, via the repository service.