Interfejs C#: zrozumienie przewodzenia formularza nagród (Tim Corey, Lekcja 09)
W serii Tima Coreya "C# App Start To Finish", Lekcja 09 koncentruje się na podłączeniu formularza nagrody. Na pierwszy rzut oka ten formularz wydaje się prosty — wystarczy zebrać dane wejściowe od użytkownika, walidować je, stworzyć model i zapisać. Ale Tim wyjaśnia, że prawdziwa złożoność leży w decyzji, gdzie zapisać dane: w bazie danych, pliku tekstowym czy obu. Wideo Tima przeprowadza nas przez rozwiązanie, wprowadzając podstawową koncepcję w programowaniu C#: interfejsy.
W tym artykule przyjrzymy się bliżej interfejsom przez wyjaśnienia Tima, aby lepiej zrozumieć, jak pomagają tworzyć skalowalne, łatwe w utrzymaniu aplikacje.
Problem: Gdzie zapisać dane?
Tim zaczyna od podania celu formularza nagrody: przyjmuje dane wejściowe, waliduje je i zapisuje do przechowywania. Ale ostrzega, że trudna część to decyzja, gdzie przechować dane. Podkreśla, że samouczki często to pomijają, ponieważ to nie jest łatwe, ale chce, aby uczestnicy zetknęli się z tym bezpośrednio.
Wyjaśnia, że na początku możesz spróbować prostego rozwiązania: sprawdzić, czy używasz SQL czy pliku tekstowego, a następnie wykonać proces zapisywania. Ale Tim szybko pokazuje, jak brzydki i trudny do utrzymania się to staje. Jeśli każdy formularz musi sprawdzać, jaki typ przechowywania użyć, kod staje się zduplikowany, niechlujny i trudny do zmiany.
Brzydki sposób: Zakodowane warunki
Tim szkicuje przykład pseudokodu. Wyjaśnia, że możesz zacząć od sprawdzenia wartości logicznej, jak usingSQL == true, następnie otworzyć połączenie z bazą danych, zapisać model i zwrócić go z ID. Następnie możesz zrobić to samo dla plików tekstowych, ręcznie generując ID, ponieważ pliki tekstowe nie robią tego automatycznie.
Zauważa, że to szybko staje się powtarzalne. Wiele formularzy potrzebuje tej logiki i za każdym razem, gdy dodajesz nowe źródło danych jak MySQL, musisz aktualizować każdy formularz. Tim nazywa to "nie skalowalnym" i podkreśla, że to narusza zasadę "DRY" (Don't Repeat Yourself). Wyraźnie stwierdza: "Musi być lepszy sposób."
Wciągnięcie nitki: Lepsze podejście
Tim przedstawia swoją strategię: wciągnięcie nitki. Zaczyna od pytania, jakich informacji potrzebuje kod i skąd one pochodzą. Określa dwa kluczowe pytania:
Jak wiemy, które źródło danych użyć?
Jak połączyć się z dwoma różnymi źródłami danych, aby wykonać to samo zadanie?
Tim wyjaśnia, że akt zapisywania to jedyna rzecz, która się różni. Z perspektywy formularza potrzeba tylko powiedzmy: "Oto model. Zapisz go." Formularz nie powinien się przejmować, czy zapisuje do SQL czy pliku tekstowego.
Rozwiązanie: Globalna konfiguracja + interfejs
Tim sugeruje system globalnej konfiguracji. Mówi, że aby wiedzieć, które źródło danych użyć, aplikacja potrzebuje globalnie dostępnych danych, i proponuje użycie klasy statycznej do przechowywania tej informacji. Zaznacza, że globalne zmienne są zazwyczaj unikane, ale w tym przypadku globalne dane są dokładnie tym, co jest potrzebne.
Następnie Tim wyjaśnia kluczową koncepcję: interfejsy. Definiuje interfejs jako kontrakt — obietnicę, że każda klasa implementująca go będzie zawierała określone metody lub właściwości. Tim podkreśla, że to pozwala aplikacji wywoływać tę samą metodę niezależnie od źródła danych. Formularz nie martwi się, czy to SQL czy plik tekstowy; interfejsy dbają tylko o wywołanie metody.
Tim mówi: "Jeśli trzeba wykonać to samo zadanie, ale za kulisami będzie to zrobione na dwa różne sposoby, potrzebny jest interfejs."
Tworzenie interfejsu
Tim przechodzi do praktycznej implementacji przez utworzenie interfejsu w Tracker Library. Nadaje mu nazwę IDataConnection i wyjaśnia konwencję poprzedzania interfejsów przedrostkiem "I". Podkreśla, że to ważne, aby wyraźnie zidentyfikować go jako interfejs.
Tim dodaje jedną metodę do interfejsu:
PrizeModel CreatePrize(PrizeModel model);
Wyjaśnia, że ta metoda to kontrakt: musi istnieć w każdej klasie implementującej IDataConnection. Formularz wywoła tę metodę i oczekuje, że otrzyma z powrotem model nagrody z ID. Tim wyjaśnia, że tak formularz pozostaje agnostyczny wobec typu przechowywania.
Tworzenie statycznej klasy Global Config
Następnie Tim tworzy klasę statyczną nazwaną GlobalConfig. Wyjaśnia, że klasa statyczna nie może być instancjowana i jest globalnie dostępna. To tutaj aplikacja będzie przechowywać listę dostępnych połączeń danych.
Definiuje właściwość:
public static List<IDataConnection> Connections { get; private set; }
Tim wyjaśnia użycie private set, aby tylko klasa sama mogła modyfikować listę, a inne części aplikacji mogły ją tylko odczytywać.
Następnie tworzy metodę:
public static void InitializeConnections(bool database, bool textFiles)
Ta metoda konfiguruje dostępne połączenia danych. Tim podkreśla, że lista pozwala na wiele połączeń, co oznacza, że aplikacja może zapisywać do SQL, plików tekstowych, lub obu.
Zrozumienie interfejsów: Przykład ze świata rzeczywistego
Tim zatrzymuje się, aby uspokoić uczących się, że jest to złożony materiał, ale osiągalny. Zaleca obejrzenie filmu raz, a następnie ponowne obejrzenie podczas programowania.
Wyjaśnia, że interfejs jest kontraktem i każda klasa go implementująca musi go przestrzegać. Demonstruje to, tworząc klasę SQLConnector implementującą IDataConnection.
Kiedy klasa jest tworzona, Visual Studio ostrzega, że kontrakt nie jest spełniony. Tim pokazuje, jak używać "Implement Interface" do automatycznego generowania metody CreatePrize. Wyjaśnia również NotImplementedException i dlaczego istnieje - pozwala na skompilowanie kodu bez udawania, że metoda działa.
Tworzenie konektorów SQL i tekstowego
Tim dodaje klasy SQLConnector i TextConnector, obie implementujące IDataConnection. Wyjaśnia, że chociaż zapisywanie do bazy danych SQL i zapisywanie do pliku tekstowego to bardzo różne procesy, oba spełniają ten sam kontrakt interfejsu.
Dodaje teraz prostą przykładową wartość zwrotną i umieszcza komentarze typu TODO, aby przypomnieć sobie o wdrożeniu rzeczywistej logiki zapisywania później. To utrzymuje funkcjonalność aplikacji, jednocześnie postępując z lekcją.
Ostateczna konfiguracja: Łączenie globalnej konfiguracji
Tim wraca do klasy GlobalConfig i łączy rzeczywiste połączenia. Pokazuje, jak zainicjować listę Connections i dodać do niej instancje SQLConnector i TextConnector.
Wyjaśnia, dlaczego potrzebne są dwa osobne instrukcje if zamiast if-else - ponieważ użytkownik może chcieć zapisać do obu źródeł danych jednocześnie.
Gdzie wywołać InitializeConnections?
Tim wyjaśnia, że InitializeConnections musi być wywołane na początku aplikacji. Modyfikuje Program.cs i wywołuje:
GlobalConfig.InitializeConnections(true, true);
przed uruchomieniem formularza. Zapewnia to, że lista połączeń jest gotowa i dostępna w całej aplikacji.
Następnie zmienia formularz startowy na CreatePrizeForm, aby natychmiast przetestować funkcjonalność.
Walidacja formularza nagrody
Tim otwiera formularz i wyjaśnia pierwsze zadanie: walidację czterech pól. Preferuje utrzymać zdarzenia czyste, więc tworzy prywatną metodę o nazwie ValidateForm().
Tim wyjaśnia, że tę metodę można wywołać z dowolnego miejsca, nie tylko kliknięciu przycisku. Zwraca wartość logiczną wskazującą, czy formularz jest prawidłowy, czy nie. Pokazuje swój wzór użycia zmiennej wyjściowej:
bool output = true; return output;
Mówi, że lubi zaczynać od true, ponieważ łatwiej jest zmienić na false, gdy coś jest nie tak, niż ustawiać na true po każdej kontroli.
Sprawdzanie numeru miejsca
Tim wyjaśnia pierwszą walidację: numer miejsca musi być liczbą całkowitą większą od zera.
Używa int.TryParse, aby przekonwertować PlaceNumberValue.Text (ciąg znaków) na liczbę całkowitą. Tim analizuje, jak działa TryParse:
-
Pobiera ciąg i próbuje przekonwertować na liczbę.
-
Zwraca wartość logiczną wskazującą sukces lub porażkę.
- Używa parametru wyjściowego, aby wyprowadzić przekonwertowaną wartość.
Tim podkreśla, że TryParse jest bezpieczniejszy niż Parse, ponieważ nie zawiesza się przy błędnych danych wejściowych - zwraca false i ustawia wyjście na zero.
Następnie wyjaśnia logikę:
-
Jeśli placeNumberValidNumber jest false, ustaw output = false.
- Jeśli placeNumber < 1, ustaw output = false.
Tim ostrzega przed używaniem tutaj instrukcji else, ponieważ ta metoda ma wiele kontroli. Jeśli jedno sprawdzenie zawiedzie, metoda powinna mimo to ocenić inne, aby zebrać wszystkie błędy.
Walidacja nazwy miejsca
Tim przechodzi do kolejnej walidacji: nazwa miejsca nie może być pusta.
Sprawdza:
if (placeNameValue.Text.Length == 0) { output = false; }
Tim tłumaczy, że w rzeczywistej aplikacji wyświetlisz komunikaty błędów dla każdej nieudanej walidacji. Ale na razie trzyma to prosto i zwraca tylko true/false.
Walidacja kwoty nagrody vs procentu nagrody
Tim wyjaśnia, że formularz musi zawierać albo kwotę nagrody, albo procent nagrody (jeden z nich musi być większy niż zero). Zwraca uwagę na ważną różnicę:
-
Procent nagrody to liczba całkowita (int)
- Kwota nagrody to liczba dziesiętna (decimal), ponieważ pieniądze mogą zawierać grosze.
Tworzy zmienne:
decimal prizeAmount = 0; int prizePercentage = 0;
Następnie używa TryParse dla obu:
bool prizeAmountValid = decimal.TryParse(prizeAmountValue.Text, out prizeAmount); bool prizePercentageValid = int.TryParse(prizePercentageValue.Text, out prizePercentage);
Tim wyjaśnia, że oba muszą być ważnymi liczbami. Jeśli jeden z nich jest nieważny, formularz jest nieprawidłowy.
Następnie sprawdza, czy co najmniej jeden z nich jest większy niż zero:
if (prizeAmount <= 0 && prizePercentage <= 0) { output = false; }
Tim dodaje również kontrolę, aby upewnić się, że procent mieści się między 0 a 100:
if (prizePercentage < 0 || prizePercentage > 100) { output = false; }
Wyjaśnia dlaczego: 150% oznaczałoby, że oddajesz więcej niż pula nagród, co jest niemożliwe.
Korzystanie z wyników walidacji
Po zakończeniu wszystkich kontroli Tim wyjaśnia, jak korzystać z wyniku:
if (ValidateForm()) { // utwórz model i zapisz } else { MessageBox.Show("Formularz zawiera nieprawidłowe informacje. Proszę sprawdzić i spróbować ponownie."); }
Tim zauważa, że można by przerwać wcześniej na pierwszym błędzie, ale wybiera przeprowadzenie wszystkich kontroli, żeby użytkownicy mogli zobaczyć wszystkie błędy walidacji jednocześnie. To zmniejsza frustrację, ponieważ mogą naprawić wszystko za jednym razem.
Tworzenie modelu nagrody
Tim wyjaśnia, że gdy formularz jest prawidłowy, kolejnym krokiem jest utworzenie PrizeModel.
Demonstruje, jak tworzyć model:
PrizeModel model = new PrizeModel(); model.PlaceName = placeNameValue.Text; model.PlaceNumber = placeNumberValue.Text; // problem: to jest ciąg znaków
Tim podkreśla problem: PlaceNumber to int, ale wartość w formularzu to ciąg. Aby to rozwiązać, wyjaśnia dwie opcje:
-
Ponowne parsowanie każdej wartości w formularzu (powtarzalne).
- Dodanie przeciążenia konstruktora w PrizeModel.
Tim wybiera opcję 2.
Przeciążony konstruktor w PrizeModel
Tim dodaje przeciążony konstruktor, który przyjmuje cztery ciągi:
public PrizeModel(string placeName, string placeNumber, string prizeAmount, string prizePercentage)
{
PlaceName = placeName;
PlaceNumber = int.TryParse(placeNumber, out int placeNumberValue) ? placeNumberValue : 0;
PrizeAmount = decimal.TryParse(prizeAmount, out decimal prizeAmountValue) ? prizeAmountValue : 0;
PrizePercentage = double.TryParse(prizePercentage, out double prizePercentageValue) ? prizePercentageValue : 0;
}
Tim wyjaśnia, że nie martwi się nieudanym parsowaniem, ponieważ domyślnie będzie to zero, co jest domyślną wartością dla liczb.
Ten konstruktor pozwala formularzowi na tworzenie PrizeModel bezpośrednio z wejściami typu string i pozwala modelowi zarządzać parsowaniem.
Zapisywanie modelu za pomocą IDataConnection
Teraz, gdy model istnieje, Tim wyjaśnia, jak go zapisać, używając globalnej listy połączeń.
Używa pętli foreach:
foreach (IDataConnection db in GlobalConfig.Connections) { db.CreatePrize(model); }
Tim wyjaśnia, że ta pętla wywołuje CreatePrize() na każdym połączeniu (SQL i plik tekstowy). Mimo że metody nie są jeszcze zaimplementowane, formularz działa i udaje, że zapisuje dane. To dowodzi, że wzór interfejsu i konfiguracji globalnej działa.
Testowanie formularza
Tim podkreśla znaczenie wczesnego testowania. Dodaje punkty przerwania i uruchamia aplikację.
-
Najpierw testuje pusty formularz.
-
Przechodzi przez ValidateForm().
-
Widzi, że wyjście to fałsz i walidacja zawodzi zgodnie z oczekiwaniami.
-
Następnie wprowadza poprawne dane i potwierdza, że konstruktor poprawnie wypełnia model.
- Potwierdza również, że pętla przechodzi przez oba połączenia.
Tim demonstruje, że formularz działa, a wzór jest zweryfikowany.
Ostateczne porządki: Czyszczenie formularza
Tim wprowadza kilka ostatnich usprawnień:
-
Po pomyślnym utworzeniu nagrody, wyczyść pola formularza.
- Ustaw wartości domyślne 0 dla kwoty nagrody i procentu, aby użytkownicy nie musieli wpisywać zera za każdym razem.
Następnie potwierdza, że formularz poprawnie się czyści po prawidłowym złożeniu.
Co dalej?
Tim kończy wideo, mówiąc, że następnym krokiem jest połączenie klas połączeń SQL i tekstowej, aby faktycznie zapisywały dane.
Przypomina widzom, aby śledzili kolejną lekcję, w której zaimplementuje konektor SQL i rzeczywiście połączy się z bazą danych.
