Jak stworzyć aplikację do zarządzania kontaktami w C#: Praktyczny poradnik dla początkujących

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?

  1. Po uruchomieniu aplikacja wyświetla menu główne z dostępnymi opcjami.
  2. Użytkownik wybiera opcję, wpisując numer w konsoli.
  3. 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

  1. 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ę.
  2. Testowalność
    Klasa Contact i ContactManager mogą być testowane niezależnie, co upraszcza proces tworzenia testów jednostkowych.
  3. 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

  1. Lepsze rozdzielenie odpowiedzialności
    Każda klasa ma jasno zdefiniowaną rolę, co sprawia, że kod jest bardziej przejrzysty i zgodny z dobrymi praktykami projektowymi.
  2. Łatwiejsze testowanie i rozszerzanie
    Klasa ContactManager 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.
  3. Czystość klasy Contact
    Klasa Contact 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 klasy ContactManager. 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 implementacji ContactManager. Aby spełnić tę zasadę, Program powinien korzystać z abstrakcji (np. interfejsu IContactManager).
  • 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:

  1. Stworzenie interfejsu IContactManager z metodami
    AddContact, DisplayContacts, i SearchContact.
  2. Klasa ContactManager będzie implementowała ten interfejs.
  3. 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 implementacji ContactManager.
  • 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 lub CloudContactManager 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:

  1. Otwórz swoje rozwiązanie w Visual Studio.
  2. Kliknij prawym przyciskiem myszy na solucję i wybierz:
    Add > New Project.
  3. Wybierz szablon projektu xUnit Test Project (.NET Core).
  4. 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ę:

  1. W oknie Solution Explorer kliknij prawym przyciskiem myszy na projekt testowy (ContactManager.Tests) i wybierz Add > Project Reference.
  2. 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:

  1. 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:

  1. Otwórz Test Explorer w Visual Studio (View > Test Explorer).
  2. 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

  1. Imię i nazwisko: Pole nie może być puste.
  2. Email: Musi mieć poprawny format, który zostanie sprawdzony za pomocą wyrażenia regularnego.
  3. 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ątek ArgumentException.
  • 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

  1. Centralizacja walidacji:
    Wszystkie zasady są zdefiniowane w jednym miejscu – w klasie Contact.
  2. Czytelność kodu:
    Dzięki metodom pomocniczym kod jest prosty do zrozumienia i łatwy do rozszerzenia.
  3. Ochrona przed błędami:
    Walidacja na poziomie klasy Contact 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

  1. Test poprawnych danych:
    • Sprawdzamy, czy konstruktor klasy Contact poprawnie przypisuje właściwości, gdy podane dane są prawidłowe.
    • Test: Constructor_ValidData_SetsPropertiesCorrectly.
  2. 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.
    • Adres e-mail:
      Testowane są typowe błędne formaty adresów e-mail, np. brak domeny lub znaku @.
      • Test: Constructor_InvalidEmail_ThrowsArgumentException.
    • Numer telefonu:
      Testowane są przypadki zawierające litery, znaki specjalne lub puste wartości.
      • Test: Constructor_InvalidPhoneNumber_ThrowsArgumentException.
  3. Test metody ToString:
    • Sprawdzamy, czy metoda ToString zwraca ciąg tekstowy w oczekiwanym formacie, np.:
      "Imię Nazwisko, email, numer telefonu".
    • Test: ToString_ReturnsExpectedFormat.

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 klasy Contact 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

  1. 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.
  2. 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.
  3. 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! 👋

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *