Frameworks: Deel 2, De Basisarchitectuur
Erik Stok (aka Baldo)
Als vervolg op de theorie die is besproken in het eerste
deel uit deze reeks artikelen volgt nu het begin van de implementatie
van een framework. In dit artikel is te vinden hoe de theorie in
de praktijk kan worden gebracht en welke overwegingen daarbij komen
kijken.
Afbakening
De eerste stap bij het bouwen van het framework is het bepalen
van het doel en de grenzen van het framework. Hierbij komt al wat
creativiteit kijken, want het is de kunst jezelf doelen en grenzen
te stellen waardoor je antwoord op vragen kunt geven over wat wel
en wat niet in je framework moet worden opgenomen zonder dat je
jezelf teveel beperkt.
Ik probeer als voorbeeld een framework te maken dat aansluit op
applicaties die de meeste ontwikkelaars die NLdelphi bezoekers maken
(naar mijn gevoel). Het framework zal een end-user client-server
database applicationframework worden. Het is dus een framework waarop
applicaties voor eindgebruikers gebouwd kunnen worden. Deze applicaties
zullen stand alone of client-server applicaties zijn die op de desktop
draaien, het type applicaties dus wat door NLDelphi bezoekers volgens
mij het meest gemaakt wordt.
Even twee kleine punten van aandacht. De code die ik zal gaan bespreken
is allemaal Delphi 6 code (de versie waar ik op dit moment mee werk),
maar zal grotendeels ook toepasbaar zijn voor eerdere en latere
Delphi versies. Tevens wil ik benadrukken dat dit slechts een
implementatie van een framework is: er zijn duizenden implementaties
op duizenden manieren mogelijk. Het gaat hier om een voorbeeld van
een implementatie dat zoveel mogelijk mensen kunnen herkennen.
De globale architectuur
Het volgende wat ik ga doen is bepalen hoe de architectuur van
het framework in elkaar steekt. Dit wordt grotendeels bepaald door
de applicaties die ik op het framework wil gaan bouwen. Dit is denk
ik het moeilijkste onderdeel van het maken van een framework en
veel keuzes zijn hierbij gebaseerd op ervaring. Het is aan te raden
om bij dit onderdeel te overleggen met andere ontwikkelaars om te
bepalen of de architectuur toestaat de uiteindelijke gewenste applicaties
mee te bouwen.
De architectuur die ik hier kies is zeker niet de enige mogelijke
architectuur van een Delphi framework. Het is mogelijk om allerlei
verschillende architecturen te kiezen, net welke voor de afbakening
het meeste geschikt is. In dit artikel probeer ik aan te sluiten
op een gemiddelde kennis van Delphi, dus zal de architectuur geen
technieken vereisen die alleen voor Delphi experts is weggelegd.
Ik kies voor de opzet om een applicatie onder te verdelen in modules
van functionaliteit, omdat software modulair bouwen (en verkopen)
de strategie is die momenteel nog veel wordt toegepast. De modules
maken het mogelijk de applicaties die gebouwd gaan worden in delen
op te leveren (per module). Dat heeft als voordeel dat er gefaseerd
opgeleverd kan worden, dat maatwerk als module gezien kan worden
en dat groepen ontwikkelaars op een module aan het werk gezet kunnen
worden. Voorbeelden van modules zijn: een klantenmodule, een ordermodule,
een magazijnmodule en een maatwerkmodule voor klant "Jansen
BV".
Een module is op zich weer een verzameling functies. Een functie
is het kleinste onderdeel van de applicatie waar zelfstandig een
handeling mee kan worden verricht. Als de magazijnmodule als voorbeeld
genomen wordt, dan zijn voorbeelden van functies: onderhoud op artikelen,
herwaardering en rapport magazijnaanvullingen.
Er kan toegang tot de functies worden verkregen via een user interface.
Dit user interface noem ik de shell. Elke module moet informatie
kunnen geven welke functies zich in de module bevinden. De shell
is verantwoordelijk voor het bieden van toegang tot deze functies
via een menu en/of toolbar.
Schematisch ziet een applicatie er dus als volgt uit:

Deze architectuur is redelijk simpel en volgens mij kan ik gerust
zeggen dat dit idee door iedere willekeurige ontwikkelaar bedacht
kan worden.
Management classes
Om deze architectuur te kunnen reguleren met een framework zullen
naast classes die de basis vormen voor de genoemde onderdelen ook
een aantal management classes noodzakelijk zijn. Deze classes zorgen
voor het life-cycle management en de communicatie tussen verschillende
classes.
Voor deze architectuur heb ik twee management classes bedacht:
De module manager is de manager van de verschillende modules.
De modules melden zich aan bij de module manager zodat de module
manager informatie heeft over de aanwezige modules en hun inhoud.
De shell kan aan de module manager vragen welke modules er zijn
en hij kan toegang verkrijgen tot individuele modules om op te vragen
welke functies er in deze modules zitten. De shell kan dan toegang
bieden tot deze functies. De module manager is verantwoordelijk
voor de life-cycle van modules.
Zodra vanuit de shell uiteindelijk een functie wordt gestart dan
zal vanuit de module waarin de functie zich bevindt een beroep gedaan
worden op de function manager. Deze manager zorgt ervoor
dan een functie opgestart wordt. Deze manager stelt de programmeur
tevens in staat te kiezen hoe een functie wordt opgestart (slechts
een keer, meerdere keren, modaal, als child, en meer van dat soort
zaken). De function manager is verantwoordelijk voor de life-cycle
van functies.
Het skeleton of non-visual classes
Het skelet van niet-visuele classes van het framework komt er dan
als volgt uit te zien:
Ik heb in dit framework gekozen om het skelet van classes grotendeels
op basis van inheritance op te zetten. Ik had ook kunnen kiezen
voor andere technieken om functionaliteit uit te breiden, maar voor
de meeste Delphi ontwikkelaars zal inheritance bekend voorkomen
en praktisch uitvoerbaar zijn. Ook dat is weer een keuze die gemaakt
moet worden bij de implementatie van het framework.
De regels
Voordat ik de uitwerking van de verschillende classes ga behandelen
wil ik eerst op de regels ingaan. De theorie verteld namelijk dat
de regels onderdeel uitmaken van het framework en dat iedere implementatie
van classes binnen of op het framework deze regels zal moeten respecteren.
De uitwerking van de classes kan dus alleen goed worden gedaan als
de regels bekend zijn. De regels die ik hier noem zijn slechts voorbeelden
uit mijn praktijk, je kunt voor je eigen framework net zoveel regels
maken als jou handig lijkt.
Naamgeving classes
De eerste regel is een regel over naamgeving. Zoals in het class
diagram van het skeleton al te zien is, wordt er voor de classes
een standaard naamgeving gehanteerd. Iedere class heeft een naam
volgens de volgende structuur: T[Designer prefix]Ndf<Class description>.
De T is de gebruikelijke Dephi class T.
De designer prefix (Dtm voor datamodules, Frm voor Forms, en voor
overige classes geen prefix) geeft informatie hoe de class in de
Delphi IDE weergegeven zal worden. Dit is geen noodzakelijk onderdeel
van een naamgeving, maar ik vind het prettig werken. Ik weet dan
namelijk of er bij een class unit nog een dfm bestand hoort, de
forms en datamodules staan mooi gegroepeerd in diverse selectie
schermen in Delphi, etc.).
De letters Ndf staan kortweg voor "NLDelphi Framework".
Door deze prefix kunnen naamgevingsconflicten worden voorkomen.
De class description is een zo duidelijk mogelijke omschrijving
van wat de class is en implementeert. Als een class dus een implementatie
is van een klant beheersfunctionaliteit, dan is voor de omschrijving
CustomerMaintenanceFunction een redelijk voorstel. Het gedeelte
"Function" geeft aan dat de class een "functie"
is, het gedeelte "CustomerMaintenance" geeft het onderhoud
op klanten aan.
Naamgeving Units
Als naamgevingsconventie voor de units hanteer ik de volgende regel
<Unit prefix><Classname zonder T>.pas.
De unit prefix is ook weer een prefix waardoor ik gemakkelijk bestanden
uit elkaar kan houden. Voor classes die afgeleid zijn van een TDataModule
hanteer ik een d (van datamodule), voor TForm afgeleiden een f (van
form). Voor framework management classes gebruik ik de m (van management)
en voor de overige classes gebruik ik de u (van unit). Wederom geldt
dat ieder voor zijn of haar eigen framework ook eigen regels kan
opstellen; dit is slechts een voorbeeld dat in ik in de praktijk
gebruik.
Coding standards
Het is goed om ook een coding standard af te spreken. Een coding
standard zegt iets over de layout en manier van programmeren. Een
aantal voorbeelden van de coding standard die ik voor dit framework
hanteer zijn de volgende:
- Gebruik voor naamgeving de InitCaps notatie.
- Als er in een if-then-else blok bij de if of bij de else een begin-end
wordt gebruikt, dan wordt voor zowel het if blok als voor het else
blok een begin-end gebruikt.
- De dubbele punten tussen variabelen en hun type worden onder elkaar
uitgelijnd.
- Alle protected en public methods worden virtual gemaakt.
- Programmeer variabelen in hun kleinst mogelijke scope (minimaliseer
het gebruik van globale variabelen en class private fields).
- De maker van een object is verantwoordelijk voor het opruimen
ervan.
- Etcetera
Het voert te ver om alle regels in dit artikel te zetten, maar
het is duidelijk dat dit soort regels de leesbaarheid van de code
verhoogt. Ook zal code vaker aansluiten op wat een programmeur verwacht
indien deze gewend is met de betreffende regels te werken.
De shell
De shell wordt alleen ingericht via de informatie uit de module
manager en zaken die in de shell implementatie direct zijn opgenomen.
De shell wordt niet van buitenaf gemanipuleerd.
Functies
Functies worden alleen gestart via de function manager. Nergens
wordt zelf een functie class aangemaakt (de function manager is
immers verantwoordelijk voor de life-cycle van functies).
Uitwerking van de verschillende classes
Nu de architectuur en de regels bekend zijn kan de implementatie
beginnen. De units van het framework zijn allemaal van veel commentaar
voorzien om de werking van het framework toe te lichten, maar ik
zal de hoofdpunten hier toch bespreken.
Het begint allemaal bij de shell, in het framework geïmplementeerd
als een form met als classname TFrmNdfShell, te vinden in de unit
fNdfShell. Bij het bouwen van een applicatie op het framework wordt
van dit form een afgeleide gemaakt. Die afgeleide wordt dan als
main form van de applicatie aangewezen en de TFrmNdfShell class
zal dan zorgen dat het framework bij het opstarten van de applicatie
in werking treedt.
Dit starten van de werking van het framework wordt geïmplementeerd
in de constructor. Hier wordt aan de module manager gevraagd welke
modules er zijn. Elke van de modules wordt gevraagd welke functies
deze module implementeert. Een functie in een module kent een toeganspad
(path) en een action waarmee de functie kan worden
opgestart (bij de implementatie van de module class wordt toegelicht
hoe deze informatie kan worden opgenomen in een module). Met de
informatie van de module kan de shell dynamisch een menu opbouwen.
Ik heb als voorbeeld gekozen voor een TMainMenu, maar het is natuurlijk
mogelijk allerlei mogelijke controls te werken. Wederom geldt: jouw
fantasie is je enige beperking.
constructor TFrmNdfShell.Create(AOwner: TComponent);
...
begin
...
// Vraag aan de module manager voor iedere module welke functies er
// geregistreerd zijn en hoe deze in de shell geplaatst moeten worden
for i := 0 to ModuleManager.ModuleCount - 1 do
begin
// Loop door de functies van de module
for f := 0 to ModuleManager.Modules[i].FunctionCount - 1 do
begin
// Bepaal path en action van de function
ModuleManager.Modules[i].FunctionInfo(f, Path, Action);
// Maak menuitems aan voor het path, of gebruik bestaande menuitems als
// deze overeenkomen
MenuItem := PathToMenuItem(Path);
// Als er een menuitem is aangemaakt, dan kan de action onder dit item
// als subitem worden aangemaakt.
NewItem := TMenuItem.Create(Self);
if Assigned(MenuItem) then
begin
// Maak een nieuw subitem aan onder het path item
MenuItem.Add(NewItem);
end
else
begin
// Anders wordt een nieuw item aangemaakt in het hoofdmenu
mnuMain.Items.Add(NewItem);
end;
// Koppel de action
NewItem.Action := Action;
end;
end;
...
end;
De module manager kent een RegisterModule en UnregisterModule
method. Via deze methods kunnen module classes zich registreren
bij de module manager. De module manager maakt bij registratie een
instantie van de module class, en ruimt deze bij de-registratie
weer op (wederom: life-cycle management).
Via de ModuleCount en Modules properties kan de shell
bij de noodzakelijke module informatie komen.
TNdfModuleManager = class(TObject)
...
public
...
// Methods voor modules
procedure RegisterModule(ModuleClass: TNdfModuleClass); virtual;
procedure UnregisterModule(ModuleClass: TNdfModuleClass); virtual;
// Methods voor de shell
function ModuleCount: Integer; virtual;
property Modules[Index: Integer]: TDtmNdfModule read GetModule;
end;
In de module manager unit zit tevens een singleton implementatie
van de manager, zodat deze gemakkelijk toegankelijk is. Het singleton
design pattern zorgt ervoor dat er altijd maar één
instantie van de betreffende class aanwezig is. In dit geval is
dat dus erg prettig, want het framework moet maar één
module manager hebben waar iedereen mee werkt. Door gebruik te maken
van de ModuleManager singleton functie kan iedereen dus dezelfde
module manager gebruiken.
// Singleton implementatie van de ModuleManager
function ModuleManager: TNdfModuleManager;
begin
// Als de manager nog niet is aangeroepen (de interne instantie van de manager
// is nog niet gezet), maak dan een interne instantie van de manager aan
if not Assigned(InternalModuleManager) then
InternalModuleManager := TNdfModuleManager.Create;
// Geef de instantie van de manager terug
Result := InternalModuleManager;
end;
De module class, TDtmNdfModule geïmplementeerd in unit dNdfModule,
is een TDataModule afgeleide. Ik heb hiervoor gekozen zodat ik eenvoudig
non-visual components aan de module kan toevoegen. Een goed voorbeeld
van een dergelijk non-visual component is een TActionList waarin
de actions worden opgenomen die de functies in de module opstarten
(daarover later meer).
TDtmNdfModule = class(TDataModule)
...
protected
// Ten behoeve van het registreren van functions
procedure RegisterFunction(const Path: String;
const Action: TAction); virtual;
procedure RegisterFunctions; virtual;
public
...
// Methods voor de shell
function FunctionCount: Integer; virtual;
procedure FunctionInfo(const Index: Integer;
var Path: String;
var Action: TAction);
end;
Zoals te zien is kan aan de module gevraagd worden hoeveel functies
de module bevat via de FunctionCount method. Informatie over
individuele functies kan worden opgevraagd via de FunctionInfo
procedure.
Met de gegevens uit de FunctionInfo method is de shell in
staat een visuele toegang tot de functies te maken. Het pad is een
lijst van items gescheiden door een \, zodat functies in verschillende
categorieën geplaatst kunnen worden. Aangezien de shell zo
is geïmplementeerd dat er per item een menu-item wordt aangemaakt,
is het dus mogelijk om bijvoorbeeld twee functies onder één
sub-menu te plaatsen. Als twee functies als pad 'Klanten\Beheer'
hebben, zullen ze beiden onder hoofdmenu 'Klanten', submenu 'Beheer'
geplaatst worden. De action van iedere functie bepaalt vervolgens
de caption in het menu, omdat deze direct aan een menu item onder
het submenu 'Beheer' gekoppeld zal worden. Er is dan ook direct
zorgt gedragen voor het aanroepen van code om een functie op te
starten als deze in de action OnExecute event handler van de action
(in de module class) wordt geïmplementeerd. Verderop bij de
implementatie van een applicatie op het framework zal dit worden
gedemonstreerd.
In de module class is de eerste inhaakmogelijkheid op het framework
terug te vinden: de RegisterActions method. Het is de bedoeling
dat module afgeleiden deze method overriden en daarin RegisterFunction
aanroepen voor iedere te registreren functie.
De RegisterFunction method zorgt ervoor dat een nieuwe functie
aan de interne lijst van functies wordt toegevoegd. Er is geen UnregisterFunction
method, want de module zal direct na creatie de lijst van geregisteerde
functies beschikbaar moeten hebben om de shell te kunnen bedienen
en er zijn geen faciliteiten beschikbaar om wijzigingen op de lijst
van geregistreerde functies door te geven aan de shell. De-registratie
vind dus effectief plaats bij het opruimen van de module.
Het opstarten van een functie zal dus in de module afgeleide plaatsvinden
in een action OnExecute event handler. Daarbij zal de functie worden
gestart via de function manager, aldus de regels. Dat brengt ons
bij de function manager, de TNdfFunctionManager class geïmplementeerd
in unit mNdfFunction.
TNdfFunctionManager = class(TObject)
...
protected
// Event handler voor functies om aan te roepen als ze klaar zijn
procedure FunctionExecuteComplete(FunctionInstance: TObject); virtual;
...
public
...
// Ten behoeve van het starten van een functie
function CreateFunction(const FunctionClass: TNdfFunctionClass;
const Identifier: String;
var FunctionInstance: TDtmNdfFunction): Boolean;
virtual;
end;
Het opstarten van een functie gebeurd met de CreateFunction
method. Via de FunctionClass parameter kan worden aangegeven
welke functie moet worden gestart. Door middel van de Identifier
kan worden gedetecteerd of een functie reeds is opgestart. Indien
dit niet het geval is wordt een nieuwe instantie van de opgegeven
class aangemaakt. Anders wordt de bestaande instantie teruggegeven.
Als er altijd een nieuwe instantie aangemaakt dient te worden dan
kan de identifier leegelaten worden. Het functieresultaat geeft
aan of er een nieuwe instantie is aangemaakt of niet.
function TNdfFunctionManager.CreateFunction(
const FunctionClass: TNdfFunctionClass;
const Identifier: String;
var FunctionInstance: TDtmNdfFunction): Boolean;
var
FunctionIndex: Integer;
begin
// Kijk of er een identifier is opgegeven
if Identifier = '' then
begin
// Zo nee, start dan altijd een nieuwe instantie van de functie
FunctionInstance := FunctionClass.Create;
// Koppel de eventhandler voor het afronden van de functie
FunctionInstance.OnExecuteComplete := FunctionExecuteComplete;
// En voeg deze aan de lijst toe
FFunctionList.AddObject(Identifier, FunctionInstance);
// Geef terug dat er een nieuwe instantie is aangemaakt
Result := True;
end
else
begin
// Als er wel een identifier is opgegeven, kijk dan of deze zich al
// in de lijst bevindt
FunctionIndex := FFunctionList.IndexOf(Identifier);
if FunctionIndex <> -1 then
begin
// Zo ja, geef dan de bestaande instantie terug
FunctionInstance := TDtmNdfFunction(FFunctionList.Objects[FunctionIndex]);
// En geef aan dat er geen nieuwe instantie is aangemaakt
Result := False;
end
else
begin
// Zo nee, start dan altijd een nieuwe instantie van de functie
FunctionInstance := FunctionClass.Create;
// Koppel de eventhandler voor het afronden van de functie
FunctionInstance.OnExecuteComplete := FunctionExecuteComplete;
// En voeg deze aan de lijst toe
FFunctionList.AddObject(Identifier, FunctionInstance);
// Geef terug dat er een nieuwe instantie is aangemaakt
Result := True;
end;
end;
end;
Voor het life-cycle management is er nog een hele belangrijke event
handler te vinden in deze class: de FunctionExecuteComplete
method. Deze event handler wordt aan een door de manager aangemaakte
instantie van een functie gekoppeld. Hiermee kan een functie aan
de function manager doorgeven dat de functie klaar is met zijn taak,
zodat de function manager zijn life-cycle management taak op zich
kan nemen en de functie instantie kan opruimen.
procedure TNdfFunctionManager.FunctionExecuteComplete(FunctionInstance: TObject);
begin
// Ruim de referentie naar deze functie instantie op uit de lijst
FFunctionList.Delete(FFunctionList.IndexOfObject(FunctionInstance));
// En ruim de functie instantie op
FreeAndNil(FunctionInstance);
end;
De functionmanager maakt functies aan. Deze functies zijn allemaal
afgeleid van de TDtmNdfFunction class, geïmplementeerd in de
dNdfFunction unit. De functies zijn waar het werk uiteindelijk allemaal
gebeurd in de applicatie. Daar zal dus de functionaliteit van de
applicatie moeten worden geïmplementeerd.
Het implementeren van de functionaliteit zal moeten gebeuren in
een override van de Execute method. Execute kan worden
gebruikt op twee manieren: als functie of al procedure. Indien de
later genoemde ExecutionStyle modaal is, dan kan het resultaat
van het uitvoeren van de functie worden teruggegeven als functie
resultaat. Indien de ExecutionStyle niet modaal is, dan zal
het resultaat van Execute worden teruggegeven via een andere
manier, vrij te implementeren in een afgeleide van de functie.
Het resultaat van de functie wordt in beide gevallen bepaald via
de FunctionResult property, die dus kan worden gezet zodra
het resultaat van de functie bekend is. Het is natuurlijk van belang
dat het resultaat gezet wordt voordat een class van buitenaf op
een of andere manier aan de function dit resultaat zal opvragen.
Execute kan op verschillende manieren worden geïmplementeerd.
Voor een rapportage functie kan dit betekenen dat het rapport wordt
uitgevoerd, voor een functie die een form met gegevens moet tonen
kan het betekenen dat het form met de gegevens op het scherm wordt
weergegeven.
De ExecutionStyle bepaald hoe een functie wordt uitgevoerd.
Hoewel de TDtmNdfFunction class nog geen invulling geeft aan het
daadwerkelijk uitvoeren van de functie, is al wel bekend hoe een
functionaliteit binnen een applicatie kan worden uitgevoerd. Dit
kan op drie manieren worden gedaan: normal, modal of child. Een
functie die ExecutionStyle normal heeft wordt opgestart en
wordt op een onbekend moment in de tijd afgerond. Een functie die
ExecutionStyle child heeft werkt op dezelfde wijze, alleen
wordt de functie als child van de hoofdflow gezien (een voorbeeld
van de toepassing hiervan is een childform van een mdi applicatie).
Een functie die ExecutionStyle modal heeft wordt opgestart
en er wordt gewacht op het resultaat van deze functie alvorens de
reguliere flow van de applicatie doorgaat (te vergelijken met een
modal form).
Ook in de function class is een inhaakmogelijkheid van het framework
geïmplementeerd. De Initialize method is een hook om de initialisatie
van privates te regelen. Het framework draagt er via de TDtmNdfFunction
class zorg voor dat de Initialize hook altijd op het juiste moment
wordt aangeroepen (tussen de constructor en de OnCreate event handler
in).
Om stand alone applicaties te kunnen maken met het framework is
het skelet van classes uitgebreid met een afgeleide van de basis
functie class. Deze afgeleide, TDtmNdfFunctionForm geïmplementeerd
in unit dNdfFunctionForm, voegt aan de functie toe dat deze een
form toont. Zoals gezegd zal in de Execute method de meer
specifieke implementatie plaatsvinden.
function TDtmNdfFunctionForm.Execute: String;
begin
// Maak eerst het form aan
CreateForm;
// Stel eventuele properties van het form in als het is aangemaakt
if Assigned(FForm) then
begin
SetFormProperties;
// En voer dan de eigenlijke execute uit
if ExecutionStyle = esModal then
begin
// Voor een modal execution betekent dat: toon het form modal. Er wordt
// dus gewacht met het teruggeven van het resultaat totdat het form
// afgesloten wordt.
FForm.ShowModal;
// Geef vervolgens het resultaat terug
Result := inherited Execute;
// Voltooi de functie
CompleteFunction;
end
else
begin
// Voor een niet modal execution betekent dat: toon het form. Er wordt dus
// NIET gewacht tot het form gesloten is. Eventuele resultaten van
// handelingen op het form zullen via de OnResult event handler naar buiten
// worden doorgegeven aan de module.
FForm.Show;
// Geef het resultaat terug zodat de flow wel hetzelfde blijft
Result := inherited Execute;
end;
end;
end;
Ook in deze class zijn inhaakmogelijkheden ingebouwd, want het
is te verwachten dat bij het bouwen van een applicatie op dit framework
de programmeur op verschillende momenten het framework gedrag wil
beïnvloeden. De SetFormProperties method is een dergelijke
mogelijkheid.
In deze class wordt een form getoond, maar het uitvoeren van de
functie is pas afgerond als het form wordt gesloten. Om terugkoppeling
van het voltooien van de functie naar de function manager te verzorgen
is de CompleteFunction method gemaakt. Via deze method wordt
de OnExecuteComplete event handler aangeroepen, welke door
de function manager aan de functie is gekoppeld.
Het is voor een functie in esModal execution style eenvoudig
te bepalen wanneer de CompleteFunction method moet worden
aangeroepen, namelijk direct na de ShowModal van het form.
Voor niet modale forms is een andere constructie gemaakt. De function
class haakt in op het OnFormFinished event dat in het form
geïntroduceerd wordt (zie SetFormProperties). In een
override van de DoClose method van het form wordt vervolgens
deze event handler aangeroepen. Zo wordt altijd bij het sluiten
van het form uiteindelijk de CompleteFunction method aangeroepen.
procedure TFrmNdfFunction.DoClose(var Action: TCloseAction);
begin
inherited;
// Als een mdi child form gesloten wordt, dan is de default actie "minimize",
// maar dan minimizen alle child forms. Om dit te voorkomen wordt hier de
// actie alsnog op "none" gezet
if (FormStyle = fsMdiChild) and (Action = caMinimize) then
Action := caNone;
// Nadat het reguliere sluitproces is voltooid kan aan de function datamodule
// worden doorgegeven dat het form is gesloten
if Assigned(FOnFormFinished) then
FOnFormFinished(Self);
end;
Is het framework compleet?
Nee, het framework is nog lang niet compleet. Er zijn diverse zaken
nog niet geregeld. Zo is er nog geen communicatie tussen functies
geregeld. Het framework biedt ook nog geen faciliteiten om database
applicaties te maken en ook de voordelen van het modulair bouwen
komen in deze architectuur nog niet tot zijn recht (wellicht biedt
dit framework mogelijkheden voor dynamische
applicaties?). Genoeg stof voor volgende artikelen in deze reeks
dus.
Een eerste applicatie gebaseerd op het framework
Toch kan er met deze basis al een eerste kleine applicatie worden
gebouwd. Het is niet de bedoeling een ingewikkelde applicatie te
maken (daar helpt de basisarchitectuur ook nog niet echt bij) maar
een applicatie waarin het gebruik van de architectuur naar voren
komt.
De applicatie is opgenomen in de sourcecode,
maar voor de volledigheid beschrijf ik hoe de applicatie tot stand
is gekomen.
Ik heb gekozen om het framework op een een bepaalde locatie te
plaatsen, op mijn D: schijf. Je kunt het framework en de
voorbeeldcode op iedere willekeurige
plaats neerzetten, zolang de framework source en de applicatie source
maar ten opzichte van elkaar dezelfde locatie behouden. Mijn mappen
zien er als volgt uit:

