Dziś na zajęciach z OJP usłyszałem pierwszy raz jak wygląda obiektowe programowanie w ujęciu "ewolucji". Powiedzmy, że robimy bazę danych samochodów:
int : rocznik
int : przebieg
string : rejestracja
string : kolor
Operujemy na 4 tablicach - funkcja, która miałaby operować na samochodzie musiałby przyjmować 4 parametry (porażka)... lub działać na zmiennych (tablicach) globalnych. Sortowanie takich samochodów np. według rocznika czy przebiegu sprawia, że drobny błąd w algorytmie (operującym na 4 tablicach) rozsynchronizuje bazę.
Kolejnym pomysłem jest zamknąć te dane w strukturze samochód:
struct samochod {
int : rocznik
int : przebieg
string : rejestracja
string : kolor
}
Dzięki temu funkcja (np. funkcja info() wyświetlająca informację o samochodzie) może przyjmować już tylko jeden parametr (samochód)... Jednak teraz programista może celowo (lub przypadkowo) zmniejszyć przebieg lub zmienić rocznik - czego byśmy nie chcieli. Nasz obiekt nie jest bezpieczny, nie jest niezmienniczy. Nie możemy zagwarantować, że przebieg się nie zmieniszy, że rocznik się nie zmieni.
Idąc dalej możemy zamknąć wszystko w klasie a dane umieścić w sekcji private.
class samochod {
// Interfejs: co sie składa na samochód
private:
int : rocznik
int : przebieg
string : rejestracja
string : kolor
string : numer_silnika
string : marka
public:
// Implementacja: jak się zachowuje samochód
zmien_przebieg() {
++przebieg;
}
}
Dzięki temu programista nie może zmniejszyć przebiegu:
samochod * mazda_jasia = new samochod();
mazda_jasia->przebieg = 200 km;
Dzięki temu jesteśmy pewnie, że przebieg nigdy się nie zwiększy. Minusem tego jest jednak, że nie możemy odczytać przebiegu:
cout <<>przebieg;
No i mamy problem. Musimy więc dopisać nową metodę.
int function Samochod::pobierz_przebieg() {
return this.przebieg;
}
Dzięki temu mamy pełna kontrolę nad przebiegiem. Jesteśmy pewni, że nie zmniejszy się jednak możemy sprawdzić jego wartość i monotonicznie go zwiększać. Jednak jaki rocznik będzie miał samochód, który jest nowo utworzony ? No właśnie. Często zero, jednak nie można na tym polegać... Musimy więc zainicjować wartości na nowo - robimy to w konstruktorze.
Samochod::Samochod() {
this.rocznik = 1970;
this.przebieg = 0;
}
Jednak taki samochód nie ma ani marki ani koloru. Możemy więc zrobić inny konstruktor.
Samochod::Samochod(string _rejestracja, string _silnik, unsigned int _rocznik ...) {
rejestracja = _rejestracja;
silnik = _silnik;
rocznik = _rocznik;
}
Jednak kiepskim pomysłem jest wypisywanie w liście wszystkich parametrów. W powyższym przypadku tworzą się kopie obiektów, które są przekazywane przez do metody. Możemy jednak nie przekazywać parametrów i nie przypisywać ich do skałdowych klasy w ciele konstruktora. Do tego słuzy lista inicjalizacyjna:
Samochod::Samochod(string _rejestracja, string _silnik ...):rejestracja(_rejestracja), silnik(_silnik), kolor("czerwony") {
[...] // tutaj ciało metody
}
Mamy teraz przeciążony konstruktor (istnieją i moga istnieć różne funkcje o tych samych nazwach a o różnych parametrach). Możemy np. zrobić dodatkową metodę zwiększ przebieg:
Samochod::zwieksz_przebieg(int ile) {
if (ile > 0) {
przebieg += ile;
}
}
W C++ mamy możliwość dawać parametrow wartości domyslne, co pozwala uiżytkownikom daje mozliwośc na opuszczanie parametrów. Na z 2 metod zwieksz przebieg możemy zrobić 1:
class {
[...]
zwieksz_przebieg(int ile = 1);
-
-
Samochod::zwieksz_przebieg(int ile) {
}
mazda_jasia->zwieksz_przebieg(); // += 1
mazda_jasia->zwieksz_przebieg(12); // += 12
Wartości domyślne (zmienne z wartościami domyślnymi) dajemy na końcu.
Dalej mówiac o konstruktorze. Jeżeli tworzymy knstruktor dla klasy to musimy zadbać, żeby wszystko było w nim zainicjowane.
Dziedziczenie:
------------------------------------------------
Stosujemy kiedy chcemy stworzyć specjalizację klasy, lub dodać jakieś cechy lub zachowania, których nie miała klasa oryginalna.
-------------
samochod < [ samochod_sportowy ] (bool spoiler, unsigned int data_tuningu)
-------------
^
i a kind of
^
--------------
ciezarowka
--------------
Kiedy zaczniemy dodawac do klas pochodnych coraz wiecej metod czy skladowych pokaze, ze nie warto tworzyc jednej wielkiej klasy samochod z ogromna ilosccia skladowych, poniewaz niektore skladowe (spoilery) dotycza tylko i wylacznie samochodwo sportowych, a w samochodach musialby by zaincjalizowane na jakies dizwne bezsensowen wartosci. Jak wyglada dziedziczenie:
class Ciezarowka public Samochod {
private:
// ciezarowka stala sie samochodem. Dodajemy tylko to co chcemy dodać:
int ladownosc; // Tym się różni od samochodu.
}
Jakie można popełnić błędy ? 1) Otóż mazda nie DZIEDZICZY po samochodzie. Jest samochodem ! a nie klasą... Powinna być obiektem klasy samochód ! a nie samochodem !
2) Kompozycja (is a part of) -> jest częścią. Na przykład silnik jest częścią samochodu, ale nie dziedziczy po samochodzie ! Jest natomiast częścią samochodu ! (diamencik).
3)
elipsa <- punkt : połżenie_środka, float : szerokość, float : wysokość, narysuj()...
Koło nie może dziedziczyć po elpisie ! Koło => (szerokość = wysokość). Wszystko jest ok do momentu, kiedy my nie skomplikujemy elipsy.... np po dodaniu Elipsa::Rozciagnij() { szerokosc *= 2) okazuje się, że koło nie jest dobryn przykładem na dziedziczenie z elipsy... bo koła nie wolno dodawać.
Zasada substytucji Miskowa.
-----------------
Zasłanianie !
-----------------
Samochód: function f(int par), f(string par), f(float par);
class SamochodSportowy public Samochod {
funtion f(string par) {
// Ta metoda zaslania WSZYSTKIE funkcje f z Samochod
}
}
Przeciązanie: w jednej klasie te same nazwy różne parametry
Zasłanianie: w klasie pochodnej metoda o tej samej nazwie.
Dwa sposoby na wybrnięcie z problemu:
1) using Samochod::f; <- pierwszy sposób
2) Wywołać Samochod::f() <- w metodzie klasy.
Projekty ! (wymyślić obiekt do hierarchi dziedziczenia)
No comments:
Post a Comment