Singleton

Singleton

Singleton jest kreacyjnym wzorcem projektowym, który zapewnia, że klasa ma tylko jedną instancję i zapewnia globalny punkt dostępu do tej instancji.

Cel

Intencją wzorca singleton jest zapewnienie, że klasa ma tylko jedną instancję i zapewnienie globalnego punktu dostępu do niej. Singleton rozwiązuje jednocześnie dwa problemy:
– zapewnia istnienie wyłącznie jednej instancji danej klasy,
– pozwala na dostęp do tej instancji w przestrzeni globalnej.
Jednocześnie łamie Zasadę pojedynczej odpowiedzialności.

Problem

Bardzo częstym i prostym przykładem jest loger. Zazwyczaj potrzebujemy tylko jednego wystąpienia logera dla całej aplikacji, aby uniknąć zakłócania wielu wystąpień, co może spowodować, że komunikaty będą rejestrowane wiele razy lub wcale.

Teraz moglibyśmy po prostu trzymać instancję klasy w zmiennej globalnej, ale jest problem z tym podejściem. Nie uniemożliwia to klientom tworzenia innych instancji klasy.

Rozwiązanie

Poprzez wzorzec Singleton możemy sprawić, że sama klasa będzie odpowiedzialna za zapewnienie istnienia tylko jednej instancji klasy. Inną opcją byłoby przeniesienie odpowiedzialności na inny komponent, taki jak kontener IOC.

Aby nasza klasa loggera była odpowiedzialna za zarządzanie tylko jedną jej instancją, powinniśmy uczynić jej konstruktor prywatnym.

W ten sposób inne części kodu nie mogą bezpośrednio tworzyć instancji naszego loggera. Jeśli chcemy zezwolić na subklasowanie loggera, akceptowalny jest również konstruktor chroniony. Oprócz tego klasa Logger będzie zawierać metodę lub właściwość, która zwróci swoją instancję, na przykład właściwość Instance z prywatnym polem.

Ta właściwość jest statyczna, co zapewnia, że gdy istnieje wiele wystąpień klasy Logger, nadal będzie tylko jedna instancja właściwości Instance. Gdy geter właściwości pobiera instancje po raz pierwszy, instancja jest tworzona i przechowywana. Gdy getter właściwości pobiera, instancje po raz kolejny pobiera już wcześniej utworzona instancję, co zapewnia, że w danym momencie istnieje tylko jedna instancja.

Oprócz tego klasa Logger zazwyczaj zawiera metody takie jak na przykład Log.

Są one zdefiniowane jako zwykłe metody instancji, więc nie są statyczne. Dostęp do nich uzyskuje się za pośrednictwem właściwości Instancji. I to wszystko. Przyjrzyjmy się teraz strukturze wzorca.

Struktura

Mapowanie naszego przykładu loggera do struktury wzorca Singleton jest dość proste. Sama klasa Logger nazywana jest Singletonem.

Singleton definiuje operację instancji, która umożliwia klientom dostęp do jej unikalnej instancji. Na nim istnieje publiczne wystąpienie właściwości statycznej. Prywatne pole zawiera przechowywaną wartość wystąpienia. Metoda log odwzorowuje tak zwaną operację singletona w strukturze wzorca. Są to operacje na pojedynczej instancji. I to wszystko. Zaimplementujmy to w demo.

Przykład implementacji

W przypadku wzorca Singleton chcemy zacząć od klasy Logger. To nasz Singleton. Do tej klasy dodajemy chroniony konstruktor, aby klienci nie mogli go tworzyć, ale mogły być podklasy.

/// <summary>
/// Singleton
/// </summary>
public class Logger
{
   protected Logger()
   {
   }
}

Następną rzeczą, którą chcemy dodać, jest statyczna właściwość instancji i prywatne pole dla tej właściwości. To musi być nullable.

/// <summary>
/// Singleton
/// </summary>
public class Logger
{
   private static Logger? _instance;

   /// <summary>
   /// Instance
   /// </summary>
   public static Logger Instance
   {
      get
      {
         if (_instance == null)
         {
            _instance = new Logger();
         }
         return _instance;
      }
   }

