Usuń wzór

W programowaniu zorientowanym obiektowo wzorzec usuwania jest wzorcem projektowym służącym do zarządzania zasobami . W tym wzorcu zasób jest przetrzymywany przez obiekt i zwalniany przez wywołanie konwencjonalnej metody — zwykle nazywanej close , disse , free , release w zależności od języka — która uwalnia wszelkie zasoby, które trzyma obiekt. Wiele języków programowania oferuje konstrukcje językowe , aby uniknąć konieczności jawnego wywoływania metody usuwania w typowych sytuacjach.

Wzorzec usuwania jest używany głównie w językach, których środowisko uruchomieniowe ma automatyczne wyrzucanie elementów bezużytecznych (patrz motywacja poniżej).

Motywacja

Pakowanie zasobów w obiekty

Zawijanie zasobów w obiekty jest zorientowaną obiektowo formą enkapsulacji i leży u podstaw wzorca usuwania.

Zasoby są zwykle reprezentowane przez uchwyty (odniesienia abstrakcyjne), konkretnie zwykle liczby całkowite, które są używane do komunikacji z zewnętrznym systemem, który udostępnia zasób. Na przykład pliki są dostarczane przez system operacyjny (w szczególności system plików ), który w wielu systemach reprezentuje otwarte pliki z deskryptorem pliku (liczbą całkowitą reprezentującą plik).

Uchwytów tych można używać bezpośrednio, przechowując wartość w zmiennej i przekazując ją jako argument do funkcji korzystających z zasobu. Jednak często przydatne jest abstrahowanie od samego uchwytu (na przykład, jeśli różne systemy operacyjne inaczej reprezentują pliki) i przechowywanie dodatkowych danych pomocniczych z uchwytem, ​​aby uchwyty mogły być przechowywane jako pole w rekordzie wraz z innymi dane; jeśli jest to nieprzezroczysty typ danych , zapewnia to ukrywanie informacji , a użytkownik jest abstrahowany od rzeczywistej reprezentacji.

Na przykład w C file input/output pliki są reprezentowane przez obiekty typu FILE (myląco nazywane „ uchwytami plików ”: są to abstrakcja na poziomie języka), które przechowują uchwyt (systemu operacyjnego) do pliku (taki jak deskryptor pliku ), wraz z informacjami pomocniczymi, takimi jak tryb wejścia/wyjścia (odczyt, zapis) i pozycja w strumieniu. Obiekty te są tworzone przez wywołanie funkcji fopen (w terminologii zorientowanej obiektowo konstruktora ), która uzyskuje zasób i zwraca do niego wskaźnik; zasób jest zwalniany przez wywołanie fclose na wskaźniku do obiektu FILE . W kodzie:

    

 PLIK  *  f  =  fopen  (  nazwa pliku  ,  tryb  );  // Zrób coś z f.  fzamknij  (  fa  ); 

Zauważ, że fclose jest funkcją z parametrem PLIK * . W programowaniu obiektowym jest to zamiast tego metoda instancji obiektu pliku, tak jak w Pythonie:

  

 f  =  open  (  nazwa pliku  )  # Zrób coś z f.  fa  .  zamknij  () 

Jest to dokładnie wzorzec usuwania, który różni się jedynie składnią i strukturą kodu od tradycyjnego otwierania i zamykania plików. Inne zasoby mogą być zarządzane dokładnie w ten sam sposób: są pozyskiwane w konstruktorze lub fabryce i zwalniane przez jawną zamykania lub usuwania .

Natychmiastowe wydanie

Podstawowym problemem, który ma rozwiązać uwolnienie zasobów, jest to, że zasoby są drogie (na przykład może istnieć ograniczenie liczby otwartych plików), dlatego należy je szybko zwolnić. Co więcej, czasami potrzebne są pewne prace finalizacyjne, szczególnie w przypadku operacji we/wy, takie jak opróżnianie buforów, aby upewnić się, że wszystkie dane zostały faktycznie zapisane.

