C# 7 – Pattern matching

Przy okazji ostatniego wpisu konkursowego zapowiedziałem serię wpisów poświęconą CQRS oraz event sourcing-owi. Posty oczywiście wkrótce się ukażą, ale dziś chciałbym w ramach tej tematyki omówić nowy „mechanizm”, który zagości w siódmej wersji naszego ulubionego języka 🙂 Wyobraźmy sobie, że naszym zadaniem jest implementacja odtwarzania stanu obiektu ze zdarzeń, które przetrzymujemy w tzw. Event Store. Zdarzenia wyglądają następująco:

 

public class Event
{
    public Guid AggregateId { get; set; }
}

public class ItemCreatedEvent : Event
{
    public string Name { get; set; }

    public int Quantity { get; set; }
}

public class ItemNameChangedEvent : Event
{
    public string Name { get; set; }
}

public class ItemQuantityChangedEvent : Event
{
    public int Quantity { get; set; }
}

public class ItemDeletedEvent : Event
{
}

 

Pusta metoda, która zajmie się odtwarzaniem stanu obiektu wygląda natomiast tak:

public class ItemAggregate
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int Quantity { get; set; }

    void LoadFromHistory(IEnumerable<Event> events)
    {        
    }
}

 

Jest to dość naturalne posunięcie. Znając typ bazowy wszystkich zdarzeń (Event) jesteśmy w stanie przekazać je do metody, ponieważ parametry metod są kontrawariancją. Oznacza to, że każdy obiekt zawarty w kolekcji events może być typu Event jak i typu, który po nim dziedziczy. Tu jednak dochodzimy do pewnego problemu. W zależności od typu każdego zdarzenia chcielibyśmy, aby nasz kod zachował się różnie. Przykładowo obiekt typu ItemQuantityChangedEvent powinien zmienić wartość właściwości Quantity, a obecność zdarzenia ItemDeletedEvent powinna wywołać wyjątek, ponieważ oznacza to, że obiekt został już usunięty. Ktoś może w tym momencie pomyśleć, że to nie problem. Wszak od kilku lat w C# dostępny jest zarówno operator is jak i as. Zobaczmy zatem jak wobec tego wygląda implementacja metody LoadFromHistory:

 

void LoadFromHistory(IEnumerable<Event> events)
{
    foreach (var @event in events)
    {
        ItemCreatedEvent ice;
        ItemNameChangedEvent ince;
        ItemQuantityChangedEvent iqce;
        ItemDeletedEvent ide;

        if ((ice = @event as ItemCreatedEvent) != null)
        {
            Name = ice.Name;
            Quantity = ice.Quantity;
        }
        else if ((ince = @event as ItemNameChangedEvent) != null)
            Name = ince.Name;

        else if ((iqce = @event as ItemQuantityChangedEvent) != null)
            Quantity = iqce.Quantity;

        else if((ide = @event as ItemDeletedEvent) != null)
            throw new InvalidOperationException("Cannot load deleted aggregate");
    }
}

 

Wygląda ładnie? Nie do końca. Tu dochodzimy do nowości dostępnej w nowej wersji C# – pattern matching-u. Nasz kod będziemy mogli teraz napisać następująco:

 

void LoadFromHistory(IEnumerable<Event> events)
{
    foreach (var @event in events)
    {
        switch (@event)
        {
            case ItemCreatedEvent ice:
                {
                    Name = ice.Name;
                    Quantity = ice.Quantity;
                }
                break;

            case ItemNameChangedEvent ince:
                Name = ince.Name;
                break;

            case ItemQuantityChangedEvent iqce:
                Quantity = iqce.Quantity;
                break;

            case ItemDeletedEvent ide:
                throw new ArgumentException("Cannot load deleted aggregate");
        }
    }
}

 

Zapis znaczeni się nam uprościł. Po pierwsze nie jesteśmy zmuszeni do deklaracji zmiennych pomocniczych, po drugie zamieniliśmy serię else-if w switcha (fani optymalizacji tym bardziej powinni być ucieszeni 🙂 ).  Co ciekawe przy pattern matching-u będziemy mieli także możliwość zawężania naszego kryterium poprzez użycie klauzuli when. Przykładowo:

 


case ItemDeletedEvent ide when ide.AggregateId == Guid.Empty :
        throw new InvalidOperationException("Cannot load deleted aggregate");

 

Oznacza to, że wyjątek nie zostanie rzucony w przypadku gdy zdarzenie będzie posiadało niepusty identyfikator AggregateId. Mamy także możliwość zadeklarowania tzw. wildcard czyli mówiąc krótko warunku, który zostanie zawsze spełniony. W naszym przypadku mogłoby wyglądać to następująco:

 

case *:
    throw new ArgumentException("Canot load base event")

 

Dla osób, które nie przepadają za konstrukcją switch, twórcy języka przygotowali także alternatywny zapis:

 


public string GetEventMessage(Event @event) =>
        @event match(
                case ItemCreatedEvent ice:
                    "item created"
                case ItemNameChangedEvent ince :
                    "name changed"   
                case ItemQuantityChangedEvent iqce:
                    "quantity changed"
                case ItemDeletedEvent ide when ide.AggregateId == Guid.Empty :
                    "item deleted"
                case *:
                    throw new ArgumentException("Base event usage is not allowed")
        );

 

Ostatnia rzecz, którą chciałem Wam dziś pokazać to możliwość tworzenia dopasowań na „memberach” typu (ta odmiana angielskich nazw…). Przykład wygląda następująco:

 


public string GetEventMessage(Event @event) =>
        @event match(
                case ItemCreatedEvent ice:
                    "item created"
                case ItemNameChangedEvent ince :
                    "name changed"   
                case ItemQuantityChangeEvent {Quantity is 1}
                    "quantity changed. one item left!"
                case ItemQuantityChangedEvent iqce:
                    "quantity changed"
                case ItemDeletedEvent {AggregateId is var id} :
                    $"item {id} deleted"
                case *:
                    throw new ArgumentException("Base event usage is not allowed")
        );

 

W pierwszym przypadku używając operatora is informujemy, że oprócz typu ItemQuantityChangedEvent interesują nasz zdarzenia, które posiadają właściwość Quantity równą 1. W Drugim przypadku przypisujemy właściwość AggregateId do zmiennej id, widocznej jedynie w klauzuli case.

Na dziś to wszystko 🙂 Mam nadzieję, że temat się Wam podobał. Jak wspomniałem na początku, już wkrótce pojawią się wpisy poświęcone CQRS/ES. Żeby ich nie przegapić możecie śledzić mnie na twitterze facebooku.

Na razie !

You may also like...