Krótki wpis o statycznym polu…

Dziś krótki i nieplanowany wpis z cyklu „ku przestrodze”, a będzie o… polu statycznym, które skutecznie zmieniło moje plany dotyczące ubiegłego wieczoru (ok 2h debugowania). Nie ma jednak tego złego, prawda? Ja mam nauczkę, a Ty drogi czytelniku być może też wyniesiesz z tego coś dobrego. Zacznijmy od tego, aby odpowiedzieć sobie po co i kiedy stosować słowo kluczowe static? Myślę, że każdy, czy to w szkole, na uczelni, na kursie czy w pracy zna „oklepaną” formułkę, która mówi, że:

 

„static używamy gdy chcemy czegoś użyć bez konieczności tworzenia instancji klasy.”

 

Ja nie za bardzo przepadam za tą definicją, ponieważ nie przekazuje ona wprost ważnej właściwości pół/metod statycznych, której pominięcie (jak widać) może nas słono kosztować. Przyjmijmy więc inną wersję:

 

„static sygnalizuje, że dana metoda/pole przynależą do typu, a nie utworzonej instancji

 

Po tym krótkim wprowadzeniu możemy w końcu przejść do kodu, nad którym to wczoraj siedziałem. Było to kilka dodatkowych funkcji do jednej z moich bibliotek dla .NET Core. Chciałem ułatwić tworzenie obiektu kontekstu poprzez utworzenie pomocniczej klasy budowniczego. Wygląda ona następująco:

 

    internal sealed class SagaContextBuilder : ISagaContextBuilder
    {
        private Guid _correlationId;
        private string _originator;
        private readonly List<ISagaContextMetadata> _metadata;

        public SagaContextBuilder()
            => _metadata = new List<ISagaContextMetadata>();

        public ISagaContextBuilder WithCorrelationId(Guid correlationId)
        {
            _correlationId = correlationId;
            return this;
        }

        public ISagaContextBuilder WithOriginator(string originator)
        {
            _originator = originator;
            return this;
        }
        
        public ISagaContextBuilder WithMetadata(string key, object value)
        {
            var metadata = new SagaContextMetadata(key, value);
            _metadata.Add(metadata);
            return this;
        }

        public ISagaContext Build()
            => SagaContext.Create(_correlationId, _originator, _metadata);
    }

 

Jak widzisz klasa budowniczego przechowuje 3 pola prywatne, które są stopniowo uzupełniane poprzez publiczne API, a następnie w metodzie Build buduje obiekt kontekstu. Zobaczmy zatem jak wyglądała implementacja ów kontekstu:

 

    public class SagaContext : ISagaContext
    {
        public Guid CorrelationId { get; }

        public string Originator { get; }
        public IReadOnlyCollection<ISagaContextMetadata> Metadata { get; }
        
        public static ISagaContextBuilder Builder = new SagaContextBuilder();

        private SagaContext(Guid correlationId, string originator, IEnumerable<ISagaContextMetadata> metadata)
        {
            CorrelationId = correlationId;
            Originator = originator;

            var areMetadataKeysUnique = metadata.GroupBy(m => m.Key).All(g => g.Count() == 1);

            if (!areMetadataKeysUnique)
            {
                throw new ChronicleException("Metadata keys are not unique");
            }

            Metadata = metadata.ToList().AsReadOnly();
        }

        public static ISagaContext Empty
            => new SagaContext(Guid.NewGuid(), string.Empty, Enumerable.Empty<ISagaContextMetadata>());

        public static ISagaContext Create(Guid correlationId, string originator, IEnumerable<ISagaContextMetadata> metadata)
            => new SagaContext(correlationId, originator, metadata);
        
        public ISagaContextMetadata GetMetadata(string key)
            => Metadata.Single(m => m.Key == key);

        public bool TryGetMetadata(string key, out ISagaContextMetadata metadata)
        {
            metadata = Metadata.SingleOrDefault(m => m.Key == key);
            return metadata != null;
        }
    }

 

Zwróć uwagę, że obiekt kontekstu posiada publiczne, statyczne pole budowniczego. Dzięki temu nie musiałem udostępniać klasy SagaContextBuilder publicznie, a jedynie jej interfejsUżycie wyglądało więc następująco:

 

              var context = SagaContext
                .Builder
                .WithCorrelationId(Id)
                .WithOriginator(Origin)
                .WithMetadata("key", "value")
                .Build();

 