Jeśli zasób jest nieograniczony lub faktycznie nieograniczony i nie jest konieczna żadna wyraźna finalizacja, nie jest ważne, aby go zwolnić, aw rzeczywistości programy krótkotrwałe często nie zwalniają jawnie zasobów: ze względu na krótki czas działania jest mało prawdopodobne, aby wyczerpały zasoby i polegają na systemie wykonawczym lub systemie operacyjnym , aby przeprowadzić finalizację.

Jednak ogólnie należy zarządzać zasobami (szczególnie w przypadku programów długotrwałych, programów wykorzystujących wiele zasobów lub ze względów bezpieczeństwa, aby zapewnić zapisywanie danych). Jawne usuwanie oznacza, że ​​finalizowanie i zwalnianie zasobów jest deterministyczne i szybkie: usuwania nie kończy się, dopóki nie zostaną wykonane.

Alternatywą dla wymagania jawnego usuwania jest powiązanie zarządzania zasobami z okresem życia obiektu : zasoby są nabywane podczas tworzenia obiektu i uwalniane podczas niszczenia obiektu . To podejście jest znane jako Resource Acquisition Is Initialization (RAII) i jest używane w językach z deterministycznym zarządzaniem pamięcią (np. C++ ). W tym przypadku w powyższym przykładzie zasób jest pozyskiwany, gdy tworzony jest obiekt pliku, a kiedy kończy się zakres zmiennej f , obiekt pliku, do którego odnosi się f, jest niszczony, a częścią tego zasobu jest wydany.

RAII opiera się na deterministycznym okresie życia obiektu; jednak przy automatycznym zarządzaniu pamięcią programista nie martwi się o czas życia obiektu : obiekty są niszczone w pewnym momencie po tym, jak nie są już używane, ale po abstrakcji. Rzeczywiście, czas życia często nie jest deterministyczny, chociaż może być, zwłaszcza jeśli stosuje się liczenie referencji . Rzeczywiście, w niektórych przypadkach nie ma gwarancji, że obiekty zostaną kiedykolwiek sfinalizowane: kiedy program się kończy, może nie sfinalizować obiektów, a zamiast tego pozwolić systemowi operacyjnemu odzyskać pamięć; jeśli wymagana jest finalizacja (np. opróżnienie buforów), może dojść do utraty danych.

W ten sposób, nie łącząc zarządzania zasobami z okresem istnienia obiektu, wzorzec usuwania umożliwia szybkie zwolnienie zasobów , zapewniając jednocześnie elastyczność implementacji zarządzania pamięcią. Kosztem tego jest konieczność ręcznego zarządzania zasobami, co może być żmudne i podatne na błędy.

Wczesne wyjście

Kluczowym problemem związanym ze wzorcem utylizacji jest to, że jeśli metoda utylizacji nie zostanie wywołana, nastąpi wyciek zasobu. Częstą przyczyną tego jest wczesne wyjście z funkcji z powodu wcześniejszego powrotu lub wyjątku.

Na przykład:

 
      
     
         
    
      def  func  (  nazwa pliku  ):  f  =  otwórz  (  nazwa pliku  )  if  a  :  return  x  f  .  zamknij  ()  wróć  y 

Jeśli funkcja powraca przy pierwszym zwrocie, plik nigdy nie jest zamykany, a zasób wycieka.

 
      
      
     def  func  (  nazwa pliku  ):  f  =  open  (  nazwa pliku  )  g  (  f  )  # Zrób coś z f, co może zgłosić wyjątek.  fa  .  zamknij  () 

Jeśli interweniujący kod zgłosi wyjątek, funkcja kończy działanie wcześniej, a plik nigdy nie jest zamykany, więc zasób wycieka.

