Dynamiczne podstawianie klas w Delphi 7.

0

Tak się zastanawiałem, czy istnieje w Delphi możliwość wywołania pola/metody obiektu nieokreślonej klasy, a potem w innym miejscu w kodzie określenia jaka to klasa? Załóżmy, że mamy kilka podobnych klas wykonujących te same operacje ale każda eksportuje wyniki do pliku o innym rozszerzeniu. Teraz chcę napisać funkcję wykonującą kilka operacji wspólnych dla wszystkich tych klas, a następnie eksportującą wynik do pliku. Szkoda by było dla każdego rozszerzenia pisać osobne funkcje wykorzystujące w każdym przypadku te same pola/metody różnych klas, więc zastanawiałem się czy nie byłoby możliwe napisać jedną funkcję a później w zależności od wyboru użytkownika dynamicznie określić którą klasę wykorzystuje. Przykładowo, załóżmy że mamy dwa takie same pola w różnych klasach:

ExportToBMP.Filename
ExportToPNG.Filename

Czy dałoby radę napisać funkcję która wykorzystywałaby to pole w następujący sposób:

procedure Exportuj;
begin
	// (...)
	temp.Filename = Edit1.Text;
	// (...)
end;

...a później gdzieś w kodzie obiekt temp zostałby ustalony jako instancja ExportToBMP, ExportToPNG etc. w zależności od np. wartości ComboBoxa? Takie coś bardzo by uprościło kod. Można tak w Delphi zrobić? Czy raczej trzeba będzie każdą funkcję pisać z osobna?

2

Jeśli klasy ExportToPNG i ExportToPNG dziedziczą po tej samej klasie bazowej np CustomExport gdzie znajduje się pole/właściwość FileName to to swoje temp zadeklaruj jako CustomExport a w parametrze przekazuj już obiekt właściwej klasy.

Tu miałem podobny problem (link do odpowiedzi)//4programmers.net/Forum/1068614

1
Thunderlane napisał(a)

Tak się zastanawiałem, czy istnieje w Delphi możliwość wywołania pola/metody obiektu nieokreślonej klasy, a potem w innym miejscu w kodzie określenia jaka to klasa?

Nie, taka możliwość nie istnieje - zawsze musi być wiadomo z której klasy zostaje wywołana metoda;

Thunderlane napisał(a)

Załóżmy, że mamy kilka podobnych klas wykonujących te same operacje ale każda eksportuje wyniki do pliku o innym rozszerzeniu. Teraz chcę napisać funkcję wykonującą kilka operacji wspólnych dla wszystkich tych klas, a następnie eksportującą wynik do pliku.

W najlepszym wypadku wszystkie te klasy będą spokrewnione, o czym napisał poprzednik; Jeżeli wszystkie dziedziczą z tej samej klasy bazowej, to możesz korzystać ze wszystkiego, co posiada właśnie ta klasa bazowa (z wyjątkiem metod abstrakcyjnych oraz tych, które nie posiadają definicji, ale są zadeklarowane);

Przykładem jest korzystanie np. z bazowej klasy strumieni TStream; Jeżeli zadeklarujesz sobie w jakiejś metodzie parametr tej klasy, to możesz w nim przekazać referencję do instancji klasy TStream, TFileStream, TMemoryStream czy TStringStream, czyli wszystkich klasy dziedziczących z TStream oraz nią samą; Ale to nie zmienia faktu, że cokolwiek by się nie przekazało w parametrze - dostęp będzie jedynie do składowych klasy TStream; Podobnie będzie w Twoim przypadku - będziesz mógł skorzystać jedynie ze składowych klasy bazowej, o ile taka istnieje;

Wymyślony przykład z dziedziczeniem klas do obsługi zapisu plików bmp i svg, dziedziczących z jednej klasy bazowej;

type
  TPictureObject = class(TObject)
  public
    procedure SaveToFile(AFileName: TFileName); virtual;
  end;

  procedure TPictureObject.SaveToFile(AFileName: TFileName);
  begin
    raise Exception.Create('cannot save picture to file in base class');
  end;

type
  TBitmapObject = class(TPictureObject)
  public
    procedure SaveToFile(AFileName: TFileName); override;
  end;

  procedure TBitmapObject.SaveToFile(AFileName: TFileName);
  begin
    WriteLn('saving to BMP file: ', AFileName);
  end;

type
  TSVGObject = class(TPictureObject)
  public
    procedure SaveToFile(AFileName: TFileName); override;
  end;

  procedure TSVGObject.SaveToFile(AFileName: TFileName);
  begin
    WriteLn('saving to SVG file: ', AFileName);
  end;

  procedure SavePictureToFile(APicture: TPictureObject; AFileName: TFileName);
  begin
    APicture.SaveToFile(AFileName);
  end;

var
  bmpObject: TBitmapObject;
  svgObject: TSVGObject;
begin
  bmpObject := TBitmapObject.Create();
  svgObject := TSVGObject.Create();
  try
    SavePictureToFile(bmpObject, 'C:\Picture.bmp');
    SavePictureToFile(svgObject, 'C:\Picture.svg');
  finally
    bmpObject.Free();
    svgObject.Free();
  end;
end.

Na wyjściu otrzymamy:

saving to BMP file: C:\Picture.bmp
saving to SVG file: C:\Picture.svg

A teraz przykład z brakiem powiązania pomiędzy tymi klasami:

type
  TBitmapObject = class(TObject)
  public
    procedure SaveToFile(AFileName: TFileName);
  end;

  procedure TBitmapObject.SaveToFile(AFileName: TFileName);
  begin
    WriteLn('saving to BMP file: ', AFileName);
  end;

type
  TSVGObject = class(TObject)
  public
    procedure SaveToFile(AFileName: TFileName);
  end;

  procedure TSVGObject.SaveToFile(AFileName: TFileName);
  begin
    WriteLn('saving to SVG file: ', AFileName);
  end;

  procedure SavePictureToFile(APicture: TObject; AFileName: TFileName);
  begin
    if APicture is TBitmapObject then
      TBitmapObject(APicture).SaveToFile(AFileName)
    else
      if APicture is TSVGObject then
        TSVGObject(APicture).SaveToFile(AFileName)
      else
        raise Exception.Create('unknown object to save to file');
  end;

var
  bmpObject: TBitmapObject;
  svgObject: TSVGObject;
begin
  bmpObject := TBitmapObject.Create();
  svgObject := TSVGObject.Create();
  try
    SavePictureToFile(bmpObject, 'C:\Picture.bmp');
    SavePictureToFile(svgObject, 'C:\Picture.svg');
  finally
    bmpObject.Free();
    svgObject.Free();
  end;
end.

Wyjście będzie dokładnie takie samo jak w poprzednim przkładnie, jednak procedura SavePictureToFile musi działać inaczej - sprawdzać co zostało przekazane w parametrze APicture i wykonać rzutowanie;

Sprawdź więc jak wygląda hierarchia Twoich dwóch klas i wybierz obpowiednią metodę ich obsługi.

1
furious programming napisał(a):
Thunderlane napisał(a)

Tak się zastanawiałem, czy istnieje w Delphi możliwość wywołania pola/metody obiektu nieokreślonej klasy, a potem w innym miejscu w kodzie określenia jaka to klasa?

Nie, taka możliwość nie istnieje - zawsze musi być wiadomo z której klasy zostaje wywołana metoda;

Po pierwsze istnieje tak możliwość i nazywamy to polimorfizmem.
Ba - możemy nawet wywołać dowolną metodę dowolnego obiektu za pomocą RTTI (ok, ale to nie w Delphi7) - wtedy nie musimy znać żadnego typu klasy, wystarczy sama wiedza o nazwie metody i referencja na obiekt.
Po drugie - po co mi konkretna klasa, skoro wystarczy mi sam adapter na metody? Implementacja takiego adaptera może być np. za pomocą klasy abstrakcyjnej lub za pomocą interfejsu implementowanego przez delegację (co polecam).
Tak czy siak oba rozwiązania eliminują to co napisałeś - powiązanie klas na etapie deklaracji, co jest zawsze dobrym pomysłem, ale ma jedną wadę - trzeba ciut więcej napisać kodu, a nie tylko go na chama spinać drutem.

Thunderlane napisał(a)

Załóżmy, że mamy kilka podobnych klas wykonujących te same operacje ale każda eksportuje wyniki do pliku o innym rozszerzeniu. Teraz chcę napisać funkcję wykonującą kilka operacji wspólnych dla wszystkich tych klas, a następnie eksportującą wynik do pliku.

W najlepszym wypadku wszystkie te klasy będą spokrewnione, o czym napisał poprzednik; Jeżeli wszystkie dziedziczą z tej samej klasy bazowej, to możesz korzystać ze wszystkiego, co posiada właśnie ta klasa bazowa (z wyjątkiem metod abstrakcyjnych oraz tych, które nie posiadają definicji, ale są zadeklarowane);

Ty chyba nie rozumiesz idei klas abstrakcyjnych, bo używa się ich w dokładnie odwrotnym celu!
Właśnie chodzi o to, że posługujemy się w kodzie typem klasy abstrakcyjnej i jej metodami - również abstrakcyjnymi.
Ale w czasie wykonania, należy dostarczyć obiekt klasy skonkretyzowanej, a nie abstrakcyjnej.
Efekt jest taki, że kod używający takiej klasy nie ma pojęcia i jej szczegółach implantacyjnych - i bardzo dobrze. ponieważ to pozwala łatwo rozszerzać taki kod o kolejne konkretne implementacje - np. obsługę dodatkowych typów plików graficznych.
Zresztą tak to jest zrealizowane w samym VCL przy obsłudze typów plików graficznych - polecam przejrzenie źródeł...
/ciach/

0

Ba - możemy nawet wywołać dowolną metodę dowolnego obiektu za pomocą RTTI (ok, ale to nie w Delphi7) - wtedy nie musimy znać żadnego typu klasy, wystarczy sama wiedza o nazwie metody i referencja na obiekt.

Nie sądzę, aby pytacz wiedział czym jest RTTI, umiał się tym posługiwać i było mu to potrzebne; Czasem sam korzystam z RTTI, tyle że w przypadku typów prostych, a nie klas; Ale Ok - napisać o tym nie zaszkodziło, ale nie przeze mnie - nigdy nie łączyłem RTTI z klasami, więc trudno abym coś o tym pisał;

Po drugie - po co mi konkretna klasa, skoro wystarczy mi sam adapter na metody? Implementacja takiego adaptera może być np. za pomocą klasy abstrakcyjnej lub za pomocą interfejsu implementowanego przez delegację (co polecam).

Owszem, pod warunkiem że pytacz dopiero stworzy takie klasy, a nie korzysta z już istniejących; Ale w sumie nic nie wiemy o tych jego klasach, więc nie ma co opisywać wszystkich możliwości, bo to nie artykuł;

Ty chyba nie rozumiesz idei klas abstrakcyjnych, bo używa się ich w dokładnie odwrotnym celu!

Moment, bo nie zrozumiałeś moich słów; To zdanie:

ja napisał(a)

Jeżeli wszystkie dziedziczą z tej samej klasy bazowej, to możesz korzystać ze wszystkiego, co posiada właśnie ta klasa bazowa (z wyjątkiem metod abstrakcyjnych oraz tych, które nie posiadają definicji, ale są zadeklarowane);

jest prawdziwe, bo nie można skorzystać z metod abstrakcyjnych - trzeba je dopiero przedefiniować; Wystarczyłoby w moim pierwszym przykładzie przerobić metodę TPictureObject.SaveToFile na abstrakcyjną i już nie można by z niej skorzystać, jeśli otrzymałoby się w parametrze obiekt tej klasy; Jeżeli podany byłby do procedury obiekt klasy TBitmapObject lub TSVGObject, to wszystko by działało prawidłowo, tak jak to wygląda właśnie w tym przykładzie;

W klasach bazowych używa się też metod nieabstrakcyjnych, ale pustych, bez kodu ich definicji - trudno więc z takich metod korzystać, bez ich przedefiniowania; To są jakieś marginalne przypadki, rzadko ale można i takie spotkać - np. TStrings.PutObject czy TStrings.SetCapacity, które co prawda są wirtualne i posiadają swoje definicje, ale puste; Rozumiem ideę klas abstrakcyjnych, korzystam z nich, a Ty po prostu nie zrozumiałeś moich słów;

Zresztą tak to jest zrealizowane w samym VCL przy obsłudze typów plików graficznych - polecam przejrzenie źródeł...

Nie mam możliwości przeglądnięcia źródeł VCL, bo nie posiadam wersji Enterprise.

0
furious programming napisał(a):

Ba - możemy nawet wywołać dowolną metodę dowolnego obiektu za pomocą RTTI (ok, ale to nie w Delphi7) - wtedy nie musimy znać żadnego typu klasy, wystarczy sama wiedza o nazwie metody i referencja na obiekt.

Nie sądzę, aby pytacz wiedział czym jest RTTI, umiał się tym posługiwać i było mu to potrzebne; Czasem sam korzystam z RTTI, tyle że w przypadku typów prostych, a nie klas; Ale Ok - napisać o tym nie zaszkodziło, ale nie przeze mnie - nigdy nie łączyłem RTTI z klasami, więc trudno abym coś o tym pisał;

Ja niczego nie zakładam, czy ktoś coś wie czy nie. Napisałem o możliwościach, a co kto z tym zrobi - to jego sprawa.

Po drugie - po co mi konkretna klasa, skoro wystarczy mi sam adapter na metody? Implementacja takiego adaptera może być np. za pomocą klasy abstrakcyjnej lub za pomocą interfejsu implementowanego przez delegację (co polecam).

Owszem, pod warunkiem że pytacz dopiero stworzy takie klasy, a nie korzysta z już istniejących; Ale w sumie nic nie wiemy o tych jego klasach, więc nie ma co opisywać wszystkich możliwości, bo to nie artykuł;

To nie jest konieczne, ale to naprawdę nie ten temat. Ale jak kogoś to naprawdę interesuje, to niech sobie sprawdzi np. jak we frameorku mORMomt zrealizowano implementację interfejsu serwera w kliencie - genialne!

Ty chyba nie rozumiesz idei klas abstrakcyjnych, bo używa się ich w dokładnie odwrotnym celu!

Moment, bo nie zrozumiałeś moich słów; To zdanie:

ja napisał(a)

Jeżeli wszystkie dziedziczą z tej samej klasy bazowej, to możesz korzystać ze wszystkiego, co posiada właśnie ta klasa bazowa (z wyjątkiem metod abstrakcyjnych oraz tych, które nie posiadają definicji, ale są zadeklarowane);

jest prawdziwe, bo nie można skorzystać z metod abstrakcyjnych - trzeba je dopiero przedefiniować; Wystarczyłoby w moim pierwszym przykładzie przerobić metodę TPictureObject.SaveToFile na abstrakcyjną i już nie można by z niej skorzystać, jeśli otrzymałoby się w parametrze obiekt tej klasy; Jeżeli podany byłby do procedury obiekt klasy TBitmapObject lub TSVGObject, to wszystko by działało prawidłowo, tak jak to wygląda właśnie w tym przykładzie;

Naprawdę, czytaj uważnie wypowiedzi innych - szkoda mi czasu na tłumaczenie że mówimy to samo używając innych pojęć.
I tak - klasy abstrakcyjnej się nie przedefiniuje, tylko konkretyzuje.
Metody pustej też się nie "przedefiniuje" (bo jej definicja jest identyczna, ale implementacja już nie), tylko nadpisuje lub przesłania (override).
OK, zostawmy tę semantykę.

W klasach bazowych używa się też metod nieabstrakcyjnych, ale pustych, bez kodu ich definicji - trudno więc z takich metod korzystać, bez ich przedefiniowania;

Nadpisania ;-)

To są jakieś marginalne przypadki, rzadko ale można i takie spotkać - np. TStrings.PutObject czy TStrings.SetCapacity, które co prawda są wirtualne i posiadają swoje definicje, ale puste;

OK, ale wiesz dlaczego są zadeklarowane (i zaimplmentowane) jako puste i wirtualne, a nie abstrakcyjne i wirtualne?

Rozumiem ideę klas abstrakcyjnych, korzystam z nich, a Ty po prostu nie zrozumiałeś moich słów;

Nie, ja się po prostu czepiłem Twojej nieprecyzyjnej wypowiedzi.

Zresztą tak to jest zrealizowane w samym VCL przy obsłudze typów plików graficznych - polecam przejrzenie źródeł...

Nie mam możliwości przeglądnięcia źródeł VCL, bo nie posiadam wersji Enterprise.

No popatrz, a ja to widzę w wersji Pro.

0

Naprawdę, czytaj uważnie wypowiedzi innych - szkoda mi czasu na tłumaczenie że mówimy to samo używając innych pojęć.
I tak - klasy abstrakcyjnej się nie przedefiniuje, tylko konkretyzuje.

No właśnie - czytaj uważnie wypowiedzi innych; Ja napisałem o metodzie abstrakcyjnej, a Ty komentujesz moje słowa pisząc o klasach abstrakcyjnych;

OK, zostawmy tę semantykę.

Zostawmy, bo nadpisywanie i przedefiniowywanie oznacza to samo; Przynajmniej w materiałach z których się uczyłem programowania w Delphi; Ale niech będzie, że nadpisywanie - to w sumie tłumaczenie słowa Override;

OK, ale wiesz dlaczego są zadeklarowane (i zaimplmentowane) jako puste i wirtualne, a nie abstrakcyjne i wirtualne?

Nie zastanawiałem się nad tym, ale mile widziane wytłumaczenie;

Nie, ja się po prostu czepiłem Twojej nieprecyzyjnej wypowiedzi.

Skup się raczej na pomocy pytaczom, a nie czepianiu się wypowiedzi odpowiadających;

No popatrz, a ja to widzę w wersji Pro.

No popatrz, a ja nie widzę tego w wersji Personal; Nie mam źródeł i już, więc obojętne jaką bym wersję ze źródłami nie podał, to i tak takiej nie posiadam.

0
furious programming napisał(a):

Naprawdę, czytaj uważnie wypowiedzi innych - szkoda mi czasu na tłumaczenie że mówimy to samo używając innych pojęć.
I tak - klasy abstrakcyjnej się nie przedefiniuje, tylko konkretyzuje.

No właśnie - czytaj uważnie wypowiedzi innych; Ja napisałem o metodzie abstrakcyjnej, a Ty komentujesz moje słowa pisząc o klasach abstrakcyjnych;

Zgoda, trafiony.

OK, zostawmy tę semantykę.

Zostawmy, bo nadpisywanie i przedefiniowywanie oznacza to samo; Przynajmniej w materiałach z których się uczyłem programowania w Delphi; Ale niech będzie, że nadpisywanie - to w sumie tłumaczenie słowa Override;

A co to za materiały, jeśli można wiedzieć? Bo jeśli to dostępne tu poradniki i książka założyciela portalu, to... są zdecydowanie lepsze, a na pewno bardziej aktualne, źródła ;-)
Zapytam inaczej - książkę Nicka Hodegesa "Coding in Delphi" ktoś czytał?

OK, ale wiesz dlaczego są zadeklarowane (i zaimplmentowane) jako puste i wirtualne, a nie abstrakcyjne i wirtualne?

Nie zastanawiałem się nad tym, ale mile widziane wytłumaczenie;

Żeby to zrozumieć, to trzeba podać rozbudowany przykład, z którego i tak 3/5 nic nie zrozumie, bo nie ma tam komponentów do klikania...
Nie chce mi się.
Natomiast kuriozalne jest to, że wyjaśniasz cokolwiek ale sam nie wiesz po co tak i dlaczego tak.
Ale żeby nie było, to jednym zdaniem; po to, aby zachować ciągłość logiki w danej klasie, czyli wywołać konkretną metodę (nawet abstrakcyjną), która jest konkretyzowana w klasach potomnych.
I np. dlatego RTL posługuje się często klasą TStrings, która jest konkretyzowana przez TStringList - bezpośrednie utworzenie obiektu klasy TStrings skończyłoby się wyjątkiem.

Nie, ja się po prostu czepiłem Twojej nieprecyzyjnej wypowiedzi.

Skup się raczej na pomocy pytaczom, a nie czepianiu się wypowiedzi odpowiadających;

Nawet jeśli odpowiedzi odpowiadających są od czapy?
Odpowiem za Ciebie - oczywiście że tak!
A przynajmniej ja to tak rozumiem, co zabija wszelką dyskusję, która mogłaby się ciekawie rozwinąć.

No popatrz, a ja to widzę w wersji Pro.

No popatrz, a ja nie widzę tego w wersji Personal; Nie mam źródeł i już, więc obojętne jaką bym wersję ze źródłami nie podał, to i tak takiej nie posiadam.

To po kiego grzyba pisałeś o Enterpirse? ;-)

0

Tak, to te materiały, ale skoro uważasz je za słabe - popraw je, każdy zarejestrowany użytkownik może to zrobić; Co do przykładu - nie to nie, skoro Ci "się nie chce"... Jeśli chodzi o źródła - mam wersję Personal bez źródeł - zadowolony? Czy jeszcze dokładniej wyjaśnić, aby nie dawać Ci kolejnego powodu do czepiania się?

0
furious programming napisał(a):

Tak, to te materiały, ale skoro uważasz je za słabe - popraw je, każdy zarejestrowany użytkownik może to zrobić;

To że mogę, nie znaczy że chcę, czytaj: nie mam tyle czasu.
I tak - są słabe. Nie tyle błędne, bo przecież działają, ale niektóre patenty są... kuriozalne.

Dam przykład, "Rozdział 17. Bazy danych dbExpress".
Autor całkowicie pominął obsługę parametrów (sparametryzowanych zapytań) i z uporem maniaka wszędzie skleja stringi.
To jest ZŁE! Jest nieoptymalne (brak preparacji zapytań), pozwala na SQLInjection (wszystko leci jako string) itd.

Ładowanie danych do ListView będzie działać poprawnie, pod warunkiem że w tej przykładowej bazie danych nie będzie wartości null. Tylko widzisz, autor na etapie deklaracji tabeli towary nie dopuszcza wartości null dla żadnego pola (ale nie ma ani słowa o tym dlaczego tak), taki sprytny zabieg ;-) Ale gdyby null pojawił się chociaż raz i aplikacja pada na twarz.

Samo ładowanie jest absolutnie nieoptymalne (brak begin/end update dla ListView - załaduj tam 1000 rekordów to sam zobaczysz o czym mowa) i błędne, ponieważ opiera się na pętli for i odczytuje RecordCount, który dla baz SQL (a tylko z takimi dbExpress działa) nie zwraca poprawnej wartości; czasem może być tam i -1 - i co taki biedny newbie ma z tym zrobić?

Za coś takiego powinno się lać linijką po łapach.
Rozumiem, że to jest przykład - ale ten przykład uczy złych nawyków i nie tłumaczy dobrych.

Itd. itp.

Co do przykładu - nie to nie, skoro Ci "się nie chce"... Jeśli chodzi o źródła - mam wersję Personal bez źródeł - zadowolony? Czy jeszcze dokładniej wyjaśnić, aby nie dawać Ci kolejnego powodu do czepiania się?

Nie wiem, zobaczymy ;-)

0

A jak inaczej iterować jak nie w pętli z użyciem RecordCount, pytam z ciekawości :)?

1 użytkowników online, w tym zalogowanych: 0, gości: 1