PB161 Programování v jazyce C++
Přednáška 8
Dědičnost
Návrhové principy v OOP
Nikola Beneš
13. listopadu 2018
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 1 / 30
Dědičnost
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 2 / 30
Dědičnost
Co už víte z minula?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 3 / 30
Dědičnost
Co už víte z minula?
třída může dědit od jiné třídy
co všechno se dědí?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 3 / 30
Dědičnost
Co už víte z minula?
třída může dědit od jiné třídy
co všechno se dědí?
atributy i metody
dědičnost realizuje podtypy
vztah IS-A
lučištník je pozemní jednotka (an archer is a landunit)
pes je zvíře (a dog is an animal)
ukazatel/reference na předka se může odkazovat na potomka
dědičnost může usnadnit znovupoužívání kódu
není to ale jediná možnost
kdy je to dobrý nápad?
s dědičností souvisí pojmy časná a pozdní vazba
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 3 / 30
Dědičnost v C++
lecture08_01.cpp
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
v tomto předmětu používáme pouze public dědičnost
Derived má přístup k:
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 4 / 30
Dědičnost v C++
lecture08_01.cpp
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
v tomto předmětu používáme pouze public dědičnost
Derived má přístup k:
veřejným a protected atributům a metodám Base
uživatelé Derived mají přístup k:
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 4 / 30
Dědičnost v C++
lecture08_01.cpp
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
v tomto předmětu používáme pouze public dědičnost
Derived má přístup k:
veřejným a protected atributům a metodám Base
uživatelé Derived mají přístup k:
veřejným atributům a metodám Derived včetně zděděných veřejných
atributů a metod
v jakém pořadí se volají konstruktory?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 4 / 30
Dědičnost v C++
lecture08_01.cpp
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
v tomto předmětu používáme pouze public dědičnost
Derived má přístup k:
veřejným a protected atributům a metodám Base
uživatelé Derived mají přístup k:
veřejným atributům a metodám Derived včetně zděděných veřejných
atributů a metod
v jakém pořadí se volají konstruktory?
konstruktor předka se provede před konstruktorem potomka
předek je jakoby prvním atributem potomka
v jakém pořadí se volají destruktory?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 4 / 30
Dědičnost v C++
lecture08_01.cpp
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
v tomto předmětu používáme pouze public dědičnost
Derived má přístup k:
veřejným a protected atributům a metodám Base
uživatelé Derived mají přístup k:
veřejným atributům a metodám Derived včetně zděděných veřejných
atributů a metod
v jakém pořadí se volají konstruktory?
konstruktor předka se provede před konstruktorem potomka
předek je jakoby prvním atributem potomka
v jakém pořadí se volají destruktory?
destruktor potomka se provede před destruktorem předka
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 4 / 30
Časná a pozdní vazba
Mám-li referenci/ukazatel na objekt a zavolám na něm metodu,
která metoda se zavolá?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 5 / 30
Časná a pozdní vazba
Mám-li referenci/ukazatel na objekt a zavolám na něm metodu,
která metoda se zavolá?
časná vazba (early binding): podle typu ukazatele/reference
pozdní vazba (late binding): podle skutečného typu objektu
Který typ vazby je v C++ implicitní a proč?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 5 / 30
Časná a pozdní vazba
Mám-li referenci/ukazatel na objekt a zavolám na něm metodu,
která metoda se zavolá?
časná vazba (early binding): podle typu ukazatele/reference
pozdní vazba (late binding): podle skutečného typu objektu
Který typ vazby je v C++ implicitní a proč?
Vynucení pozdní vazby – klíčové slovo virtual
lecture08_02.cpp
metoda s klíčovým slovem virtual ve třídě předka
překrývající metody ve třídách potomků můžou a nemusí mít klíčové
slovo virtual (metoda je virtuální každopádně)
od C++11: klíčové slovo override
sdělujeme úmysl překrýt (override) metodu z předka
překladač nás hlídá
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 5 / 30
Časná a pozdní vazba – Destruktory
V čem je problém?
lecture08_03.cpp, lecture08_04.cpp
class Base {
public:
~Base() { std::cout << "~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "~Derived()\n"; }
};
int main() {
Derived d;
std::unique_ptr uptr = std::make_unique();
}
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 6 / 30
Časná a pozdní vazba – Destruktory
V čem je problém?
lecture08_03.cpp, lecture08_04.cpp
class Base {
public:
~Base() { std::cout << "~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "~Derived()\n"; }
};
int main() {
Derived d;
std::unique_ptr uptr = std::make_unique();
}
Co s tím?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 6 / 30
Časná a pozdní vazba – Destruktory
V čem je problém?
lecture08_03.cpp, lecture08_04.cpp
class Base {
public:
~Base() { std::cout << "~Base()\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "~Derived()\n"; }
};
int main() {
Derived d;
std::unique_ptr uptr = std::make_unique();
}
Co s tím? A co když Base ani Derived nemají destruktor?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 6 / 30
Virtuální destruktory
Herb Sutter: A base class destructor should be either public and virtual,
or protected and nonvirtual.
In brief, then, you’re left with one of two situations. Either:
1 you want to allow polymorphic deletion through a base pointer, in
which case the destructor must be virtual and public; or
2 you don’t, in which case the destructor should be nonvirtual and
protected, the latter to prevent the unwanted usage.
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 7 / 30
Doporučení
Kdy psát virtual?
je daná třída součástí objektové hierarchie?
bude se od dané třídy dědit?
je metoda určená k tomu, aby ji potomci překrývali vlastní
implementací?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 8 / 30
Doporučení
Kdy psát virtual?
je daná třída součástí objektové hierarchie?
bude se od dané třídy dědit?
je metoda určená k tomu, aby ji potomci překrývali vlastní
implementací?
Jak psát virtuální metody?
v předkovi použijte klíčové slovo virtual
v potomcích použijte klíčové slovo override
ušetří to spoustu času stráveného hledáním chyb
v potomcích není potřeba psát virtual
co když nemám v předkovi žádnou vhodnou implementaci?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 8 / 30
Doporučení
Kdy psát virtual?
je daná třída součástí objektové hierarchie?
bude se od dané třídy dědit?
je metoda určená k tomu, aby ji potomci překrývali vlastní
implementací?
Jak psát virtuální metody?
v předkovi použijte klíčové slovo virtual
v potomcích použijte klíčové slovo override
ušetří to spoustu času stráveného hledáním chyb
v potomcích není potřeba psát virtual
co když nemám v předkovi žádnou vhodnou implementaci?
použijte čistě virtuální metodu (syntax = 0)
lecture08_05.cpp
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 8 / 30
Abstraktní třídy, rozhraní
Abstraktní třída
má alespoň jednu čistě virtuální metodu
nelze od ní vytvářet instance
Čistě abstraktní třída
všechny její metody jsou čistě virtuální (s případnou výjimkou
destruktoru)
Rozhraní (interface)
čistě abstraktní třída bez atributů (s případnou výjimkou statických
položek)
nedrží žádná data, deklaruje pouze, které metody bude možno volat
v C++ není speciální klíčové slovo, jen dohoda
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 9 / 30
Vícenásobná dědičnost
Je možno dědit od více tříd?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 10 / 30
Vícenásobná dědičnost
Je možno dědit od více tříd?
ano
Je to dobrý nápad?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 10 / 30
Vícenásobná dědičnost
Je možno dědit od více tříd?
ano
Je to dobrý nápad?
jak kdy; bez problému, pokud dědíme pouze od rozhraní
class IPrinter { /* ... */ };
class IScanner { /* ... */ };
class Copier: public IPrinter, public IScanner { /* ... */ };
Co když dědíme od jiných druhů tříd?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 10 / 30
Vícenásobná dědičnost
Je možno dědit od více tříd?
ano
Je to dobrý nápad?
jak kdy; bez problému, pokud dědíme pouze od rozhraní
class IPrinter { /* ... */ };
class IScanner { /* ... */ };
class Copier: public IPrinter, public IScanner { /* ... */ };
Co když dědíme od jiných druhů tříd?
může nastat problém, pokud se tyto třídy nějakým způsobem
překrývají (mají stejnou metodu/atribut, dědí od stejné třídy)
problém s děděním od stejné třídy se nazývá diamond problem
řeší se virtuální dědičností (pokročilejší téma, v následujícím
předmětu) lecture08_06.cpp
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 10 / 30
Dědičnost a přetypování
Co už víte o přetypování v C++
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 11 / 30
Dědičnost a přetypování
Co už víte o přetypování v C++
Céčkové přetypování ve stylu (typ)hodnota není typově bezpečné a
nemělo by se v C++ používat; proč?
tohle přetypování postupně zkouší všechny možnosti
(včetně reinterpret_cast a const_cast)
taky není syntakticky moc pěkné (jakou má prioritu?)
pro přetypování mezi primitivními typy můžeme použít
static_cast(hodnota)
změna celočíselné hodnoty na enum
změna ukazatele na void * a naopak (pozor na nedef. chování)
http://en.cppreference.com/w/cpp/language/explicit_cast
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 11 / 30
Dědičnost a přetypování
Přetypování v objektové hierarchii
ukazatel na potomka se umí implicitně konvertovat na ukazatel na
předka (podobně pro reference)
opačný směr?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 12 / 30
Dědičnost a přetypování
Přetypování v objektové hierarchii
ukazatel na potomka se umí implicitně konvertovat na ukazatel na
předka (podobně pro reference)
opačný směr?
static_cast(hodnota)
jen když zcela určitě vím, že objekt je skutečně daného typu
co když to nevím? nedefinované chování!
dynamic_cast(hodnota)
rozhoduje se za běhu programu
jen pokud je někde v hierarchii virtuální metoda (proč?)
používejte zřídka a jen pokud k tomu máte dobrý důvod, preferujte
rozumně navržené virtuální metody
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 12 / 30
Dědičnost a přetypování
lecture08_07.cpp
dynamic_cast(hodnota)
k určení, zda je hodnota skutečně daného typu, se používá ukazatel na
virtuální tabulku
pokud dynamic_cast neuspěje:
pokud je typ ukazatel, vrátí nullptr
pokud je typ reference, vyhodí výjimku typu std::bad_cast
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 13 / 30
Kompozice vs. dědičnost
Dědičnost je vztah IS-A
potomek má vlastnosti předka
potomka je možno přetypovat na předka
Kompozice je vztah HAS-A
objekt se skládá z jiných objektů
auto se skládá z motoru, kol, dveří, …
počítač se skládá z procesoru, paměti, disků, …
reprezentace v OOP jazyce:
třída má atributy, které jsou typu jiné třídy
třída tím obsahuje vlastnosti těchto jiných tříd
není ale jejich potomkem, nemůže je zastoupit
Proč o tom mluvíme? Časté zneužití dědičnosti tam, kde by byla lepší
kompozice.
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 14 / 30
Příklady špatného použití dědičnosti
class Laptop : public CPU, public RAM { /* ... */ };
class Stack : public Vector { /* ... */ };
class Properties : public HashTable { /* ... */ };
class Square : public Rectangle { /* ... */ }; // ???
class ComputerWithPrinter : public Computer { /* ... */ };
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 15 / 30
Příklady špatného použití dědičnosti
class Laptop : public CPU, public RAM { /* ... */ };
class Stack : public Vector { /* ... */ };
class Properties : public HashTable { /* ... */ };
class Square : public Rectangle { /* ... */ }; // ???
class ComputerWithPrinter : public Computer { /* ... */ };
Složitější:
class Teacher : public Person { /* ... */ };
class Student : public Person { /* ... */ };
co když je někdo zároveň student a učitel?
kompozice místo dědičnosti: role
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 15 / 30
Návrhové principy a vzory
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 16 / 30
Návrhové principy v OOP
Jak správně navrhnout objektovou hierarchii?
co umístit do jedné třídy?
jaké vztahy mezi třídami?
Jak se pozná špatný návrh?
malé změny vyžadují velké úpravy
změny způsobí nečekané problémy
velká provázanost (s konkrétní aplikací, s jiným kódem)
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 17 / 30
Návrhové principy v OOP (pokr.)
Základní návrhové principy pro OOP
SRP – The Single Responsiblity Principle
OCP – The Open/Closed Principle
LSP – The Liskov Substitution Principle
ISP – The Interface Segregation Principle
DIP – The Dependency Inversion Principle
https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)
(odkazy v části References)
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 18 / 30
Principy SOLID
The Single Responsiblity Principle
(už jsme viděli)
„Třída by měla mít jediný důvod ke změně.“
obecnější princip: každý se stará o jednu věc
Důsledky porušení
změna jedné z funkcionalit vyžaduje rekompilaci
úprava kódu poruší více funkcionalit
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 19 / 30
Principy SOLID
Příklad porušení SRP
třída Rectangle zároveň vykresluje čtverec a počítá jeho obsah
někteří uživatelé potřebují obě tyto funkcionality, někteří jen jednu
Lepší řešení
rozdělit funkcionalitu Rectangle do dvou tříd
o počítání obsahu se postará GeometricRectangle
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 20 / 30
Principy SOLID
The Open/Closed Principle
„Třídy by mělo být možné rozšiřovat, ale bez jejich modifikace.“
nejen třídy (moduly, jiné entity)
open for extension: možnost přidávat chování
closed for modification: možnost použití jiným kódem
dosahuje se použitím dědičnosti a abstrakce
Související
nepoužívat globální proměnné
soukromé atributy objektů
používat dynamic_cast opatrně (proč?)
Důsledek porušení: kaskáda změn v kódu
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 21 / 30
Principy SOLID
The Liskov Substitution Principle
(už jsme v podstatě viděli)
„Potomek může zastoupit předka.“
funkce očekávající objekt typu předka musí fungovat s objekty typu
potomka, aniž by o tom věděly
úzce souvisí s OCP
Důsledek porušení: neočekávané chování (špatné předpoklady)
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 22 / 30
Principy SOLID
Příklad porušení LSP
mějme třídu Rectangle a třídu Square, která z ní dědí
obě mají metody setWidth a setHeight (a odpovídající get)
void f(Rectangle& r) {
r.setWidth(5);
r.setHeight(6);
assert(r.getWidth() * r.getHeight() == 30);
}
kde je problém?
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 23 / 30
Principy SOLID
Příklad porušení LSP
mějme třídu Rectangle a třídu Square, která z ní dědí
obě mají metody setWidth a setHeight (a odpovídající get)
void f(Rectangle& r) {
r.setWidth(5);
r.setHeight(6);
assert(r.getWidth() * r.getHeight() == 30);
}
kde je problém?
čtverec je pravoúhelník, ale objekt typu Square není objektem typu
Rectangle – mají jiné chování
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 23 / 30
Principy SOLID
The Interface Segregation Principle
„Vytvářejte specifická rozhraní pro specifické klienty.“
více malých rozhraní je lepší než jedno obrovské
klienti by neměli být nuceni záviset na rozhraních, které nepoužívají
souvisí s SRP
Jak dosáhnout?
rozhraní s mnoha metodami: kdo je používá?
opravdu používají všichni všechny metody?
rozdělit metody do samostatných rozhraní
použití vícenásobné dědičnosti
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 24 / 30
Principy SOLID
The Dependency Inversion Principle
obecné třídy by neměly záviset na specializovaných třídách;
všichni by měli záviset na abstrakcích
abstrakce by neměly záviset na detailech, ale naopak
souvisí s OCP a LSP
použití rozhraní (čistě abstraktních tříd)
Důsledky porušení:
přidání podpory nového typu vede ke změně v obecné třídě
obecná třída použitelná jen pro co byla implementována
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 25 / 30
Principy SOLID
Příklad porušení:
třída Worker s metodou work() a třída Manager, která ji používá
závislost Manager → Worker
co když budeme chtít přidat novou třídu SuperWorker?
Řešení:
vytvoření abstrakce (rozhraní) IWorker
závislosti Manager → IWorker, Worker → IWorker, …
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 26 / 30
Keep It Simple, Stupid!
Princip KISS
jednoduché, postačující řešení může být lepší než komplikované a
rafinované
snazší pochopení (dalšími vývojáři, námi samotnými)
menší riziko chyby
kompromis mezi aktuálními a budoucími požadavky
příliš mnoho budoucích požadavků návrh komplikuje
omezení na aktuální požadavky vadí budoucí rozšiřitelnosti
průběžný vývoj, refaktorizace (nebát se!)
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 27 / 30
Návrhové vzory
Motivace
opakující se programátorské problémy
opakující se způsoby řešení
vhodné konstrukce pro řešení: návrhové vzory
kniha Design Patterns: Elements of Reusable Object-Oriented
Software (23 vzorů)
autoři Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
(tzv. Gang of Four, GoF)
existují i jiné návrhové vzory
kde začít? https://en.wikipedia.org/wiki/Software_design_pattern
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 28 / 30
Návrhové vzory
Co je návrhový vzor?
(jednoduchý) způsob, jak řešit skupiny podobných problémů
opakovatelný, (víceméně) nezávislý na konkrétní aplikaci či jazyce
Pozitiva
řeší běžné problémy
mohou zlepšovat kód a jeho udržovatelnost
Kritika
zbytečně složité na jednoduché problémy
nadužívání může vést ke komplikovanějšímu kódu
často řeší jen nedokonalost používaného jazyka
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 29 / 30
Závěrečný kvíz
https://kahoot.it
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 30 / 30
Závěrečný kvíz
https://kahoot.it
class A {
public:
void f() { std::cout << "Af "; g(); }
virtual void g() { std::cout << "Ag "; }
};
class B : public A {
public:
void f() { std::cout << "Bf "; }
void g() override { std::cout << "Bg "; }
};
int main() {
B b; A* ptr = &b; ptr->f();
}
PB161 přednáška 8: dědičnost, návrhové principy 13. listopadu 2018 30 / 30