   protected Logger()
   {
   }
}

W geterze właściwości możemy następnie sprawdzić, czy ma wartość null. Jeśli tak, tworzymy i przechowujemy instancję Loggera, a następnie zwracamy tę instancję. Zapewnia nam to, że w danym momencie istnieje tylko jedna instancja klasy Singleton, w tym przypadku nasz Logger.

Dodajemy również jedną metodę Log, do celów testowych. Nazywa się to operacją Singletona.

/// <summary>
/// Singleton
/// </summary>
public class Logger
{
   private static Logger? _instance;

   /// <summary>
   /// Instance
   /// </summary>
   public static Logger Instance
   {
      get
      {
         if (_instance == null)
         {
            _instance = new Logger();
         }
         return _instance;
      }
   }

   protected Logger()
   {
   }

   /// <summary>
   /// SingletonOperation
   /// </summary> 
   public void Log(string message)
   {
      Console.WriteLine($"Wiadomość do logowania: {message}");
   }
}

Każde wystąpienie Logger, które jest zwracane podczas pobierania właściwości Instance, ma do niej dostęp i zawsze jest to dokładnie to samo wystąpienie, które jest zwracane. Przetestujmy teraz naszego Singletona. Przechodzimy do Program.cs

Najpierw zobaczmy, czy możemy Singletona utworzyć.

var singleton = new Logger();

I nie możemy. Konstruktor jest niedostępny ze względu na stopień ochrony. Jak na razie dobrze, tak powinno być.

Możemy jednak pobrać instancję przez Logger.Instance. Więc weźmy dwie i porównajmy je.

var instance1 = Logger.Instance;
var instance2 = Logger.Instance;

if (instance1 == instance2 && instance2 == Logger.Instance)
{
    Console.WriteLine("Instancje są takie same.");
}

Jeśli te dwie instancje są tą samą instancją i pasują do właściwości Logger.Instance, wypisujemy to w oknie danych wyjściowych konsoli, a następnie rejestrujemy kilka komunikatów jako przykład.

instance1.Log($"Wiadomość od {nameof(instance1)}");
instance1.Log($"Wiadomość od {nameof(instance2)}");
Logger.Instance.Log($"Wiadomość od {nameof(Logger.Instance)}");

Każde z tych wywołań powinno działać dokładnie tak samo. Przetestujmy to.

Wynik działania

Instancje są takie same.
Wiadomość do logowania: Wiadomość od instance1
Wiadomość do logowania: Wiadomość od instance2
Wiadomość do logowania: Wiadomość od Instance

Wygląda na to, że instancje są rzeczywiście tą samą instancją, co oznacza, że te trzy komunikaty wypisane ze zmiennych instance1, instance2 i Logger.Instance dały dokładnie to samo.

Wydaje się, że to działa, ale jest z tym potencjalny problem.

To, co tutaj zrobiliśmy, jest naiwną formą leniwej inicjalizacji. Inicjalizacja z opóźnieniem to zasada, która mówi, że utworzymy instancję klasy tylko wtedy, gdy będziemy jej potrzebować, a nie po jej skonstruowaniu. Problem polega na tym, że ten kod nie gwarantuje bezpieczeństwa wątków. Moglibyśmy stworzyć własny mechanizm blokujący, ale nie ma już takiej potrzeby.

Lazy<T>

.NET zawiera sposób radzenia sobie z leniwą inicjalizacją w sposób bezpieczny dla wątków przy użyciu Lazy<T>. Kiedy tego używamy, kompilator dba o to, aby ten wątek był bezpieczny, więc zróbmy to.

// Lazy<T>
private static readonly Lazy<Logger> _lazyLogger = new Lazy<Logger>(() => new Logger());

Dodajemy nowe pole statyczne, aby zastąpić naszą statyczną prywatną instancję Loggera. Jest to typ Lazy<T>, w tym przypadku Lazy<Logger>. I stwierdzamy, że jest to tylko do odczytu, ponieważ nie będziemy musieli zmieniać tego obiektu podczas życia singletona.

Natychmiast go inicjujemy, początkowo jako Lazy<Logger>, a to nie znaczy, że sama instancja Loggera zostanie zbudowana. Zamiast tego przechodzimy przez metodę, która zostanie użyta do skonstruowania Loggera, gdy po raz pierwszy uzyskamy dostęp do właściwości value LazyLoggera.

Więc nie potrzebujemy już tej starej prywatnej statycznej instancji Loggera, to usuwamy.

private static Logger? _instance;

I nie potrzebujemy jej również w pobieraniu właściwości. Zamiast tego zwracamy wartość naszego lazyLoggera.

public static Logger Instance
{
   get
   {
      return _lazyLogger.Value;
   }
}

Przy pierwszym dostępie do tego logera, zostanie skonstruowany przy użyciu akcji, przez którą przeszliśmy podczas inicjowania lazyLoggera. Za każdym razem po tym zostanie zwrócona ta sama instancja, a wszystko to dzieje się w sposób bezpieczny dla wątków.

kod po modyfikacji

public class Logger
{
   // Lazy<T>
   private static readonly Lazy<Logger> _lazyLogger = new Lazy<Logger>(() => new Logger());

   /// <summary>
   /// Instance
   /// </summary>
   public static Logger Instance
   {
      get
      {
         return _lazyLogger.Value;
      }
   }

   protected Logger()
   {
   }

   /// <summary>
   /// SingletonOperation
   /// </summary> 
   public void Log(string message)
   {
      Console.WriteLine($"Wiadomość do logowania: {message}");
   }
}

Nie ma potrzeby zmiany kodu klienta, powinniśmy uzyskać dokładnie takie same dane wyjściowe, jak wcześniej.

Wynik działania

Instancje są takie same.
Wiadomość do logowania: Wiadomość od instance1
Wiadomość do logowania: Wiadomość od instance2
Wiadomość do logowania: Wiadomość od Instance

I rzeczywiście tak jest, ale tym razem pracujemy w sposób bezpieczny dla wątków.

Zastosowanie

Możesz użyć tego wzorca, gdy musi istnieć dokładnie jedna instancja klasy i musi być ona dostępna dla klientów z dobrze znanego punktu dostępu. Nasz loger jest tego najlepszym przykładem.

Singleton uniemożliwia tworzenie obiektów danej klasy inaczej niż przez stosowną metodę kreacyjną. Ta z kolei zwróci albo nowy obiekt, albo wcześniej stworzony.

Jest to również przydatne, gdy jedyna instancja powinna być rozszerzalna przez podklasy, a wszyscy klienci powinni mieć możliwość korzystania z rozszerzonej instancji bez modyfikowania swojego kodu. To właśnie umożliwiliśmy za pomocą chronionego konstruktora.

Stosuj wzorzec Singleton, gdy potrzebujesz ściślejszej kontroli nad zmiennymi globalnymi. W przeciwieństwie do zmiennych globalnych, wzorzec Singleton gwarantuje istnienie tylko jednego obiektu danej klasy. Nic, oprócz samej klasy, nie jest w stanie zamienić tego obiektu.

Zalety i wady

Masz pewność, że istnieje tylko jedna instancja klasy. Singleton hermetyzuje swoją jedyną instancję, ma ścisłą kontrolę nad tym, jak i kiedy klienci uzyskują do niej dostęp.

Zyskujesz globalny dostęp do tej instancji. Jest również lepszy niż zmienne globalne, ponieważ pozwala uniknąć zanieczyszczania przestrzeni nazw tymi zmiennymi, które służą do przechowywania pojedynczych instancji.

Wzorzec ten wymaga specjalnej uwagi w środowisku wielowątkowym, w którym trzeba unikać tworzenia wielu instancji singletona przez wiele wątków.

Wzorzec singleton narusza zasadę pojedynczej odpowiedzialności, ponieważ obiekty nie tylko kontrolują sposób ich tworzenia. Zarządzają również własnym cyklem życia.

Zastosowanie wzorca Singleton może prowadzić do niewłaściwego projektowania. Można na przykład doprowadzić do sytuacji, w której komponenty programu wiedzą zbyt wiele o sobie nawzajem.

Kod źródłowy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *