AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Thema durchsuchen
Ansicht
Themen-Optionen

Generisches Visitor-Pattern

Ein Thema von mael · begonnen am 11. Mär 2020 · letzter Beitrag vom 11. Mär 2020
Antwort Antwort
Benutzerbild von mael
mael

Registriert seit: 13. Jan 2005
391 Beiträge
 
Delphi XE3 Professional
 
#1

Generisches Visitor-Pattern

  Alt 11. Mär 2020, 11:16
Delphi-Version: 5
Hallo,

Ich versuche ein etwas modifiziertes Visitor-Pattern mit Generics auszustatten.

Hier mal ein minimales Beispiel ohne Generics (bis auf TObjectList<TNode>, aber das ist hier nicht relevant):

Code:
type
  TStringVisitor = class;

  TNode = class
  strict private
    FChildren: TObjectList<TNode>;
    function GetChild(Index: Integer): TNode;
    function GetChildCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Accept(Visitor: TStringVisitor; out Result: string); virtual; abstract;

    procedure AddChild(Node: TNode);

    property ChildCount: Integer read GetChildCount;
    property Children[Index: Integer]: TNode read GetChild;
  end;

  TNodeA = class(TNode)
  strict private
    FValue: Integer;
  public
    constructor Create(Value: Integer);

    procedure Accept(Visitor: TStringVisitor; out Result: string); override;

    property Value: Integer read FValue;
  end;

  TNodeB = class(TNode)
  strict private
    FValue: Double;
  public
    constructor Create(Value: Double);

    procedure Accept(Visitor: TStringVisitor; out Result: string); override;

    property Value: Double read FValue;
  end;

  TStringVisitor = class
  public
    procedure Visit(Node: TNodeA; out Result: string); overload; virtual; abstract;
    procedure Visit(Node: TNodeB; out Result: string); overload; virtual; abstract;
  end;

  TConcatVisitor = class(TStringVisitor)
  public
    procedure Visit(Node: TNodeA; out Result: string); override;
    procedure Visit(Node: TNodeB; out Result: string); override;
  end;



{ TNode }

procedure TNode.AddChild(Node: TNode);
begin
  FChildren.Add(Node);
end;

constructor TNode.Create;
begin
  inherited Create;
  FChildren := TObjectList<TNode>.Create(True);
end;

destructor TNode.Destroy;
begin
  FChildren.Free;
  inherited;
end;

function TNode.GetChild(Index: Integer): TNode;
begin
  Result := FChildren[Index];
end;

function TNode.GetChildCount: Integer;
begin
  Result := FChildren.Count;
end;

{ TNodeA }

procedure TNodeA.Accept(Visitor: TStringVisitor; out Result: string);
begin
  Visitor.Visit(Self, Result);
end;

constructor TNodeA.Create(Value: Integer);
begin
  inherited Create;
  FValue := Value;
end;

{ TNodeB }

procedure TNodeB.Accept(Visitor: TStringVisitor; out Result: string);
begin
  Visitor.Visit(Self, Result);
end;

constructor TNodeB.Create(Value: Double);
begin
  inherited Create;
  FValue := Value;
end;

{ TConcatVisitor }

procedure TConcatVisitor.Visit(Node: TNodeA; out Result: string);
var
  i: Integer;
  ChildRes: string;
begin
  Result := IntToStr(Node.Value);

  for i := 0 to Node.ChildCount - 1 do
  begin
    Node.Children[i].Accept(Self, ChildRes);

    Result := Result + SLineBreak + Childres;
  end;
end;

procedure TConcatVisitor.Visit(Node: TNodeB; out Result: string);
var
  i: Integer;
  ChildRes: string;
begin
  Result := FloatToStr(Node.Value);

  for i := 0 to Node.ChildCount - 1 do
  begin
    Node.Children[i].Accept(Self, ChildRes);

    Result := Result + ', ' + Childres;
  end;
