Jak stworzyć aplikację do zarządzania kontaktami w C#: Praktyczny poradnik dla początkujących
Dziś pokażę Ci, jak krok po kroku stworzyć prostą, ale funkcjonalną aplikację konsolową w języku C#. Dzięki niej będziesz mógł zarządzać listą kontaktów, jednocześnie ucząc się podstawowych zasad
programowania zgodnych z dobrymi praktykami, takimi jak SOLID. Brzmi ciekawie? Zaczynajmy! 🚀
Co stworzysz?
Nasza aplikacja, nazwana ContactManager, pozwoli Ci:
- Dodawać nowe kontakty – wprowadź imię, nazwisko, email i numer telefonu.
- Wyświetlać listę kontaktów – przeglądaj wszystkie zapisane dane.
- Wyszukiwać kontakty – szybko znajdź osobę po imieniu.
- Zachować dane po zamknięciu – zapisz kontakty w pliku i wczytaj je przy ponownym uruchomieniu.
Krok 1: Podstawowy kod aplikacji
Rozpoczniemy od prostego kodu. W głównej klasie Program
znajdziesz menu, które obsługuje podstawowe funkcje, takie jak dodawanie kontaktów, ich wyświetlanie i wyszukiwanie.
Oto przykładowa implementacja w C#:
using System;
using System.Collections.Generic;
namespace ContactManager;
class Program
{
static void Main(string[] args)
{
List<Contact> contacts = new List<Contact>();
string userInput;
Console.WriteLine("=== Witaj w aplikacji do zarządzania kontaktami ===");
do
{
Console.WriteLine("\nWybierz opcję:");
Console.WriteLine("1. Dodaj nowy kontakt");
Console.WriteLine("2. Wyświetl wszystkie kontakty");
Console.WriteLine("3. Wyszukaj kontakt po imieniu");
Console.WriteLine("4. Wyjdź z aplikacji");
Console.Write("Twój wybór: ");
userInput = Console.ReadLine();
switch (userInput)
{
case "1":
AddContact(contacts);
break;
case "2":
DisplayContacts(contacts);
break;
case "3":
SearchContact(contacts);
break;
case "4":
Console.WriteLine("Dziękujemy za skorzystanie z aplikacji. Do zobaczenia!");
break;
default:
Console.WriteLine("Nieprawidłowy wybór. Spróbuj ponownie.");
break;
}
} while (userInput != "4");
}
static void AddContact(List<Contact> contacts)
{
Console.WriteLine("\n=== Dodaj nowy kontakt ===");
Console.Write("Podaj imię: ");
string firstName = Console.ReadLine();
Console.Write("Podaj nazwisko: ");
string lastName = Console.ReadLine();
Console.Write("Podaj email: ");
string email = Console.ReadLine();
Console.Write("Podaj numer telefonu: ");
string phoneNumber = Console.ReadLine();
contacts.Add(new Contact(firstName, lastName, email, phoneNumber));
Console.WriteLine("Kontakt został dodany!");
}
static void DisplayContacts(List<Contact> contacts)
{
Console.WriteLine("\n=== Lista kontaktów ===");
if (contacts.Count == 0)
{
Console.WriteLine("Brak kontaktów na liście.");
return;
}
foreach (var contact in contacts)
{
Console.WriteLine(contact);
}
}
static void SearchContact(List<Contact> contacts)
{
Console.WriteLine("\n=== Wyszukaj kontakt ===");
Console.Write("Podaj imię do wyszukania: ");
string searchName = Console.ReadLine();
var foundContacts = contacts.FindAll(c => c.FirstName.Equals(searchName, StringComparison.OrdinalIgnoreCase));
if (foundContacts.Count > 0)
{
Console.WriteLine($"Znaleziono {foundContacts.Count} kontakt(ów):");
foreach (var contact in foundContacts)
{
Console.WriteLine(contact);
}
}
else
{
Console.WriteLine("Nie znaleziono kontaktów o podanym imieniu.");
}
}
}
namespace ContactManager;
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public Contact(string firstName, string lastName, string email, string phoneNumber)
{
FirstName = firstName;
LastName = lastName;
Email = email;
PhoneNumber = phoneNumber;
}
public override string ToString()
{
return $"Imię: {FirstName}, Nazwisko: {LastName}, Email: {Email}, Telefon: {PhoneNumber}";
}
}
Jak działa aplikacja?
- Po uruchomieniu aplikacja wyświetla menu główne z dostępnymi opcjami.
- Użytkownik wybiera opcję, wpisując numer w konsoli.
- W zależności od wyboru użytkownik może:
– dodawać nowe kontakty,
– przeglądać listę kontaktów,
– wyszukiwać kontakty po imieniu
– lub zakończyć działanie aplikacji.
Refactoring – Dlaczego warto rozdzielić odpowiedzialności w kodzie? Przykład z zarządzaniem kontaktami
Często podczas pracy nad projektem pojawia się pokusa, by łączyć funkcjonalności w jednym miejscu. Za przykład może posłużyć klasa Program
, w której umieściliśmy metody AddContact
, DisplayContacts
i SearchContact
. Na pierwszy rzut oka mogłoby się wydawać, że lepszym rozwiązaniem byłoby przeniesienie tych metod do klasy Contact
, co wydaje się intuicyjne i logiczne. W końcu te metody operują na kontaktach, więc naturalnym wydaje się, by to właśnie klasa reprezentująca pojedynczy kontakt je zawierała.
Jednak takie podejście ma swoje pułapki. Zrozumienie i stosowanie zasad projektowania, takich jak SOLID, może pomóc nam stworzyć bardziej przejrzysty i łatwiejszy do utrzymania kod.
Klasa Contact – prostota i jedno zadanie
Klasa Contact
powinna mieć jedno, dobrze zdefiniowane zadanie: reprezentować pojedynczy kontakt. Jej odpowiedzialność ogranicza się do przechowywania danych, takich jak imię, nazwisko, numer telefonu czy e-mail, oraz ewentualnego formatowania tych danych na potrzeby wyświetlania. Wprowadzenie do niej funkcji zarządzających całą kolekcją kontaktów byłoby sprzeczne z zasadą SRP (Single Responsibility Principle), która mówi, że klasa powinna mieć jedną odpowiedzialność.
Zarządzanie kolekcją – czas na ContactManager
Metody takie jak AddContact
, DisplayContacts
czy SearchContact
operują na zbiorze obiektów. Z tego powodu lepiej pasują do dedykowanej klasy, której zadaniem jest zarządzanie listą kontaktów. Taką klasą może być ContactManager
. Dzięki temu kod staje się bardziej czytelny i podzielony zgodnie z zasadą SRP.
Nowy podział wyglądałby tak:
- Contact – przechowuje dane jednego kontaktu.
- ContactManager – odpowiada za operacje na kolekcji kontaktów (dodawanie, wyszukiwanie, wyświetlanie itd.).
Korzyści z takiego podejścia
- Czytelność i łatwość utrzymania kodu
Rozdzielając odpowiedzialności, ułatwiamy sobie pracę nad kodem w przyszłości. Każda klasa ma jasno określoną rolę. - Testowalność
KlasaContact
iContactManager
mogą być testowane niezależnie, co upraszcza proces tworzenia testów jednostkowych. - Zgodność z SOLID
Dążenie do stosowania zasady SRP sprawia, że kod jest zgodny z dobrymi praktykami projektowymi.
Podsumowanie
Przeniesienie metod zarządzających kontaktami z klasy Program
do nowo utworzonej klasy ContactManager
to krok w stronę bardziej przejrzystego i modułowego projektu. Klasa Contact
pozostaje prostą reprezentacją danych, a zarządzanie kolekcją kontaktów zostaje oddelegowane do dedykowanej klasy. Dzięki takiemu podejściu nasz kod będzie łatwiejszy do rozbudowy, testowania i utrzymania – co jest celem każdego dobrze zaprojektowanego systemu.
Dodajemy klasę ContactManager
Przenosimy operacje zarządzania listą kontaktów do dedykowanej klasy ContactManager
. Klasa Contact
pozostaje prostą reprezentacją danych.
namespace ContactManager;
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public Contact(string firstName, string lastName, string email, string phoneNumber)
{
FirstName = firstName;
LastName = lastName;
Email = email;
PhoneNumber = phoneNumber;
}
public override string ToString()
{
return $"Imię: {FirstName}, Nazwisko: {LastName}, Email: {Email}, Telefon: {PhoneNumber}";
}
}
using System;
using System.Collections.Generic;
namespace ContactManager;
class Program
{
static void Main(string[] args)
{
var contactManager = new ContactManager();
string userInput;
Console.WriteLine("=== Witaj w aplikacji do zarządzania kontaktami ===");
do
{
Console.WriteLine("\nWybierz opcję:");
Console.WriteLine("1. Dodaj nowy kontakt");
Console.WriteLine("2. Wyświetl wszystkie kontakty");
Console.WriteLine("3. Wyszukaj kontakt po imieniu");
Console.WriteLine("4. Wyjdź z aplikacji");
Console.Write("Twój wybór: ");
userInput = Console.ReadLine();
switch (userInput)
{
case "1":
contactManager.AddContact();
break;
case "2":
contactManager.DisplayContacts();
break;
case "3":
contactManager.SearchContact();
break;
case "4":
Console.WriteLine("Dziękujemy za skorzystanie z aplikacji. Do zobaczenia!");
break;
default:
Console.WriteLine("Nieprawidłowy wybór. Spróbuj ponownie.");
break;
}
} while (userInput != "4");
}
}
namespace ContactManager;
class ContactManager
{
private List<Contact> contacts = new List<Contact>();
public void AddContact()
{
Console.WriteLine("\n=== Dodaj nowy kontakt ===");
Console.Write("Podaj imię: ");
string firstName = Console.ReadLine();
Console.Write("Podaj nazwisko: ");
string lastName = Console.ReadLine();
Console.Write("Podaj email: ");
string email = Console.ReadLine();
Console.Write("Podaj numer telefonu: ");
string phoneNumber = Console.ReadLine();
contacts.Add(new Contact(firstName, lastName, email, phoneNumber));
Console.WriteLine("Kontakt został dodany!");
}
public void DisplayContacts()
{
Console.WriteLine("\n=== Lista kontaktów ===");
if (contacts.Count == 0)
{
Console.WriteLine("Brak kontaktów na liście.");
return;
}
foreach (var contact in contacts)
{
Console.WriteLine(contact);
}
}
public void SearchContact()
{
Console.WriteLine("\n=== Wyszukaj kontakt ===");
Console.Write("Podaj imię do wyszukania: ");
string searchName = Console.ReadLine();
var foundContacts = contacts.FindAll(c => c.FirstName.Equals(searchName, StringComparison.OrdinalIgnoreCase));
if (foundContacts.Count > 0)
{
Console.WriteLine($"Znaleziono {foundContacts.Count} kontakt(ów):");
foreach (var contact in foundContacts)
{
Console.WriteLine(contact);
}
}
else
{
Console.WriteLine("Nie znaleziono kontaktów o podanym imieniu.");
}
}
}
Kluczowe zmiany w architekturze zarządzania kontaktami
Podczas refaktoryzacji projektu zarządzającego kontaktami wprowadziliśmy istotne zmiany, które poprawiają organizację kodu oraz ułatwiają jego utrzymanie. Oto jak teraz wygląda podział odpowiedzialności pomiędzy poszczególne klasy.
1. Klasa Contact – prostota i jedno zadanie
Klasa Contact
pozostaje minimalistyczna, co ułatwia jej zrozumienie i użycie. Służy wyłącznie do przechowywania danych kontaktu (takich jak imię, nazwisko, numer telefonu czy e-mail) oraz dostarcza metodę ToString
, która pozwala w prosty sposób wyświetlić dane kontaktu w formie tekstowej. Dzięki temu klasa jest czysta, czytelna i spełnia zasadę SRP (Single Responsibility Principle).
2. Klasa ContactManager – zarządzanie kolekcją kontaktów
Wszystkie operacje na liście kontaktów zostały przeniesione do klasy ContactManager
. To ona odpowiada za takie funkcje jak:
- AddContact – dodawanie nowych kontaktów,
- DisplayContacts – wyświetlanie wszystkich kontaktów,
- SearchContact – wyszukiwanie kontaktów na podstawie kryteriów.
Dzięki temu klasa ContactManager
staje się centrum logiki zarządzania kolekcją, co czyni ją bardziej elastyczną i łatwiejszą do rozbudowy w przyszłości.
3. Program (Main) – interakcja z użytkownikiem
Funkcje obsługujące interakcję z użytkownikiem zostały skoncentrowane w głównej części programu (Main
). To tutaj użytkownik wybiera operacje, które są następnie delegowane do klasy ContactManager
. Takie podejście pozwala oddzielić logikę aplikacji od interfejsu użytkownika, co jest szczególnie istotne w większych projektach.
Zalety nowego układu
- Lepsze rozdzielenie odpowiedzialności
Każda klasa ma jasno zdefiniowaną rolę, co sprawia, że kod jest bardziej przejrzysty i zgodny z dobrymi praktykami projektowymi. - Łatwiejsze testowanie i rozszerzanie
KlasaContactManager
może być testowana i rozwijana niezależnie od innych elementów aplikacji. Dodanie nowych funkcji, takich jak sortowanie kontaktów czy eksport do pliku, będzie proste i intuicyjne. - Czystość klasy Contact
KlasaContact
jest ograniczona wyłącznie do reprezentowania pojedynczego kontaktu, co czyni ją prostą w implementacji i utrzymaniu.
Podsumowanie
Refaktoryzacja projektu znacząco poprawiła jego strukturę. Dzięki wprowadzeniu klasy ContactManager
i ograniczeniu odpowiedzialności klasy Contact
, kod stał się bardziej modularny i zgodny z zasadami SOLID. W efekcie projekt jest łatwiejszy w utrzymaniu, testowaniu i rozwijaniu – co powinno być celem każdego programisty dążącego do tworzenia wysokiej jakości oprogramowania.
Refaktoryzacja w kontekście zasad SOLID: Analiza i możliwe usprawnienia
Po wprowadzeniu zmian w kodzie i podziale odpowiedzialności między klasy, nasz projekt stał się bardziej zgodny z zasadami SOLID. Jednak zawsze można dokonać dodatkowych ulepszeń, aby kod był jeszcze bardziej elastyczny, modularny i łatwiejszy w utrzymaniu. Poniżej omawiamy każdą z zasad SOLID, oceniamy obecny stan projektu i wskazujemy potencjalne usprawnienia.
1. SRP – Zasada jednej odpowiedzialności
Każda klasa powinna mieć jedno, jasno określone zadanie.
Ocena:
- Contact – Reprezentuje pojedynczy kontakt. Zadanie jest jasno określone i ograniczone do przechowywania danych oraz ich formatowania.
- ContactManager – Zarządza całą listą kontaktów, w tym dodawaniem, wyszukiwaniem i wyświetlaniem.
- Program (Main) – Zarządza interakcją z użytkownikiem, delegując operacje do klasy
ContactManager
.
Wniosek: Zasada SRP jest w pełni spełniona.
2. OCP – Zasada otwarte/zamknięte
Kod powinien być otwarty na rozszerzenia, ale zamknięty na modyfikacje.
Ocena:
- Dodanie nowych funkcji, takich jak zapis kontaktów do pliku czy filtrowanie według dodatkowych kryteriów, nie wymaga modyfikowania istniejących metod. Możemy po prostu rozszerzyć
ContactManager
. - Klasy są w dużej mierze zgodne z OCP dzięki możliwości dziedziczenia i ewentualnego wprowadzenia interfejsów.
Wniosek: Zasada jest spełniona.
3. LSP – Zasada podstawienia Liskov
Obiekty klas bazowych powinny być zastępowane przez obiekty klas pochodnych bez zmiany funkcjonalności programu.
Ocena:
- Obecny kod nie korzysta z dziedziczenia, więc nie ma ryzyka naruszenia tej zasady.
- W przyszłości, jeśli zdecydujemy się na różne typy kontaktów (np. osobisty, służbowy), należy upewnić się, że klasy pochodne będą mogły być używane zamiennie z klasą
Contact
.
Wniosek: Zasada nie jest naruszona.
4. ISP – Zasada segregacji interfejsów
Klasy nie powinny być zmuszane do implementowania metod, których nie używają.
Ocena:
- Brak interfejsów oznacza, że zasada nie jest naruszona.
- Warto rozważyć wprowadzenie interfejsu (np.
IContactManager
) dla klasyContactManager
. Dzięki temu ułatwimy testowanie, rozszerzalność i wprowadzanie nowych implementacji, np. do obsługi kontaktów w bazie danych.
Wniosek: Zasada jest spełniona, ale można ją ulepszyć.
5. DIP – Zasada odwrócenia zależności
Moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu. Oba powinny zależeć od abstrakcji.
Ocena:
- W obecnym kodzie klasa
Program
(moduł wyższego poziomu) bezpośrednio zależy od konkretnej implementacjiContactManager
. Aby spełnić tę zasadę,Program
powinien korzystać z abstrakcji (np. interfejsuIContactManager
). - Klasa
ContactManager
mogłaby być jedną z wielu możliwych implementacji interfejsu, co zwiększa elastyczność i umożliwia łatwą zamianę na inne rozwiązania.
Wniosek: Zasada jest częściowo spełniona i wymaga poprawy.
Propozycje poprawek dla pełnej zgodności z SOLID
Wprowadzenie interfejsów:
- Stworzenie interfejsu IContactManager z metodami
AddContact, DisplayContacts, i SearchContact. - Klasa ContactManager będzie implementowała ten interfejs.
- Main będzie używał IContactManager zamiast konkretnej klasy ContactManager.
1. Stworzenie interfejsu
namespace ContactManager;
interface IContactManager
{
void AddContact();
void DisplayContacts();
void SearchContact();
}
2. ContactManager implementowała ten interfejs.
class ContactManager : IcontactManager
3. Main używał IContactManager
IContactManager contactManager = new ContactManager();
Zalety wprowadzenia interfejsu w projekcie zarządzania kontaktami
Wprowadzenie interfejsu, takiego jak IContactManager
, w projekcie niesie ze sobą szereg korzyści, które znacząco poprawiają jakość kodu oraz jego możliwości rozwoju i testowania. Poniżej przedstawiamy dwie kluczowe zalety tego podejścia.
1. Łatwość testowania
Dzięki zastosowaniu interfejsu możemy łatwo zamienić rzeczywistą implementację klasy ContactManager
na wersję testową, np. MockContactManager
, podczas pisania testów jednostkowych.
Korzyści:
- Izolacja testów: Możemy testować klasę
Program
(lub inną część projektu) bez konieczności uruchamiania prawdziwej implementacjiContactManager
. - Symulowanie zachowań: W mockach możemy zasymulować różne scenariusze, takie jak brak kontaktów, duża liczba kontaktów czy błędy podczas operacji.
- Większa niezależność od zmian w implementacji: Zmiana sposobu działania
ContactManager
nie wymusza modyfikacji testów, jeśli interfejs pozostaje taki sam.
2. Lepsza zgodność z zasadą odwrócenia zależności (DIP) i większa elastyczność
Zgodnie z zasadą DIP (Dependency Inversion Principle) moduły wyższego poziomu, takie jak klasa Program
, powinny zależeć od abstrakcji, a nie od konkretnych implementacji. Wprowadzenie interfejsu IContactManager
pozwala zrealizować tę zasadę.
Korzyści:
- Elastyczność: Możemy łatwo zastąpić
ContactManager
inną implementacją, np.DatabaseContactManager
do obsługi kontaktów w bazie danych lubCloudContactManager
do przechowywania kontaktów w chmurze. - Łatwiejsze rozszerzanie projektu: Nowe funkcje mogą być wprowadzane przez dodanie nowych klas implementujących interfejs, bez modyfikacji istniejącego kodu.
- Przygotowanie na przyszłe zmiany: Projekt staje się bardziej odporny na zmiany technologiczne, np. przejście z listy w pamięci na bazę danych wymaga jedynie zmiany implementacji, a nie modyfikacji głównej logiki aplikacji.
Podsumowanie
Zastosowanie interfejsu, takiego jak IContactManager
, zwiększa modularność projektu i ułatwia jego utrzymanie. Otrzymujemy bardziej elastyczny kod, który jest zgodny z zasadami SOLID, a jednocześnie prostszy w testowaniu i rozwijaniu. Dzięki temu projekt zyskuje profesjonalny charakter i jest lepiej przygotowany na przyszłe wyzwania.
Testy jednostkowe w xUnit – krok po kroku
Wprowadzenie testów jednostkowych to kluczowy krok w procesie zapewnienia jakości kodu. W tym przewodniku pokażemy, jak skonfigurować i rozpocząć testowanie projektu za pomocą xUnit.
1. Utworzenie projektu testowego
Aby dodać testy jednostkowe do swojego projektu, wykonaj następujące kroki:
- Otwórz swoje rozwiązanie w Visual Studio.
- Kliknij prawym przyciskiem myszy na solucję i wybierz:
Add > New Project. - Wybierz szablon projektu xUnit Test Project (.NET Core).
- Nadaj projektowi nazwę, np. ContactManager.Tests, i kliknij Create.
2. Dodanie odwołania do projektu ContactManager
Aby testy mogły działać na klasach i metodach w Twoim głównym projekcie, musisz dodać do niego referencję:
- W oknie Solution Explorer kliknij prawym przyciskiem myszy na projekt testowy (ContactManager.Tests) i wybierz Add > Project Reference.
- Zaznacz projekt główny, np. ContactManager, i kliknij OK.
Teraz projekt testowy będzie mógł korzystać z klas i metod projektu głównego.
3. Konfiguracja klasy testowej
W nowo utworzonym projekcie testowym znajdziesz przykładowy plik testowy UnitTest1.cs. Możesz go usunąć lub zmodyfikować. Następnie:
- Utwórz nowy plik testowy, np. ContactManagerTests.cs.
namespace ContactManager.Tests;
public class ContactTests
{
[Fact]
public void Constructor_SetsAllPropertiesCorrectly()
{
// Arrange & Act
var contact = new Contact("Marcin", "Nowak", "marcin.nowak@dev-hobby.pl", "567657285");
// Assert
Assert.Equal("Marcin", contact.FirstName);
Assert.Equal("Nowak", contact.LastName);
Assert.Equal("marcin.nowak@dev-hobby.pl", contact.Email);
Assert.Equal("567657285", contact.PhoneNumber);
}
[Fact]
public void Contact_ToString_ReturnsCorrectFormat()
{
// Arrange
var contact = new Contact("Marcin", "Nowak", "marcin.nowak@dev-hobby.pl", "567657285");
// Act
var result = contact.ToString();
// Assert
Assert.Equal("Imię: Marcin, Nazwisko: Nowak, Email: marcin.nowak@dev-hobby.pl, Telefon: 567657285", result);
}
}
4. Uruchamianie testów
Aby uruchomić testy:
- Otwórz Test Explorer w Visual Studio (View > Test Explorer).
- Kliknij przycisk Run All, aby uruchomić wszystkie testy w projekcie.
Jeśli testy zakończą się powodzeniem, obok ich nazw pojawią się zielone znaczniki.
Podsumowanie
Dzięki xUnit wprowadzenie testów jednostkowych do projektu jest proste i intuicyjne. Testy nie tylko zwiększają pewność działania aplikacji, ale również ułatwiają rozwój projektu w przyszłości. Dodając odpowiednie testy dla kluczowych funkcji, takich jak dodawanie i wyszukiwanie kontaktów, zyskujemy narzędzie do weryfikacji poprawności działania kodu po każdej zmianie.
Wprowadzenie walidacji danych w klasie Contact
Poprawność danych wprowadzanych do aplikacji to kluczowy aspekt, który pozwala uniknąć błędów oraz problemów podczas korzystania z systemu. W celu zapewnienia odpowiedniej jakości danych wprowadźmy walidację w klasie Contact
.
Zasady walidacji
- Imię i nazwisko: Pole nie może być puste.
- Email: Musi mieć poprawny format, który zostanie sprawdzony za pomocą wyrażenia regularnego.
- Numer telefonu: Powinien zawierać wyłącznie cyfry.
Dane, które nie spełniają powyższych wymagań, spowodują rzucenie wyjątku ArgumentException
.
using System.Text.RegularExpressions;
namespace ContactManager;
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public Contact(string firstName, string lastName, string email, string phoneNumber)
{
if (string.IsNullOrWhiteSpace(firstName))
throw new ArgumentException("Imię nie może być puste.");
if (string.IsNullOrWhiteSpace(lastName))
throw new ArgumentException("Nazwisko nie może być puste.");
if (!IsValidEmail(email))
throw new ArgumentException("Nieprawidłowy format adresu email.");
if (!IsValidPhoneNumber(phoneNumber))
throw new ArgumentException("Nieprawidłowy numer telefonu. Dozwolone są tylko cyfry.");
FirstName = firstName;
LastName = lastName;
Email = email;
PhoneNumber = phoneNumber;
}
public override string ToString()
{
return $"Imię: {FirstName}, Nazwisko: {LastName}, Email: {Email}, Telefon: {PhoneNumber}";
}
private static bool IsValidEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
private static bool IsValidPhoneNumber(string phoneNumber)
{
return Regex.IsMatch(phoneNumber, @"^\d+$");
}
}
Wyjaśnienie wprowadzonej walidacji danych
Wprowadzenie walidacji w klasie Contact
opiera się na prostych, ale skutecznych mechanizmach, które zapewniają poprawność danych. Poniżej znajduje się szczegółowe wyjaśnienie zastosowanych rozwiązań.
1. Walidacja danych w konstruktorze
W konstruktorze klasy Contact
sprawdzamy poprawność każdego pola:
- Imię i nazwisko:
Wartość musi być niepusta. Jeśli jest pusta lub zawiera tylko białe znaki, rzucany jest wyjątekArgumentException
. - Email:
Sprawdzamy poprawność adresu e-mail za pomocą wyrażenia regularnego. Pozwala ono upewnić się, że email ma format zgodny ze standardem, np.użytkownik@domena.com
. - Numer telefonu:
Używamy wyrażenia regularnego, aby upewnić się, że numer zawiera wyłącznie cyfry.
2. Metody pomocnicze
Aby zachować czytelność i modularność kodu, walidacja jest realizowana przez dedykowane metody:
IsValidPhoneNumber
:
Metoda ta sprawdza, czy podany numer telefonu zawiera wyłącznie cyfry.IsValidEmail
:
Sprawdza, czy podany adres e-mail ma poprawny format.
3. Wyjątki
Jeśli dane wejściowe nie spełniają wymagań, w odpowiednich miejscach rzucany jest wyjątek ArgumentException
.
- Wyjątki są rzucane w momencie przypisywania wartości do pól (setterów) lub w konstruktorze.
- Komunikat wyjątku precyzyjnie informuje o problemie, co ułatwia użytkownikowi poprawienie błędu.
Zalety podejścia
- Centralizacja walidacji:
Wszystkie zasady są zdefiniowane w jednym miejscu – w klasieContact
. - Czytelność kodu:
Dzięki metodom pomocniczym kod jest prosty do zrozumienia i łatwy do rozszerzenia. - Ochrona przed błędami:
Walidacja na poziomie klasyContact
zapobiega przypadkowemu wprowadzeniu niepoprawnych danych w innych częściach aplikacji.
Podsumowanie
Walidacja danych w klasie Contact
zapewnia wysoką jakość danych w systemie. Dzięki wykorzystaniu wyrażeń regularnych oraz obsługi wyjątków aplikacja jest bardziej odporna na błędy, a użytkownicy są informowani o nieprawidłowościach w przejrzysty sposób. To podejście jest fundamentem solidnej i profesjonalnej aplikacji.
Testy jednostkowe dla walidacji w klasie Contact
Testowanie walidacji danych to kluczowy element zapewnienia niezawodności aplikacji. Poniżej znajduje się szczegółowy opis przykładowych testów jednostkowych oraz ich implementacja z wykorzystaniem xUnit.
Implementacja testów jednostkowych
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Constructor_InvalidFirstName_ThrowsArgumentException(string invalidFirstName)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new Contact(invalidFirstName, "Nowak", "marcin.nowak@dev-hobby.pl", "567657285"));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public void Constructor_InvalidLastName_ThrowsArgumentException(string invalidLastName)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new Contact("Marcin", invalidLastName, "marcin.nowak@dev - hobby.pl", "567657285"));
}
[Theory]
[InlineData("john.doe@com")]
[InlineData("john.doe@.com")]
[InlineData("john.doe.com")]
[InlineData("john.doe@com.")]
[InlineData("@example.com")]
[InlineData("")]
public void Constructor_InvalidEmail_ThrowsArgumentException(string invalidEmail)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new Contact("Marcin", "Nowak", invalidEmail, "567657285"));
}
[Theory]
[InlineData("123abc")]
[InlineData("123-456")]
[InlineData("(123)456")]
[InlineData("")]
public void Constructor_InvalidPhoneNumber_ThrowsArgumentException(string invalidPhoneNumber)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new Contact("Marcin", "Nowak", "john.doe@example.com", invalidPhoneNumber));
}
Wyjaśnienie testów
- Test poprawnych danych:
- Sprawdzamy, czy konstruktor klasy
Contact
poprawnie przypisuje właściwości, gdy podane dane są prawidłowe. - Test:
Constructor_ValidData_SetsPropertiesCorrectly
.
- Sprawdzamy, czy konstruktor klasy
- Testy krawędziowe:
- Imię i nazwisko:
Testowane są przypadki, gdy dane są puste, zawierają tylko spacje lub mają wartośćnull
.- Test:
Constructor_InvalidFirstName_ThrowsArgumentException
. - Test:
Constructor_InvalidLastName_ThrowsArgumentException
.
- Test:
- Adres e-mail:
Testowane są typowe błędne formaty adresów e-mail, np. brak domeny lub znaku@
.- Test:
Constructor_InvalidEmail_ThrowsArgumentException
.
- Test:
- Numer telefonu:
Testowane są przypadki zawierające litery, znaki specjalne lub puste wartości.- Test:
Constructor_InvalidPhoneNumber_ThrowsArgumentException
.
- Test:
- Imię i nazwisko:
- Test metody
ToString
:- Sprawdzamy, czy metoda
ToString
zwraca ciąg tekstowy w oczekiwanym formacie, np.:"Imię Nazwisko, email, numer telefonu"
. - Test:
ToString_ReturnsExpectedFormat
.
- Sprawdzamy, czy metoda
Zalety testów
- Pewność poprawności walidacji:
Testy sprawdzają różnorodne przypadki niepoprawnych danych, dzięki czemu ryzyko błędów w aplikacji jest minimalizowane. - Modularność:
Każdy aspekt klasyContact
jest testowany osobno, co pozwala szybko wykrywać i naprawiać błędy. - Czytelność i zrozumiałość:
Użycie atrybutu[Theory]
i danych testowych ([InlineData]
) pozwala łatwo dodawać kolejne przypadki testowe.
Podsumowanie
Przygotowanie testów jednostkowych dla walidacji w klasie Contact
to kluczowy krok w zapewnieniu jakości aplikacji. Dzięki zastosowaniu xUnit i przemyślanej strukturze testów, kod jest łatwy w utrzymaniu, a potencjalne problemy są wykrywane jeszcze przed wdrożeniem.
Zapis i odczyt kontaktów do/z pliku JSON
Przechowywanie kontaktów w plikach umożliwia łatwe zachowanie danych między sesjami aplikacji. W tym przykładzie omówimy, jak zrealizować funkcjonalność zapisu i odczytu kontaktów w formacie JSON.
Implementacja
1. Zmiany w klasie ContactManager
i IContactManager
Dodajemy dwie nowe metody do zarządzania plikami:
SaveToFile(string filePath)
: Zapisuje kontakty do pliku w formacie JSON.LoadFromFile(string filePath)
: Wczytuje kontakty z pliku JSON.
namespace ContactManager;
interface IContactManager
{
void AddContact();
void DisplayContacts();
void SearchContact();
void SaveToFile(string filePath);
void LoadFromFile(string filePath);
}
public void SaveToFile(string filePath)
{
try
{
var json = JsonSerializer.Serialize(contacts, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, json);
Console.WriteLine($"Kontakty zostały zapisane do pliku: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Błąd podczas zapisywania do pliku: {ex.Message}");
}
}
public void LoadFromFile(string filePath)
{
try
{
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
contacts = JsonSerializer.Deserialize<List<Contact>>(json) ?? new List<Contact>();
Console.WriteLine("Kontakty zostały wczytane z pliku.");
}
else
{
Console.WriteLine("Plik nie istnieje. Nie można wczytać kontaktów.");
}
}
catch (Exception ex)
{
Console.WriteLine($"Błąd podczas wczytywania z pliku: {ex.Message}");
}
}
2. Aktualizacja klasy Program
W klasie Program
dodajemy obsługę zapisu i odczytu kontaktów w menu aplikacji.
static void Main(string[] args)
{
IContactManager contactManager = new ContactManager();
string userInput;
string filePath = "contacts.json";
contactManager.LoadFromFile(filePath);
Console.WriteLine("=== Witaj w aplikacji do zarządzania kontaktami ===");
do
{
Console.WriteLine("\nWybierz opcję:");
Console.WriteLine("1. Dodaj nowy kontakt");
Console.WriteLine("2. Wyświetl wszystkie kontakty");
Console.WriteLine("3. Wyszukaj kontakt po imieniu");
Console.WriteLine("4. Zapisz kontakty do pliku");
Console.WriteLine("5. Wczytaj kontakty z pliku");
Console.WriteLine("6. Wyjdź z aplikacji");
Console.Write("Twój wybór: ");
userInput = Console.ReadLine();
switch (userInput)
{
case "1":
contactManager.AddContact();
break;
case "2":
contactManager.DisplayContacts();
break;
case "3":
contactManager.SearchContact();
break;
case "4":
contactManager.SaveToFile(filePath);
break;
case "5":
contactManager.LoadFromFile(filePath);
break;
case "6":
Console.WriteLine("Dziękujemy za skorzystanie z aplikacji. Do zobaczenia!");
break;
default:
Console.WriteLine("Nieprawidłowy wybór. Spróbuj ponownie.");
break;
}
} while (userInput != "6");
}
Wyjaśnienie
- Zapis do pliku:
- Metoda
SaveToFile
serializuje listę kontaktów do JSON, co umożliwia przechowywanie danych w czytelnym formacie. - Plik jest zapisywany pod ścieżką podaną w parametrze
filePath
.
- Metoda
- Odczyt z pliku:
- Metoda
LoadFromFile
odczytuje dane z pliku JSON i deserializuje je do listy kontaktów. - Obsługuje przypadki, gdy plik nie istnieje, jest pusty lub uszkodzony.
- Metoda
- Integracja z menu:
- Opcje 4 i 5 w menu umożliwiają ręczne zapisanie lub wczytanie kontaktów.
Gratulacje! 🎉
Udało nam się stworzyć pełną aplikację do zarządzania kontaktami w języku C#! W tym artykule nauczyliśmy się:
🔹 Jak dodawać, przeglądać i wyszukiwać kontakty,
🔹 Jak organizować kod zgodnie z zasadami SOLID,
🔹 Jak zapisywać oraz odczytywać dane z pliku, aby nasze kontakty były zawsze dostępne.
To dopiero początek – możliwości rozwoju tego projektu są ogromne! 🌱 Możemy dodać zaawansowane funkcje, takie jak:
🔸 Filtrowanie kontaktów
🔸 Integracja z bazami danych
🔸 Obsługa formatu CSV
Jeśli artykuł Ci się spodobał, zostaw komentarz, aby podzielić się swoimi wrażeniami lub zadać pytanie. Chętnie odpowiem na wszystkie wątpliwości! 💬
Rozwój Umiejętności w C#
Jeśli czujesz, że brakuje Ci wiedzy, albo chcesz poszerzyć swoje umiejętności w C#, mam dla Ciebie wyjątkową ofertę! 🎓
Sprawdź moje płatne kursy, w których nauczysz się:
🔹 C# Podstawy Programowania – idealny dla tych, którzy zaczynają swoją przygodę z C#.
🔹 C# Podstawy Programowania Obiektowego – zrozumiesz fundamenty programowania obiektowego.
🔹 C# Najlepsze Praktyki – odkryjesz, jak pisać czytelny, wydajny i profesjonalny kod.
🔹 C# Wprowadzenie do Kolekcji – dowiesz się, jak efektywnie zarządzać danymi.
🔹 C# Generics – opanujesz zaawansowane techniki programowania generycznego.
Te kursy zostały zaprojektowane, by pomóc Ci osiągnąć swoje cele programistyczne – bez względu na to, czy dopiero zaczynasz, czy chcesz stać się ekspertem.
📌 Kliknij w link w opisie, aby wybrać kurs idealny dla siebie i rozpocząć naukę już teraz!
Inwestycja w rozwój swoich umiejętności to najlepsza decyzja, jaką możesz podjąć. 💪
Dodatkowa Oferta! 🎁
Jeśli chcesz otrzymać zniżkę na wybrane kursy, napisz do mnie maila na adres mariuszjurczenko@dev-hobby.pl. Przygotuję dla Ciebie specjalną zniżkę na wybrane kursy!
Zniżki wynoszą od 30% do 50%, a także dodatkowe niespodzianki! 🎉
Dziękuję za przeczytanie tego artykułu i do zobaczenia w kolejnych! 👋