Klasa (programowanie obiektowe)

Klasa – częściowa lub całkowita definicja dla obiektów. Definicja obejmuje dopuszczalny stan obiektów oraz ich zachowania. Obiekt, który został stworzony na podstawie danej klasy nazywany jest jej instancją. Klasy mogą być typami języka programowania – przykładowo, instancja klasy Owoc będzie mieć typ Owoc. Klasy posiadają zarówno interfejs, jak i strukturę. Interfejs opisuje, jak komunikować się z jej instancjami za pośrednictwem metod, zaś struktura definiuje sposób mapowania stanu obiektu na elementarne atrybuty.

Zależnie od implementacji, klasy mogą istnieć w programie tylko na etapie jego kompilacji, w wyniku której są tłumaczone na kod strukturalny lub też posiadać swoją reprezentację w kodzie wynikowym w postaci metaobiektów. Za ich pomocą program może odczytywać i manipulować informacjami o klasie podczas wykonywania.

Języki programowania implementujące klasy różnią się pod względem oferowanej funkcjonalności. Większość z nich wspiera różne formy dziedziczenia oraz hermetyzacji.

Zagadnienia

Z pojęciem klasy związanych jest wiele dodatkowych zagadnień oraz funkcjonalności, które są implementowane w językach programowania.

Instancjonowanie

Klasa nie jest samodzielnym bytem, lecz szablonem do tworzenia nowych obiektów określonego typu i posiadających określone zachowanie. Obiekt utworzony na podstawie danej klasy nazywany jest jej instancją, a proces jego tworzenia – instancjonowaniem.

Poszczególne instancje klasy posiadają ten sam zbiór zachowań i atrybutów, lecz różnią się przechowywanymi w nich wartościami. Przykładowo, klasa Samochód opisuje pojęcie „samochodu” poprzez wymienienie charakteryzujących go atrybutów: prędkości maksymalnej, mocy silnika, producenta czy modelu. Jednak dwie różne instancje tej klasy będą różnić się od siebie wartościami tych atrybutów: jeden samochód może mieć prędkość maksymalną 220 km/h, zaś drugi – 240 km/h.

Interfejs

Uwaga: w tej sekcji termin „interfejs” nie odnosi się do pojęcia interfejsu funkcjonującego w języku Java, aczkolwiek są one ze sobą powiązane.

Obiekty wchodzą w interakcje ze światem zewnętrznym poprzez metody. Określają one możliwe zachowania, jakie na obiekcie można wykonać, a ich definicje znajdują się w klasie danego obiektu. Metoda jest rodzajem podprogramu języka programowania z dodatkową właściwością, tj. dostępem do atrybutów obiektu, na którym została wywołana. Metody mogą zarówno odczytywać, jak i modyfikować atrybuty obiektu, dlatego określane są także mianem „zachowań”. Zbiór metod, którymi dysponuje obiekt, nazywany jest jego interfejsem.

W przykładzie z samochodem klasa może definiować następujące metody: jedź, hamuj, skręć lub aktualnaPrędkość. Część z nich wpływa na aktualny stan obiektu, powodując np. zatrzymanie pojazdu, zaś inne służą wyłącznie do uzyskiwania dodatkowych informacji.

W programowaniu obiektowym zazwyczaj metody interfejsu są tak dobierane, aby były niezależne od siebie nawzajem, tj. nie ma żadnych ograniczeń na kolejność ich wywoływania.

Struktura

Oprócz interfejsu, klasa definiuje także strukturę danych przechowywanych w obiektach. Są one dzielone na elementarne atrybuty, zwane także polami lub właściwościami. Zbiór wartości wszystkich atrybutów tworzy stan konkretnego obiektu, który przechowywany jest w pamięci lub innym nośniku danych pod określonym adresem, dzięki czemu możliwy jest dostęp do niego za pośrednictwem referencji.

W większości języków programowania struktura pamięci wykorzystywanej do przechowywania stanu obiektu jest ustalana w momencie kompilacji programu na podstawie definicji klasy oraz właściwości architektury sprzętowej. Alternatywnym podejściem jest model języka Python, w którym wartości atrybutów obiektu są zapisane jako tablica asocjacyjna par (klucz, wartość). Dzięki temu poszczególne obiekty tej samej klasy mogą różnić się ilością i znaczeniem atrybutów, które mogą być do nich dynamicznie dodawane.

Klasy definiują również zbiór niezmienników, które są zachowywane przez wszystkie metody klasy. Niezmiennik jest pewnym wyrażeniem odnoszącym się do atrybutu, które musi być zawsze spełnione aby stan obiektu był prawidłowy, niezależnie od tego, jakie operacje na nim wykonamy. Jeśli przy pomocy obiektów reprezentujemy różne samochody, możemy zdefiniować niezmiennik definiujący minimalny promień skrętu. Programista nie może utworzyć obiektu samochodu, którego wartość promienia skrętu będzie mniejsza, niż minimalna, a gwarantuje nam to obecność niezmiennika. Niezmienniki mogą być implementowane poprzez zabronienie bezpośredniego dostępu do atrybutów obiektu i utworzenie dodatkowych metod dostępowych, które oprócz ich ustawiania, sprawdzają także niezmienniki. W niektórych językach niezmienniki można definiować bezpośrednio jako część specyfikacji klasy.

Elementy statyczne

Część języków dopuszcza tworzenie tzw. metod oraz atrybutów statycznych. Nie są one związane z żadnym konkretnym obiektem klasy, lecz tworzą globalny stan oraz globalnie dostępne operacje, które można wywoływać nawet wtedy, gdy nie posiadamy żadnej instancji klasy.

Rodzaje klas

Istnieje wiele rodzajów klas różniących się właściwościami i zastosowaniami. Należy zauważyć, że poniższe rodzaje nie są rozłączne. Przykładowo, ponieważ nie może istnieć klasa, która jest jednocześnie finalna i abstrakcyjna, można wnioskować, że klasy finalne są szczególnym przypadkiem klas właściwych.

Klasy właściwe

Klasą właściwą nazywamy każdą klasę, która może być instancjonowana.

Klasy abstrakcyjne

Klasa abstrakcyjna to przeciwieństwo klasy właściwej – nie można utworzyć obiektu takiej klasy. Ma ona zastosowanie jedynie wtedy, gdy język programowania obsługuje dziedziczenie. Klasa abstrakcyjna stanowi wtedy wzorzec do dalszego rozszerzenia, który sam w sobie nie może być pełnoprawnym, poprawnym obiektem.

Klasy abstrakcyjne najczęściej posiadają przynajmniej jedną metodę abstrakcyjną. Jest to rodzaj metody, dla którego zdefiniowana jest wyłącznie lista argumentów, nazwa oraz zwracane wartości, natomiast nie jest ona zaimplementowana. Implementacją podanego interfejsu muszą zająć się klasy potomne.

Niektóre języki takie, jak Java czy C# obsługują ponadto szczególny rodzaj klas abstrakcyjnych zwany interfejsem. Interfejs nie może definiować żadnych atrybutów, a wszystkie jego metody są abstrakcyjne. Ponadto najczęściej nie dotyczą ich ograniczenia pojedynczego dziedziczenia.

Klasy finalne

Klasa finalna ma sens jedynie w przypadku dziedziczenia – nazywamy tak klasę, której nie można rozszerzyć.

Klasy lokalne i wewnętrzne

Niektóre języki programowania umożliwiają tworzenie klas w innych przestrzeniach, niż globalna. Istnieje kilka typów takich klas.

Klasa wewnętrzna lub zagnieżdżona to klasa, która jest zdefiniowana wewnątrz innej klasy. Klasy wewnętrzne posiadają dostęp do metod statycznych klasy głównej, lecz nie muszą być instancjonowane wraz z nią. Zależnie od języka, mogą, ale nie muszą być dostępne spoza klasy głównej.

Klasy lokalne mogą być tworzone wewnątrz funkcji i metod. Obiekty tych klas nie mogą istnieć poza tymi funkcjami i są niszczone wraz z zakończeniem ich wykonywania. Język programowania może ponadto narzucać dodatkowe ograniczenia na takie klasy.

Klasy anonimowe

W większości języków programowania każda klasa posiada swą unikalną nazwę, dzięki czemu możliwe jest odwoływanie się do niej w różnych sytuacjach. Niektóre języki wspierają także tworzenie klas anonimowych, które nie posiadają nazwy.

Metaklasy

Metaklasa to klasa, której instancjami są inne klasy. Ta specjalna konstrukcja umożliwia dostęp do właściwości oraz definicji klasy w trakcie wykonywania programu.

Klasy częściowe

W przeciwieństwie do zwykłych klas, definicja klasy częściowej może być rozszerzona na więcej, niż jeden plik. Umożliwia to lepsze radzenie sobie z dużymi ilościami kodu. Podczas kompilacji wszystkie częściowe definicje łączone są w kompletną klasę, dzięki czemu nie ma żadnych funkcjonalnych różnic między nimi a zwykłymi klasami.

Przykład

Przykład klasy w języku Java:

public class Samochod {

    private int predkoscMax;
    private String producent;
    private String model;
    private int predkosc;

    public Samochod(String producent, String model, int predkoscMax) {
        this.predkoscMax = predkoscMax > 0 ? predkoscMax : 0;
        this.producent = producent != null ? producent : "nieznany";
        this.model = model != null ? model : "model";
        this.predkosc = 0;
    }

    public void przyspiesz(int i) {
        int nowaPredkosc = this.predkosc + i;
        this.predkosc = nowaPredkosc < this.predkoscMax ? nowaPredkosc : this.predkoscMax;
    }

    public void zwolnij(int i) {
        int nowaPredkosc = this.predkosc - i;
        this.predkosc = nowaPredkosc > 0 ? nowaPredkosc : 0;
    }

    public int aktualnaPredkosc() {
        return this.predkosc;
    }

    public String toString() {
        return this.producent + " " + this.model;
    }

}

Zobacz też