1. Wprowadzenie
Wyobraź sobie, że projektujesz nowoczesny samochód. W środku — setki skomplikowanych części, czuła elektronika, precyzyjne ustawienia. Oczywiste jest, że nikt nie powinien tak po prostu dostać się do sterownika silnika i zacząć kręcić jakimiś śrubkami. Jeśli każdy będzie grzebał tam, gdzie nie powinien, auto może zacząć zachowywać się nieprzewidywalnie, a Ty będziesz musiał robić przegląd i ustalać, kto i co popsuł.
Tak samo jest w programowaniu. Gdy tworzysz klasę, chcesz chronić jej wnętrze przed obcymi rękami — żeby nikt przypadkiem nie popsuł ważnych danych albo nie namieszał w metodach, o których świat zewnętrzny nie powinien wiedzieć. Na tym właśnie polega enkapsulacja — jeden z trzech kluczowych filarów programowania obiektowego.
Enkapsulacja pozwala ukryć szczegóły implementacji i jasno określić, co jest dostępne „z zewnątrz”, a co powinno zostać w środku. To tak, jakbyś otworzył użytkownikowi schowek na rękawiczki, ale maskę zamknął na klucz. Klasa sama decyduje, które dane pokazać zewnętrznemu kodowi, a które — zostawić w tajemnicy. Dzięki temu kod jest solidniejszy, bardziej logiczny i łatwiejszy w utrzymaniu.
Mówiąc bardziej formalnie, enkapsulacja — to sposób na połączenie danych (pól) i zachowania (metod), które są z nimi związane, w jedną strukturę, ograniczając przy tym bezpośredni dostęp do wewnętrznych komponentów obiektu.
2. Modyfikatory dostępu: pilnujemy swojego terenu
W C# (i innych językach OOP) enkapsulacja realizowana jest za pomocą modyfikatorów dostępu. To takie „etykietki”, które określają, które człony klasy (pola, metody, właściwości itd.) są dostępne z zewnątrz, a które — tylko od środka.
Już się z nimi spotkaliśmy, ale przypomnijmy sobie, co już wiemy i trochę rozszerzmy listę modyfikatorów:
| Modyfikator | Dostępny dla... |
|---|---|
|
Dla wszystkich! Każdy kod w projekcie (i poza nim, jeśli projekt to biblioteka) |
|
Tylko od środka tej samej klasy |
|
Z tej klasy i wszystkich jej potomków |
|
Tylko w ramach bieżącego assembly (projektu) |
|
Albo z bieżącego assembly, albo z klasy potomnej |
|
Tylko z klasy potomnej w bieżącym assembly |
W tym wykładzie skupimy się na tych najczęściej używanych: public, private i krótko wspomnimy o protected, żeby nie przeładować głowy.
Oddzielamy wnętrze od zewnętrza
Napisaćmy klasę Dog:
public class Dog
{
public string Name;
public int Age;
public void Bark()
{
Console.WriteLine($"{Name} mówi: Hau!");
}
}
Tutaj wszystkie pola i metody są zadeklarowane z modyfikatorem public. To znaczy, że każdy może zmienić imię lub wiek psa:
Dog rex = new Dog("Reks", 5);
rex.Name = "Szarik"; // Niespodziewana zmiana tożsamości!
rex.Age = -999; // Poważne problemy z wiekiem
A przecież pola Name i Age — to najważniejsze dane, których nie chcemy oddawać każdemu.
Tego lepiej nie robić
Zostawiając wszystkie pola public, ryzykujemy naruszenie logiki działania klasy: każda zewnętrzna ingerencja może sprawić, że obiekt stanie się nieważny. Na przykład, można ustawić psu ujemny wiek albo nadać mu imię typu "%$#!??".
3. Ukrywamy pola: używamy private
Zazwyczaj pola klasy robi się prywatne (z modyfikatorem private). To znaczy, że ich wartości można zmieniać tylko od środka klasy, a z zewnątrz — wcale.
public class Dog
{
private string name;
private int age;
public void Bark()
{
Console.WriteLine($"{name} mówi: Hau!");
}
}
Teraz próba dostania się do pól bezpośrednio z zewnątrz:
Dog rex = new Dog("Reks", 5);
rex.name = "Szarik"; // Błąd kompilacji
Kompilator od razu powie: „Brak dostępu!”.
Po co tak ostro? Gdzie elastyczność?
Cała „elastyczność” zapewniana jest przez specjalne metody lub właściwości (o nich więcej w następnym wykładzie), które pozwalają kontrolować dostęp i zmieniać dane tylko według określonych zasad.
4. Enkapsulacja w praktyce: przykład z kontrolą
Załóżmy, że chcemy, żeby nie dało się psu ustawić ujemnego wieku:
public class Dog
{
private string name;
private int age;
public Dog(string name, int age)
{
this.name = name;
if (age >= 0)
this.age = age;
else
this.age = 0; // Nie pozwalamy ustawić "dziwnego" wieku
}
public void Bark()
{
Console.WriteLine($"{name} mówi: Hau!");
}
// Metoda do bezpiecznej zmiany wieku
public void SetAge(int newAge)
{
if (newAge >= 0)
age = newAge;
// Można dodać else: komunikat o niepoprawnej wartości
}
}
Teraz nikt z zewnątrz nie popsuje pól bezpośrednio. Do zmiany wieku jest specjalna metoda, która robi odpowiednią weryfikację.
5. Pola kontra metody dostępu (gettery/settery)
Taki sposób — robienie pól private i udostępnianie metod do pracy z nimi — nazywa się enkapsulacją danych (data encapsulation). Do odczytu wartości pola często tworzy się metody-gettery, a do zapisu — metody-settery.
public class Dog
{
private string name;
public Dog(string name)
{
this.name = name;
}
public string GetName()
{
return name;
}
public void SetName(string newName)
{
// Tu można dodać sprawdzenie poprawności imienia
name = newName;
}
}
Ale! Odkąd w C# pojawiły się właściwości (properties), takie metody stosuje się coraz rzadziej — właściwości sprawiają, że kod jest dużo czytelniejszy i wygodniejszy (więcej o nich — w następnym wykładzie).
6. Modyfikatory dostępu dla metod i klas
Pola to nie wszystko! Modyfikatory dostępu stosuje się też do metod (funkcji członkowskich klasy), a nawet do samych klas.
Metody używane tylko wewnątrz klasy (np. pomocnicze funkcje do logiki wewnętrznej), standardowo robi się private.
Metody publiczne — to tzw. interfejs klasy (nie mylić ze słowem kluczowym interface), czyli to, z czego użytkownik klasy może korzystać.
7. Typowe błędy przy pracy z enkapsulacją
Błąd nr 1: wszystko jest zadeklarowane jako public.
Wydaje się, że im więcej dostępne, tym łatwiej korzystać. W praktyce — to otwiera dostęp do wnętrza klasy, które nie powinno być zmieniane z zewnątrz. Taki kod staje się podatny na błędy i nieprzewidywalny, szczególnie w dużych projektach.
Błąd nr 2: zmiana modyfikatora dostępu psuje zewnętrzny kod.
Przy refaktoryzacji można przypadkiem zmienić modyfikator dostępu metody lub pola i wtedy cały inny kod, który z nich korzystał, nagle przestaje się kompilować albo działać poprawnie. To szczególnie ważne w publicznych API.
Błąd nr 3: mylenie zmiennych lokalnych z polami klasy.
Czasem programiści zapominają, że zmienna zadeklarowana w metodzie żyje tylko w tej metodzie. A pola klasy — są dostępne we wszystkich jej metodach. To prowadzi do nieoczywistych błędów, zwłaszcza jeśli nazwy się pokrywają.
Błąd nr 4: lekceważenie private i protected.
Wielu boi się używać ograniczonego dostępu, bo potem nie będą mogli dostać się do potrzebnego elementu. Ale właśnie o to chodzi w enkapsulacji — ukrywać wszystko, co zbędne, a na zewnątrz wystawiać tylko to, co naprawdę potrzebne.
GO TO FULL VERSION