Enkapsulacja

SebaZ

Enkapsulacja

Enkapsulacja inaczej zwana hermetyzacją (kapsułkowaniem) jest to jedno z głównych założeń programowania obiektowego. Polega na ukrywaniu metod i atrybutów dla klas zewnętrznych. Dostęp do nich możliwy jest tylko z wewnątrz klasy, do której należą, z klas zaprzyjaźnionych lub z klas dziedziczących.

Kontrola dostępu

Wiele obiektowych języków programowania daje możliwość regulowania poziomu dostępu do poszczególnych elementów klasy. Służą do tego celu odpowiednie modyfikatory:

*public - słowo kluczowe, pozwala na wolny, nieograniczony dostęp do atrybutu z dowolnej, innej klasy
*private - atrybuty są dostępne tylko i wyłącznie dla klasy, której są elementami
*protected - słowo kluczowe, atrybuty będą dostępne jedynie dla bieżącej klasy oraz dla wszystkich klas po niej dziedziczących

Przykładowa klasa

//klasa Czlowiek
public class Czlowiek
{
    private String Imie, Nazwisko; // atrybuty prywatne

    // konstruktory
    public Czlowiek() { }
    public Czlowiek(String imie, String nazwisko) {
        this.Imie = imie;
        this.Nazwisko = nazwisko;
    }
}

Powyższa przykładowa klasa Czlowiek() zdefiniowana w języku JAVA obok konstruktorów zawiera atrybuty prywatne Imie oraz Nazwisko, do których będziemy uzyskiwać dostęp z zewnątrz.

Ściśle z enkapsulacją związane są pojęcia mutatorów oraz akcesorów. Określają one metody, które pozwalają na modyfikację i pobieranie wartości atrybutów klasy, które mają status prywatnych (private). Charakterystyczną cechą tychże metod jest to, że są publiczne (public).

Mutatory

Inaczej określane setter'ami od nazw metod jakie przyjęło się dla nich stosować, np. setImie(String imie), setNazwisko(String nazwisko). Pozwalają one modyfikować wartości atrybutów klasy objętych klauzulą private, czyli ukrytych dla zewnętrznych klas. Nic nie zwracają (void).

    // mutatory
    public void setImie(String imie) {
        this.Imie = imie;
    }
    public void setNazwisko(String nazwisko) {
        this.Nazwisko = nazwisko;
    }

Akcesory

Inaczej określane getter'ami od nazw metod jakie przyjęło się dla nich stosować, np. getImie(), getNazwisko(). Zwracają pobrane (wyciągnięte) wartości atrybutów prywatnych - muszą być zgodne z ich typami.

    // akcesory
    public String getImie() {
        return this.Imie;
    }
    public String getNazwisko() {
        return this.Nazwisko;
    }

Zalety stosowania enkapsulacji

*uodparnia tworzony model na błędy polegające np. na błędnym przypisywaniu wartości
*umożliwia rozbicie modelu na mniejsze elementy, które nie muszą i nie powinny obchodzić użytkownika

Końcowy efekt działania abstrakcyjnej klasy

//klasa Czlowiek
public class Czlowiek
{
    private String Imie, Nazwisko;

    // konstruktory
    public Czlowiek() { }
    public Czlowiek(String imie, String nazwisko) {
        this.Imie = imie;
        this.Nazwisko = nazwisko;
    }

    // mutatory
    public void setImie(String imie) {
        this.Imie = imie;
    }
    public void setNazwisko(String nazwisko) {
        this.Nazwisko = nazwisko;
    }

    // akcesory
    public String getImie() {
        return this.Imie;
    }
    public String getNazwisko() {
        return this.Nazwisko;
    }
}

A poniżej prosta klasa, której kod i uruchomienie w konsoli przedstawia jak używać metod metod enkapsulacji na konkretnych obiektach.

    // klasa Test
public class Test
{
    public static void main(String args[]) {
        String imie, nazwisko;
        if (args.length != 2) {          // jesli dlugosc tablicy argumentow jest rozna od zera
           Czlowiek c1 = new Czlowiek(); // przpisz dane domyslne
           c1.setImie("Jan");
           c1.setNazwisko("Kowalski");
           imie = c1.getImie();
           nazwisko = c1.getNazwisko();
        } else { // w przeciwnym wypadku
           Czlowiek c1 = new Czlowiek(args[0],args[1]);
           imie = c1.getImie();
           nazwisko = c1.getNazwisko();
        }
        System.out.println("Imie: " + imie + "\n"
                          +"Nazwisko: " + nazwisko);
    }
}

18 komentarzy

Taki rodzaj enkapsulacji maja jezyki lekko ulomne (np. Java), gdzie faktycznie trzeba to stosowac jesli kod ma byc gietki. Normalne jezyki programowania maja trzy rodzaje elementow klasy - wartosci, wlasciwosci i metody. Pierwsze od drugiego jest rozroznialne tylko dla tworcy klasy i rozni sie tym, ze wlasciwosc ma dodatkowy kod odpowiedzialny za operacje na danej wartosci.

Jak to sie ma w praktyce? Zalozmy, ze mamy taki sobie pseudokod:

klasa A
{
wartosc = 1;
}

I zalozmy, ze uzywamy go juz w 10 roznych programach w postaci A.wartosc = 2; itp.

Teraz, jesli chcemy wprowadzic sprawdzanie przypisywanej wartosci (np. zeby nie mogla byc ponizej 0) to w Javie sie tak poprostu nie da - trzeba dopisac te durne setWartosc i getWartosc, sama wartosc zmienic na chroniona i zmienic 10 roznych programow na nowy model. Dlatego wlasnie ludzie programujacy w Javie z gory robia nadmiarowy kod tworzac te falszywe settery i gettery od razu, przez co kod im sie rozrasta, traci przejrzystosc itp. W 90% przypadkow setter i getter w Javie nie robi nic poza tym co normalnie dzieje sie z wartoscia publiczna.

W normalnym jezyku np. C# sa prawdziwe settery i gettery pisane w ten sposob: public int wartosc { set{/** TU KOD ODPALANY PRZY USTAWIANIU /} get{/ KOD ZWRACAJACY WARTOSC **/} }. Jak to sie ma do naszego programu? Otoz piszac go normalnie tworzymy publiczne wartosci, tak jak od poczatku powinno byc, a jak nam sie zachce nagle spradzania to poprostu w klasie dopisujemy set{} i get{} dla danej zmiennej i po krzyku - zero zmian w zadnym z programow, ktore ze starszej wersji korzystaja!

Zmienne publiczne sa wymyslone po to, zeby byly publiczne - ukrywanie ich za fasada z metod jest glupota, pisaniem nadmiarowego kodu i brudzeniem go. Jednakze takie jezyki jak Java wymuszaja takie nieeleganckie podejscie, gdyz lepiej napisac z gory setke niepotrzebnych getterow i setterow niz potem obudzic sie z reka w nocniku kiedy dla ktorejs wartosci bedzie potrzebna dodatkowa obrobka.

Przeslanie mojego posta jest takie: nie bojcie sie uzywac publicznych zmiennych - one sa od tego, zeby wystawaly na zewnatrz, sa elementem interface'u klasy. Tak jak autor porownal publiczne metody do guziczkow, tak publiczne zmienne mozna porownac do pokretel ustawiajacych na tablicy naszej "czarnej skrzynki" parametry pracy. No chyba, ze jezyk w ktorym piszecie wam na to niepozwala - wtedy mozecie tylko zaplakac nad krotkowzrocznoscia tworcow jezyka i klepac ten w wiekszosci zbedny nadmiarowy kod.

DONE :)

btw. w komentarzach jest opcja: Usuń, wiec przydało by sie, żebyś wywalił zbędne i nadmiarowe.

SebaZ, myślę jednak, że powinieneś to zmienić ze względu na konflikt z prawdziwą klasą abstrakcyjną.

Abstrakcyjna w sensie: raczej nie mająca zastosowania, a nie w znaczeniu programowania obiektowego.

Jeszcze słówko do autora artykułu. W Javie chyba nie ma czegoś takiego jak klasa abstrakcyjna, a już na pewno w Twoim artykule ani razu taka klasa nie wystąpiła. W Javie mamy interfejsy (interface). Ogólnie, w programowaniu obiektowym, klasa abastrakcyjna to taka, która posiada wyłącznie publiczne deklaracje (nie definicje(!)) metod wirtualnych. Współcześnie pojęcie klasy abstrakcyjnej jest wypierane właśnie przez interfejs.

Pytacie po co komplikować sobie życie, po co to wszystko? Przede wszystkim, dziwią mnie tego rodzaju wątpliwości wśród czytelników tej witryny, ale mniejsza... Po pierwsze, dla bezpieczeństwa, po drugie, dla zwiększenia elastyczności (ang. flexibility). Klasa jest dla jej użytkownika czarnym pudełkiem (ang. black box), który w środku ma jakiś mechanizm, którym steruje się za pomocą guziczków na obudowie, tzw. interfejsem. Akcesor albo mutator to właśnie odpowiedniki takich guziczków. Wyobraźcie sobie, że zamiast przycisku, musicie zwierać kabelki w odpowiednich kolorach. Upierdliwe by to było i jeszcze niebezpieczne, bo może was prąd walnąć.

No dobrze, to jak się to przekłada na inżynierię oprogramowania? Autor artykułu zapodał trywialne przykłady. W praktyce zastosowanie interfejsów jest dużo efektywniejsze. Wyobraźmy sobię klasę 'Data', która ma trzy pola całkowitoliczbowe: dzień, miesiąc, rok. Gdyby były one publiczne, moglibyśmy dowolnie modyfikować te pola niezależnie od siebie - katastrofa. Użytkownik takiej klasy musiałby dbać cały czas o poprawność przechowywanej daty. Dzięki enkapsulacji, użytkownik klasy nie martwi się o nic (no prawie o nic). Klasa 'Data' posiada prywatne pola całkowitoliczbowe (dzień, miesiąc, rok), natomiast stosowne mutatory i akcesory publiczne "same dbają" o poprawność daty. Próbując ustawić obiekt klasy 'Data' na nieprawidłową datę, mutator z tej klasy może zgłosić na wyjątek bądź zwrócić błąd. Poza tym, poprzez przeciążony mutator można wprowadzać datę na wiele różnych sposobów, np. za pomocą tekstu lub liczb. Użytkownik klasy w tym wypadku nie przejmuje się ewentualną konwersją tekstowo-liczbową. Podobnie w przypadku akcesorów. Możemy chcieć odczytać datę w postaci liczbowej lub tekstowej. Przeciążony akcesor ułatwia takie korzystanie z klasy. Wyobraźmy sobie jeszcze, że nasza klasa 'Data' potrafi zwrócić dzień tygodnia, odpowiadający przypadającej dacie. Gdyby taki dzień tygodnia był zwykłym publicznym polem całkowitoliczbowym, użytkownik takiej klasy mógłby go omyłkowo zmienić (przykład realny; użytkownik jak najbardziej może zapomnieć o tym, że pole z dniem tygodnia ustawiane jest automatycznie). Wówczas dzień tygodnia przestałby odpowiadać przechowywanej dacie. Natomiast enkapsulując dzień tygodnia, tzn. czyniąc to pole prywatnym i udostępniając publicznie jedynie akcesor, mamy sprawę rozwiązaną. Użytkownik klasy może jedynie odczytać dzień tygodnia, nie może go zmienić wprost. Zmienia go tylko, zmieniając datę. A dzięki temu, że również robi to za pomocą mutatora, to dzień tygodnia zmieni się automatycznie. Enkapsulacja to jeden z paradygmatów projektowania zorientowanego obiektowo i nie jest to jedynie wymysł Stroustrupa na potrzeby zrobienia kasy.

Na zakończenie pokażę w C++, jak w zarysie może wyglądać klasa przechowująca datę:

#include <string>

class Date
{
private:
	int day;
	int month;
	int year;
	int week_day;

public:
	/* ---MUTATORY--- */
	
	//Przyjmuje datę w postaci liczbowej.
	bool Set(int d, int m, int y)
	{
		if(data d.m.y nieprawidłowa)
			return false;
		day = d;
		month = m;
		year = y;
		week_day = ... //Tutaj wyliczamy dzień tygodnia.
		return true;
	}

	//Przyjmuje datę w postaci tekstowej.
	bool Set(const string& str)
	{
		if(tekst w str ma zły format)
			return false;
		int d, m, y;
		d = ... //Tutaj konwertujemy
		m = ... //datę w postaci tekstowej
		y = ... //na liczbową.

		if(data d.m.y nieprawidłowa)
			return false;
		day = d;
		month = m;
		year = y;
		week_day = ... //Tutaj wyliczamy dzień tygodnia.
		return true;
	}

	/* ---AKCESORY--- */

	int Day() { return day; } //Zwraca dzień miesiąca.
	int Month() { return month; } //Zwraca miesiąc.
	int Year() { return year; } //Zwraca rok.
	int WeekDay() { return week_day; } //Zwraca dzień tygodnia.

	//Zwraca datę w postaci tekstowej.
	string Get(string* str)
	{
	string result = ...//Tutaj konwersja daty na tekst.
	if(str != NULL)
		*str = result;
	return result;
	}
	
	//Zwraca datę w postaci liczbowej.
	//Użytkownik za pomocą NULL-i decyduje, które elementy daty chce wydobyć.
	void Get(int* d, int* m, int* y, int* wd)
	{
		if(d != NULL)
			*d = day;
		if(m != NULL)
			*m = month;
		if(y != NULL)
			*y = year;
		if(wd != NULL)
			*wd = week_day;
	}
};

SebaZ, dzięki za podpowiedź. To był chyba mój pierwszy komentarz na tym portalu i nie zauważyłem możliwości edycji i usuwania. Jeszcze raz dzięki. :)

tylko, ż link jaki podałeś to joke

Ja też jestem ciekaw, po co tak sobie komplikować życie. Nie pamiętam, żeby w assemblerze były jakieś prywatne dane ukrywane przed innymi procedurami.

Proste pytanie. Po co tworzyć prywatne dane?

Jak kolega wspomniał, wad jest sporo. Bo zamiast bezpośrednio odnieść się do jakiegoś rekordu,trzeba tworzyć procedury odczytująco-zapisujące. Po co? Po co komplikować sobie życie?

Tak, przy okazji kiedyś czytałem wywiad z twórcą języka C++, gdzie przyznał, że robił go dla kasy i dlatego jest tak zagmatwany i zawiera wiele niepotrzebnych elementów.

[quote]
Stroustrup: Otóż pewnego dnia, kiedy siedziałem w swoim biurze, myślałem nad czymś co wprowadziłoby trochę równowagi do ówczesnej sytuacji. Zastanawiałem się co by było, gdyby istniał język na tyle skomplikowany i trudny do nauczenia, że nikt nie byłby w stanie zalewać rynku programistami.
[/quote]

http://ja.gibbon.pl/2006/09/26/wywiad-z-bjarne-stroustrupem/

//lol, przecież ten wywiad to żart - Cold
//dopisane: ah, SebaZ już odpisał...

  1. wywołanie funkcji to większy narzut czasu niż odczytanie z pamięci
  2. privatność i protectedowność prowadzi do często dziwnych błędów, szczególnie przy dziedziczeniu
  3. za dużo czasu się spędza na myślenie, co powinno być private, co public, co protected

zresztą, jeżeli coś z jakiegokolwiek powodu nie powinno być odczytywane/zapisywane z zewnątrz klasy to po prostu tego nie nie róbmy, przecież jako programiści mamy kontrolę nad kodem. poza tym, możemy przecież mieć gettery i settery nawet i bez słówek kluczowych.

To je wymień, a nie się wymądrzasz.

w sumie zawsze się zastanawiałem, po co komu te wszystkie public/private, jednak z artykułu się tego nie dowiedziałem. te zalety są naprawdę niejasne, a wad mogę wymienić naprawdę sporo.

A kto napisał, że nie ma? Na pewno nie ja :P

Dodana kontrola dostępu oraz zalety enkapsulacji

To w języku C nie ma czegoś takiego jak "Property" ?

Właściwości

A gdzie niby to wcześniej było?

Bo tworzenie tekstu rozpoczynałem w Inżynierii - gdzieś mnie widocznie przeniosło.

cześć

Przeniosłem to do inżynierii oprogramowania ponieważ:

  • jest to zagadnienie typowo programistyczne z zakresu projektowania kodu
  • występuje w kilku języka nie tylko w Javie

Dodaj opis zalet i wad enkapsulacji bo nijak z tego opisu one nie wynikaja. Do tego zaznacz gdzies na poczatku co oznacza private (ze dostep tylko i wylacznie ze srodka klasy) a co public, bo poczatkujacy uczacy sie obiektowosci moze miec problem.

Poza tym 'atrybuty prywatne Imie oraz Nazwisko, do których będziemy uzyskiwać dostęp z zewnątrz.' to skrot myslowy, bo na tym etapie (przedstawionego kodu) wlasnie NIE bedziemy do nich uzyskiwac dostepu.

Poza tym ok, choc troszke krotko.

Czy coś dodać? Coś zmienić?

btw. ni cholery nie mogę zmusić przypisu do działania (według dokumentacji poprawnie wstawiony :/)