Prywatny konstruktor – kiedy może się przydać?

Opublikowane przez Tomasz Prasołek w dniu

grayscale photo of black and white wooden sign

W tym wpisie opowiem Ci jak użyć konstruktor prywatny do stworzenia obiektu z od razu prawidłowymi danymi, bez potrzeby robienia walidacji parametrów.

Gra karciania “GameOver”

GameOver okładka gry
Zdjęcie pochodzi ze strony wydawnictwa Nasza Księgarnia

W domu gramy trochę w gry planszowe i karciane. W ramach zabawy i poznania frameworka Blazor postanowiłem zaprogramować grę “GameOver”. Jest to gra karciana wydana przez wydawnictwo Nasza księgarnia.

Więcej o grze przeczytasz tutaj: https://nk.com.pl/game-over/2539/gra.html#.XzEWPogzZhE

Gra jest już prawie na ukończeniu. Został do poprawy bug podczas napotkania smoka + drobne poprawki w interfejsie. Jak skończę ją pisać to opublikuję informację o tym.

Jak pewnie zauważyłeś, blog jest o systemie kontroli wersji Git, a ja tutaj piszę na całkowicie inny temat. O co kaman? A no o to, że chciałem napisać posta o tym, czego się ostatnio nauczyłem i go od razu opublikować. Skoro mam już bloga to dlaczego nie wykorzystać go od razu. Później – w wolnej chwili – raczej założę nowego bloga pod różne treści programistyczne, nie tylko związane z gitem.

Obiekt Card

W swoim kodzie posiadam klasę Card, która odpowiada za pojedynczą kartę do gry. Taka karta może być różnego typu:

  • Klucz
  • Skrzynia ze skarbem danego gracza
  • Wróg z którym się walczy
  • Drzwi
  • Smok – też to jest wróg, ale jego nie da się pokonać, dlatego dałem go jako oddzielny typ.

Dodatkowo jeśli karta jest skrzynią, musimy określić, którego gracza to skrzynia. A jeśli karta to wróg, to musimy wiedzieć jaką ma broń. W każdym innym przypadku ważny jest tylko typ. Na początku klasę zaprojektowałem tak:

public class Card
{
    public CardType Type { get; }
    public PlayerType? ChestOwner { get; }
    public Weapon? Weapon { get; }

    public Card(CardType type)
    {
        Type = type;
    }

    public Card(CardType type, Weapon weapon)
    {
        Type = type;
        Weapon = weapon;
    }

    public Card(CardType type, PlayerType chestOwner)
    {
        Type = type;
        ChestOwner = chestOwner;
    }
}

CardType, PlayerType oraz Weapon to enumy.

Klasa zawiera 3 konstruktory publiczne, które mają różne przeznaczenia. Pierwszy służy do stworzenia kart typu: smok, drzwi i klucz. Drugi do stworzenia karty wroga, a trzeci skrzyni gracza.

Czy używając tych konstruktorów mógłbym stworzyć kartę skrzyni wraz z jakąś bronią? Oczywiście, że tak, jeśli użyłbym drugiego konstruktora.

Można się przed tym zabezpieczyć sprawdzają jakie dane są przekazywane do konstruktora i np. wyrzucić wyjątek. Jednak jest lepsze rozwiązanie.

Publiczne statyczne metody

Zamiast tego można zrobić odpowiednie metody statyczne tworzące obiekt. W tych metodach przekazujemy tylko te dane, które są niezbędne. Reszta jest ustawiania wewnątrz metody, dzięki temu nie można już pomylić parametrów. Moje metody tworzące wyglądają tak:

public static Card CreateCard(CardType type)
{
    return new Card(type);
}

public static Card CreateChestCard(PlayerType owner)
{
    return new Card(CardType.Chest, owner);
}

public static Card CreateEnemyCard(Weapon Weapon)
{
    return new Card(CardType.Enemy, Weapon);
}

Pierwszą zaletą tego rozwiązania jest to, że nie stworzymy już obiektu ze złymi właściwościami.

  • W metodzie CreateChestCard przekazujemy tylko, który gracz będzie właścicielem skrzyni. Typ karty Chest znajduje się wewnątrz metody.
  • W metodzie CreateEnemyCard przekazujemy tylko typ broni. Typ karty Enemy znajduje się wewnątrz metody.

Drugą zaletą jest to, że mamy jasno nazwane metody, widać od razu co robię. Nie musimy przeglądać parametrów konstruktora i czytać komentarzy, aby dowiedzieć się co on robi. Zwiększa się również czytelność kodu.

Konstruktor prywatny

I tutaj właśnie wkraczają całe na biało konstruktory prywatne. Bo żeby stworzyć obiekt, trzeba użyć konstruktora, tak jak zrobiłem w przykładzie wyżej. Ale jeśli zostawimy je publiczne, to ktoś może z nich skorzystać i zrobić obiekt ze złymi parametrami. Po refaktoringu klasa Card wygląda następująco:

public class Card
{
    public CardType Type { get; }
    public PlayerType? ChestOwner { get; }
    public Weapon? Weapon { get; }

    private Card(CardType type)
    {
        Type = type;
    }

    private Card(CardType type, Weapon weapon)
    {
        Type = type;
        Weapon = weapon;
    }

    private Card(CardType type, PlayerType chestOwner)
    {
        Type = type;
        ChestOwner = chestOwner;
    }

    public static Card CreateCard(CardType type)
    {
        return new Card(type);
    }

    public static Card CreateChestCard(PlayerType owner)
    {
        return new Card(CardType.Chest, owner);
    }

    public static Card CreateEnemyCard(Weapon Weapon)
    {
        return new Card(CardType.Enemy, Weapon);
    }
}

Klasa zawiera teraz po 3:

  • właściwości,
  • prywatne konstruktory,
  • publiczne statyczne metody, które służą do stworzenia obiektów.

Oczywiście tej gry już nikt nie będzie zmieniał, mógłbym zostawić klasę Card w takiej postaci jak była na początku wpisu. Ale stwierdziłem, że to akurat jest idealne zastosowanie metod tworzących i dobry temat na wpis, więc postanowiłem zrefaktorować kod 🙂

Podsumowanie

W ten oto łatwy sposób, używając metod tworzących i konstruktorów prywatnych, można zabezpieczyć tworzenie obiektów. Od teraz już nikt (no chyba, że ktoś zmieni kod 🙂 ) nie stworzy nieprawidłowych obiektów kart.


0 Komentarzy

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *