Artykuł

freeimages.com freeimages.com
kwi 17 2015
0

Polimorficzne tworzenie obiektów - usuwamy switcha

Sporo w ostatnim czasie pisałem o wzorcach projektowych. Starałem się w miarę dokładnie zgłębić ten temat i po stworzeniu kilku tekstów z tego cyklu, naszło mnie na małą refleksję - wzorce projektowe są nierozerwalnie związane z polimorfizmem.

Jeśli spojrzymy na poszczególne przypadki (zwłaszcza te z grupy konstrukcyjnych wzorców projektowych), to szybko przekonamy się, że elementem spójnym jest wspólny interfejs. Pojawia się on naprawdę w wielu wzorcach.

W wielu przypadkach jest on kluczem do sukcesu. Dzięki interfejsom możemy stworzyć kilka różnych klas, które będą zachowywać się inaczej, ale sumarycznie będzie można z nich korzystać w taki sam, uniwersalny sposób. I nie byłoby w tym nic złego, gdyby nie fakt, że tak utworzone byty trzeba jakoś wywoływać.. Niestety bardzo często robi się to za pomocą rozwiniętej logiki warunkowej. A przecież takich właśnie elementów chcieliśmy się pozbyć - prawda:)?

Na szczęście logikę warunkową można stosunkowo prosto wyrzucić, a za przykład posłuży nam kod opisywany w tekście poświęconym metodzie wytwórczej.

Dlaczego w ogóle usuwamy IFa/Switcha?

Na początku warto sobie odpowiedzieć na pytanie o co ten cały szum - czy instrukcję if/switch są rzeczywiście czymś złym? I tak i nie - wszystko zależy od kontekstu. Tak naprawdę nie da się napisać kawałka kodu bez użycia jednej z tych dwóch instrukcji, ale często w sytuacji gdy logika warunkowa jest długa, można pomyśleć o refaktoryzacji takiego kodu, który ciężko się czyta i zajmuje dużo miejsca. Wspomniana wcześniej metoda wytwórcza jest idealnym przykładem.

We wspomnianym wyżej wzorcu, z reguły wykorzystujemy pewną, powtarzalną logikę tworzenia obiektu. Bardzo często jest tak, że tworzymy obiekt w zależności od jakiegoś parametru/typu. W zalinkowanym wyżej tekście była to enumeracja. Każdemu elementowi z naszego enuma odpowiadała określona klasa. Łatwo na tej bazie zbudować słownik, w którym kluczem będzie określony enum, natomiast wartością konkretny typ obiektu. Mając taką mapę, możemy stworzyć metodę, która będzie przyjmowała klucz, a zwracać obiekt implementujący nasz interfejs. I tak oto dotarliśmy do naszego przykładu:)

Przykład - klasy bazowe

W przykładzie wykorzystamy częściowo kod, ze wspomnianego wcześniej wpisu. Dla uproszczenia pozwolę sobie na przeklejenie części ze stworzonego wcześniej kodu:

enum Animals
{
    Cat, Dog, Wolf
}

interface IAnimal
{
    void MakeSound();
}

class Cat : IAnimal
{
    #region IAnimal Members
    public void MakeSound()
    {
        Console.WriteLine("Miauuuu!");
    }
    #endregion
}

class Dog : IAnimal
{
    #region IAnimal Members
    public void MakeSound()
    {
        Console.WriteLine("Hauuuu!");
    }
    #endregion
}

class Wolf : IAnimal
{
    #region IAnimal Members
    public void MakeSound()
    {
        Console.WriteLine("Auuuu!");
    }
    #endregion
}

Naszym celem jest utworzenie teraz metody wytwórczej, która w zależności od przekazanej wartości enumeracji, zwróci odpowiedni obiekt. Najszybszym wyborem zawsze jest switch i tak też zrobiłem w moim wcześniejszym wpisie. Dziś jednak nie skorzystamy z jego usług, a do pracy zaprzęgniemy rozwiązanie słownikowe:

static class AnimalFactory
{
    private static Dictionary<Animals, Lazy<IAnimal>> objectsDict =
        new Dictionary<Animals, Lazy<IAnimal>>()
    {
        { Animals.Cat, new Lazy<IAnimal>(() => new Cat()) },
        { Animals.Dog, new Lazy<IAnimal>(() => new Dog()) },
        { Animals.Wolf, new Lazy<IAnimal>(() => new Wolf()) },
 
    };

    public static IAnimal CreateAnimalObject(Animals animalType)
    {
        Lazy<IAnimal> animal = null;
        if (!objectsDict.TryGetValue(animalType, out animal))
        {
            throw new ArgumentOutOfRangeException("animalType", "Nieznany rodzaj zwierzaka");
        }
        return animal.Value;
    }
}

Cały kod jest stosunkowo prosty. Mamy statyczną klasę oraz metodę wytwórczą, która przyjmuje enuma. Wewnątrz klasy znajduje się słownik, w którym tłumaczymy wartości enumeracji na konkretne obiekty. Aby uniknąć przedwczesnego tworzenia obiektów, opakowaliśmy je w klasę Lazy wprowadzoną w .Net Frameworku w wersji 4.0. Przy dodaniu kolejnego typu, wystarczy że utworzymy kolejne mapowanie w naszym słowniku. Poniżej przykładowe wywołania w klasie Program:

class Program
{
    static void Main(string[] args)
    {
        IAnimal cat = AnimalFactory.CreateAnimalObject(Animals.Cat);
        cat.MakeSound();

        IAnimal dog = AnimalFactory.CreateAnimalObject(Animals.Dog);
        dog.MakeSound();

        IAnimal wolf = AnimalFactory.CreateAnimalObject(Animals.Wolf);
        wolf.MakeSound();

        Console.ReadKey();
    }
}

Dla osób, które jeszcze bardziej chciałyby uprościć proces tworzenia obiektów, pewnym rozwiązaniem może być refleksja wsparta klasą Activator, ale to już bardziej złożony temat i nadaje się na osobny wpis.

Adnotacja - Lazy vs Func

Jeszcze jedna adnotacja w odniesieniu do komentarza użytkownika DK - mechanizm w tej postaci, zwraca zawsze ten sam obiekt. Jeśli chcemy korzystać zawsze z nowej instancji, lepszym wyjściem będzie użycie Dictionary z Func, np. w postaci:

private static Dictionary<Animals, Func<IAnimal>> objectsDict =
    new Dictionary<Animals, Func<IAnimal>>()
{
    { Animals.Cat, new Func<IAnimal>(() => new Cat()) },
    { Animals.Dog, new Func<IAnimal>(() => new Dog()) },
    { Animals.Wolf, new Func<IAnimal>(() => new Wolf()) },
};

Data ostatniej modyfikacji: 29.10.2015, 12:56.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus