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.