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.
HttpRequest
, Player
,
Shape
, Font
HttpRequest
nepředstavuje nic opravdu hmatatelného, je to
(obvykle) jen sekvence bajtů zaslaná po síti.
currentFont.size = 16
currentFont.size = PointsToPixels (12);
currentFont.sizeInPixels = PointsToPixels (12);
currentFont.attribute |= 0x02;
currentFont.attribute |= FONT_ATTRIBUTE_BOLD;
currentFont.bold = true
currentFont.setSizeInPixels (sizeInPixels);
currentFont.setSizeInPoints (sizeInPoints);
currentFont.setWeight (FontWeight.BOLD);
currentFont.setTypeFace (typeFaceName);
currentFont.setStyle (FontStyle.ITALIC);
ErrorMessages
představuje seznam chybových
hlášení, reprezentovaných třídou Message
.
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);
...
}
public class Program {
...
public void initializeProgram ();
public void shutdownProgram ();
...
}
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í:
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.
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).
public class Team extends ArrayList <Athlete> {
...
// 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);
...
}
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 <Athlete> members;
}
public class Employee {
...
public Employee (...);
public FullName getFullName ();
public Address getAddress ();
public PhoneNumber getWorkPhone ();
public PhoneNumber getHomePhone ();
public TaxId getTaxId ();
public JobClassification getJobClassification ();
...
}
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 ();
...
}
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.
class Car {
private Engine engine;
private Wheel[] wheels;
}
Square
vs Rectangle
Employee
dědí z třídy Person
Supervisor
dědí z třídy ... ?Employee
a Supervisor
mohou být roleProperties extends Hastable
nebo Stack extends Vector
v Javě
public class CountingHashSet <E> extends HashSet <E>
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;
}
}
CountingHashSet <String> s = new CountingHashSet <String> ();
s.addAdll (Arrays.asList ("Snap", "Crackle", "Pop"));
System.out.println ("Element additions: "+ s.getAddCount ());
public class ForwardingSet <E> implements Set <E>
private final Set <E> target;
public ForwardingSet (Set <E> 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 (); }
}
public class CountingSet <E> extends ForwardingSet <E>
private int addCount = 0;
public CountingSet () {}
public CountingSet (Set <E> 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;
}
}
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jak bychom použili
dědičnost?
Complex
dědí od Real
Real
dědí od Complex
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jak bychom použili
dědičnost?
Complex
dědí od Real
Real
dědí od Complex
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jak bychom použili
dědičnost?
Complex
dědí od Real
Real
dědí od Complex
Real
bude jednoduše mít nulovou komplexní složku
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jak bychom použili
dědičnost?
Complex
dědí od Real
Real
dědí od Complex
Real
bude jednoduše mít nulovou komplexní složkuJak z toho ven?
Nepoužijeme dědičnost, ale kompozici!
Complex
a Real
poskytnou operace relevatní typuReal
jako Complex
umožní typová konverze/přetížené operacefinal
(Java), sealed
(C#)String
v Javě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, na kterou nejste připraveni apod. V praxi naštěstí zdá se k těmto problémům příliš nedochází – kvůli nim si zatím nikdo příliš nestěžoval, že v Javě jsou všechny metody implicitně virtuální.
Často se však objevují stížnosti z důvodu výkonu. Zavolání virtuální metody trvá o něco déle než zavolání obyčejné, takže ve výkonnostně kritických aplikacích (např. hry) nebo úsecích kódu se někdy vyplatí nad virtuálními metodami uvažovat i z tohoto hlediska. Nejjistější je jako obvykle takové rozhodnutí založit na nějakých faktech.
Ke snižování flexibility: Tím, že zabráníme uživateli dědit od
nějaké třídy, zabraňujeme mu vytvořit si její upravené verze, ve
kterých si např. dopíše různé pomocné metody. To je velký problém u
tříd v knihovnách Javy, které jsou často final
a
přitom jsou navrženy poměrně minimalisticky. Důsledek je, že skoro
v každém větším projektu v Javě se najde spousta pomocných tříd
(např. StringUtils
), které potřebné metody
implementují jako statické.
Jednou ze zajímavých vlastností C# 3.0 je, že obsahuje mechanizmus, který zajišťuje, že se tyto pomocné statické metody dají volat, jako by to byly metody původní třídy. Něco podobného se již dá najít i v řadě jiných jazyků, protože to umožňuje zachovat původní typ malý a potřebná rozšíření nechat na uživatelích.
K třídám s jen jednou podtřídou: Podobně platí i pro rozhraní s jen jednou implementací, nicméně tam může být situace trochu odlišná. Rozhraní může být použito k omezení rozhraní poskytovaného třídou nebo k vytvoření určitého pohledu (facet) na třídu.
Stream.flush
a
MemoryStream.flush
final
atributuclone ()
nebo readObject ()
v Javě
K prázdné předefinované metodě: Příkladem budiž abstraktní třída
Stream
, představující nějaký výstupní datový proud.
Mezi jejími metodami je i metoda flush
, která zapíše na
disk data držená v bufferu. V memory-based streamu
MemoryStream
taková metoda pochopitelně bude prázdná.
To je špatně. Správné by bylo zjemnit hierarchii tříd, rozdělit je
na bufferované/nebufferované nebo disk-based/memory-based a metodu
flush
přidat jen do té větve hierarchie, kde ji třídy
budou opravdu implementovat.
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
K mnohonásobné dědičnosti: Dědičnost rozhraní je v C++ realizována ryze abstraktními třídami, v Javě a C# pak pomocí interfaců.
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
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
(nebo ekvivalentní if
)
se dívejte s podezřením a přemýšlejte, zda není lepší ho nahradit
polymorfismem
class Shape {
public void drawRectangle ();
public void drawShape ();
}
class Graphics {
public void drawShapes (Collection <Shape> 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);
}
}
}
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 <Shape> shapes) {
for (Shape shape : shapes) {
shape.draw ();
}
}
}
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 {
Collection <Shape> shapes;
public void draw () {
for (Shape shape : shapes) {
shape.draw ();
}
}
}
Třída je immutable právě tehdy, pokud po vytvoření instance nejdou data instance žádným způsobem změnit.
private
a final
(Java)final
, nebo je final
celá třída
(Java)
final
je v C# modifikátor sealed
,
který je potřeba použít buď na třídu, nebo na metody, které byly označeny
virtual
. V případě atributů je možné použít readonly
(na rozdíl od final
umožňuje více zápisů). V C++ lze podobného
efektu dosáhnout pomocí const
.
hashCode
objektu se nesmí měnit, dokud je použit jako klíč
Ke klíčům hashtabulek: Jedním z požadavků na klíč hashtabulky v Javě
je, aby se po dobu, co je klíčem, nezměnila hodnota, kterou vrací
jeho metoda hashCode
. Tato hodnota je v drtivé většině
případů počítána z atributů objektu. V případě immutable objektů
se atributy nikdy nezmění a tím pádem se nezmění ani hodnota vracená
metodou hashCode
objektu. Podmínka kladená na klíč
hashtabulky je tedy automaticky splněna.
BigInteger.negate
K snadnému sdílení vnitřností: Třída BigInteger
ze
standardní knihovny Javy implementuje "velká čísla". Technicky jsou
implementována pomocí pole. Metoda negate
vrátí nové
velké číslo, které bude negací toho, na kterém byla zavolána.
Protože instance třídy BigInteger
jsou immutable,
původní i znegované číslo můžou sdílet pole s číslicemi bez obav,
že bude přepsáno. Každá instance musí mít jen svoji informaci o
znaménku. Ušetří se tak paměť.
DateInterval
postavený z mutable tříd Date
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.begin = end.clone();
}
}
Data
Date date = new Date ();
Scheduler.scheduleTask (task1, date);
date.setTime (d.getTime() + ONE_DAY);
Scheduler.scheduleTask (task2, date);
DateInterval
postavený z immmutable tříd Date
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.begin = end;
}
}
String
vs. StringBuilder
Dimension
vracená metodou Component.getSize ()
/**
* 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 {
/* ... */
}