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

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”

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
CreateChestCardprzekazujemy tylko, który gracz będzie właścicielem skrzyni. Typ kartyChestznajduje się wewnątrz metody. - W metodzie
CreateEnemyCardprzekazujemy tylko typ broni. Typ kartyEnemyznajduje 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