2009-04-20 13 views
9

Dla jednego konkretnego problemu w architekturze aplikacji, nad którą pracuję, interfejsy wydają się być dobrym rozwiązaniem. W szczególności niektóre "obiekty biznesowe" zależą od zestawu ustawień, które są pobierane z bazy danych w rzeczywistej aplikacji. Pozwalając obiektom biznesowym poprosić o interfejs (poprzez Inwersja sterowania), i pozwalając centralnego obiektu TDatabaseSettings zaimplementować te interfejsy, pozwala na lepsze odizolowanie, a tym samym o wiele łatwiejsze testowanie jednostkowe.Omijanie (wyłączanie) Licznik odwołań Delphi dla interfejsów

Jednak w Delphi interfejsy wydają się pochodzić z, w tym przypadku, nieprzyjemnej premii: liczenie odwołań. Oznacza to, że jeśli zrobię coś takiego:

type 
IMySettings = interface 
    function getMySetting: String; 
end; 

TDatabaseSettings = class(..., IMySettings) 
    //... 
end; 

TMyBusinessObject = class(TInterfacedObject, IMySettings) 
    property Settings: IMySettings read FSettings write FSettings; 
end; 

var 
    DatabaseSettings: TDatabaseSettings; 
    // global object (normally placed in a controller somewhere) 

//Now, in some function... 
O := TMyBusinessObject.Create; 
O.Settings := DatabaseSettings; 
// ... do something with O 
O.Free; 

na ostatniej linii (O.Free) mój globalny DatabaseSettings obiekt jest teraz również uwolniony od ostatniego odniesienia interfejs do niego (co zostało zawarte w O) jest stracone !

Jednym z rozwiązań byłoby przechowywanie "globalnego" obiektu DatabaseSettings za pomocą interfejsu; innym rozwiązaniem byłoby przesłonięcie referencyjnego mechanizmu liczącego dla klasy TDatabaseSettings, więc mogę dalej zarządzać DatabaseSettings jako normalnym obiektem (co jest dużo bardziej zgodne z resztą aplikacji).

Podsumowując, moje pytanie brzmi: w jaki sposób wyłączyć mechanizm liczenia odwołań do interfejsu dla konkretnej klasy?

udało mi się znaleźć kilka informacji, które sugeruje przesłanianie metod _AddRefIInterface i _Release dla klasy (TDatabaseSettings w przykładzie); czy ktokolwiek to zrobił?

Czy powiedziałbyś, że nie powinienem tego robić (myląc - tylko zły pomysł?) I znaleźć inne rozwiązanie problemu architektonicznego?

Wielkie dzięki!

Odpowiedz

13

Ok, można go ominąć, ale pytanie brzmi, czy naprawdę chce to. Jeśli chcesz korzystać z interfejsów, lepiej z nich korzystać. Tak, jak go doświadczyłeś, masz problemy, jeśli mieszasz zmienne klasy i interfejsu.

var 
    // DatabaseSettings: TDatabaseSettings; 
    DatabaseSettings : IMySettings; 

//Now, in some function... 
O := TMyBusinessObject.Create; 
O.Settings := DatabaseSettings; 
// ... do something with O 
O.Free; 

Masz teraz drugie odniesienie do interfejsu, a utrata pierwszego nie zwalnia obiektu.

To jak również możliwe, aby utrzymać zarówno klasy i obiektu:

var 
    DatabaseSettings: TDatabaseSettings; 
    DatabaseSettingsInt : IMySettings; 

Pamiętaj, aby ustawić interfejs zaraz po obiekt został utworzony.

Jeśli naprawdę chcesz wyłączyć liczenie odwołań, po prostu musisz utworzyć nowego potomka TObject, który implementuje interfejs IInterface.Ja testowałem na przykładzie poniżej w D2009 i to działa:

// Query Interface can stay the same because it does not depend on reference counting. 
function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult; 
begin 
    if GetInterface(IID, Obj) then 
    Result := 0 
    else 
    Result := E_NOINTERFACE; 
end; 

constructor TMyInterfacedObject.Create; 
begin 
    FRefCount := 1; 
end; 

procedure TMyInterfacedObject.FreeRef; 
begin 
    if Self = nil then 
    Exit; 
    if InterlockedDecrement(FRefCount) = 0 then 
    Destroy;  
end; 

function TMyInterfacedObject._AddRef: Integer; 
begin 
    Result := InterlockedIncrement(FRefCount); 
end; 

function TMyInterfacedObject._Release: Integer; 
begin 
    Result := InterlockedDecrement(FRefCount); 
    if Result = 0 then 
    Destroy; 
end; 

FreeRef tylko obniża RefCount podobnie jak _Release. Możesz go używać tam, gdzie zwykle używasz Wolnego.

+0

Wielkie dzięki za bardzo obszerną odpowiedź, jest to bardzo doceniane! Tak, prawdopodobnie powinienem pomyśleć nieco więcej, zanim przejdę do ciemnej ścieżki wyłączania liczenia referencji. – onnodb

+0

OK, testowałem to w D2009 i działało jak czar ;-). –

+0

Czy mam rację, że twój przykład faktycznie nie wyłącza całkowicie liczenia referencji, ale zamiast tego powoduje, że licznik odwołań zaczyna się od 1? Sądzę, że można to osiągnąć poprzez jawne wywołanie "TMyInterfacedObject._AddRef" zaraz po utworzeniu obiektu, a następnie wykonanie "_Release", gdzie normalnie nazywacie "Free"? – onnodb

7

_AddRef, _Release i _QueryInterface są w rzeczywistości tym, co chcesz zastąpić. Powinieneś bardzo jasno określić, co robisz, ponieważ może to spowodować wycieki pamięci lub dziwne, trudne do znalezienia błędy.

Nie schodzić z TInterfacedObject, zamiast schodzić z TObject i realizować swoje własne wersje dwóch pierwszych z tych metod, które zwracają 1.

+1

To naprawdę szybko ... dzięki! Czy uważasz, że sposób, w jaki chcę korzystać z interfejsów, ma sens? Wydaje mi się dziwne, że w ogóle w ogóle pojawia się odniesienie, naprawdę - dlaczego nie wykorzystać ich jako naprawdę fajnego sposobu rozłączania klas? – onnodb

+0

Liczenie odwołań jest rzeczywiście bardzo proste. Nie musisz zwolnić obiektu; Delphi zrobi to za ciebie, gdy zmienna, do której jest przypisany, wykracza poza zakres. –

+1

Tak, to prawda, to * jest * śliskie (chociaż nie znalazłem jeszcze dla niego zastosowania). Ale fajnie jest też móc go wyłączyć, ponieważ powoduje to niespójność w połączeniu z "tradycyjnym" zarządzaniem obiektem :) – onnodb

3

Wyłączenie liczenia odwołań dla tego rodzaju problemu jest nieprzyjemne. O wiele ładniejszym i architektonicznym rozwiązaniem byłoby użycie pewnego rodzaju wzoru "singleton". Najprostszym sposobem wdrożenia to będzie wyglądać:

interface 

type 

TDatabaseSettings = class(..., IMySettings) 
end; 

function DatabaseSettings: IMySettings; 

implementation 

var 
    GDatabaseSettings: IMySettings; 

function DatabaseSettings: IMySettings; 
begin 
if GDatabaseSettings = nil then GDatabaseSettings := TDatabaseSettings.Create; 
Result := GDatabaseSettings; 
end; 

O := TMyBusinessObject.Create; 
O.Settings := DatabaseSettings; 
O.Free; 

Przy okazji: podczas korzystania z interfejsów: zawsze używać zmiennych interfejsu! Nie mieszaj obu vars interfejsu class en (użyj "var Settings: IMySettings" zamiast "var Settings: TDatabaseSettings"). W przeciwnym razie liczenie odwołań stanie Ci na drodze (automatyczne niszczenie, nieprawidłowe operacje na wskaźnikach itp.). W powyższym rozwiązaniu GDatabaseSettings jest również typu "IMySettings", więc otrzymuje odpowiednią liczbę odwołań i będzie trwać do momentu zakończenia programu.

+0

Tak, to pachnie, a wszyscy tutaj wydają się zgodni. Co za szkoda, ponieważ interfejsy wydają się być dobrym sposobem na rozłączanie obiektów (tylko używanie ich jako "umowy"). Dzięki za wkład! – onnodb

+0

@onnodb: Interfejsy są naprawdę dobrą pomocą dla prawidłowo rozłączonego projektu. Jeśli nie musisz mieć okrągłych odniesień, przeliczanie nie jest wcale złe - po prostu musisz zachować ostrożność podczas mieszania interfejsów z odniesieniami do obiektów implementujących. Nie rób tego, a będziesz szczęśliwy. Ponieważ klasy mogą implementować wiele interfejsów, generalnie nie ma potrzeby bezpośredniego korzystania z referencji do obiektów, wystarczy zaimplementować i użyć bardziej wyspecjalizowanego interfejsu dodatkowego. – mghie

+2

Kolejna wskazówka: nie używaj techniki w tej odpowiedzi, wszystkie negatywne strony singletonów również mają zastosowanie tutaj - jeśli próbujesz wyeliminować globale, zamiast tego udawaj się na takie zamaskowane globale. Posiadanie odniesienia do interfejsu w miejscu, które kontrolujesz, jest tak samo dobre, jak powyższa technika, z dodatkową korzyścią, że obiekt implementujący zostanie faktycznie zniszczony, gdy wszystkie odniesienia do niego zostaną zresetowane, zamiast pozostania w pamięci do momentu zakończenia działania aplikacji. To byłoby niewiele lepsze niż wyciek pamięci. – mghie

4

Aby wyłączyć liczenia odniesienia, AddRef i Release powinna zrobić nic, ale powrót -1

function TMyInterfacedObject._AddRef: Integer; 
begin 
    Result := -1; 
end; 

function TMyInterfacedObject._Release: Integer; 
begin 
    Result := -1; 
end; 

Jest sporo użyteczności interfejsów bez liczenia odniesienia. Jeśli używasz liczenia odwołań, nie można mieszać odwołań obiektów i interfejsów, ponieważ wystąpią złe rzeczy. Po wyłączeniu liczenia referencji można z powodzeniem wymieszać odniesienia do interfejsu i obiektu, nie martwiąc się, że obiekty nagle zostaną automatycznie zniszczone.

+2

Jedna uwaga z tym podejściem: upewnij się, że wyczyściłeś wszystkie odniesienia do interfejsu obiektu przed jego zwolnieniem, w przeciwnym razie dostaniesz dziwne błędy. –

0

Albo po prostu użyć poniższy kod:

 
    var 
     I: IMyInterface; 
    begin 
     I := ...; 
     ... 
     Do whatever you want in a scope; 
     Initialize(I); //- this will clear the interface variable without calling the _release. 
    end. 
7

Nie pochodzą od TInterfacedObject, zamiast schodzić z TSingletonImplementation ze standardowego System.Generics.Defaults urządzenia.

  • TSingletonImplementation jest podstawą do prostych zajęć, które potrzebują podstawowej implementacji IInterface, z liczenia odniesienia wyłączone.
  • TSingletonImplementation to klasa bazowa bezpieczna dla klas Delphi obsługujących interfejsy. W przeciwieństwie do TInterfacedObject, TSingletonImplementation nie implementuje liczenia odwołań.
+0

Dobra odpowiedź, ale coś, co nie było dostępne w standardowej bibliotece w mojej wersji Delphi w tym czasie (<= 2007) :) – onnodb