# Doporučené postupy v programování ## Návrh tříd ### Abstrakce · Zapouzdření · Dědičnost vs. kompozice · Polymorfizmus · Immutability #### Lubomír Bulej ##### KDSS MFF UK
# Návrh tříd ## Abstrakce
# Abstrakce ## Zjednodušený pohled na složité věci - eliminace (pro daný kontext) nepodstatných detailů ## Třída jako nositel abstrakce - reálné a abstraktní objekty z domény problému - abstraktní datové typy + dědičnost a polymorfizmus Note: Steve McConnel: Způsob uvažování při programování se vyvíjel spolu se složitostí programů, které bylo nutné vytvářet. Zatímco v dávných dobách programátor přemýšlel o jednotlivých příkazech a jejich sekvenci, v 70. a 80. letech 20. století začal pracovat s konceptem rutin. V 90. letech se rozšířilo (vniklo již dříve) objektové programování, které stále představuje hlavní paradigma pro tvorbu programů. Dnes tedy programátoři pracují s konceptem tříd a objektů. Třída představuje spojení dat a rutin, které slouží nějakému dobře definovanému účelu. Třída se obejde i bez dat – v takovém případě představuje sdružení služeb, které opět pojí nějaký společný účel. Klíčem k efektivitě programátora je maximalizace části programu, kterou je možné pustit z hlavy aniž by to mělo negativní důsledky tu na část, se kterou pracuje. V tomto ohledu třídy představují hlavní nástroj, jak toho dosáhnout. Bez ohledu na podporu v konkrétmím jazyce třídy představují pouze technický prostředek k zápisu programů. Pokrok v programování však přicházel vždy především se zvyšováním úrovně abstrakce, na které se o programu přemýšlí. Tento posun v abstrakci představují abstraktní datové typy, které popisují zároveň data i operace, které s těmito daty pracují. Objektově-orientované programování je tedy primárně o práci s abstraktními datovými typy.
# Abstraktní datové typy?
## Abstraktní matematické struktury - mějme množinu ..., prvky mají následující vlastnosti ... - definujme operace ..., předpokládejme ..., lze ukázat ... - definice ..., lemma ..., věta ..., důkaz ..., složitost ...
## Nástroj pro práci v jazyce problému - definují data a operace pro manipulaci s nimi - umožňují manipulovat s "reálnými" entitami, i když ty nemusí být nutně hmatatelné - `HttpRequest`, `Player`, `Shape`, `Font` - při přemýšlení o problému umožňují nezabývat se implementačními detaily - místo vložení položky do seznamu se bavíme o vložení buňky do tabulky, přidání nového typu okna do seznamu typů, přidání vagónu k soupravě při simulaci vlaku, atd.
Note: Slovo "reálné" je v uvozovkách, protože problém, který řešíme, se reality mimo počítač vůbec nemusí týkat. Třeba třída `HttpRequest` nepředstavuje nic opravdu hmatatelného, je to (obvykle) jen sekvence bajtů zaslaná po síti.
# Příklad: práce s fonty ## Ad-hoc řešení ```java bad-code stretch currentFont.size = 16 currentFont.size = PointsToPixels (12); currentFont.sizeInPixels = PointsToPixels (12); currentFont.attribute |= 0x02; currentFont.attribute |= FONT_ATTRIBUTE_BOLD; currentFont.bold = true ```
## Abstraktní datový typ ```java okay-code stretch currentFont.setSizeInPixels (sizeInPixels); currentFont.setSizeInPoints (sizeInPoints); currentFont.setWeight (FontWeight.BOLD); currentFont.setTypeFace (typeFaceName); currentFont.setStyle (FontStyle.ITALIC); ```
Note: V případě fontu bychom spíše chtěli immutable typ a uvedenou sadu setterů bychom našli spíše na builderu.
# Hlavní výhody ADT ## Skrytí implementačních detailů - omezuje složitost, se kterou je nutno pracovat - umožňuje výměnu implementace - omezuje šíření změn programem
## Informativní rozhraní - správnost programu je zjevnější - operace jsou samovysvětlující
## Související věci jsou pohromadě - není nutné např. předávat struktury po celém programu
# Třída jako nositel abstrakce ## Abstrakce je určena rozhraním třídy - abstrahuje od implementačních detailů, které skrývá - důležité je vytvářet dobré, konzistentní abstrakce - rozhraní je tedy nejdůležitější částí návrhu třídy ## Jak se pozná dobrá abstrakce? - orientace na problém - konzistence
# Zásady pro návrh třídy ## Jasně definujte povinnosti/zodpovědnost - třída by měla dělat jednu věc, a dělat ji dobře - *Třída `ErrorMessages` představuje seznam chybových hlášení, reprezentovaných třídou `Message`.* - pozor na "božské" třídy, které všechno ví a všechno umí - uvažujte na úrovni abstraktních datových typů - Jaký ADT třída reprezentuje? ## Specifikujte kontrakt objektu - invarianty - interakce s okolím - kontrakty jednotlivých metod
# Příklad: třída reprezentující program ## Rozhraní plné různých konceptů ```java bad-code stretch public class Program { ... public void initializeCommandStack(); public void pushCommand(Command command); public Command popCommand(); public void shutdownCommandStack(); ... public void initializeReportFormatting(); public void formatReport(Report report); public void printReport(Report report); ... } ```
## Zjednodušené rozhraní ```java okay-code stretch public class Program { ... public void initializeProgram(); public void shutdownProgram(); ... } ```
Note: Rozhraní se striktně omezuje na životní cyklus programu, ale z pohledu návrhu API by bylo nejspíš stále podezřelé, že metody nepříjmají žádné parametry a nic nevrací.
# Zásady pro návrh třídy
## Vytvářejte konzistentní abstrakce - poskytuje zjednodušený pohled na složité věci * umožňuje členit inherentní a odfiltrovat zavlečenou složitost * umožňuje zabývat se pouze (pro daný kontext) podstatnými detaily
- u software eliminuje nutnost znalosti implementačních detailů * projevuje se na všech úrovních návrhu
- řada objektů v reálném světě představuje nějakou formu abstrakce * dům, dveře, klika, auto, ...
Note: Abstrakce samozřejmě nejsou samospasitelné, většina z nich má nějak díry a k jejich efektivnímu používání většinou potřebujeme vědět, co abstrahují: - [Joel Spolsky: The Law of Leaky Abstractions](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/) Což však z pohledu návrhu zas tolik nevadí, důležité je, že nám umožňují se některými nepodstatnými detaily nezabývat, pokud to není potřeba.
# Zásady pro návrh třídy ## Dbejte na jednotnou úroveň abstrakce v rozhraní - sledujte kohezi metod poskytovaných v rozhraní - nízká koheze indikuje špatnou abstrakci – opačně to však neplatí - nepřidávejte do rozhraní metody, které nejsou konzistentní s poskytovanou abstrakcí - eroze rozhraní v důsledku modifikace – broken windows - pozor na použití dědičnosti – přebírá rozhraní base class - nepřidávejte do rozhraní metody jen proto, že používají pouze jeho veřejné metody Note: Eroze v tomto případě znamená tendenci přidávat do objektů různé pomocné metody, které se hodí uživatelům objektu v různých jiných částech kódu. Zejména u větších projektů s více programátory se často stává, že přidané metody nesouvisí s abstrakcí představovanou objektem a jeho zodpovědností. Vzniká tak zamotaný, nestrukturovaný kód. Asi jediný způsob, jak se erozi vyhnout, je disciplína programátorů a případně jednoznačné vlastnictví kódu (kdy vlastník nepovolí nesystematický zásah do svého kódu).
# Příklad: třída reprezentující tým sportovců ## Rozhraní s různými úrovněmi abstrakce ```java bad-code stretch public class Team extends ArrayList
{ ... // public methods public void setName(TeamName teamName); public void setCountry(CountryCode countryCode); ... // public methods inherited from ArrayList public void add(Athlete athlete); public void clear(); public boolean isEmpty(); public void ensureCapacity(int minCapacity); ... } ```
# Příklad: třída reprezentující tým sportovců ## Rozhraní s konzistentní úrovni abstrakce ```java good-code stretch public class Team { ... // public methods public void setName(TeamName teamName); public void setCountry(CountryCode countryCode); ... public void addMember(Athlete athlete); public void removeMember(Athlete athlete); ... private List
members; } ```
# Příklad: třída reprezentující zaměstnance ## Původní rozhraní (před modifikací) ```java good-code stretch public class Employee { ... public Employee(...); public FullName getFullName(); public Address getAddress(); public PhoneNumber getWorkPhone(); public PhoneNumber getHomePhone(); public TaxId getTaxId(); public JobClassification getJobClassification(); ... } ```
# Příklad: třída reprezentující zaměstnance ## Eroze rozhraní při modifikaci ```java bad-code stretch public class Employee { ... public Employee(...); public FullName getFullName(); public Address getAddress(); public PhoneNumber getWorkPhone(); public PhoneNumber getHomePhone(); public TaxId getTaxId(); public JobClassification getJobClassification(); ... public boolean isJobClassificationValid(JobClassification job); public boolean isZipCodeValid(Address address); public boolean isPhoneNumberValid(PhoneNumber phoneNumber); ... public SqlQuery getQueryToCreateNewEmployee(); public SqlQuery getQueryToModifyEmployee(); ... } ```
# Návrh tříd ## Zapouzdření
# Zapouzdření
## Doplněk k abstrakci - abstrakce odfiltruje nepodstatné detaily
## Zapouzdření znemožňuje abstrakci opustit - WYSIWYG → WYSIAYG (What You See Is All You Get) - skrývání vnitřních informací o objektu před okolím - důsledkem je volnější vazba ke zbytku systému - poskytuje větší flexibilitu implementaci - umožňuje nezávislé testování malých celků
# Jak dosáhnout zapouzdření? ## Minimalizujte viditelnost všeho - veřejné třídy by neměly mít žádné veřejné atributy - modifikátor protected používejte po pečlivém zvážení - přístup se dá uvolnit vždy, omezit už málokdy
## Přístup k atributům tříd - mutable atributy znemožňují vynucování invariantů a jsou thread-unsafe - atributy jsou součástí rozhraní, nelze je např. přesunout do nadtřídy - použijte pomocné metody – getters/setters, accessors - analogie ke SmallTalkovské komunikaci pomocí zpráv
# Jak dosáhnout zapouzdření? ## Skryjte implementační detaily - implementace perzistence, low-level výjimky, ... - výjimečně možno lehce porušit – "Leaky abstraction" - pozor na dědičnost – porušuje zapouzdření Note: K porušování skrývání implementačních detailů: - Existuje mnoho toolkitů obalujících část Win32 API pro tvorbu GUI. Typicky jde o hierarchii tříd, na jejímž konci se nacházejí třídy pro jednotlivé ovládací prvky. Málokdy je možné a vhodné implementovat do obalující třídy všechny vlastnosti, které dotyčný ovládací prvek ve Win32 API má – třída by až příliš narostla. - Pragmatické řešení je porušit zapouzdření a zveřejnit handle ovládacího prvku z Win32 API. Kdo bude potřebovat, může vlastnosti ovládacího prvku, které zapouzdřující třída neobsahuje, používat přímo pomocí handle a Win32 API funkcí. Takto byl problém například vyřešen v Borland Delphi.
# Jak dosáhnout zapouzdření? ## Vyhněte se sémantickému porušení zapouzdření - používání znalostí o fungování vnitřností třídy, které nejsou uvedeny v jejím kontraktu - spoléhání na automatické zavření souboru při destrukci příslušného objektu - předpoklady o platnosti ukazatelů vrácených metodou objektu, který už není viditelný - zrádné – kompilátor ani jiný nástroj nezkontroluje - porušujete kdykoliv se koukáte na kód třídy místo její dokumentace Note: K porušování sémantického zapouzdření: Pokud jste nuceni se podívat do zdrojového kódu používané třídy, znamená to, že třída má buď špatnou úroveň abstrakce nebo nedostatečnou dokumentaci. V takové chvíli je správnou akcí donucení/přesvědčit autora třídy k nápravě.
# Návrh tříd ## Dědičnost vs. kompozice
# Kompozice ## Objekt obsahuje více jiných objektů - A **«HAS-A»** B
- _A car has an engine and four wheels._ ```java stretch class Car { private Engine engine; private Wheel[] wheels; } ```
agregace
objekt obsahuje jiné objekty jako části, ty však mohou být součástí více objektů a často mohou existovat i bez samotného agregátu
kompozice
objekty mohou být součástí pouze jednoho objektu, jejich existence bez kompozitu často nedává smysl
# Dědičnost ## Objekt je specializací jiného objektu - A **«IS-A»** B
- _A race car is a car._ ```java stretch class RaceCar extends Car { private RollCage rollCage; } ```
generalizace
vymezení společných vlastností nějaké množiny objektů
specializace
vymezení podmnožin v nějaké množině objektů
dědičnost rozhraní
po nadtřídě dědíme pouze a jen signaturu rozhraní
dědičnost implementace
po nadtřídě dědíme signaturu rozhraní, kód i vnitřní reprezentaci
Note: - **Dědičnost tříd** umožňuje definovat implementaci jednoho objektu pomocí implementace jiného objektu. Jedná se tedy o mechanizmus pro sdílení kódu a reprezentace. - **Dědičnost rozhraní** (subtyping) popisuje kdy může být jeden objekt použit na místo jiného.
# Hlavní výhody dědičnosti ## Omezení duplicity kódu - společné rysy skupiny třídy sdílejí definici (a implementaci) - base class definuje společné rysy na jednom místě - odvozené třídy definují své specifické rysy
## Stejné zacházení s více třídami - pro některé operace stačí rysy, které vykazuje společná nadtřída - mechanizmus pro realizaci polymorfizmu
# Použití dědičnosti ## Substituční princip (Barbara Liskov) - **nejdůležitější zásada dědičnosti** - použití dědičnosti pouze pokud je (IS-A) podtřída skutečně specializací nadtřídy - kdekoliv lze použít nějakou třídu, musí být možné použít i její podtřídu aniž by uživatel poznal rozdíl (Hunt & Thomas) - **netýká se jen syntaxe, ale i sémantiky** - podtřída musí dodržovat kontrakt nadtřídy
## Vztah «IS-A» musí být trvalý - třída `Employee` dědí z třídy `Person` - třída `Supervisor` dědí z třídy ... ? - `Employee` and `Supervisor` mohou být role
# Problémy s dědičností ## Dědičnost má tendenci dělat věci složitější - nadužívána pro technické vlastnosti - porušuje zapouzdření, často sémanticky
## Přemýšlejte v pojmech «IS-A» a «HAS-A» - nepoužívejte dědičnost jen kvůli ušetření kódu - dědičnost propaguje rozhraní nadtřídy (včetně nedostatků) - kompozice vyžaduje forwarding metod, ale umožňuje definovat vlastní rozhraní
## Preferujte kompozici před dědičností - (typicky) nemá přímou jazykovou podporu – pracnější při prvním použití
Note: Viz Effective Java: Programming Language Guide, Item 14.
# Příklad: počet přidání elementu do množiny
## S použitím dědičnosti... ```java stretch public class CountingHashSet
extends HashSet
{ private int addCount = 0; public CountingHashSet() {} public CountingHashSet(int initCap, float loadFactor) { super(initCap, loadFactor); } @Override public boolean add(E e) { addCount++; return super.add (e); } @Override public boolean addAll(Collection extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } } ```
## Co zobrazí následující kód? ```java stretch CountingHashSet
s = new CountingHashSet
(); s.addAll(Arrays.asList("Snap", "Crackle", "Pop")); System.out.println("Element additions: " + s.getAddCount()); ```
```java bad-code stretch 6 ```
## Příčiny? - `super.addAll()` nejspíš volá `add()` - **mylné předpoklady o implementaci!**
## Co s tím?
# Příklad: počet přidání elementu do množiny ## 1. část řešení: obecná forwardovací třída ```java good-code stretch public class ForwardingSet
implements Set
{ private final Set
target; public ForwardingSet(Set
target) { this.target = target; } public void clear() { target.clear(); } public boolean contains(Object obj) { return target.contains(obj); } ... public boolean add(E element) { return target.add(element); } public boolean addAll(Collection extends E> elements) { return target.addAll(elements); } ... @Override public boolean equals(Object obj) { return target.equals(obj); } @Override public int hashCode() { return target.hashCode(); } @Override public String toString() { return target.toString(); } } ``` Note: Viz Effective Java, 2nd Edition: Item 17.
# Příklad: počet přidání elementu do množiny ## 2. část řešení: rozšíření forwardovací třídy ```java good-code stretch public class CountingSet
extends ForwardingSet
{ private int addCount = 0; public CountingSet() {} public CountingSet(Set
target) { super(target); } @Override public boolean add(E element) { addCount++; return super.add(element); } @Override public boolean addAll(Collection extends E> elements) { addCount += elements.size(); return super.addAll(elements); } public int getAddCount() { return addCount; } } ```
# Příklad: reálná a komplexní čísla
Chceme definovat třídy `Real` a `Complex`, představující reálná a komplexní čísla – jak bychom použili dědičnost?
## 1. možnost: `Complex` dědí od `Real`
- Vztah «IS-A» neodpovídá realitě a umožňuje podivné chování - Je-li vyžadováno reálné číslo,
**můžeme**
dosadit komplexní - Je-li vyžadováno komplexní číslo,
**nemůžeme**
dosadit reálné - Komplexní čísla generalizují, nikoliv specializují reálná čísla
## 2. možnost: `Real` dědí od `Complex`
- V matematickém smyslu reálné číslo «IS-A» komplexní číslo - Je-li vyžadováno reálné číslo,
**nemůžeme**
dosadit komplexní - Je-li vyžadováno komplexní číslo,
**můžeme**
dosadit reálné - `Real` bude jednoduše mít nulovou komplexní složku - Nulová komplexní složka **zabírá paměť** - Pokud by `Complex` dědilo z `Quaternion`, bude zabírat paměti ještě víc
## Jak z toho ven?
# Příklad: reálná a komplexní čísla
Nepoužijeme dědičnost, ale kompozici!
## Komplexní číslo je složené z reálných - Komplexní číslo «HAS-A» reálné číslo (dvě instance) - Třídy `Complex` a `Real` poskytnou operace relevatní typu - Použití `Real` jako `Complex` umožní typová konverze/přetížené operace
# Zásady pro práci s dědičností ## Třídu navrhněte pro dědění, nebo jej zakažte - **2. nejdůležitější zásada dědičnosti** - k návrhu patří i dokumentace, tj. jakým způsobem se má využít rozhraní dědičnosti - zákaz dědičnosti: `final` (Java), `sealed` (C#) ## Návrh pro dědičnost = netriviální rozhodnutí a odpovědnost - nutno navrhnout vnější rozhraní a rozhraní pro dědičnost - rozhraní pro dědičnost je náchylné k porušení zapouzdření Note: Viz Effective Java: Programming Language Guide, Item 15.
# Návrh pro dědičnost ## Co vše je nutné zvážit? - Jaká má být viditelnost atributů, metod a dalších prvků? - Které metody mají být virtuální? - virtuální metoda = extension point - pozor na jazykové defaults (Java vs C#) - Které metody mají být abstraktní? - Použít abstraktní bázovou třídu a šablonové metody? Jaké? - Nesnížíme příliš flexibilitu pokud dědičnost zakážeme? - Příklad: `String` v Javě Note: K virtuálním metodám: Každá virtuální metoda je extension point, do kterého může svůj kód umístit odvozená třída. Ta může "vyvádět psí kusy", porušit některé invarianty, způsobit reetrantnost...
# Zásady pro práci s dědičností ## Vyhněte se příliš složitým hierarchiím - Více jak 3 úrovně ⇒ co je opravdu cílem? - Někdy pomůže návrhový vzor Decorator ## Společné věci přesuňte v hierarchii co nejvýše - usnadňuje jejich použití v podtřídách - pozor na zachování konzistence abstrakce ## Pozor na třídy s jen jednou podtřídou - signalizuje "přemýšlení dopředu" - nemusí platit pro knihovny
# Zásady pro práci s dědičností ## Pozor na prázdnou předefinovanou metodu - Možné narušení sémantiky (absence chování) - Příklad: `Stream.flush()` a `MemoryStream.flush()` ## Nevolejte virtuální metody z konstruktoru - atributy, které metoda používá, nemusí být ještě inicializovány - týká se i metod `clone()` nebo `readObject()` v Javě
# Zásady pro práci s dědičností
The one indisputable fact about multiple inheritance in C++ is that it opens up a Pandora's box of complexities that simply do not exist under single inheritance.
Scott Meyers
## Vyhněte se vícenásobné dědičnosti implementace - málokdy opravdu potřeba, výjimkou jsou např. mixiny - vícenásobná dědičnost rozhraní je OK
# Návrh tříd ## Polymorfizmus
# Co je to polymorfizmus? ## Schopnost vystupovat v různých formách - specificky v OOP schopnost jazyka pracovat s objekty různými způsoby v závislosti na jejich typu ## Mechanizmy pro implementaci polymorfizmu - přetěžování metod - dědičnost rozhraní - dědičnost rozhraní a implementace - bázová třída specifikuje rozhraní - podtřídy v rámci něj implementují odlišné chování - v kódu jednotlivé podtřídy nerozlišujeme - technicky: virtuální metody, pozdní vazba
# Polymorfizmus a příkaz `switch`
Anytime you find yourself writing code of the form "if the object is of type T1, then do something, but if it's of type T2, then do something else," slap yourself.
Scott Meyers, Effective C++
## Switch je promarněná příležitost k polymorfizmu - Jedna z nejdůležitějších zásad OOP! - Podstata návrhových vzorů *State* a *Strategy* - Není lepší ho nahradit polymorfismem? - Na každý příkaz `switch` (nebo ekvivalentní `if`) se dívejte s podezřením
# Příklad: promarněná příležitost k polymorfizmu ## Explicitní změna chování podle druhu objektu ```java stretch bad-code class Shape { public void drawRectangle (); public void drawShape (); } class Graphics { public void drawShapes (Collection
shapes) { for (Shape shape : shapes) { draw (shape); } } private void draw (Shape shape) { if (shape.kind == ShapeKind.RECTANGLE) { shape.drawRectangle (); } else if (shape.kind == ShapeKind.CIRCLE) { shape.drawCircle (); } else { throw new AssertionError ("unexpected shape: "+ shape.kind); } } } ```
# Příklad: promarněná příležitost k polymorfizmu ## Implicitní změna chování pomocí polymorfizmu ```java stretch good-code interface Shape { public void draw (); } class Rectangle implements Shape { public void draw () { // draw rectangle } } class Circle implements Shape { public void draw () { // draw circle } } class Graphics { public void drawShapes (Collection
shapes) { for (Shape shape : shapes) { shape.draw (); } } } ```
# Příklad: promarněná příležitost k polymorfizmu ## Využití polymorfizmu + composite patternu ```java stretch good-code interface Shape { public void draw (); } class Rectangle implements Shape { public void draw () { // draw rectangle } } class Circle implements Shape { public void draw () { // draw circle } } class Graphics implements Shape { private Collection
shapes; public void draw () { for (Shape shape : shapes) { shape.draw (); } } } ```
# Návrh tříd ## Immutability Note: Viz Effective Java: Programming Language Guide, Item 13.
# Definice immutability
Třída je *immutable* právě tehdy, pokud po vytvoření instance nejdou data instance žádným způsobem změnit.
- Všechna data jsou tedy zafixována v konstruktoru. Note: Výraz "immutable" nepřekládám, protože neznám žádný překlad, který by nezněl divně. Pokud vás nějaký napadne, rád o něm uslyším.
# Jak vyrobit immutable třídu? ## Prostředky jazyka - všechny atributy `private` a `final` (Java) - žádná metoda třídy nemění data - žádné metody nejdou předefinovat v podtřídách ## Vnitřní struktura objektu - není možné změnit obsažené objekty - exkluzivní přístup - defenzivní kopie - jsou rovněž immutable
# Výhody immutability ## Redukce počtu možných stavů objektu na *jeden* - invarianty stačí ohlídat v konstruktoru ## Inherentně thread-safe - nemůže dojít ke kolizím při změnách stavu ## Dobré klíče hashovacích tabulek - `hashCode` objektu se nesmí měnit, dokud je použit jako klíč
# Výhody immutability ## Snadné sdílení objektů - není nutné kopírování, klidně ho i zakázat ## Snadné sdílení vnitřností - např. `BigInteger.negate()` ## Snadné cacheování - cache bude vždy aktuální
# Výhody immutability ## Umožňuje využít techniky funkcionálního programování - transformace objektů na objekty - žádné vedlejší efekty - důsledkem opět redukce stavového prostoru ## Poskytuje dobré "stavební bloky" - složitější data složená z jednodušších - v důsledku zjednodušuje i návrh mutable tříd
# Příklad: dobré "stavební bloky" ## Immutable `DateInterval` postavený z *mutable* tříd `Date` ```java okay-code stretch class DateInterval { private Date begin; private Date end; // JE nutné používat defenzivní kopie. public Date getBegin() { return begin.clone(); } public Date getEnd() { return end.clone(); } public DateInterval(Date begin, Date end) { this.begin = begin.clone(); this.end = end.clone(); } } ```
## Podobně problematické použití mutable třídy `Date` ```java bad-code stretch Date date = new Date (); Scheduler.scheduleTask (task1, date); date.setTime (d.getTime() + ONE_DAY); Scheduler.scheduleTask (task2, date); ```
# Příklad: dobré "stavební bloky" ## Immutable `DateInterval` postavený z *immutable* tříd `Date` ```java good-code stretch class DateInterval { private Date begin; private Date end; // NENÍ nutné používat defenzivní kopie. public Date getBegin() { return begin; } public Date getEnd() { return end; } public DateInterval(Date begin, Date end) { this.begin = begin; this.end = end; } } ```
# Nevýhody immutability ## S každou změnou atributu vzniká nová instance - vadí při mnoha malých operacích za sebou - alokace objektů téměř nic nestojí - problémem je zvýšený tlak na garbage collector - možno vyřešit dočasným použitím *mutable counterpart* - `String` vs. `StringBuilder` ## Paradoxně mutability může vést ke stejné situaci - `Dimension` vracená metodou `Component.getSize ()` - použití mutable tříd ke stavbě immutable třídy
# Kdy má být třída immutable? ## Pokud nemáte velmi dobrý důvod, aby byla mutable - mutable třídy by se měly měnit co nejméně - immutabilitu zdokumentovat
```java stretch /** * Class which stores information about timing of the experiments * in the regression analysis. * * The class is immutable and especially Misho should never ever * try to make it mutable :-) * * @author David Majda */ public class SchedulerInfo implements Serializable { /* ... */ } ```
# Častí kandidáti na immutabilitu ## Obecně malé "value objects" - identifikátory, data, časy - intervaly, dvojice, trojice,... - geometrické útvary (bod, úsečka,...) - třídy popisující layout/strukturu něčeho (dokument, GUI,...) - třídy vzešlé z DSL - uzly v AST - metadata o nějaké entitě (soubor, proces,...) ## Nevhodní kandidáti na immutabilitu - velké objekty, kontejnerové objekty - postupně konstruované objekty