Onder de First framework application map maak ik voor het project
vervolgens drie mappen aan: source, dcu en bin. De source map bevat
de sourcecode van het project, de dcu map is waar de dcu's naartoe
gecompileerd zullen worden en de bin map is de map waar de uiteindelijke
applicatie naartoe gecompileerd wordt.
Na het aanmaken van de map D:\Projects\First framework application\Source
kan in Delphi een nieuw project worden aangemaakt met behulp van
File - New application. Het form kan uit het project worden
gehaald met Project - Remove from project (en dan natuurlijk
unit1 selecteren). Vervolgens worden alle framework units aan het
project toegevoegd met Project - Add to project (selecteer
dan alle .pas bestanden in de map D:\NLDelphiFramework\Source,
of waar je het framework ook hebt geplaatst). Hierna kan worden
ingesteld waar de dcu's en executable naartoe gecompileerd moeten
worden. Dit gebeurt door via Project - Options - Directories/conditionals
onder "output directory" ..\bin en onder "unit output
directory" ..\dcu in te vullen. Het project kan dan worden
opgeslagen in de map D:\Projects\First framework application\Source.
Als eerste onderdeel kan de shell worden aangemaakt. De framework
shell class doet bijna al het werk dat nodig is, dus het aanmaken
van een afgeleide van de TFrmNdfShell class is zo goed als voldoende.
Via File - New - Other en dan de FirstFrameworkApplication
tab kan een afgeleide van de in het project opgenomen shell form
class worden gemaakt. Een nette caption "First application"
en een nette class name "FrmMain" zorgen voor de afwerking.
De unit kan worden opgeslagen onder de naam fMain.pas en dit form
moet via Project - Options in de lijst van auto-create forms
worden opgenomen (als Delphi dit al niet voor je gedaan heeft).
De shell wordt tevens uitgerust met een menu optie Program -
Close, waarmee de applicatie kan worden afgeloten. Er wordt
voor gezorgd dat deze optie altijd onderaan in het "Program"
menu komt te staan.
De testapplicatie zal uit drie modules bestaan: de "Browse"
module, de "Mail" module en de "Custom" module.
De "Browse" module implementeert twee functies, browse
naar de NLDelphi site en browse naar de Borland site. De "Mail"
module implementeert twee functies: een selectie functie of er gemaild
moet worden en een mail naar NLDelphi functie die gestart wordt
(afhankelijk van het antwoord in de selectie functie). De "Custom"
module integreert maatwerk in de "Browser" module, er
komt een functie bij om naar ErikStok.nl te browsen. Ook worden
er een datum form en een tijd form aan de applicatie toegevoegd
via de "Custom" module.
Als eerste wordt de "Browse" module gemaakt. Door File
- New - Other te kiezen en op de tab FirstFrameworkApplication
te kiezen voor een afgeleide van TDtmNdfModule kan de module worden
aangemaakt. De naam TDtmBrowseModule ligt voor de hand en als bestandsnaam
is dBrowseModule.pas een passende keuze. De var declaratie van DtmBrowseModule:
TDtmBrowseModule kan worden verwijderd, deze is immers niet van
toepassing want de module manager zorgt voor de life-cycle van de
module.
Om deze module manager zijn werk te kunnen laten doen moet de module
zich registreren bij die manager. Dit kan gebeuren in de initialization
section van de module unit. De-registreren kan worden gedaan in
de finalization.
initialization
ModuleManager.RegisterModule(TDtmBrowseModule);
finalization
ModuleManager.UnregisterModule(TDtmBrowseModule);
Zoals gezegd is de RegisterFunctions method bedoeld om te
overriden zodat de in de module opgenomen functies bekend gemaakt
kunnen worden. In dit geval zijn er twee functies te registreren,
de functie die worden gestart via actBrowseToNLDelphi en de functie
die wordt gestart via actBrowseToBorland, beide actions opgenomen
in een aan de module toegevoegde actionlist.
procedure TDtmBrowseModule.RegisterFunctions;
begin
inherited;
// Registreer beide in deze module opgenomen functies onder het
// hoofdmenu 'Browse'
RegisterFunction('Browse', actBrowseToNLDelphi);
RegisterFunction('Browse', actBrowseToBorland);
end;
Het uitvoeren van een functie gebeurt in de OnExecute event
handler van de action. De code van de "browse to NLDelphi"
action ziet er als volgt uit:
procedure TDtmBrowseModule.actBrowseToNLDelphiExecute(Sender: TObject);
var
d : TDtmNdfFunction;
begin
// Voer de functie BrowseToNLdelphi uit
if FunctionManager.CreateFunction(TDtmBrowseToNLDelphiFunction, '', d) then
d.Execute;
end;
Zoals te zien is wordt er een TDtmBrowseToNLDelphiFunction functie
opgestart. Deze functie is gemaakt door File - New - Other
te kiezen en op de tab FirstFrameworkApplication te kiezen
voor een afgeleide van TDtmNdfFunction. In een override van de Execute
method wordt de werking van de functie geïmplementeerd. In
dit geval is het een eenvoudige ShellExecute aanroep.
function TDtmBrowseToNLDelphiFunction.Execute: String;
begin
// Browse naar NLdelphi
ShellExecute(0, 'open', 'http://www.nldelphi.com', '','', SW_NORMAL);
end;
Op dezelfde manier zijn de "browse to Borland" en "browse
to ErikStok.nl" functies opgezet, waarbij de laatste in een
eigen module is geïmplementeerd. Deze module mengt echter zijn
functie zonder problemen in het menu waar ook de actions van de
"browse" module geplaatst zijn.
De "mail" module bevat een functie die een form start.
In dit geval wordt het form modaal getoond en alleen als de selectie
"mail to NLDelphi" gemaakt is wordt een tweede functie
gestart.
procedure TDtmMailModule.actMailToNLDelphiExecute(Sender: TObject);
var
d : TDtmNdfFunction;
FunctieResultaat : String;
begin
// Voer de functie MailToNLdelphi functie uit
if FunctionManager.CreateFunction(TDtmMailtoNLDelphiSelectionFunction,
'', d) then
begin
// Voer de functie modaal uit, met andere woorden: wacht op het resultaat
d.ExecutionStyle := esModal;
FunctieResultaat := d.Execute;
// Als het functieresultaat 'mail' is, voer dan een mailto uit volgens de
// functie PerformMail
if FunctieResultaat = 'mail' then
begin
if FunctionManager.CreateFunction(TDtmMailtoNLDelphiFunction,
'', d) then
d.Execute;
end;
end;
end;
In de TDtmMailtoNLDelphiSelectionFunction functie, op dezelfde
manier aangemaakt als eerder genoemde functies, wordt in de DataModuleCreate
aangegeven welke formclass gebruikt moet worden.
Deze formclass, TFrmMailToNLDelphiSelectionFunction, is aangemaakt
door File - New - Other te kiezen en op de tab FirstFrameworkApplication
te kiezen voor een afgeleide van TFrmNdfFunction. Aan de formclass
is een IsMailSelected method toegevoegd, via welke aan het
form gevraagd kan worden of de selectie is gemaakt dat er mail verstuurd
moet worden.
De TDtmMailtoNLDelphiSelectionFunction functie haakt in op de FormFinished
method om van het form te weten te komen welke selectie er is gemaakt.
Aan de hand van de selectie wordt het functieresultaat gezet, wat
uiteindelijk als resultaat van de Execute method zal worden
teruggegeven aan de aanroeper.
Het laatste deel van de applicatie wat de aandacht verdient is
het starten van de datum en de tijd functionaliteit. Alle classes
nodig voor beide functies zijn aangemaakt op dezelfde wijze als
bij eerder genoemde functies.
Zoals in de code te zien is wordt bij deze functies gebruik gemaakt
van een basis class, de display functie. Deze basis class kan worden
gestuurd door middel van properties, die in de implementatie van
beide afgeleiden anders worden ingesteld (ik weet dat er andere
mogelijkheden zijn om gelijkwaardig gedrag te bereiken, maar het
gaat hier om het voorbeeld).
function TDtmDateFunction.Execute: String;
begin
// Stel in wat er moet worden weergegeven voordat de functie wordt uitgevoerd
DisplayName := 'date';
DisplayText := FormatDateTime('DD-MM-YYYY', Date);
// Voer de functie uit
Result := inherited Execute;
end;
Bij de aanroep van beide functies wordt door gebruik te maken van
verschillende identifiers bereikt dat een functie leidt tot één
of meer schermen. Bij de "time" functionaliteit wordt
voor iedere minuut een nieuwe instantie van de functie gestart.
Bij de "date" functionaliteit wordt altijd maar één
instantie gestart. Door de aansturing van het framework, via de
identifier parameter van de function manager, wordt het gedrag van
de applicatie dus gestuurd.
procedure TDtmCustomModule.actCurrentTimeExecute(Sender: TObject);
var
d : TDtmNdfFunction;
begin
// Voer de functie "time" uit. Maak voor iedere "nieuwe" tijd (iedere
// minuut) een eigen instantie van de functie aan door als identifier
// de tijd te nemen op een minuut nauwkeurig.
if FunctionManager.CreateFunction(TDtmTimeFunction,
FormatDateTime('HH:NN', Time), d) then
begin
d.ExecutionStyle := esChild;
d.Execute;
end
else
begin
d.Activate;
end;
end;
procedure TDtmCustomModule.actCurrentDateExecute(Sender: TObject);
var
d : TDtmNdfFunction;
begin
// Voer de functie "date" uit. Start deze functie maar 1 keer op, onder
// de identifier 'date'
if FunctionManager.CreateFunction(TDtmDateFunction, 'date', d) then
begin
d.ExecutionStyle := esChild;
d.Execute;
end
else
begin
d.Activate;
end;
end;
Terugblik op de theorie
Tot zover de implementatie van een eerste applicatie op basis van
het framework. In het vorige artikel
over frameworks gaf ik al aan dat deze reeks artikelen vooral bedoeld
is om te laten zien hoe de theorie van frameworks in de Delphi praktijk
toe te passen is. Daarom wil ik nog even controleren of de basisarchitectuur
die nu is neergezet voldoet aan de theorie.
Zoals gezegd bestaat een framework ondere ander uit een skelet
van non-visual classes. Het skelet van non-visual classes is duidelijk:
de module manager, de module, de function manager, de function,
een function form en een shell form. Velen zullen zich afvragen
waarom hierbij een form toch als non-visual class bestempeld wordt.
Met non-visual wordt in het geval van de gehanteerde framework definitie
bedoeld: een class die geen GUI functionaliteit implementeert. Daarbij
beschouw ik een leeg form als dusdanig. Een form waaraan controls
zijn toegevoegd dus niet meer. Ik ben mij bewust dat daarover uren
kan worden gediscussieerd, maar dat laat ik aan de puristen over.
De set regels is de andere basis van een framework. Die zijn duidelijk
genoemd, dus ook daar sluit deze basisarchitectuur aan op de theorie.
Een kwaliteit van een goed framework is dat het eenvoudig is. Daaraan
voldoet dit framework volgens mij wel. Lezers die zich al meer verdiept
hebben in frameworks zullen wellicht zelfs vinden dat het té
eenvoudig is. Ik ben mij bewust dat bijvoorbeeld de implementatie
van een model-view-contollers pattern tot een sterker framework
leidt, maar dat komt de eenvoud voor veel lezers niet ten goede.
Bovendien wordt het framework in volgende artikelen nog iets uitgebreid,
dus de complexiteit neemt nog wel iets toe.
Ook is een goed framework helder. Te zien is dat de class hierarchy,
de naamgeving van units en classes, het gebruik van de managers
en het afleiden van de basis classes heel transparant en volgens
verwachting is.
De grenzen van een goed framework moeten ook duidelijk zijn. Het
is te zien dat de grenzen van deze basis heel duidelijk zijn. Alleen
het management van modules en functies wordt nog maar door het framework
geregeld. De rest moet de programmeur zelf bouwen. Qua begrenzing
van het type applicatie dat met het framework gemaakt kan worden
is te zien dat het desktop applicaties betreft.
De uitbreidbaarheid is begrenst door de fanatasie van de ontwikkelaar.
Er kunnen allerlei afgeleiden van deze basis worden gemaakt waarin
de programmeur kan implementeren wat hij wil.
Er kan al op verschillende plaatsen worden ingehaakt op het gedrag
van het framework door methods te overriden of door parameters van
aan framework functies te manipuleren.
Zijn de voordelen bij het gebruik van een framework terug te vinden?
Het framework is nog wat beperkt om hier al een volmondig ja op
te zeggen, maar het ziet er tot dusverre goed uit.
Is dus er een standaard manier van applicatieontwikkeling? Ja,
door iedere applicatie op te delen in een shell, modules en functies
is er al een standaard manier van ontwikkelen. Er zijn echter nog
maar weinig richtlijnen voor bijvoorbeeld hoe een functie in te
vullen, dus volgende artikelen zullen hier nog meer invulling aan
moeten geven.
En is er een hogere onderhoudbaarheid? Ik denk het wel. Alleen
al door de naamgevingsconventies. Maar ook door het splitsen in
modules en functies. De verlaagde koppelingsgraad die volgt uit
deze splitsing helpt bij het isoleren van te onderhouden code. Ook
hier zal in volgende artikelen nog meer invulling aan gegeven worden.
Is er ook hergebruik van code? Dat lijkt me duidelijk. De basis
classes van het framework zijn een voorbeeld van sterk hergebruik.
Leidt dit framework tot betere software ontwikkeling in groepen?
Kleine applicaties zoals het voorbeeld zullen natuurlijk niet snel
door groepen gemaakt worden. Maar het is denkbaar dat er een hele
grote applicatie op deze basis gebouwd wordt en dan leidt de splitsing
in modules en functies al tot mogelijkheden in het verdelen van
werkzaamheden. Ook door de helderheid, de implementatie van zaken
op voorspelbare plaatsen en het hergebruik zijn voordelen te verwachten.
Conclusie
Er ligt een basisarchitectuur voor een afgebakend type applicatie
waarin de kenmerken en kwaliteiten van een framework naar voren
komen. Het is duidelijk dat dit slechts een eenvoudige implementatie
is van een framework in Delphi en dat er veel meer omvangrijke en
betere implementaties denkbaar zijn. Er ligt ook genoeg stof voor
volgende artikelen. In het volgende artikel zal het framework verder
worden uitgebouwd, onder meer door communicatie tussen functies
toe te voegen.
|