Obydwa te elementy można obsłużyć za pomocą konstrukcji try...finally , która gwarantuje, że klauzula ultimate jest zawsze wykonywana przy wyjściu:

 
    
          
        
    
         def  func  (  nazwa pliku  ):  try  :  f  =  open  (  nazwa pliku  )  # Zrób coś.  wreszcie  :  f  .  zamknij  () 

Bardziej ogólnie:

   
 
    
    
  
    
    
 Zasób  zasób  =  getResource  ();  try  {  // Zasób został zdobyty; wykonać czynności z zasobem.   ...  }  ostatecznie  {  // Zwolnij zasób, nawet jeśli został zgłoszony wyjątek.  zasób  .  pozbyć się  ();  } 

try ...finally jest niezbędna do prawidłowego zabezpieczenia wyjątków , ponieważ blok ultimate umożliwia wykonanie logiki czyszczenia niezależnie od tego, czy w bloku try został zgłoszony wyjątek, czy nie .

Wadą tego podejścia jest to, że wymaga ono od programisty jawnego dodania kodu czyszczącego w bloku ultimate . Prowadzi to do wzrostu rozmiaru kodu, a niezastosowanie się do tego doprowadzi do wycieku zasobów w programie.

Konstrukcje językowe

Aby bezpieczne użycie wzorca usuwania było mniej szczegółowe, kilka języków ma wbudowaną obsługę zasobów przechowywanych i udostępnianych w tym samym bloku kodu .

Język C# zawiera instrukcję using , która automatycznie wywołuje metodę Dispose na obiekcie, który implementuje interfejs IDisposable :

    

    
    
 using  (  Resource  resource  =  GetResource  ())  {  // Wykonaj działania na zasobie.  ...  } 

co jest równe:

   
 

    
    

 

    
        
         
 Zasób  zasobu  =  GetResource  ()  try  {  // Wykonaj działania z zasobem.  ...  }  ostatecznie  {  // Zasób mógł nie zostać pozyskany lub został już zwolniony  if  (  zasób  !=  null  )  ((  IDisposable  )  zasób  ).  Wyrzuć  ();  } 

Podobnie język Python ma instrukcję with , której można użyć w podobny sposób z obiektem menedżera kontekstu . Protokół menedżera kontekstu wymaga zaimplementowania metod __enter__ i __exit__ , które są automatycznie wywoływane przez konstrukcję instrukcji with , aby zapobiec powielaniu kodu, który w przeciwnym razie wystąpiłby we wzorcu try / finale .

   
    
    

 with  resource_context_manager  ()  jako  zasób  :  # Wykonaj działania z zasobem.  ...  # Wykonaj inne działania, w których zasób ma gwarancję cofnięcia alokacji.  ... 

Język Java wprowadził nową składnię o nazwie try -with-resources w Javie w wersji 7. Można jej używać na obiektach, które implementują interfejs AutoCloseable (który definiuje metodę close()):

      
    
    
    

  
  try  (  OutputStream  x  =  new  OutputStream  (...))  {  // Zrób coś z x  }  catch  (  IOException  ex  )  {  // Obsługa wyjątku  // Zasób x jest automatycznie zamykany  }  // try 

Problemy

Oprócz kluczowego problemu poprawnego zarządzania zasobami w obecności zwrotów i wyjątków oraz zarządzania zasobami opartego na stercie (usuwanie obiektów w innym zakresie niż miejsce, w którym zostały utworzone), istnieje wiele innych komplikacji związanych ze wzorcem usuwania. Tych problemów w dużej mierze unika RAII . Jednak w zwykłym prostym użyciu te zawiłości nie pojawiają się: zdobądź pojedynczy zasób, zrób coś z nim, automatycznie go zwolnij.

Podstawowym problemem jest to, że posiadanie zasobu nie jest już niezmiennikiem klasowym (zasób jest wstrzymywany od utworzenia obiektu do momentu jego usunięcia, ale w tym momencie obiekt nadal jest aktywny), więc zasób może nie być dostępny, gdy obiekt próbuje użyj go, na przykład próbując odczytać z zamkniętego pliku. Oznacza to, że wszystkie metody na obiekcie, które korzystają z zasobu, potencjalnie zawodzą, konkretnie zwykle przez zwrócenie błędu lub zgłoszenie wyjątku. W praktyce jest to niewielkie, ponieważ użycie zasobów może zwykle zakończyć się niepowodzeniem również z innych powodów (na przykład próba przeczytania poza końcem pliku), więc te metody już mogą zawieść, a brak zasobu po prostu dodaje kolejną możliwą awarię . Standardowym sposobem implementacji tego jest dodanie do obiektu pola boolowskiego, zwanego „ disposed” , które jest ustawiane na true przez „ dispose” i sprawdzane przez klauzulę ochronną dla wszystkich metod (które korzystają z zasobu), zgłaszając wyjątek (taki jak ObjectDisposedException w .NET), jeśli obiekt został usunięty.

Co więcej, możliwe jest wywołanie metody usuwania na obiekcie więcej niż jeden raz. Chociaż może to wskazywać na błąd programistyczny (każdy obiekt przechowujący zasób musi zostać usunięty dokładnie raz), jest prostszy, bardziej niezawodny, a zatem zwykle preferowany jest sposób usuwania idempotentny ( co oznacza „wielokrotne wywołanie jest tym samym, co wywołanie raz”) . Można to łatwo zaimplementować, używając tego samego boolowskiego utylizacji i sprawdzając je w klauzuli ochronnej na początku metody delete , w takim przypadku zwracając natychmiast, zamiast zgłaszać wyjątek. Java rozróżnia typy jednorazowe (te, które implementują AutoCloseable ) od typów jednorazowych, w których utylizacja jest idempotentna (podtyp Closeable ).

Usuwanie w obecności dziedziczenia i składu obiektów, które przechowują zasoby, ma analogiczne problemy jak niszczenie/finalizowanie (poprzez destruktory lub finalizatory). Ponadto, ponieważ wzorzec usuwania zwykle nie obsługuje tego języka, konieczny jest kod wzorcowy . Po pierwsze, jeśli klasa pochodna przesłania metodę usuwania w klasie bazowej, metoda przesłaniająca w klasie pochodnej zazwyczaj musi wywołać metodę usuwania w klasie bazowej, aby prawidłowo zwolnić zasoby przechowywane w bazie. Po drugie, jeśli obiekt ma relację „ma” z innym obiektem, który posiada zasób (tj. jeśli obiekt pośrednio korzysta z zasobu poprzez inny obiekt, który bezpośrednio korzysta z zasobu), czy obiekt korzystający pośrednio powinien być jednorazowy? Odpowiada to temu, czy relacja jest posiadaniem ( skład obiektów ), przeglądaniem ( agregacja obiektów ), czy nawet tylko komunikacją ( skojarzenie ), i obie konwencje są spełnione (użytkownik pośredni jest odpowiedzialny za zasób lub nie jest odpowiedzialny). Jeśli za zasób odpowiada użycie pośrednie, musi on być jednorazowy i pozbywać się posiadanych obiektów w momencie ich usuwania (analogicznie do niszczenia lub finalizowania posiadanych obiektów).

Kompozycja (posiadanie) zapewnia enkapsulację (należy śledzić tylko obiekt, który jest używany), ale kosztem znacznej złożoności, gdy istnieją dalsze relacje między obiektami, podczas gdy agregacja (przeglądanie) jest znacznie prostsza, kosztem braku enkapsulacji. W .NET konwencja polega na tym, że odpowiedzialny jest tylko bezpośredni użytkownik zasobów: „Powinieneś zaimplementować IDisposable tylko wtedy, gdy twój typ używa bezpośrednio niezarządzanych zasobów”. Zobacz zarządzanie zasobami , aby uzyskać szczegółowe informacje i dalsze przykłady.

Zobacz też

Notatki

Dalsza lektura