end;

procedure TForm2.FormCreate(Sender: TObject);
var
  t: TNode;
  s: string;
  ConcatVisitor: TConcatVisitor;
begin
  t := TNodeA.Create(10);
  try
    t.AddChild(TNodeB.Create(2.5));

    ConcatVisitor := TConcatVisitor.Create;
    try
      t.Accept(ConcatVisitor, s);
    finally
      ConcatVisitor.Free;
    end;

    ShowMessage(s);
  finally
    t.Free;
  end;
end;
Das funktioniert wie gewünscht.

Aber mal angenommen ich will einen zweiten Visitor machen, der ein Double zurückgibt (die Summe aller Werte der Knoten), müsste ich so etwas verwenden:

Code:
  TSumVisitor = class(TVisitor<Double>)
  public
    procedure Visit(Node: TNodeA; out Result: Double); override;
    procedure Visit(Node: TNodeB; out Result: Double); override;
  end;
Dafür müsste ich allerdings TNode.Accept auch generisch machen, was nicht erlaubt ist weil die Methode auch virtuell ist:

Code:
  TNode = class
  public
    procedure Accept<T>(Visitor: TVisitor<T>; out Result: T); virtual; abstract;
  ...
  end;
Hat jemand eine Idee wie man das lösen kann, so dass man verschiedene Typen zurückgeben kann?
Typecasting eines Pointers würde natürlich gehen, möchte ich aber wegen Typsicherheit vermeiden.

P.S.: Meine Delphi-Version ist XE3, keine Ahnung warum hier 5 steht (nie gehabt).
HxD, schneller Hexeditor:
http://mh-nexus.de/hxd

