Frameworks - Deel 2, De basisarchitectuurGeplaatst door Baldo op 14-10-03Als 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.
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 .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.net 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.net" 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. |