Enkapsulacja — obiekt pilnuje swoich zasad
Reguła „nie może być minus pięciu kabli” zapisana w trzech miejscach programu to trzy okazje do pomyłki. Zapisana w jednym — w środku obiektu — nie da się ominąć.
Czego się dziś nauczysz
- Wypiszesz reguły, których obiekt ma pilnować przez cały czas.
- Zamkniesz stan w polach prywatnych i udostępnisz go tylko do odczytu.
- Sprawdzisz dane w konstruktorze, żeby obiekt nie powstał w złym stanie.
- Rozpoznasz wyciek prywatnej kolekcji i zabezpieczysz go.
- Wyjaśnisz, dlaczego
{ get; set; }dla każdego pola to nie jest enkapsulacja.
Przygotowanie: lekcje 01–41. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Reguła, której pilnuje każdy — czyli nikt
W szkolnym magazynie nie może być minus pięciu kabli. Bez enkapsulacji reguła wygląda tak:
class Magazyn
{
public int Sztuki;
}
// w jednym miejscu programu
if (magazyn.Sztuki - 3 >= 0) magazyn.Sztuki -= 3;
// w drugim miejscu ktos zapomnial
magazyn.Sztuki -= 10;
// w trzecim sprawdzil, ale inaczej
if (magazyn.Sztuki > 3) magazyn.Sztuki -= 3; // blad o jeden
Trzy miejsca, trzy różne sprawdzenia, jedno z błędem. Przy dziesiątym wywołaniu nikt już nie wie, która wersja jest właściwa.
Enkapsulacja to przeniesienie reguły do środka obiektu — tam, gdzie leżą dane, których dotyczy.
class Magazyn
{
private int sztuki;
public int Sztuki => sztuki;
public bool Wydaj(int ile)
{
if (ile <= 0 || ile > sztuki)
{
return false;
}
sztuki -= ile;
return true;
}
}
Teraz nie ma żadnej drogi, która omija sprawdzenie. Nie dlatego, że wszyscy o nim pamiętają, tylko dlatego, że innej drogi nie ma.
Dwa słowa, które warto rozróżniać
Ukrywanie danych to zamknięcie pól przez private — środek techniczny. Enkapsulacja to pomysł: obiekt sam odpowiada za spójność swojego stanu. Można ukryć pola i nie osiągnąć enkapsulacji — wystarczy dorobić do każdego właściwość { get; set; }.
Czego obiekt pilnuje przez cały czas
Zanim napiszesz klasę, wypisz zdania, które muszą być prawdziwe zawsze, od utworzenia obiektu aż po koniec programu. Dla magazynu:
- liczba sztuk nigdy nie jest ujemna,
- liczba sztuk nigdy nie przekracza pojemności,
- nazwa towaru nigdy nie jest pusta.
Takie zdanie nazywamy regułą obiektu. Enkapsulacja polega na tym, żeby nie dało się jej złamać — ani przez pomyłkę, ani celowo.
| Miejsce | Co robi |
|---|---|
| konstruktor | ustala stan początkowy zgodny z regułami; odrzuca dane, z których nie da się zbudować sensownego obiektu |
| metody | jedyna droga zmiany stanu; każda sprawdza, czy po zmianie reguły nadal są spełnione |
| właściwości | odczyt bez ograniczeń, zapis tylko tam, gdzie nie łamie reguł |
| pola prywatne | przechowują stan i są niedostępne z zewnątrz |
class Magazyn
{
private int sztuki;
private readonly int pojemnosc;
public string Nazwa { get; }
public Magazyn(string nazwa, int pojemnosc)
{
if (string.IsNullOrWhiteSpace(nazwa))
{
throw new ArgumentException("Nazwa nie moze byc pusta.", nameof(nazwa));
}
if (pojemnosc < 1)
{
throw new ArgumentOutOfRangeException(nameof(pojemnosc));
}
Nazwa = nazwa;
this.pojemnosc = pojemnosc;
}
}
Obiekt nie powstaje w złym stanie
Sprawdzenie w konstruktorze to najtańsze zabezpieczenie w całej klasie. Jeśli obiekt nie może powstać z błędnych danych, żadna metoda nie musi później sprawdzać, czy nazwa nie jest pusta.
Prywatne pole, które i tak wycieka
Najczęstszy błąd na tym etapie: pole jest prywatne, a mimo to każdy może je zmienić.
class Kurs
{
private readonly List<string> uczestnicy = new List<string>();
public List<string> Uczestnicy => uczestnicy; // WYCIEK
}
Kurs k = new Kurs();
k.Uczestnicy.Add("ktokolwiek"); // dodano z pominieciem wszelkich regul
k.Uczestnicy.Clear(); // i wyczyszczono cala liste
Właściwość oddaje tę samą listę, a nie jej kopię — dlatego wywołujący ma pełną władzę nad wnętrzem obiektu. Słowo readonly nie pomaga: chroni referencję, nie zawartość (lekcja 43).
class Kurs
{
private readonly List<string> uczestnicy = new List<string>();
private const int LIMIT = 12;
public int Ilu => uczestnicy.Count;
public bool Pelny => uczestnicy.Count >= LIMIT;
// tylko do odczytu - nie da sie dodac ani usunac
public IReadOnlyList<string> Uczestnicy => uczestnicy;
public bool Zapisz(string imie)
{
if (string.IsNullOrWhiteSpace(imie)) return false;
if (Pelny) return false;
if (uczestnicy.Contains(imie)) return false;
uczestnicy.Add(imie);
return true;
}
}
Trzy sposoby na wyciek
1. Oddanie kolekcji jako List<T> zamiast IReadOnlyList<T>. 2. Oddanie tablicy — tablicy nie da się zabezpieczyć, trzeba zwrócić kopię przez ToArray. 3. Oddanie obiektu, którego pola są publiczne. W każdym z tych przypadków wnętrze wypływa na zewnątrz mimo słowa private.
Przykład: magazyn, którego nie da się oszukać
using System.Collections.Generic;
class Magazyn
{
private int sztuki;
private readonly List<string> historia = new List<string>();
private readonly int pojemnosc;
public string Nazwa { get; }
public int Sztuki => sztuki;
public bool Pusty => sztuki == 0;
public IReadOnlyList<string> Historia => historia;
public Magazyn(string nazwa, int pojemnosc, int stanPoczatkowy)
{
if (string.IsNullOrWhiteSpace(nazwa))
{
throw new ArgumentException("Nazwa nie moze byc pusta.", nameof(nazwa));
}
if (pojemnosc < 1 || stanPoczatkowy < 0 || stanPoczatkowy > pojemnosc)
{
throw new ArgumentOutOfRangeException(nameof(pojemnosc));
}
Nazwa = nazwa;
this.pojemnosc = pojemnosc;
sztuki = stanPoczatkowy;
historia.Add($"utworzono ze stanem {stanPoczatkowy}");
}
public bool Przyjmij(int ile)
{
if (ile <= 0 || sztuki + ile > pojemnosc)
{
historia.Add($"odrzucono przyjecie {ile}");
return false;
}
sztuki += ile;
historia.Add($"przyjeto {ile}, stan {sztuki}");
return true;
}
public bool Wydaj(int ile)
{
if (ile <= 0 || ile > sztuki)
{
historia.Add($"odrzucono wydanie {ile}");
return false;
}
sztuki -= ile;
historia.Add($"wydano {ile}, stan {sztuki}");
return true;
}
}
class Program
{
static void Main()
{
Magazyn m = new Magazyn("Kable HDMI", 20, 5);
m.Przyjmij(10);
m.Wydaj(3);
m.Wydaj(100); // odrzucone
m.Przyjmij(50); // odrzucone - ponad pojemnosc
Console.WriteLine($"{m.Nazwa}: {m.Sztuki} szt., pusty: {m.Pusty}");
Console.WriteLine();
foreach (string wpis in m.Historia)
{
Console.WriteLine(" " + wpis);
}
// m.Sztuki = 999; // CS0200 - brak settera
// m.Historia.Add("oszustwo"); // CS1061 - IReadOnlyList nie ma Add
}
}
Kable HDMI: 12 szt., pusty: False
utworzono ze stanem 5
przyjeto 10, stan 15
wydano 3, stan 12
odrzucono wydanie 100
odrzucono przyjecie 50
Co dzieje się po kolei
- Konstruktor odrzuca dane, z których nie da się zbudować sensownego magazynu. Dzięki temu żadna metoda nie musi później sprawdzać nazwy ani pojemności.
- Obie metody zwracają
boolzamiast zgłaszać wyjątek — odmowa wydania to normalna sytuacja, a nie błąd wykonania (lekcja 35). - Historia rośnie także przy odmowach. To też jest stan obiektu i też jest chroniony.
SztukiiPustysą tylko do odczytu, aHistoriaoddaje listę jakoIReadOnlyList, więc nikt z zewnątrz nic do niej nie dopisze.- Dwa zakomentowane wiersze to sedno lekcji: obie drogi obejścia reguł są zamknięte już na etapie kompilacji.
Zestawienie elementów
| Element | Rola w enkapsulacji | Uwagi |
|---|---|---|
pole private | przechowuje stan | Jedyne miejsce, w którym stan naprawdę leży. |
readonly | referencja nie do podmiany | Chroni wskazanie, nie zawartość kolekcji. |
const | stała znana przy kompilacji | Limity i progi — private const int LIMIT = 12;. |
właściwość => | odczyt bez zapisu | Najprostszy sposób na udostępnienie stanu. |
IReadOnlyList<T> | kolekcja tylko do odczytu | Da się przejść foreach, nie da się zmienić. |
metoda zwracająca bool | operacja z odmową | Gdy odmowa jest normalną sytuacją. |
| wyjątek w konstruktorze | obiekt nie powstaje w złym stanie | Dla danych, z których nie da się zbudować obiektu. |
ToArray() / new List<T>(…) | kopia zamiast oryginału | Gdy trzeba oddać coś, co wywołujący może zmieniać. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Pole prywatne plus { get; set; } dla każdego | Zmieniono zapis, nie zmieniono niczego — reguły nadal da się ominąć. |
Zwrócenie List<T> z właściwości | Wywołujący może dodawać i usuwać elementy. Zwróć IReadOnlyList<T> albo kopię. |
| Zwrócenie tablicy prywatnej | Tablicy nie da się zabezpieczyć — zwróć kopię przez ToArray(). |
Sprawdzenia w Main zamiast w klasie | Drugie miejsce wywołania na pewno o nich zapomni. |
| Konstruktor przyjmujący każde dane | Obiekt powstaje w stanie, którego reguły nie dopuszczają, i psuje się później w losowym miejscu. |
readonly na kolekcji traktowane jak ochrona | Chroni tylko przed podmianą listy; elementy nadal można dodawać. |
| Metoda cicho ignorująca błędne dane | Wywołujący nie wie, czy operacja się udała. Zwróć bool albo zgłoś wyjątek. |
Zadania
Napisz klasę Skarbonka z prywatnym polem monet, właściwością tylko do odczytu i metodą Wloz(int) przyjmującą od 1 do 10 monet. Sprawdź, że z Main nie da się ustawić liczby monet wprost.
Do klasy Skarbonka dodaj konstruktor przyjmujący pojemność. Wartość mniejsza od jednego ma zgłaszać wyjątek. Sprawdź oba przypadki.
Dla klasy Konto wypisz w komentarzu wszystkie zdania, które muszą być prawdziwe zawsze. Potem napisz klasę tak, żeby żadnej z nich nie dało się złamać, i przy każdej metodzie dopisz, której reguły pilnuje.
Napisz klasę Kurs z prywatną listą uczestników, limitem miejsc i metodą Zapisz(string) odrzucającą puste imiona, duplikaty i zapisy ponad limit. Udostępnij listę tak, żeby dało się ją przejść pętlą, ale nie dało się nic dodać. Sprawdź, jaki komunikat daje próba dodania.
Przerób metody klasy Kurs tak, żeby wywołujący dowiedział się, dlaczego zapis się nie udał — nie tylko że się nie udał. Zaproponuj dwa rozwiązania i w komentarzu uzasadnij wybór jednego z nich.
Napisz klasę Wypozyczalnia przechowującą listę sprzętu i zbiór numerów wypożyczonych. Z zewnątrz mają być dostępne wyłącznie: wypożyczenie po numerze, zwrot, sprawdzenie dostępności i przejrzenie listy dostępnego sprzętu. Żadna operacja nie może pozwolić na wypożyczenie tego samego sprzętu dwa razy ani na zwrot czegoś, czego nikt nie wypożyczył. Na koniec spróbuj celowo złamać każdą regułę z zewnątrz i zapisz, co ci to uniemożliwiło.
Co trzeba zapamiętać
- Enkapsulacja to nie samo
private, tylko odpowiedzialność obiektu za własny stan. - Reguła obiektu to zdanie prawdziwe od utworzenia do końca istnienia obiektu.
- Konstruktor nie pozwala powstać obiektowi w stanie łamiącym reguły.
- Metody są jedyną drogą zmiany stanu i każda sprawdza, czy reguły nadal obowiązują.
- Zwrócenie prywatnej kolekcji jako
List<T>otwiera wnętrze obiektu mimoprivate. - Zamiana pól na właściwości
{ get; set; }niczego nie zabezpiecza.
Dokumentacja: Microsoft Learn — temat tej lekcji.