Tak więc to co zadziało się po uruchomieniu powyższego kodu to:

  1. Obiekt budowniczego został utworzony.
  2. Budowniczy uzupełnia swoje prywatne pole _correlationId o wartość przekazaną (Id).
  3. Budowniczy uzupełnia swoje prywatne pole _originator o wartość przekazaną (Origin).
  4. Budowniczy dodaje do prywatnej listy _metadata obiekt typu ISagaContextMetadata z wartościami „key” oraz „value”.
  5. Budowniczy w metodzie Build wywołuje statyczną metodę Create przekazując zebrane parametry celem utworzenia kontekstu.
  6. Statyczna metoda Create wywołuje prywatny konstruktor klasy SagaContext gdzie dokonywana jest walidacja. Sprawdza ona czy w przekazanej kolekcji metadanych każdy obiekt ma unikalny klucz.
  7. Metoda Create zwraca obiekt kontekstu.
  8. Metoda Build zwraca obiekt kontekstu.

Raczej typowy kod, który w jakiś sposób ułatwia użytkownikowi korzystanie z biblioteki. To na czym polegał problem? Mówiąc najogólniej, powyższy kod uruchamiał się tylko raz. Za drugim razem kod się przewracał. Aby dać Ci podpowiedź „ubiorę” powyższy listing w metodę, która mocno przypomina mój wczorajszy scenariusz:

 


        internal const string CorrelationContextKey = "context";

        public static ISagaContext AsSagaContext(this ICorrelationContext context)
            => SagaContext
                .Builder
                .WithCorrelationId(context.Id)
                .WithOriginator(context.Origin)
                .WithMetadata(CorrelationContextKey, context)
                .Build();

 

Jets to zwykła metoda rozszerzająca, której zadaniem jest transformata z obiektu typu ICorrelationContext do obiektu typu ISagaContext. Zwróć jednak uwagę, że pomimo iż wartości dla Id czy np. Origin mogły by różne, to jeden parametr zawsze pozostawał taki sam… klucz dla obiektu metadanych. Teraz przypomnę scenariusz. Za pierwszym razem ten kod działał, a jego rezultatem było zwrócenie obiektu ISagaContext, wystarczyło jednak wywołać go drugi raz i otrzymywałem błąd. Jeżeli chcesz chwile nad tym pomyśleć to śmiało 😀 Rozwiązanie poniżej.

Przyznam szczerze, że na początku trochę mnie to zabiło, bo rozumiem „dziwne” zachowania w środowisku współbieżnym gdzie dochodzi do jakiś race-conditions, ale tu miałem do czynienia z uruchomienim tego samego kodu w przeciągu minuty, gdzie za pierwszym razem ZAWSZE działa, a za drugim ZAWSZE się wywala. I nagle olśnienie… Problemem polegał na tym, że pole Builder w obiekcie SagaContext jak widzisz było zadeklarowane statycznie. Jakie to ma znaczenie? Przypomnijmy sobie formułkę:

 

„static sygnalizuje, że dana metoda/pole przynależą do typu, a nie utworzonej instancji

 

Mówiąc krótko zapomniałem o podstawie podstaw. Pole statyczne jest inicjalizowane tylko raz nie ważne ile instancji klasy byśmy nie utworzyli. Można to oczywiście zmienić np. poprzez atrybut ThreadStatic, ale to nie istotne. Istotne jest to, że mimo wywołania mojej metody rozszerzającej dwa razy, tak na prawdę pod spodem korzystałem z tej samej instancji budowniczego. No dobrze, ale co to ma do błędu? Jak pamiętasz w konstruktorze klasy kontekstu walidowałem unikalność kluczy metadanych, a zatem to co się stało to:

  1. Wywołana została metoda rozszerzająca, która pod spodem budowała obiekt kontekstu. Finalnie obiekt budowniczego posiadał w sobie uzupełnione pola Id, Originator oraz 1 element w liście _metadata o kluczu „context”.
  2. Metoda rozszerzająca zostaje wywołana po raz drugi, ale ze starym obiektem budowniczego, który dalej posiada uzupełnione wyżej pola!
  3. Budowniczy nadpisuje pola Id oraz Orignator, ale także dodaje do istniejącej listy drugi obiekt metadanych o takim samym kluczu.
  4. Podczas walidacji w konstruktorze SagaContext rzucony zostaje wyjątek.

Tak oto dochodzę do końca tej nieco żenującej opowieści. Rozwiązanie było oczywiście proste. Wystarczyło zamienić pola statyczne na metodę statyczną, która to dostarczała nową instancję budowniczego:

 


        public static ISagaContextBuilder Create()
            => new SagaContextBuilder();

 

A użycie wygląda teraz następująco:

 

              var context = SagaContext
                .Create()
                .WithCorrelationId(Id)
                .WithOriginator(Origin)
                .WithMetadata("key", "value")
                .Build();

 

Poprawka wprowadzona, kod działa:

 

 

A może powinienem napisać „Damn you programming basics…”.

You may also like...