Geändert von mael (11. Mär 2020 um 11:26 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von DeddyH
DeddyH

Registriert seit: 17. Sep 2006
Ort: Barchfeld
27.619 Beiträge
 
Delphi 12 Athens
 
#2

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 11:29
Ich habe es jetzt nicht im Detail komplett nachvollzogen, aber hast Du Dir mal Uwe Raabes Blogposts zum Visitor Pattern angesehen? Im 4. Teil wird es sehr abstrakt, kann man damit nicht etwas zusammenbasteln?
Detlef
"Ich habe Angst vor dem Tag, an dem die Technologie unsere menschlichen Interaktionen übertrumpft. Die Welt wird eine Generation von Idioten bekommen." (Albert Einstein)
Dieser Tag ist längst gekommen
  Mit Zitat antworten Zitat
Benutzerbild von mael
mael

Registriert seit: 13. Jan 2005
391 Beiträge
 
Delphi XE3 Professional
 
#3

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 14:18
Danke, kenne den Post, hilft aber leider nicht weiter.
HxD, schneller Hexeditor:
http://mh-nexus.de/hxd
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

Registriert seit: 20. Jan 2006
Ort: Lübbecke
11.442 Beiträge
 
Delphi 12 Athens
 
#4

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 16:37
Ich sehe aktuell keinen Grund, das Ganze generisch zu machen. Es ist nur so, daß du das Pattern nicht korrekt umgesetzt hast und deswegen die Probleme mit dem Typ des result-Werts kommst.

So solltest die niemals t.Accept(Visitor) aufrufen, sondern immer Visitor.Visit(t) . Entsprechend muss das Accept gar nichts über den Result wissen, den es ja eh nur an den Visitor weiterreicht.

Eine brauchbare, allerdings so auf die Schnelle hingeschriebene, Implementierung angelehnt an Part 3 der oben genannten Artikelserie wäre sowas:
Delphi-Quellcode:
type
  TVisitor = class(TInterfacedPersistent)
  end;

  TNode = class
  strict private
    FChildren: TObjectList<TNode>;
    function GetChild(Index: Integer): TNode;
    function GetChildCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Accept(Visitor: TVisitor); virtual; abstract;

    procedure AddChild(Node: TNode);

    property ChildCount: Integer read GetChildCount;
    property Children[Index: Integer]: TNode read GetChild;
  end;

  TNodeA = class(TNode)
  strict private
    FValue: Integer;
  public
    constructor Create(Value: Integer);

    procedure Accept(Visitor: TVisitor); override;

    property Value: Integer read FValue;
  end;

  TNodeB = class(TNode)
  strict private
    FValue: Double;
  public
    constructor Create(Value: Double);

    procedure Accept(Visitor: TVisitor); override;

    property Value: Double read FValue;
  end;

  INodeAVisitor = interface
    ['{290FB9EF-9F7E-4008-9ECA-C52169D200E0}']
    procedure VisitNodeA(Instance: TNodeA);
  end;

  INodeBVisitor = interface
    ['{C893648E-FCA0-4FB2-9400-2AFE3B862256}']
    procedure VisitNodeB(Instance: TNodeB);
  end;

  TNodeVisitor = class(TVisitor, INodeAVisitor, INodeBVisitor)
  protected
    procedure VisitNodeA(Node: TNodeA); virtual; abstract;
    procedure VisitNodeB(Node: TNodeB); virtual; abstract;
  public
    procedure Visit(ANode: TNode); virtual;
  end;

  TConcatVisitor = class(TNodeVisitor)
  private
    FList: TStringList;
    function GetResultValue: string;
  protected
    procedure VisitNodeA(Node: TNodeA); override;
    procedure VisitNodeB(Node: TNodeB); override;
  public
    constructor Create;
    destructor Destroy; override;
    property ResultValue: string read GetResultValue;
  end;
Delphi-Quellcode:
{ TNode }

procedure TNode.AddChild(Node: TNode);
begin
  FChildren.Add(Node);
end;

constructor TNode.Create;
begin
  inherited Create;
  FChildren := TObjectList<TNode>.Create(True);
end;

destructor TNode.Destroy;
begin
  FChildren.Free;
  inherited;
end;

function TNode.GetChild(Index: Integer): TNode;
begin
  Result := FChildren[Index];
end;

function TNode.GetChildCount: Integer;
begin
  Result := FChildren.Count;
end;

{ TNodeA }

procedure TNodeA.Accept(Visitor: TVisitor);
var
  vis: INodeAVisitor;
begin
  if Supports(Visitor, INodeAVisitor, vis) then
    vis.VisitNodeA(Self);
end;

constructor TNodeA.Create(Value: Integer);
begin
  inherited Create;
  FValue := Value;
end;

{ TNodeB }

procedure TNodeB.Accept(Visitor: TVisitor);
var
  vis: INodeBVisitor;
begin
  if Supports(Visitor, INodeBVisitor, vis) then
    vis.VisitNodeB(Self);
end;

constructor TNodeB.Create(Value: Double);
begin
  inherited Create;
  FValue := Value;
end;

procedure TNodeVisitor.Visit(ANode: TNode);
begin
  ANode.Accept(Self);
end;

constructor TConcatVisitor.Create;
begin
  inherited Create;
  FList := TStringList.Create();
end;

destructor TConcatVisitor.Destroy;
begin
  FList.Free;
  inherited Destroy;
end;

function TConcatVisitor.GetResultValue: string;
begin
  Result := FList.Text;
end;

procedure TConcatVisitor.VisitNodeA(Node: TNodeA);
var
  i: Integer;
  res: string;
  saveList: TStringList;
begin
  saveList := FList;
  try
    FList := TStringList.Create;
    try
      FList.Add(IntToStr(Node.Value));
      for i := 0 to Node.ChildCount - 1 do
      begin
        Visit(Node.Children[i]);
      end;
      res := string.Join(sLineBreak, FList.ToStringArray);
    finally
      FList.Free;
    end;
  finally
    FList := saveList;
  end;
  FList.Add(res);
end;

procedure TConcatVisitor.VisitNodeB(Node: TNodeB);
var
  i: Integer;
  res: string;
  saveList: TStringList;
begin
  saveList := FList;
  try
    FList := TStringList.Create;
    try
      FList.Add(FloatToStr(Node.Value));
      for i := 0 to Node.ChildCount - 1 do
      begin
        Visit(Node.Children[i]);
      end;
      res := string.Join(', ', FList.ToStringArray);
    finally
      FList.Free;
    end;
  finally
    FList := saveList;
  end;
  FList.Add(res);
end;
Delphi-Quellcode:
procedure TForm495.FormCreate(Sender: TObject);
var
  t: TNode;
  s: string;
  ConcatVisitor: TConcatVisitor;
begin
  t := TNodeA.Create(10);
  try
    t.AddChild(TNodeB.Create(2.5));

    ConcatVisitor := TConcatVisitor.Create;
    try
      ConcatVisitor.Visit(t);
      s := ConcatVisitor.ResultValue;
    finally
      ConcatVisitor.Free;
    end;

    ShowMessage(s);
  finally
    t.Free;
  end;
end;
Uwe Raabe
Certified Delphi Master Developer
Embarcadero MVP
Blog: The Art of Delphi Programming
  Mit Zitat antworten Zitat
TiGü

Registriert seit: 6. Apr 2011
Ort: Berlin
3.070 Beiträge
 
Delphi 10.4 Sydney
 
#5

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 17:26
Ich sehe aktuell keinen Grund, das Ganze generisch zu machen.
Man könnte natürlich auch sowas machen, um bei vielen Datentypen ein paar Zeilen zu sparen, aber ist nur nice-to-have:

Delphi-Quellcode:
  TGenericNode<T> = class(TNode)
  strict private
    FValue: T;
  public
    constructor Create(Value: T);

    property Value: T read FValue;
  end;

  TNodeA = class(TGenericNode<Integer>)
  public
    procedure Accept(Visitor: TVisitor); override;
  end;

  TNodeB = class(TGenericNode<Double>)
  public
    procedure Accept(Visitor: TVisitor); override;
  end;
  Mit Zitat antworten Zitat
Benutzerbild von mael
mael

Registriert seit: 13. Jan 2005
391 Beiträge
 
Delphi XE3 Professional
 
#6

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 17:52
So solltest die niemals t.Accept(Visitor) aufrufen, sondern immer Visitor.Visit(t) .
Ich bin mir nicht sicher ob wir uns da missverstehen, aber folgendes C#-Beispiel tut es auch so:
https://en.wikipedia.org/wiki/Visito...n#C.23_example
Code:
e.Accept(expressionPrinter);
Siehe auch die Methode "Visit(Addition addition)" die "addition.Left.Accept" aufruft.

Es ist nur so, daß du das Pattern nicht korrekt umgesetzt hast und deswegen die Probleme mit dem Typ des result-Werts kommst. Entsprechend muss das Accept gar nichts über den Result wissen, den es ja eh nur an den Visitor weiterreicht.
In meinem Fall schon, da meine Anforderungen andere sind. Siehe unten.

Zitat:
Eine brauchbare, allerdings so auf die Schnelle hingeschriebene, Implementierung angelehnt an Part 3 der oben genannten Artikelserie wäre sowas:
Danke für das Codebeispiel.
Mein Minimalbeispiel kann man natürlich so umschreiben, (auch wenn Interfaces jetzt unter Delphi nicht sehr praktisch sind) das ist schon klar. Aber das Ziel ist es das Visitorpattern so zu erweitern, dass man zusätzliche Parameter/Rückgabewerte haben kann.

Das ist sehr nützlich um kompakten Code zu schreiben, der den Stack ausnutzt/Rekursion. Wenn ich das ganze explizit verwalten muss, analog zu einer StringList, wird es sehr unelegant.
HxD, schneller Hexeditor:
http://mh-nexus.de/hxd

Geändert von mael (11. Mär 2020 um 17:55 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

Registriert seit: 20. Jan 2006
Ort: Lübbecke
11.442 Beiträge
 
Delphi 12 Athens
 
#7

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 18:28
Das C# Beispiel betrachte ich mal als gutes Beispiel, wie man es nicht machen sollte. Nur weil C# davor steht, ist der Code ja nicht automatisch über jeden Zweifel erhaben.

Mein Minimalbeispiel kann man natürlich so umschreiben, (auch wenn Interfaces jetzt unter Delphi nicht sehr praktisch sind) das ist schon klar.
Nun, ich kann nur kommentieren, was ich sehe. Da ich deine realen Anforderungen nicht kenne, kann ich dazu auch keine Vorschläge machen.

Aber das Ziel ist es das Visitorpattern so zu erweitern, dass man zusätzliche Parameter/Rückgabewerte haben kann.
Das Ziel geht meiner Meinung nach konträr zum ursprünglichen Anliegen des Visitor Patterns - der Entkopplung der Strukturklassen und deren Verarbeitung. Ist aber eben auch nur meine Meinung.
Uwe Raabe
Certified Delphi Master Developer
Embarcadero MVP
Blog: The Art of Delphi Programming
  Mit Zitat antworten Zitat
Benutzerbild von mael
mael

Registriert seit: 13. Jan 2005
391 Beiträge
 
Delphi XE3 Professional
 
#8

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 21:11
Das C# Beispiel betrachte ich mal als gutes Beispiel, wie man es nicht machen sollte. Nur weil C# davor steht, ist der Code ja nicht automatisch über jeden Zweifel erhaben.
Da hast du natürlich Recht. Ist halt die Version die ich üblicherweise kenne, bin aber gerne bereit was zu lernen.
Was ist denn der praktische Vorteil deiner Lösung, also was "So solltest die niemals t.Accept(Visitor) aufrufen, sondern immer Visitor.Visit(t)" angeht? Inwiefern entkoppelt es mehr da man sich ja in beiden Fällen auf eine abstrakte Klasse/Interface bezieht?

Zitat:
Mein Minimalbeispiel kann man natürlich so umschreiben, (auch wenn Interfaces jetzt unter Delphi nicht sehr praktisch sind) das ist schon klar.
Nun, ich kann nur kommentieren, was ich sehe. Da ich deine realen Anforderungen nicht kenne, kann ich dazu auch keine Vorschläge machen.
Schwer das mit wenig Code zu illustrieren. Ein Beispiel wäre das "pretty printing" eines Syntaxbaums (AST). Hierzu muss man das Einrückungslevel mitliefern, bzw. einen Präfixstring, der je nach Knoten anders ist, an die Kindknoten weiterreichen.

Hier gibt es ähnliche Fragen:
https://stackoverflow.com/questions/...h-return-value
https://stackoverflow.com/questions/...isitor-pattern

Zitat:
Aber das Ziel ist es das Visitorpattern so zu erweitern, dass man zusätzliche Parameter/Rückgabewerte haben kann.
Das Ziel geht meiner Meinung nach konträr zum ursprünglichen Anliegen des Visitor Patterns - der Entkopplung der Strukturklassen und deren Verarbeitung. Ist aber eben auch nur meine Meinung.
Deswegen will ich die Parameter ja allgemein halten.

Im Endeffekt sollte etwas wie die Lösung für Format() gut funktionieren:
procedure Accept(Visitor: TVisitor; const Args: array of const); Den Rückgabewert könnte man dann über eine Instanz-Variable des Visitors lösen, wie in deinem Vorschlag, und auch in dem C++-Beispiel oben. Im Endeffekt ist das dann eine Art Callbackfunktion (die ja auch häufig zusätzliche "User"-Parameter haben).

Damit wäre ich eigentlich zufrieden. Bleibt die obige Frage übrig.
HxD, schneller Hexeditor:
http://mh-nexus.de/hxd

Geändert von mael (11. Mär 2020 um 21:21 Uhr)
  Mit Zitat antworten Zitat
hoika

Registriert seit: 5. Jul 2006
Ort: Magdeburg
8.275 Beiträge
 
Delphi 10.4 Sydney
 
#9

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 21:20
Hallo,
ohne mich jetzt mit dem Code tief beschäftigt zu haben

Zitat:
zusätzliche Parameter/Rückgabewerte
Hilft es, mit einer generischen, oder abstrakten Parameterklasse zu arbeiten?
Heiko
  Mit Zitat antworten Zitat
Benutzerbild von Uwe Raabe
Uwe Raabe

Registriert seit: 20. Jan 2006
Ort: Lübbecke
11.442 Beiträge
 
Delphi 12 Athens
 
#10

AW: Generisches Visitor-Pattern

  Alt 11. Mär 2020, 22:55
Inwiefern entkoppelt es mehr da man sich ja in beiden Fällen auf eine abstrakte Klasse/Interface bezieht?
Weil dann der Visitor entscheidet, wann und wie er das Accept aufruft. Er könnte das auch einem anderen Visitor überlassen, den er temporär erstellt und dann den Node an dessen Visit weitergibt. Das hatte ich mir auch schon überlegt, um die unterschiedlichen Separatoren (sLineBreak und ', ') zu abstrahieren.

Zitat:
Hierzu muss man das Einrückungslevel mitliefern, bzw. einen Präfixstring, der je nach Knoten anders ist, an die Kindknoten weiterreichen.
Das lässt sich aber ganz hervorragend innerhalb der Visitorklasse regeln. Der Knoten soll sich ja um sowas gar nicht kümmern. Der muss nur die nötigen Properties bereitstellen.

Hier (wieder) mal ein schnelles Beispiel basierend auf dem vorigen Code (Create, Destroy und GetResultValue entsprechen TContentVisitor). Es wird einfach die Baumstruktur mit entsprechenden Einrückungen und Präfix dargestellt. Dabei werden die Node-Typen unterschiedlich formatiert. Man sieht hier auch deutlich den Vorteil der virtuellen Visit Methode gegenüber einem Aufruf von Accept:
Delphi-Quellcode:
type
  TIndentVisitor = class(TNodeVisitor)
  private
    FLevel: Integer;
    FList: TStringList;
    FPrefix: string;
    function GetResultValue: string;
  protected
    procedure VisitNodeA(Node: TNodeA); override;
    procedure VisitNodeB(Node: TNodeB); override;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Visit(ANode: TNode); override;
    property ResultValue: string read GetResultValue;
  end;
Delphi-Quellcode:
procedure TIndentVisitor.Visit(ANode: TNode);
var
  i: Integer;
  savePrefix: string;
begin
  savePrefix := FPrefix;
  inherited; // hier wird implizit das Accept aufgerufen, was auf die jeweilige VisitNodeA/B Methode verzweigt
  FList.Add(StringOfChar(' ', 2*FLevel) + FPrefix);
  Inc(FLevel);
  for i := 0 to ANode.ChildCount - 1 do
  begin
    Visit(ANode.Children[i]);
  end;
  Dec(FLevel);
  FPrefix := savePrefix;
end;

procedure TIndentVisitor.VisitNodeA(Node: TNodeA);
begin
  FPrefix := FPrefix + '.' + Node.ClassName;
end;

procedure TIndentVisitor.VisitNodeB(Node: TNodeB);
begin
  FPrefix := FPrefix + '.[' + Node.ClassName + ']';
end;
Uwe Raabe
Certified Delphi Master Developer
Embarcadero MVP
Blog: The Art of Delphi Programming
  Mit Zitat antworten Zitat
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 06:22 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz