Results 1 to 9 of 9

Thread: Functie van afgeleide object aanroepen vanuit base class

  1. #1

    Functie van afgeleide object aanroepen vanuit base class

    Hallo allemaal,

    ik loop tegen het volgende probleem aan.
    Ik heb een basis object TNavReader met daarin een functie ReadFiles
    Daarvan afgeleid een TVerkoopNR en een TProdOrderNR

    De Readfile functie leest een csv file in en zet deze om naar een structuur voor mijn database. Daarna wordt de csv weer verwijderd
    het inlezen en het verwijderen van de csv is voor alle objecten gelijk echter het schrijven naar de database is voor iedere afgeleid object anders.

    Kortom wanneer ik vanuit de base class SaveToDB aanroep kom ik nooit in de functie van de TVerkoopNR.SaveToDB of TProdOrderNR.SaveToDB
    uiteraard kan je dit voor dit simpele voorbeeld wel een oplossing bedenken waarbij je een volgende opzet maakt
    Code:
     
     TVerkoopOrder.Import
     begin
      ReadCSV(fileName);  //functie van de base class
      WriteToDB; // functie van TverkoopNR
      RemoveCSV; // functie van base class
     end;
    Maar ik wil graag weten of er ook nog andere mogelijkheden zijn
    (in de praktijk zit mijn writeToDB binnen een loop van meerdere CSV files maar dit hebben voor het voorbeeld even achterwegen gelaten)

  2. #2
    Fornicatorus Formicidae VideoRipper's Avatar
    Join Date
    Mar 2005
    Location
    Vicus Saltus Orientalem
    Posts
    5,708
    Daar schrijf je normaal toch een virtual method voor in je basisklasse, die je vervolgens in
    afgeleiden implementeerd (en met inherited de voorganger weer aanroept) of begrijp ik je
    vraag niet goed?
    TMemoryLeak.Create(Nil);

  3. #3
    Klopt maar de basis functie is als volgt
    Code:
    procedure TNavReader.ImportData(Item:string);
    var FileList:Tstringlist;
      I: Integer;
    begin
     FileList:=Tstringlist.Create;
     if not CheckFileName(Item,FileList) then exit;
     for I := 0 to FileList.Count-1 do
     begin
      FileName:=FileList[i];
      WriteToDB;
     end;
     for I := 0 to FileList.Count-1 do
     begin
      RemoveFiles(FileList);
     end;
    
     FileList.Free;
    end;
    Deze functie is voor gelijk voor alle afgeleide objecten echter de WriteToDb functie is specifiek per afgeleide object
    (hopelijk zo wat duidelijker)

  4. #4
    Je kunt je afvragen of WriteToDB in NavReader thuishoort of in een aparte klasse. Als het een aparte klasse wordt, dan kun je gebruik maken van (constructor) injection. Volgens mij moet het ook mogelijk zijn om een methode door te geven als parameter, maar of dat in dit geval handig is weet ik niet.

  5. #5
    Fornicatorus Formicidae VideoRipper's Avatar
    Join Date
    Mar 2005
    Location
    Vicus Saltus Orientalem
    Posts
    5,708
    Quote Originally Posted by cpri View Post
    (hopelijk zo wat duidelijker)
    Niet helemaal, maar je kunt WriteToDB virtueel maken en implementeren in de afgeleiden.
    Dan hoef je importdata ook alleen maar in je basisklasse te hebben.

    Moet je alleen nog zorgen dat die afgeleiden ergens (een dataset) naar kunnen schrijven, dus die
    moet je dan of meenemen als parameter, of je neemt een variabele op in je basisklasse (die laatste
    optie zou persoonlijk niet mijn voorkeur hebben, overigens).

    Dus iets van:
    Delphi Code:
    1. uses
    2.   Classes, SysUtils, DB;
    3.  
    4. type
    5.   TBasisKlasse = class(TObject)
    6.   public
    7.     procedure ImportData(AFileName: TFileName);
    8.     procedure WriteToDB(ADataSet: TDataSet; AFileName: TFileName); virtual;
    9.   end;
    10.  
    11.   TAfgeleideKlasse = class(TBasisKlasse)
    12.   public
    13.     procedure WriteToDB(ADataSet: TDataSet; AFileName: TFileName); override;
    14.   end;
    15.  
    16. implementation
    17.  
    18. uses
    19.   ADODB;
    20.  
    21. { TBasisKlasse }
    22.  
    23. procedure TBasisKlasse.ImportData(AFileName: TFileName);
    24. var
    25.   Q: TADOQuery;
    26. begin
    27.   Q := TADOQuery.Create(nil);
    28.   try
    29. //      Q.Connection := Set connection here
    30.     Q.SQL.Text := 'SELECT * FROM BestandenTabel WHERE 1 = 0';
    31.     Q.Append;
    32.     WriteToDB(Q, AFileName);
    33.     Q.Post;
    34.   finally
    35.     Q.Free;
    36.   end;
    37. end;
    38.  
    39. procedure TBasisKlasse.WriteToDB(ADataSet: TDataSet; AFileName: TFileName);
    40. begin
    41.   if not FileExists(AFileName) then
    42.     raise Exception.CreateFmt('Kan bestand %s niet vinden', [AFileName]);
    43. end;
    44.  
    45. { TAfgeleideKlasse }
    46.  
    47. procedure TAfgeleideKlasse.WriteToDB(ADataSet: TDataSet; AFileName: TFileName);
    48. begin
    49.   inherited WriteToDB(ADataSet, AFileName); // De basisklasse controleert of het bestand bestaat
    50.   ADataSet.FieldByName('BestandsNaam').AsString := AFileName;
    51. end;
    Bovenstaand is een heel onzinnig (en ongerelateerd aan jouw klassen) voorbeeld, waarbij de basisklasse
    ImportData implementeerd: hier wordt een query geopend met een append, zodat WriteToDB in een
    afgeleide klasse een bepaald veld op kan slaan in de database (in dit geval het veld "Bestandsnaam").
    In dit geval roept de afgeleide klasse eerst zijn ouder aan (door de inherited), welke controleert of
    het bestand wel bestaat en wanneer er geen exception optreedt (en dus het bestand bestaat), wordt het
    veld opgeslagen.

    Overigens heb je een memory leak als CheckFileName false is (create en free moeten ook eigenlijk
    omgeven zijn van een try...finally...end-blok), maar dit terzijde.
    En beter kijkend ziet het geheel daar er sowieso wel heel vreemd uit...
    Last edited by VideoRipper; 04-Sep-18 at 16:09.
    TMemoryLeak.Create(Nil);

  6. #6
    John Kuiper
    Join Date
    Apr 2007
    Location
    Almere
    Posts
    8,747
    Dit moet toch voldoende zijn. Omdat TBasisKlasse een onderdeel is van TAfgeleideKlasse, wordt de procedure uit de basisklasse gehaald, omdat deze niet in zijn eigen class voorkomt?

    Delphi Code:
    1. uses
    2.       Classes, SysUtils, DB;
    3.      
    4.     type
    5.       TBasisKlasse = class
    6.       public
    7.         procedure ImportData(AFileName: TFileName);
    8.         procedure WriteToDB(ADataSet: TDataSet; AFileName: TFileName);
    9.       end;
    10.      
    11.       TAfgeleideKlasse = class(TBasisKlasse);
    12.      
    13.     implementation
    14.      
    15.     uses
    16.       ADODB;
    17.      
    18.     { TBasisKlasse }
    19.      
    20.     procedure TBasisKlasse.ImportData(AFileName: TFileName);
    21.     var
    22.       Q: TADOQuery;
    23.     begin
    24.       Q := TADOQuery.Create(nil);
    25.       try
    26.     //      Q.Connection := Set connection here
    27.         Q.SQL.Text := 'SELECT * FROM BestandenTabel WHERE 1 = 0';
    28.         Q.Append;
    29.         WriteToDB(Q, AFileName);
    30.         Q.Post;
    31.       finally
    32.         Q.Free;
    33.       end;
    34.     end;
    35.      
    36.     procedure TBasisKlasse.WriteToDB(ADataSet: TDataSet; AFileName: TFileName);
    37.     begin
    38.       if not FileExists(AFileName) then
    39.         raise Exception.CreateFmt('Kan bestand %s niet vinden', [AFileName]);
    40.     end;
    Delphi is great. Lazarus is more powerfull

  7. #7
    Fornicatorus Formicidae VideoRipper's Avatar
    Join Date
    Mar 2005
    Location
    Vicus Saltus Orientalem
    Posts
    5,708
    Quote Originally Posted by jkuiper View Post
    Dit moet toch voldoende zijn.
    Let op dit zinnetje:
    Quote Originally Posted by VideoRipper View Post
    Bovenstaand is een heel onzinnig (en ongerelateerd aan jouw klassen) voorbeeld
    Wat er moet gebeuren met de data is afhankelijk van de afgeleide klasse; deze laatste implementeert
    de uitzondering dus.
    De code diende slechts als basisvoorbeeld hoe je kunt overerven en in een ouder een afgeleide kunt
    aanroepen (en weer, gedeeltelijk, terug middels inherited)

    Oh ja: en het kan ook best zijn dat ik de vraag van cpri helemaal verkeerd begrijp, dan praten we
    gewoon langs elkaar heen en is het allemaal onlogisch natuurlijk.
    Last edited by VideoRipper; 04-Sep-18 at 18:17.
    TMemoryLeak.Create(Nil);

  8. #8
    Ik ben het eens met luigi, als hij tenminste ook dit bedoelt:

    Dit is typisch zo'n geval waarin je uiteindelijk een paar keer dezelfde read gebruikt en toch weer een keer een andere (misschien uit een ander format CSV, of uit een dataset i.p.v. een CSV), en meestal een verschillende write, maar toch weer een keer dezelfde....

    Uiteindelijk kom je in de verleiding om de verschillende varianten toch maar in de base class te stoppen, zodat je de stukjes kan hergebruiken, en heb je afgeleiden alleen nog om de juiste stukjes uit de base class aan elkaar te plakken...

    Al met al is compositie dan handiger dan overerving. Door het daadwerkelijk lezen en schrijven onder te brengen in aparte classses kan je die logica eruit trekken. Je import class is dan simpelweg een doorgeefluik.
    Die lees- en schrijfclasses geef je een interface, die bij voorkeur generiek is en verhult of het om een CSV gaat of om iets anders.

    Denk bijvoorbeeld aan een IVerkoopOrderStream. Zomaar een mogelijkheid.

    Delphi Code:
    1. type
    2.   TVerkoopOrder = record
    3.     //
    4.   end;
    5.  
    6.   IVerkoopOrderReadStream = interface
    7.     function Eof: Boolean;
    8.     procedure Next;
    9.     function VerkoopOrder: TVerkoopOrder;
    10.   end;

    Een interface als deze kan geimplementeerd worden door een class die een CSV inlaadt en de orders 1 voor 1 teruggeeft. Ze hoeven overigens niet 1 voor 1 uit de file gelezen te worden, desnoods lees je alles in in een TList<TVerkooporder>, en hou je een tellertje bij met waar je bent.

    Voor je import class maakt het niet veel uit. Die hoeft eigenlijk niet meer te doen dan een reader en een writer te accepteren, en orders een voor een door te geven aan de ander.

    Delphi Code:
    1. type
    2.   TVerkoopOrder = record
    3.     // Eigenschappen van de order
    4.   end;
    5.  
    6.   IVerkoopOrderReadStream = interface
    7.     function Eof: Boolean;
    8.     procedure Next;
    9.     function ReadVerkoopOrder: TVerkoopOrder;
    10.   end;
    11.  
    12.   IVerkoopOrderWriteStream = interface
    13.     procedure Append(AVerkoopOrder: TVerkoopOrder);
    14.   end;
    15.  
    16.   TVerkoopOrderTransporter = class
    17.     private
    18.       FReadStream: IVerkoopOrderReadStream;
    19.       FWriteStream: IVerkoopOrderWriteStream;
    20.    public
    21.     constructor Create(
    22.       AReadStream: IVerkoopOrderReadStream;
    23.       AWriteStream: IVerkoopOrderWriteStream);
    24.  
    25.     procedure TransportAll();
    26.   end;
    27.  
    28. implementation
    29.  
    30. constructor TVerkoopOrderTransporter.Create(
    31.   AReadStream: IVerkoopOrderReadStream;
    32.   AWriteStream: IVerkoopOrderWriteStream);
    33. begin
    34.   FReadStream := AReadStream;
    35.   FWriteStream := AWriteStream;
    36. end;
    37.  
    38. procedure TVerkoopOrderTransporter.TransportAll;
    39. begin
    40.   while not FReadStream.Eof do
    41.   begin
    42.     FWriteStream.Append(FReadStream.ReadVerkoopOrder);
    43.  
    44.     FReadStream.Next;
    45.   end;
    46. end;
    Je zou de reader en writer direct mee kunnen geven aan TransportAll, maar dan moet de aanroeper weten wat ie doet, terwijl dat misschien ook niet de juiste plaats is. Op deze manier kun je gebruik maken van constructor injection, wat Luigi al noemde. Dit betekent ook dat het het hele samenstellen van deze transporter weer onder kunt brengen in een aparte class (builder/factory), en eventueel in een dependency injection container.

    In eerste instantie lijkt het misschien wat overdreven om 4 classes te hebben i.p.v. 1, maar uiteindelijk is het nauwelijks meer code. Bovendien hou je de afzonderlijke blokjes nu netjes gescheiden, en je kan makkelijk verschillende writers implementeren zonder de andere stukken aan te raken. En je verdwaalt niet in een god-class die alle smaakjes van lezen en schrijven moet ondersteunen.

    Een ander bijkomend voordeel is dat het veel makkelijker is om unit tests of acceptatietests te schrijven voor die stukjes. Je kan het principe van het doorgeven van orders simpel testen door met mock objecten te werken om te kijken of de kernlogica werkt (bijvoorbeeld of je niet per ongeluk doorleest na een Eof).

    Vervolgens kan je afzonderlijke tests maken voor je CSV reader. Wellicht kan je het lezen uit een file daar ook nog uit weg abstraheren (bijvoorbeeld doorgebruik te maken van een -wederom geinjecteerde- TStream), zodat je het parsen van de CSV-data helemaal geautomatiseerd kunt doortesten a.d.h.v. voorbeelddata in een TStringStream, zonder afhankelijkheid naar een fysieke file op disk.
    Last edited by GolezTrol; 05-Sep-18 at 11:11.
    1+1=b

  9. #9
    Bedankt voor jullie reacties.
    Ben hieruit toch tot de conclusie gekomen dat de door mij gekozen opzet niet helemaal jofel was.
    Uiteindelijk heb ik de boel omgegooid en ben tot de volgende (werkende oplossing gekomen)

    Code:
    TNRBase = class(Tobject)
      private
       CSVData:TCSVData;
       FFileName: string;
       procedure SetFileName(const Value: string);
       procedure WriteToDB; virtual;
      public
       Constructor Create; virtual;
       Destructor Destroy; override;
      published
       property FileName: string read FFileName write SetFileName;
    end;
    
    
    TNRVerkoopOrders = class(TNRBase)
      private
       procedure WriteToDB; override;
    end;
    
    TNRProdOrder = class(TNrBase)
      private
       procedure WriteToDB; override;
    end;
    
    TNavReader = class(Tobject)
      private
       FileLocation:string;
       DBWriter:TNRBase;
       function CheckFileName(Item:ImportType;FileNames:Tstringlist): boolean;
       function RemoveFiles(FileNames:Tstringlist):boolean;
      public
       Constructor Create; virtual;
       Destructor Destroy; override;
       Procedure ImportData;
      published
    end;
    
    .
    .
    .
    
    procedure TNavReader.ImportData;
    var FileList:Tstringlist;
        I: Integer;
        Item:ImportType;
    begin
     FileList:=Tstringlist.Create;
     for item := Low(ImportType) to High(ImportType) do
     begin
      if CheckFileName(Item,FileList) then
      begin
       case Item of
        IT_VerkoopOrder: DBWriter:=TNRVerkoopOrders.Create;
        IT_PRodOrder:    DBWriter:=TNRProdOrder.Create;
        else             DBWriter:=TNRBase.Create;
       end;
       for I := 0 to FileList.Count-1 do
       begin
        DBWriter.FileName:=FileList[i];
        DBWriter.WriteToDB;
       end;
       DBWriter.free;
       for I := 0 to FileList.Count-1 do
       begin
        RemoveFiles(FileList);
       end;
      end;
     end;
     FileList.Free;
    end;

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •