Co je to programování?
Co je to programování?
"Ukážeme si praktické programátorské techniky, které vedou k psaní přehlednějšího, kvalitnějšího a lépe udržovatelného kódu. Vysvětlíme si, proč je používat, a předvedeme si spoustu dobrých i špatných příkladů."
Jazyk používaný na semináři a v úkolech = Java
Občas odbočíme i k jiným jazykům:
+ odkazy roztroušené po slajdech
void HandleStuff( CORP_DATA & inputRec, int crntQtr,
EMP_DATA empRec, double & estimRevenue, double ytdRevenue,
int screenX, int screenY, COLOR_TYPE & newColor,
COLOR_TYPE & prevColor, StatusType & status, int expenseType )
{
int i;
for ( i = 0; i < 100; i++ ) {
inputRec.revenue[i] = 0;
inputRec.expense[i] = corpExpense[ crntQtr ][ i ];
}
UpdateCorpDatabase( empRec );
estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
newColor = prevColor;
status = SUCCESS;
if ( expenseType == 1 ) {
for ( i = 0; i < 12; i++ )
profit[i] = revenue[i] - expense.type1[i];
}
else if ( expenseType == 2 ) {
profit[i] = revenue[i] - expense.type2[i];
}
else if ( expenseType == 3 )
profit[i] = revenue[i] - expense.type3[i];
}
Co je na tomto kódu špatně? Podle Steve McConnella:
HandleStuff()
tells you
nothing about what the routine does.
expenseType == 2
and expenseType == 3
.
inputRec
, is changed.
If it's an input variable, its value should not be modified (and
in C++ it should be declared const
). If the value of
the variable is supposed to be modified, the variable should not
be called inputRec
.
corpExpense
and writes to profit. It should
communicate with other routines more directly than by reading and
writing global variables.
crntQtr
equals 0
, the expression
ytdRevenue * 4.0 / (double) crntQtr
causes
a divide-by-zero error.
screenX
and screenY
are not referenced within the routine.
prevColor
is labeled as a reference parameter
(&
) even though it isn't assigned a value within the
routine.
A já dodávám:
_TYPE
× _DATA
) kryptické.int i
) by měla být
v cyklu samotném.
expenseType
by měly být
realizovány příkazem switch
.
List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");
Collections.sort(myList, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
for (String s: myList) {
System.out.println(s);
}
List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");
Collections.sort(myList, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
for (String s: myList) {
System.out.println(s);
}
List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");
Collections.sort(myList, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
for (String s: myList) {
System.out.println(s);
}
puts ["one", "two", "three"].sort_by { |item| item.length }
List<String> myList = new ArrayList<String>();
myList.add("one");
myList.add("two");
myList.add("three");
Collections.sort(myList, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
for (String s: myList) {
System.out.println(s);
}
puts ["one", "two", "three"].sort_by { |item| item.length }
Ne všechny zde probírané věci platí pro všechny typy software
strlen(s) < 255
×
strlen(s) < MAX_FILENAME_LENGTH
1.22
a převrácenou hodnotu (onoho programátora
nenapadlo psát všude 1 / 1.22
) nebylo nic příjemného.
Existují výjimky
0 | Inicializace součtů, akumulátorů, indexů při for-cyklu |
---|---|
1 |
Inicializace součinů, indexů při for-cyklu, posuny o jedničku,
odečítání od konce
|
2 | Půlení intervalů, průměry |
+ Pokud by použití konstanty výrazně snížilo čitelnost kódu
Věci, které spolu souvisí, mají být v kódu u sebe.
Každé porušení principu lokality znamená, že programátor bude muset pobíhat po zdrojovém kódu sem a tam, aby vyhledal všechny potřebné informace ke kusu kódu, na kterém právě pracuje. To narušuje koncentraci a zdržuje, a tedy snižuje produktivitu.
Dnes situaci vylepšují "chytrá" IDE, kde se po najetí nad nějaký objekt zobrazí jeho definice nebo se na ni dá rychle odskočit a vrátit se zpět.
ALL_CAPS_SEPARATED_BY_UNDERSCORES
Pokud jazyk má (C, C++, Java, C# ano), používat
public enum LogLevel { INFO, WARNING, ERROR }
Pokud jazyk nemá, emulovat konstantami
*_FIRST
a *_LAST
kvůli
iteraci
#define LL_INFO 0
#define LL_WARNING 1
#define LL_ERROR 2
#define LOG_LEVEL_FIRST 0
#define LOG_LEVEL_INFO 0
#define LOG_LEVEL_WARNING 1
#define LOG_LEVEL_ERROR 2
#define LOG_LEVEL_LAST 2
Čím menší je stavový prostor programu, tím lépe.
Šetřte čas čtenáře/upravujícího na úkor pisatele.
-1
= nekonečnoČasté je využívání dolních bitů ukazatelů, protože ty dnes bývají zarovnané na 4 bajty.
Hodí se to například u interpretů jazyků, kde jsou potřeba rychlé operace s celými čísly. Podle dolních bitů se rozhodne, zda se v horní části daných 4 bajtů nachází číslo, nebo ukazatel na nějaký složitější objekt. V případě čísla se tak ušetří jedna dereference (za cenu, že takové číslo nemůže zabrat celých 32 bitů).
Pokud jste se někdy divili, proč má JavaScript nebo Ruby o jeden bit menší celá čísla, než byste čekali, teď už víte proč.
final
resp. const
List<String> list = new ArrayList<String>();
"You can't give a variable a name the way you give a dog a name – because it's cute or it has a good sound." – Steve McConnell
inputRecord
× employeeRecord
readyFlag
× printerReady
numberOfPeopleOnTheUsOlympicTeam
numberOfSeatsInTheStadium
n
, np
, ntm
n
, ns
, nsisd
numTeamMembers
, teamMemberCount
numSeatsInStadium
, seatCount
status
×
statusOK
,
wasError
,
failed
fileIsReadable
×
fileReadable
if not notFound then ...
if (item->prev) { item->prev->next = item->next; }
if (item->next) { item->next->prev = item->prev; }
tmp
,
temp
,
apod.
file1
,
file2
×
srcFile
,
destFile
if
if
/then
if
/then
/else
if
/then
/elseif
/.../else
if
/then
/else
switch
switch
bývá v některých jazycích někdy nazýván jinak.
if
if
else
?if
-větve a který do
else
-větve?
true
/false
-like hodnot?
else
?int compare(int a, int b) {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
vs.
int compare(int a, int b) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
else
?if
/then
else
-větev byla "prázdná"if
/then
/else
Obecně: Obvyklý, "pozitivní" případ by měl mít nejkratší cestu v kódu
if (*curr_pos == '/') {
curr_pos++;
if (*curr_pos == '/') {
curr_pos++;
while (*curr_pos && !is_newline(*curr_pos)) {
curr_pos++;
}
} else {
return NULL;
}
} else {
return NULL;
}
if (*curr_pos != '/') { return NULL; }
curr_pos++;
if (*curr_pos != '/') { return NULL; }
curr_pos++;
while (*curr_pos && !is_newline(*curr_pos)) { curr_pos++; }
if
-větve dát případ...if
-větve dát případ...if (slideIndex >= 0 && slideIndex < slides.length) { ... }
&&
a ||
určit výsledek z prvního operandu, druhý operand se nevyhodnotí.
&&
)null
0
if (s != null && s.length() > 0) { ... }
true
/false
-like?null
zároveň
null
(idiomatické)
if (document.atEndOfStream() && !inputError
&& lineCount >= MIN_LINES && lineCount <= MAX_LINES
&& !errorProcessing()) {
/* ... */
}
boolean allDataRead = document.atEndOfStream()
&& !inputError;
boolean legalLineCount = lineCount >= MIN_LINES
&& lineCount <= MAX_LINES;
if (allDataRead && legalLineCount && !errorProcessing()) {
/* ... */
}
==
a =
{
a }
i když je v těle jen jeden
příkaz
{
a }
při rozšiřování
těla
&&
a ||
na
stejné úrovni
if
/then
/elseif
/.../else
else
switch
switch
===
,
neomezené množství hodnot
case inputLine
when "debug"
dumpDebugInfo
dumpSymbols
when /p\s+(\w+)/
dumpVariable($1)
when "quit", "exit"
exit
else
print "Illegal command: #{inputLine}"
end
break
v příkazu switch
case
jsou jen návěští
break
break
se zapomínáswitch (inputVar) {
case 'A': if (test) {
// statement 1
// statement 2
case 'B': // statement 3
// statement 4
}
break;
}
break
v příkazu switch
switch (errorDocumentationLevel) {
case DocumentationLevel.FULL:
displayErrorDetails(errorNumber);
// Fall through
case DocumentationLevel.SUMMARY:
displayErrorSummary(errorNumber);
// Fall through
case DocumentationLevel.NUMBER_ONLY:
displayErrorNumber(errorNumber);
break;
default:
throw new AssertionError("Invalid level.");
}
default
break
)for (int i = 0; i < a.length; i++) {
System.out.println(a[i]);
}
for (String s: a) {
System.out.println(s);
}
for (int i = 0; i < 5; i++) {
System.out.println("Hello!");
}
5.times { puts "Hello!" }
i
, j
, row
, col
,
x
, y
,...
for
cyklufor
ckylubreak
for
používat pro jakýkoliv cyklus se strukturou
"inicializace, podmínka, krok"
while
for
cyklu pracovat jen s řídící proměnnou
,
je tabu{
a }
i když je v těle jen jeden
příkaz
{
a }
při rozšiřování
těla
break
a continue
break
a continue
continue
ušetří if
break
ušetří:
continue
umístit na začátek těla/* Eat characters until end of the line. */
while (fgetc(f) != '\n') ;
/* Eat characters until end of the line. */
int ch;
do {
ch = fgetc(f);
} while (ch != '\n');
while(true) { ... }
než
for (;;) { ... }
"Každá instance objektu IdentifiedObject
při
vytvoření obdrží unikátní ID."
public IdentifiedObject() {
static int nextId = 0;
this.id = nextId++;
/* ... */
}
private int getUniqueId() {
static int nextId = 0;
return nextId++;
}
public IdentifiedObject() {
this.id = getUniqueId();
/* ... */
}
x = stack[topIndex];
topIndex--;
x = stackPop();
x = object_ptr->member;
object_ptr->member = y;
x = object_get_member(object_ptr);
object_set_member(object_ptr, y);
Každá informace v systému by v něm měla mít jednu autoritativní a jednoznačnou reprezentaci.
Znovupoužití kódu není pro vás nic nového – opakovaně používané věci se zabstrahují do funkcí a tříd.
Generátory kódu jsou o mnoho zajímavější – umožňují z autoritativní reprezentace znalosti vygenerovat konkrétní kód. Příkladem můžou být generátory parserů, kde je kód vygenerován z gramatiky, a existuje i mnohem triviálnější příklad: céčkový preprocesor, expandující makra.
Dynamické jazyky se často vyznačují tím, že umí generovat kód za běhu. To umožňuje odbourat jinak nutný krok generování a zrychlit tak vývojový cyklus. Příklad se nachází o několik slajdů níže.
O principu DRY si můžete přečíst více v článku Orthogonality and the DRY Principle.
Každá informace v systému by v něm měla mít jednu autoritativní a jednoznačnou reprezentaci.
S databázemi je kromě uvedení dat do 3. normální formy často problém synchronizace představy o schématu mezi databází a kódem aplikace. Jakoukoliv změnu je obvykle nutné provést na obou místech. Tento problém zajímavě řeší framework Ruby on Rails, který díky možnostem jazyka Ruby dynamicky generuje objekty reprezentující záznamy v databázových tabulkách, které jsou tak vždy v souladu s databází, která je v tomto případě autoritativním zdrojem informací o schématu.
Dokumentace v mnohém duplikuje kód. Částečně to lze řešit tím, že je přímo z kódu generována, např. pomocí systému JavaDoc. Není to ale řešení dokonalé – duplicitu neodstraňuje, pouze umisťuje duplicitní věci co nejvíc k sobě. Navíc zavádí do systému určitou byrokracii. O tom všem bude řeč na některém z dalších seminářů.
int[] a = { 1, 2, 3 };
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * 2;
}
for (int i = 0; i < a.length; i++) {
System.out.println(a[i]);
}
int[] a = { 1, 2, 3 };
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * 2;
}
for (int i = 0; i < a.length; i++) {
System.out.println(a[i]);
}
for
cykly s úplně stejnou hlavičkou... zjevná
duplicita kódu, která si říká o zabstrahování. Bohužel, Java tuto
možnost nenabízí, resp. nabízí, ale stálo by nás to tolik úsilí a kódu
navíc, že se to nedělá.
a = [1, 2, 3]
for i in 0...a.length do
a[i] = a[i] * 2
end
for i in 0...a.length do
puts a[i]
end
a = [1, 2, 3]
a = a.map { |item| item * 2 }
a.map { |item| puts item }
V jazycích s funkcionálními prvky, jako je třeba Ruby, existuje
obvykle abstrakce průchodu kolekcí – funkce map
.
Tato funkce si bere za parametr jinou funkci, která dostane za
parametr položku kolekce a vrátí, co má být do kolekce umístěno
místo ní. Postupným spuštěním této funkce na všechny prvky kolekce
je vytvořena nová kolekce a ta je pak funkcí map
vrácena. V našem případě je funkci map
předána funkce
anonymní. (Ostřílení Rubysté prominou zamlčení rozdílu mezi funkcí a
blokem a fakt, že správně by kód výše měl používat map!
a each
.)
Příklad je převzat a upraven z http://www.joelonsoftware.com/items/2006/08/01.html.
Pokud vás téma funkcionálního programování zaujalo, doporučuji článek Functional Programming For The Rest of Us.
class Expression { public abstract String getStringForm(); }
class BinaryOperator extends Expression {
private Expression left;
private Expression right;
/* ... */
}
class PlusOperator extends BinaryOperator {
public String getStringForm() {
return left.getStringForm() + " + " + right.getStringForm();
}
}
class MinusOperator extends BinaryOperator {
public String getStringForm() {
return left.getStringForm() + " - " + right.getStringForm();
}
}
class Expression { public abstract String getStringForm(); }
class BinaryOperator extends Expression {
private Expression left;
private Expression right;
/* ... */
}
class PlusOperator extends BinaryOperator {
public String getStringForm() {
return left.getStringForm() + " + " + right.getStringForm();
}
}
class MinusOperator extends BinaryOperator {
public String getStringForm() {
return left.getStringForm() + " - " + right.getStringForm();
}
}
Další příklad duplicity kódu, tentokrát jde o dvě třídy, které se krom svého názvu liší jedním jediným znakem.
Tento příklad je zjednodušenou verzí problému, který jsem řešil ve své diplomové práci.
Jako bonus můžete zkusit přijít na to, proč by u složitějších výrazů
metoda getStringForm
moc dobře nefungovala.
class Expression { public abstract String getStringForm(); }
class BinaryOperator extends Expression {
private Expression left;
private Expression right;
private String operator;
public String getStringForm() {
return left.getStringForm() + " " + operator + " "
+ right.getStringForm();
}
/* ... */
}
class PlusOperator extends BinaryOperator {
public PlusOperator() { operator = "+"; }
}
class MinusOperator extends BinaryOperator {
public MinusOperator() { operator = "-"; }
}
operator
do třídy
BinaryOperator
a přesunu metody
getStringForm
tamtéž podařilo omezit. Stále ale
potřebujeme tři řádky kódu na každou třídu, která se liší jen jedním
znakem.
class Expression
end
class BinaryOperator < Expression {
attr_reader :left, :right, :operator
def get_string_form()
[ @left.get_string_form,
@operator,
@right.get_string_form ].join(" ")
end
# ...
end
class PlusOperator < BinaryOperator
def initialize(); @operator = "+"; end
end
class MinusOperator < BinaryOperator
def initialize(); @operator = "-"; end
end
class Expression
end
class BinaryOperator < Expression {
attr_reader :left, :right, :operator
def get_string_form()
[ @left.get_string_form,
@operator,
@right.get_string_form ].join(" ")
end
# ...
end
[ [:PlusOperator, "+"], [:MinusOperator, "-"] ].each do |def|
module_eval %Q{
class #{def[0].to_s} < BinaryOperator
def initialize(); @operator = "#{def[1]}"; end
end
}
end
Nyní jsme kód upravili tak, že všechny věci, kterými se třídy
PlusOperator
a MinusOperator
odlišovaly,
(tedy název a textová podoba operátoru) jsme zaznamenali do pole,
které procházíme. V každém kroku hodnoty z příslušné položky
dosadíme do "šablony" kódu, který následně vyhodnotíme. Kód
definující třídu je tak přítomen jen jednou.
Na počtu řádků jsme v tomto příkladu zjevně neušetřili, ale v předobraze příkladu v mé diplomové práci bylo tříd celkem 41. To už znát je, nemluvě o eliminaci strukturální duplicity kódu.
Název | Příklad | Délka | Nejsložitější operace |
---|---|---|---|
Čárky | IIIII IIIII IIII | O(n) | Sčítání |
Římské číslice | XIV | O(n) | Sčítání |
Arabské číslice | 14 | O(logn) | Násobení |
Vědecká notace | 0.14×102 | O(log logn) | Umocňování |
Název | Příklad | Délka | Nejsložitější operace |
---|---|---|---|
Čárky | IIIII IIIII IIII | O(n) | Sčítání |
Římské číslice | XIV | O(n) | Sčítání |
Arabské číslice | 14 | O(logn) | Násobení |
Vědecká notace | 0.14×102 | O(log logn) | Umocňování |
Název | Příklad | Délka | Nejsložitější operace |
---|---|---|---|
Čárky | IIIII IIIII IIII | O(n) | Sčítání |
Římské číslice | XIV | O(n) | Sčítání |
Arabské číslice | 14 | O(logn) | Násobení |
Vědecká notace | 0.14×102 | O(log logn) | Umocňování |
Název | Příklad | Délka | Nejsložitější operace |
---|---|---|---|
Čárky | IIIII IIIII IIII | O(n) | Sčítání |
Římské číslice | XIV | O(n) | Sčítání |
Arabské číslice | 14 | O(logn) | Násobení |
Vědecká notace | 0.14×102 | O(log logn) | Umocňování |
Název | Příklad | Délka | Nejsložitější operace |
---|---|---|---|
Čárky | IIIII IIIII IIII | O(n) | Sčítání |
Římské číslice | XIV | O(n) | Sčítání |
Arabské číslice | 14 | O(logn) | Násobení |
Vědecká notace | 0.14×102 | O(log logn) | Umocňování |
Zápis je stále kratší a kratší, ale potřebujeme rozumět stále
abstraktnějším operacím. Podobně to může být i u programů – za
zkrácení a eliminování duplicity často platíme koncepčním
zesložitěním. Třeba v příkladu s for
cykly jsme museli
porozumět konceptu předávání funkce jako parametru, který jsme
předtím nepotřebovali, v dalším příkladu jsme podobně potřebovali
pochopit dynamické generování kódu.
Při programování zejména v silnějších jazycích je často třeba zvážit, jak daleko v abstrakci jít a zda nakonec abstrakce nebude spíš na škodu. Obecně se vyplatí spíše přiklánět k DRY, ale výjimky se najdou.
Pro přesnost: O(log logn) platí samozřejmě jen v případě, že se vzdáme 100% přesnosti v mantise čísla.
computeMarketingExpense(marketingData)
computeSalesExpense(salesData)
computeTravelExpense(travelData)
computePersonnelExpense(personnelData)
displayExpenseSummary(marketingData, salesData, travelData,
personnelData)
expenseData = initializeExpenseData(expenseData)
expenseData = computeMarketingExpense(expenseData)
expenseData = computeSalesExpense(expenseData)
expenseData = computeTravelExpense(expenseData)
expenseData = computePersonnelExpense(expenseData)
displayExpenseSummary(expenseData)
/* Following calls must be in correct order. Inside the call of
* the taskReachedState method, the Task Manager may decide to
* close context that contains this task and call
* HostRuntimeInterface.closeContext method. But if the calls
* are not ordered properly, the Host Runtime will not yet be
* notified about the task's end (the notification happens in
* notifyTaskFinished call) and refuse to close the context
* (throwing IllegalArgumentException). This would be a race
* condition.
*/
hostRuntime.notifyTaskFinished(TaskImplementation.this);
hostRuntime.getHostRuntimesPort().taskReachedState(
taskDescriptor.getTaskTid(),
taskDescriptor.getContextId(),
processKilledFromOutside
? TaskState.ABORTED
: TaskState.FINISHED
);
notifyTaskFinished
a taskReachedState
, ale
to, jak podrobně je chování v komentáři popsáno.
getID
, computeEquationResults
,
drawWindowBorder
Enumerable.hasMoreElements × Iterator.hasNext
Enumerable.nextElement × Iterator.next
User.getUserName
× User.getName
|
|
Modifikovatelné a výstupní parametry lze těžko odlišit od vstupních v místě volání (a někdy i v deklaraci funkce).
a, b = divmod(7, 3)
print a # => 2
print b # => 1
const
, final
fprintf(stream, format,...)
fputs(str, stream)
strncpy(dst, src, len)
memcpy(dst, src, len)
true
nebo
false
znamená.
setEnabled(boolean enabled)
Klíčová slova const
resp. final
často
znepřehledňují kód, a tak je málokdo používá. Při návrhu nových
jazyků by možná stálo za úvahu dát parametrům sémantiku konstant
automaticky, bez potřeby klíčového slova.
Ke krácení seznamu parametrů: Při zkracování seznamu parametrů funkce náhradou několika atributů jednoho objektu celým objektem je dobré se zamyslet, zda je fakt, že funkci chcete posílat celý objekt náhoda nebo systematická záležitost. Náhoda se pozná tak, že při volání funkce nemáte vždy dotyčný objekt "v ruce" – v tom případě je lepší nechat seznam parametrů být, protože budete muset u některých volání funkce objekt uměle vytvářet. Naopak, pokud zjistíte, že to náhoda není, stojí za to se chvilku zastavit nad strukturou kódu – můžete např. přijít na to, že daná funkce by měla být metoda objektu, který jí posíláte.
K booleovským parametrům: Náhrada výčtovým typem/konstantou má i výhodu flexibility v případě, že časem přibudou další možné hodnoty parametru. To se u boolovských parametrů docela často stává, více viz výstižně nazvaný článek Booleans suck.
K interfacům a abstraktním typům: Viz Effective Java: Programming Language Guide, Item 34.
return
yresult
null
HttpServletRequest.getCookies()
null
: Viz Effective Java:
Programming Language Guide, Item 34.
null
?unsigned
)
public void log(String context, String taskID, Date timestamp,
LogLevel level, String message)
throws IllegalArgumentException, NullPointerException {
if (context == null) {
throw new NullPointerException("Context name is null");
}
if (context.equals("")) {
throw new IllegalArgumentException("Context name is empty");
}
if (taskID == null) {
throw new NullPointerException("Task ID is null");
}
if (taskID.equals("")) {
throw new IllegalArgumentException("Task ID is empty");
}
if (timestamp == null) {
throw new NullPointerException("Timestamp is null");
}
if (level == null) {
throw new NullPointerException("Log level is null");
}
if (message == null) {
throw new NullPointerException("Log message is null");
}
File contextDir = new File(basedir, context);
if (!contextDir.isDirectory()) {
throw new IllegalArgumentException(
"Context not registered: " + context
);
}
File taskDir = new File(contextDir, taskID);
if (!taskDir.isDirectory()) {
throw new IllegalArgumentException(
"Task not registered: " + taskID
);
}
/* ... */
}
Příklad je mírně upravený kus kódu z našeho softwarového projektu. Autor kódu (nikoliv já) si krátce před jeho napsáním přečetl Effective Java a byl odhodlaný psát kód opravdu poctivě a neprůstřelně.
Kód na kontrolu parametrů byl nakonec delší než samotné tělo metody (to je v příkladu vynecháno). Je na místě otázka, zda, případně kdy, se opravdu vyplatí parametry poctivě kontrolovat.
atoi
)Problematika výjimky × chybové kódy je složitější – my jsme se jí tu jen zlehka dotkli. Zájemcům o hlubší vhled doporučuji k přečtení tyto články (v uvedeném pořadí):
Exception
)
Throwable
, ale ne Exception
;
typicky potomek RuntimeException
)
catch
e
K náhradě kontrolovaných výjimek za nekontrolované: Dobrý příklad
je metoda parsující text v nějakém jazyce. Text může v principu
obsahovat chyby, a je žádoucí, aby metoda v tom případě vyhazovala
výjimku. Pokud by tato výjimka byla kontrolovaná, musel by
programátor výjimku odchytávat i v případě, že by si byl jist, že
metodě předává syntakticky korektní text (např. automaticky
generovaný). Lepší řešení je použít nekontrolovanou výjimku a přidat
metodu, která bezchybnost textu otestuje a vrátí true
nebo false
.
Uvedený postup ale nelze použít vždy. Např. pokud bychom chtěli před smazáním souboru zkontrolovat, zda tento soubor existuje, může nám ho mezi testem a smazáním někdo "smazat pod rukama". Je tedy potřeba mít zaručen exkluzivní přístup k datům.
Ke kontrolovaným a nekontrolovaným výjimkám obecně: Viz Effective Java: Programming Language Guide, Item 40 + 41.
IllegalArgumentException
IllegalStateException
NullPointerException
IndexOutOfBoundsException
ConcurrentModificationException
UnsupportedOperationException
K výjimečným situacím: Viz Effective Java: Programming Language Guide, Item 39.
K standardním výjimkám: Viz Effective Java: Programming Language Guide, Item 42.
catch
K úrovni abstrakce: Viz Effective Java: Programming Language Guide, Item 43.
K detailnímu popisu problému: Viz Effective Java: Programming Language Guide, Item 45.
K prázdným catch
ům: Viz Effective Java: Programming
Language Guide, Item 47.
/[-]\d+/
, nula vždy bez znaménka), chceme získat pole
bitů reprezentující číslo v dvojkovém doplňku
0
nebo 1
reprezentující jeden bit
Úloha je lehkým zjednodušením úlohy řešené v rámci kódu mé diplomové práce. Konkrétně jsem potřeboval implementovat bitové operace na "velkých číslech".
Pokud se vám zdá ukládání bitů po jednom jako neefektivní, máte pravdu. Ale je to nejjednodušší řešení a optimalizovat se ho vyplatí až v případě, že nás opravdu někde začne "tlačit bota". Což v případě diplomové práce taky nemusí nastat vůbec :-)
Pokud přemýšlíte, proč není číslo uloženo jako dvojkový doplněk
rovnou, odpověď je inženýrská: PHP obsahuje knihovnu
bcmath, která používá řetězcový
zápis čísel a implementuje základní aritmetické operace
(+
, -
, *
, /
,
%
,...). Je méně pracné implementovat převod mezi
formáty a (triviálně) bitové operace, než (relativně obtížně)
implementovat všechny operace.
Pokud si z prvního ročníku nepamatujete, co je to dvojkový doplněk, pomůže wikipedie.
Hlavička funkce, algoritmus zapíšeme jako komentáře
function bignum_to_bits($bignum) {
/* Remember if the value is negative. */
/* If the value is negative, trim the sign (i.e. compute the
absolute value). */
/* Compute the bits of the absolute value (using standard
algorithm). */
/* If the first bit is non-zero, prefix the number with
zero bit. */
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative. */
$negative = ($bignum[0] == "-");
/* If the value is negative, trim the sign (i.e. compute the
absolute value). */
/* Compute the bits of the absolute value (using standard
algorithm). */
/* If the first bit is non-zero, prefix the number with
zero bit. */
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative. */
$negative = ($bignum[0] == "-");
/* If the value is negative, trim the sign (i.e. compute the
absolute value). */
if ($negative) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
/* If the first bit is non-zero, prefix the number with
zero bit. */
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Sloučení prvních dvou kroků
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
/* If the first bit is non-zero, prefix the number with
zero bit. */
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = array();
do {
array_unshift($bits, (int)bcmod($bignum, 2));
$bignum = bcdiv($bignum, 2);
} while ($bignum != "0");
/* If the first bit is non-zero, prefix the number with
zero bit. */
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = array();
do {
array_unshift($bits, (int)bcmod($bignum, 2));
$bignum = bcdiv($bignum, 2);
} while ($bignum != "0");
/* If the first bit is non-zero, prefix the number with
zero bit. */
if ($bits[0] != 0) {
array_unshift($bits, 0);
}
/* If the number was negative, invert the bits and add one. */
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = array();
do {
array_unshift($bits, (int)bcmod($bignum, 2));
$bignum = bcdiv($bignum, 2);
} while ($bignum != "0");
/* If the first bit is non-zero, prefix the number with
zero bit. */
if ($bits[0] != 0) {
array_unshift($bits, 0);
}
/* If the number was negative, invert the bits and add one. */
if ($negative) {
for ($i = 0; $i < count($bits); $i++) {
$bits[$i] = 1 - $bits[$i];
}
for ($i = count($bits) - 1; $i >= 0; $i--) {
if ($bits[$i] == 0) {
$bits[$i] = 1;
break;
} else {
$bits[$i] = 0;
}
}
}
/* Return the bits. */
}
Vyplňování kódu
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = array();
do {
array_unshift($bits, (int)bcmod($bignum, 2));
$bignum = bcdiv($bignum, 2);
} while ($bignum != "0");
/* If the first bit is non-zero, prefix the number with
zero bit. */
if ($bits[0] != 0) {
array_unshift($bits, 0);
}
/* If the number was negative, invert the bits and add one. */
if ($negative) {
for ($i = 0; $i < count($bits); $i++) {
$bits[$i] = 1 - $bits[$i];
}
for ($i = count($bits) - 1; $i >= 0; $i--) {
if ($bits[$i] == 0) {
$bits[$i] = 1;
break;
} else {
$bits[$i] = 0;
}
}
}
/* Return the bits. */
return $bits;
}
Refaktorizace
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = array();
do {
array_unshift($bits, (int)bcmod($bignum, 2));
$bignum = bcdiv($bignum, 2);
} while ($bignum != "0");
/* If the first bit is non-zero, prefix the number with
zero bit. */
if ($bits[0] != 0) {
array_unshift($bits, 0);
}
/* If the number was negative, invert the bits and add one. */
if ($negative) {
$bits = bits_add_one(bits_invert($bits));
}
/* Return the bits. */
return $bits;
}
Refaktorizace
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = positive_bignum_to_bits($bignum);
/* If the first bit is non-zero, prefix the number with
zero bit. */
if ($bits[0] != 0) {
array_unshift($bits, 0);
}
/* If the number was negative, invert the bits and add one. */
if ($negative) {
$bits = bits_add_one(bits_invert($bits));
}
/* Return the bits. */
return $bits;
}
Refaktorizace
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = ($bignum[0] == "-")) {
$bignum = substr($bignum, 1);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = positive_bignum_to_bits($bignum);
/* If the first bit is non-zero, prefix the number with
zero bit. */
$bits = bits_prefix_with_zero_if_needed($bits);
/* If the number was negative, invert the bits and add one. */
if ($negative) {
$bits = bits_add_one(bits_invert($bits));
}
/* Return the bits. */
return $bits;
}
Refaktorizace
function bignum_to_bits($bignum) {
/* Remember if the value is negative, trim the sign if so. */
if ($negative = bignum_is_negative($bignum)) {
$bignum = bignum_abs($bignum);
}
/* Compute the bits of the absolute value (using standard
algorithm). */
$bits = positive_bignum_to_bits($bignum);
/* If the first bit is non-zero, prefix the number with
zero bit. */
$bits = bits_prefix_with_zero_if_needed($bits);
/* If the number was negative, invert the bits and add one. */
if ($negative) {
$bits = bits_add_one(bits_invert($bits));
}
/* Return the bits. */
return $bits;
}
Odstranění víceméně zbytečných komentářů
function bignum_to_bits($bignum) {
if ($negative = bignum_is_negative($bignum)) {
$bignum = bignum_abs($bignum);
}
$bits = positive_bignum_to_bits($bignum);
$bits = bits_prefix_with_zero_if_needed($bits);
if ($negative) {
$bits = bits_add_one(bits_invert($bits));
}
return $bits;
}
struct
+ funkce, co s ním pracují"Céčko" je na slajdu v uvozovkách, protože céčková rodina není první jazyk, který s tímto pohledem přišel, ale asi vám bude nejznámější.
Céčkový pohled je technický, "od zdola". Dívá se na objekty
především jako na způsob strukturování kódu. Z tohoto pohledu je
objekt jen vylepšený struct
, který má kromě datových
položek i funkce, které s ním pracují. Do syntaxe jazyka je pak
přidána možnost tyto funkce deklarovat přímo v objektu, možnost je
na objektu volat pomocí operátorů .
a ->
a také implicitní předávání parametru this
.
Pohled SmallTalku je úplně odlišný. SmallTalk považuje objekty za krabičky, které umí s okolím komunikovat pomocí zpráv. O vnitřním uspořádání krabiček není okolí nic známo. Každá zpráva má svůj název, parametry (objekty) a případně návratovou hodnotu (objekt). Může se stát, že objekt zprávu nebude znát – to se dá obecně zjišťovat jak v compile-time, tak v run-time. V případě zjišťování v run-time může objekt volání neznámé zprávy ošetřovat, což dává programátorovi poměrně velkou flexibilitu. Skvěle se tak implementují různé proxy, delegování, apod.
K SmallTalkovskému pojetí objektů má z dnešního pohledu velmi blízko jazyk Ruby, některé vlastnosti lze najít i v Javě a C#. Ostatní běžné jazyky se drží spíše céčkového modelu.
HttpRequest
, Player
,
Shape
HttpRequest
nepředstavuje nic opravdu hmatatelného, je to
(obvykle) jen sekvence bajtů zaslaná po síti.
ErrorMessages
představuje seznam
chybových hlášení, reprezentovaných třídou
Message
."
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).
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 je problém například vyřešen v Borland Delphi.
Jedná se o případ "prosakující" abstrakce – více viz článek The Law of Leaky Abstractions.
K tranzitivním public metodám: Jde o to, že pokud nějaká metoda třídy používá jen samé public metody, není to samo o sobě důvod, aby byla také public. Analogicky s protected.
class MyClass {
private int attribute;
public int getAttribute() {
return attribute;
}
public void setAttribute(int attribute) {
this.attribute = attribute;
}
}
myObject.attribute
myObject.getAttribute()
myObject.attribute = value
myObject.setAttribute(value)
U malých projektů může být opravdu více práce psát gettery/settery, než napravovat porušení zapouzdření. Jakmile je ale projekt větší a pracuje na něm více programátorů, výhody jejich psaní převáží. Psaní getterů/setterů je vlastně speciální případ optimalizace na čtenáře.
Button1.Caption =
"Hello";
class MyClass {
private int attribute;
public int Attribute {
get { return attribute; }
set { attribute = value; }
}
}
type
TMyClass = class
private
FAttribute: Integer;
published
property Attribute: Integer
read FAttribute
write FAttribute;
end;
irb
V ukázce: Vytvořit třídu s konstruktorem, ve kterém se nastaví dva
atributy. Ukázat, že zvenku nejsou vidět. Pak na jeden použít
attr_reader
a ukázat, že už jde načíst. Totéž s
attr_writer
a následně použít
attr_accessor
na druhý atribut.
Proč správné řešení? Protože programátor nemůže atributy vůbec zveřejnit a v typickém případě není nutné psát vůbec žádný kód getterů/setterů.
Třída je immutable právě tehdy, pokud to 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
za sealed
. V C++
jde podobného efektu dosáhnout pomocí const
.
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ěť.
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.
/* Immutable DateInterval postavený z mutable tříd Date.
* Je nutné používat defenzivní kopie.
*/
class DateInterval {
private Date begin;
private Date end;
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();
}
}
/* Immutable DateInterval postavený z immutable tříd Date.
* Není nutné používat defenzivní kopie.
*/
class DateInterval {
private Date begin;
private Date end;
public Date getBegin() { return begin; }
public Date getEnd() { return end; }
public DateInterval(Date begin, Date end) {
this.begin = begin;
this.begin = end;
}
}
String
&
StringBuffer
/**
* 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 {
/* ... */
}
"Car has an engine and four wheels."
class Car {
private Engine engine;
private Wheel[] wheels;
}
final
(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í – ještě jsem například neviděl někoho si z tohoto důvodu stěžovat, že v Javě jsou všechny metody virtuální.
Naopak jsem ale viděl stěžování 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.
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 obsahauje mechanismus, který zajišťuje, že se tyto pomocné statické metody dají volat, jako by to byly metody původní třídy. Popravdě, nevím zda je tato možnost ošklivý hack nebo elegantní řešení obtížného problému :-)
public abstract class HttpServlet {
protected void doGet(
HttpServletRequest req,
HttpServletResponse resp
);
protected void doPost(
HttpServletRequest req,
HttpServletResponse resp
);
/* ... */
}
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ů.
Stream.flush
a
MemoryStream.flush
K třídám s jen jednou podtřídou: Podobně platí i pro rozhraní s jen jednou implementací.
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.
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jaký má být mezi nimi
vztah?
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jaký má být mezi nimi
vztah?
Complex
dědí od Real
Chceme definovat třídy Real
a Complex
,
představující reálná a komplexní čísla – jaký má být mezi nimi
vztah?
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ý má být mezi nimi
vztah?
Complex
dědí od Real
Real
dědí od Complex
Real
bude jednoduše mít nulovou komplexní složku
Co je správně?
Správně není ani jedno!
add
a addAll
Properties
& Hashtable
nebo Stack
& Vector
v Javě
switch
Implementujte AST pro aritmetické výrazy s binárními operátory "+" a "-" a jeho vyhodnocení.
switch
enum ExpressionType { PLUS, MINUS, VALUE }
class Expression {
private ExpressionType type;
private Expression left, right;
private int value;
/* ... */
}
class Evaluator {
public int eval(Expression expr) {
switch (expr.getType()) {
case ExpressionType.PLUS:
return eval(expr.getLeft()) + eval(expr.getRight());
case ExpressionType.MINUS:
return eval(expr.getLeft()) - eval(expr.getRight());
case ExpressionType.VALUE:
return expr.getValue();
}
}
}
switch
abstract class Expression {
public abstract int eval();
};
abstract class BinaryOperator extends Expression {
protected Expression left, right;
/* ... */
}
class PlusOperator extends BinaryOperator {
public int eval() { return left.eval() + right.eval(); }
}
class MinusOperator extends BinaryOperator {
public int eval() { return left.eval() - right.eval(); }
}
switch
class Value extends Expression {
private int value;
/* ... */
public int eval() { return value; }
}
switch
switch
(a někdy i if
) se
dívejte s podezřením a přemýšlejte, zda není lepší ho nahradit
polymorfismem
API = Application Programming Interface
Myslete na uživatele.
Pohled implementátora je druhořadý
Dělejte jen jednu věc, a dělejte ji dobře.
"Keep It Simple, Stupid." – anonym
"When in doubt, leave it out." – Joshua Bloch
"Everything should be made as simple as possible, but no simpler." – Albert Einstein
"Konstrukční dokonalosti není dosaženo tehdy, když už není co přidat, ale tehdy, když už nemůžete nic odebrat." – Antoine de Saint-Exupéry
V blogovacím systému může být vhodné udělat zvlášť API pro autory obsahu a zvlášť pro čtenáře. Tyto dvě skupiny mají totiž úplně odlišné požadavky: Autor chce obsah číst i vytvářet, a pravděpodobně ho zajímá jen jeho blog, zatímco čtenář může obsah jen číst, ale zase ho pravděpodobně bude zajímat obsah více blogů, bude požadovat agregaci, apod.
"A user interface is well-designed when the program behaves exactly how the user thought it would." – Joel Spolsky
getFile
vs. getFileName
Thread.interrupted()
Citát je převzat z Joelovy série článků o návrhu uživatelského rozhraní: 1, 2, 3, 4, 5, 6, 7, 8, 9.
Viz také API Design: The Principle of Least Surprise.
K Thread.interrupted()
: Tato metoda řekne, zda bylo
vlákno přerušeno, ale zároveň příznak přerušení odnastaví. Další
volání této metody budou tedy vždy vracet false
. Přitom
z názvu metody toto chování vůbec není zřejmé.
Co vypíše následující kus kódu?
public static void main(String[] args) {
Set<String> col = new HashSet<String>();
col.add("Dagi");
col.add("NkD");
for (String key : col) {
col.remove(key);
System.out.println(key);
}
}
Správná odpověď je "NkD" a pak je bude vyhozena výjimka
ConcurrentModificationException
. Iterátory kolekcí v
Javě jsou totiž fail-fast, tj. hlídají si, aby jim nikdo
pod rukama nepřepsal kolekci, nad kterou zrovna iterují. Pokud se
tak stane, upozorní na to výjimkou, což je lepší chování, než
nedeterministický pád někdy v budoucnu.
Hádanka je převzata od Romana "Dagiho" Pichlíka.
import org.w3c.dom.*;
import java.io.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
// DOM code to write an XML document to a specified output stream.
private static final void writeDoc(Document doc, OutputStream out)
throws IOException {
try {
Transformer t = TransformerFactory.newInstance()
.newTransformer();
t.setOutputProperty(
OutputKeys.DOCTYPE_SYSTEM,
doc.getDoctype().getSystemId()
);
t.transform(new DOMSource(doc), new StreamResult(out));
} catch (TransformerException e) {
throw new AssertionError(e); // Can’t happen!
}
}
Příklad ukazuje na naprosto odbyté sbírání požadavků na API, protože serializace XML dokumentu je jedna z nejčastějších operací s XML/DOM API vůbec a v Javě je zcela zbytečně komplikovaná. Vede to ke kopírování stále stejných kusů kódu po celém programu a tedy zanášení duplicit a chyb.
Příklad je převzat z Přednášky Joshuy Blocha.
public class Vector {
public int indexOf(Object elem, int index);
public int lastIndexOf(Object elem, int index);
/* ... */
}
public interface List {
List subList(int fromIndex, int toIndex);
/* ... */
}
Původní vyhledávací metody třídy Vector
jsou
jednoúčelové a obtížně použitelné bez dokumentace (co znamená
parametr index
?). Oproti tomu metoda
subList
v novějším interfacu List
umožňuje
vykonávat na části seznamu libovolnou operaci, nejen
vyhledávání. Je tedy silnější, má větší concept-to-weight ratio.
Příklad je převzat z Přednášky Joshuy Blocha.
Co by měl podle vás vypsat následující kód?
"abcd".substring(1, 3)
Co by měl podle vás vypsat následující kód?
"abcd".substring(1, 3)
Co by měl podle vás vypsat následující kód?
"abcd".substring(1, 3)
0
zamezuje častému přičítání jedničky
length = endPos - startPos
Range
, který může reprezentovat intervaly shora otevřené
i uzavřené.
Pokud nutíte uživatele hádat, je to špatně. Pokud je nutíte koukat se do zdrojového kódu, je to ještě hůř, protože je tím porušeno zapouzdření. Uživatelé nebudou programovat proti rozhraní, ale proti jeho jedné konkrétní implementaci. V tu chvíli ztrácíte flexibilitu ji někdy v budoucnu změnit.
Z důvodu větší flexibility implementace se vyplatí v dokumentaci příliš nepopisovat vnitřnosti – detailnost by měla být jen na takové úrovni, aby uživatel mohl s rozhraním pracovat.
int amountWithTax(int amount) {
if (n <= 1000) {
return amount;
} else if (n <= 20000) {
return Math.round(amount * 1.10);
} else if (n <= 500000) {
return Math.round(amount * 1.15);
} else {
return Math.round(amount * 1.25);
}
}
String osName = System.getProperty("os.name");
if (osName.equals("SunOS") || osName.equals("Linux")) {
System.out.println("This is a UNIX box and therefore good.");
} else if (osName.equals("Windows NT")
|| osName.equals("Windows 95")) {
System.out.println(
"This is a Windows box and therefore bad."
);
} else {
System.out.println("This is not a box.") ;
}
Příklad je převzat a upraven z článku Understanding Object Oriented Programming, kde ovšem slouží k vysvětlování něčeho úplně jiného, než na něm ukazuji já.
Z příkladu si prosím nevyvozujte, co si myslím o různých operačních systémech :-)
function getRelativeTimeString(delta) {
if (delta < 60) {
return "před méně než minutou";
} else if (delta < 120) {
return "cca před minutou";
} else if (delta < (45 * 60)) {
return "před " + (parseInt(delta / 60)).toString()
+ " minutami";
} else if (delta < (90 * 60)) {
return "cca před hodinou";
} else if (delta < (24 * 60 * 60)) {
return "před " + (parseInt(delta / 3600)).toString()
+ " hodinami";
} else if (delta < (48 * 60 * 60)) {
return "včera";
} else {
return "před " (parseInt(delta / 86400)).toString()
+ " dny";
}
}
Příklad je převzat a upraven ze skriptu na stránce Martina Hassmana.
class TaxStep {
int amount;
float coef;
/* ... gettery, konstruktor ... */
}
TaxStep[] taxSteps = {
new TaxStep(1000, 1.00),
new TaxStep(20000, 1.10),
/* ... */
}
float defaultTaxCoef = 1.25;
int amountWithTax(int amount) {
for (TaxStep step: taxSteps) {
if (amount <= step.getAmount()) {
return Math.round(amount * step.getCoef());
}
}
return Math.round(amount * defaultTaxCoef);
}
Všimněte si, jak Java tím, že nemá heterogenní pole, komplikuje
vložení dat do programu – museli jsme si na to vytvořit
vlastní třídu TaxStep
. Alternativou by bylo
data vložit do dvou samostatných polí.
V kódu metody amountWithTax
je malá duplicita kódu
(přičítání daně) – to je záměr, po odfaktorování do samostatné
metody by se mi kód už nevešel na slajd.
Je zřejmé, že položky pole taxSteps
musí být
seřazeny podle atributu amount
. V reálném programu je
na dobré na podobné věci explicitně upozornit komentářem na začátku
deklarace.
Map<String, String> osMessages = new HashMap<String, String>;
osMessages.put(
"SunOS",
"This is a UNIX box and therefore good."
);
osMessages.put(
"Linux",
"This is a UNIX box and therefore good."
);
osMessages.put(
"Windows NT",
"This is a Windows box and therefore bad."
);
osMessages.put(
"Windows 95",
"This is a Windows box and therefore bad."
);
String defaultMessage = "This is not a box.";
String message = osMessages.get(System.getProperty("os.name"));
System.out.println(message != null ? message : defaultMessage);
os_messages = {
"SunOS" => "This is a UNIX box and therefore good.",
"Linux" => "This is a UNIX box and therefore good.",
"Windows NT" => "This is a Windows box and therefore bad.",
"Windows 95" => "This is a Windows box and therefore bad.",
}
os_messages.default = "This is not a box."
puts os_messages[PLATFORM];
PLATFORM
v Ruby nabývá jiných hodnot, než property
os.name
v Javě, ale chtěl jsem pro názornost zachovat
strukturu kódu.
var timedMessages = [
[60, function(delta) { return "před méně než minutou"; }],
[120, function(delta) { return "cca před minutou"; }],
[45 * 60, function(delta) {
return "před " + parseInt(delta / 60)).toString()
+ "minutami";
}],
/* ... */
];
var defaultMessageFunction = function(delta) { /* ... */ };
function getRelativeTimeString() {
for (key in timedMessages) {
if (delta < timedMessages[key][0]) {
return timedMessages[key][1](delta);
}
}
return defaultMessageFunction(delta);
}
for
-in
není zrovna košer. Nechtělo se mi příklad ještě víc motat indexy.
Většinu problémů lze nějak zapsat pomocí konvenčních datových struktur (ostatně i kód je jen strom syntaktických elementů), ale takový popis většinou není dostatečně deskriptivní.
Více o DSL (a i jiných souvisejících věcech) se dočtete v článku Martina Fowlera Language Workbenches: The Killer-App for Domain Specific Languages?. Pěkné příklady použití uvádí Dave Thomas.
K regulárním výrazům: Ne vždy je zápis pomocí regulárního výrazu
nejpřirozenější zápis pro vyhledávací operaci v řetězce –
test, zda je přípona souboru .exe vyjádříme asi lépe pomocí
volání filename.endsWith(".exe")
, než použitím
regulárního výrazu /\.exe$/
.
Pro pořádek: Ano, vím že regulární výrazy používané v běžných programovacích jazycích už dnes nejsou ekvivalentní klasickému stavovému automatu, ale jsou silnější.
Externí DSL jsou někdy "embeddovány" do jazyků obecných – např. v případě regulárních výrazů nebo SQL.
From: http://www.czilla.cz/press_mozcz.html
To: http://www.czilla.cz/czilla/tiskove-zpravy/stav-mozilla-cz.html
From: /http:\/\/www\.czilla\.cz\/clanky\/tz-(.*)\.html/
To: http://www.czilla.cz/czilla/tiskove-zpravy/$1.html
# z bloguje.cz odkazuji na nas stary javascript
Ignore: http://www.czilla.cz/externi.js
# spamroboti
Ignore: /\/cgi-bin\/mailform.pl$/
Ignore: /\/cgi-bin\/formmail.pl$/
Ignore: /\/cgi-bin\/formmail.cgi$/
Ignore: /\/cgi-bin\/contact.cgi$/
Ignore: /\/forum\/.*$/
# Return articles for a year, year/month, or year/month/day
map.connect "blog/:year/:month/:day",
:controller => "blog",
:action => "show_date",
:requirements => { :year => /(19|20)\d\d/,
:month => /[01]?\d/,
:day => /[0-3]?\d/ },
:day => nil, # optional
:month => nil # optional
class User < ActiveRecord::Base
validates_inclusion_of :gender,
:in => ["male", "female"],
:message => "should be 'male'" +
" or 'female'"
validates_format_of :length, :with => /^\d+(in|cm)/
validates_length_of :name, :maximum => 50
validates_length_of :password, :in => 6..20
validates_length_of :address, :minimum => 10,
:message => "seems too short"
end
Lisp je vhodný především proto, že v podstatě nemá syntaxi, vše je jen volání funkcí, takže DSL v něm vypadá docela přirozeně jako součást jazyka. Důležitý je i silný systém maker.
Ruby má naopak syntaxi poměrně bohatou (dá se pomocí ní přirozeně vyjádřit mnoho konceptů) a na rozdíl od jazyků založených na C tato syntaxe "nepřekáží". Důležité syntaktické prvky jsou:
Externí | Interní | |
---|---|---|
Oddělení od kódu | ano | ne |
Syntaxe | vlastní | daná |
Zpracování | nutno napsat | v rámci jazyka |
Podpora IDE | neexistuje | dle jazyka |
Editace neprogramátory | ano | ne |
K syntaxi: Vlastní syntaxe znamená, že si ji můžete vytvořit dle libosti – tak, aby se vám dobře četla, upravovala, parsovala, automaticky zpracovávala, atd. Nevýhodou je, že tuto syntaxi musíte jako vývojář znát a používat. Pokud je v rámci jedné aplikace používáno více DSL s výrazně odlišnou syntaxí, je zaděláno na problém.
Uživatelskou dokumentací se na semináři nebudeme zabývat. Jednak to do tohoto předmětu asi nepatří a jednak její tvorbě nerozumím :-)
Uvedené rozdělení je dost neostré – co třeba když jsou vaši uživatelé programátoři?
/* Compute the square root of Num using the Newton-Raphson
approximation. */
r = num / 2;
while (abs(r - (num / r)) > TOLERANCE) {
r = 0.5 * (r + (num / r));
}
System.out.println("r = " + r);
i++; // increment i
null
?unsigned
)
/****************************************
* Tohle je sice hezky výrazný *
* komentář, ale udržovat ho bude malá *
* noční můra. *
****************************************/
if (something) {
/*
* velmi
*
* dlouhý
*
* blok
*/
} // if
public interface List<E> extends Collection<E> {
/* ... */
/**
* Returns the element at the specified position in this list.
*
* @param index index of element to return
* @return the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* (<tt>index < 0 || index >= size()</tt>).
*/
E get(int index);
/* ... */
}
V Javě se na dokumentaci rozhraní používá nástroj JavaDoc. Programátor zapíše dokumentaci do speciálně označených a formátovaných komentářů, které JavaDoc projde a sestaví z nich dokumentaci v HTML.
Výhodou tohoto přístupu je, že dokumentace je pohromadě s kódem a hrozí tedy menší riziko jejího zastarání (kód bude aktualizován, ale dokumentace ne). Na druhou stranu, dokumentace zabírá v kódu dost místa a ztěžuje tak navigaci. Tento problém dnes naštěstí vcelku uspokojivě řeší různá vývojová prostředí.
Dokumentace v JavaDocu trpí značnou mírou byrokracie – např. kromě popisu metody je potřeba popsat zvlášť i její parametry, návratovou hodnotu a vyhazované výjimky, což udělá ze stručného neformálního komentáře na tři řádky poměrně dlouhý kus textu s redundancemi (zde např. popis metody a její návratové hodnoty). Pokud bychom popis některých prvků vynechali, objeví se ve vygenerované dokumentaci prázdné místo, což není moc pěkné. Příliš vhodné není ani použití HTML pro text komentáře, což vede k nutnosti escapování.
Byrokracie při psaní dokumentace vede k tomu, že psaní dokumentace je otravná činnost a programátoři ji tedy často nevykonávají, nejsou-li k tomu nuceni. Neformální komentáře by přitom možná napsali. Opět zde částečně pomáhají vývojová prostředí, která umí připravit šablonu dokumentačních komentářů.
public interface List<E>: Collection<E> {
/* ... */
/// <summary>
/// Returns the element at the specified position in this list.
/// </summary>
///
/// <param name="index">index of element to return</param>
/// <returns>
/// the element at the specified position in this list.
/// </returns>
/// <exceptions name="ArgumentOutOfRangeException">
/// if the index is out of range
/// (<c>index < 0 || index >= size()</c>)
/// </exceptions>
E get(int index);
/* ... */
}
class List
# ...
# Returns the element at the specified position in this list.
# The position must be nonnegative and less than size of this
# list, otherwise +IndexError+ is thrown.
def get(index)
# ...
end
# ...
end
Ruby pro dokumentaci používá nástroj rdoc, v principu podobný JavaDocu, ale místo HTML a @-tagů používá jednoduchou wiki-like syntaxi. Dokumentaci umí vygenerovat v HTML a několika dalších formátech.
class List:
# ...
def get(index):
"""Returns the element at the specified position in this list.
The position must be nonnegative and less than size of this
list, otherwise IndexError is thrown."""
# ...
# ...
V Pythonu je dokumentace integrována přímo do jayzka – je-li
první výraz uvnitř třídy či metody řetězec, je považován za
dokumentační komentář. Dá se k němu dokonce přistupovat v runtime
pomocí atributu __doc__
(viz ukázka).
Je hezky vidět, že stejně jako v kódu, i v dokumentaci mají statické jazyky sklon k byrokracii a dynamické naopak k jisté neformálnosti.
Viz Effective Java: Programming Language Guide, Item 28.
Viz Effective Java: Programming Language Guide, Item 44.
Máme v programu ošklivý kus kódu, který ale funguje. Zrefaktorizujeme ho?
Otázka | Pro | Proti |
---|---|---|
Máme testy? | ano | ne |
Bude kód často upravován? | ano | ne |
Velká cena chyb? | ne | ano |
Refaktorizací musíme získat víc, než vynaložíme.
Některé metodiky vývoje software, jako extrémní programování a programování řízené testy, považují refaktorizaci za samozřejmou součást života kódu. Doporučují refaktorizovat kdykoliv je k tomu příležitost s tím, že veškerý kód je pokryt testy, takže se není třeba příliš obávat zanesení chyb.
Osobně je mi tento přístup velice sympatický a snažím se ho ve svých projektech používat, ale ne vždy je to možné a ekonomicky výhodné.
Refaktorizace má i psychologický efekt: Programátory většinou potěší, že se jim podařilo z programu dostat pryč kus entropie. A s výsledným čistším kódem se jim lépe pracuje. To má i "manažerské" dopady jako nižší burn-out rate a tedy menší fluktuaci zaměstnanců, což zvyšuje produktivitu. Může být tedy vhodné nechat programátory refaktorizovat i ve chvíli, kdy refaktorizace sama o sobě nic podstatného nepřinese.
switch
Příliš dlouhý nebo zanořený cyklus: Řešením je vyextrahovat vnitřní části do samostatných funkcí.
Špatná koheze třídy: Jinými slovy porušení zásady "Každý objekt by měl být zodpovědný jen za jednu věc".
Nekonzistentní úroveň abstrakce rozhraní třídy: Často vzniká postupnou evolucí kódu.
Nutnost dělat paralelní změny na více místech: U složitějších programů není zdaleka vždy možné se této nutnosti zbavit.
Paralelní příkazy switch
: Často je lepší využít
polymorfismus.
Metoda využívá jinou třídu více než tu, ve které byla deklarována: Bývá signálem k přesunu metody do jiné třídy.
Přílišné používání primitivních typů: Správně bychom si měli definovat vlastní datové typy odpovídající sémantice kódu. Bohužel, zdaleka ne ve všech jazycích je to jednoduše možné. Např. v Javě je jedinou možností, jak definovat vlastní datový typ, vytvořit novou třídu. S tou se pak ale nedá pohodlně pracovat (mj. kvůli absenci přetěžování operátorů). Obecně platí, že prakticky všechny typové systémy v běžné používaných jazycích jsou poměrně slabé a umožňují toho vyjádřit málo.
Řetězec funkcí posílající si stejná data: Může signalizovat špatně navrženou abstrakci.
Podtřída využívající jen málo metod svého předka: Možná je na místě kompozice místo dědičnosti.
Komentáře používané na vysvětlení obtížně pochopitelného kódu: Je-li to možné, je dobré se držet zásady "Don't document bad code – rewrite it" (Kernighan a Plauger, 1978).
Setup/teardown kód okolo volání funkce: Příkladem může být inicializace prvků nějaké datové struktury před voláním funkce a "rozbalování" té samé nebo jiné datové struktury po volání. Kdo někdy pracoval s Win32 API, dobře ví, o čem je řeč.
Všechny uvedené "code smells" jsou opsány z McConnella. Na jiných místech můžete najít mnohé další, někdy i s návody na to, kterou refaktorizaci použít k nápravě:
break
nebo return
místo kontrolní
proměnné cyklu
if
-then
-else
switch
)
polymorfismem
null
Sloučení duplicitních fragmentů kódu v různých částech podmínky:
Máte-li například stejný kus kódu na konci if
-větve a
else
-větve podmínky, měli byste ho přesunout až za
podmínku. Samozřejmě můžou nastat i podstatně složitější případy,
kdy bude nutná extrakce do metody.
Zapouzdření přetypování směrem dolů (downcastingu): Pokud například
píšete třídu EmployeeList
zapouzdřující seznam objektů
typu Employee
, měly by jeho metody vracet tento typ, i
když třeba nějaká podkladová třída pracuje s obecným
Object
. Přetypování by mělo být třídou zapouzdřeno.
Náhrada virtuálních metod inicializací dat: Pokud se podtřídy neliší kódem, ale jen hodnotou některých svých atributů, stačí v nich tyto atributy nainicializovat a společný kód ponechat v bázové třídě.
Skrytí delegáta: Představte si, že třída A volá třídu B a pak třídu C. Nebylo by lepší, kdyby volala jen třídu B a ta delegovala na třídu C? Někdy ano, někdy ne.
Přidání cizí funkce: Někdy byste potřebovali přidat k nějaké třídě novou metodu, ale nemůžete to z nějakého důvodu udělat. V tom případě musíte metodu přidat do jiné třídy, kde bude "cizí".
Přidání rozšiřující třídy: V předchozím případě (cizí funkce) se můžete rozhodnout vytvořit pro nově přidané metody novou třídu – rozšiřující třídu. Lze použít jak dědičnost (rozšiřující třída bude potomkem rozšiřované třídy), tak kompozici (rozšiřující třída bude obsahovat instanci rozšiřované třídy).
Vytvoření jednoznačného zdroje pro data, která nemáte pod kontrolou: Některá data mohou být například skrytá v třídách GUI. Spodní vrstvy programu by neměly o existenci GUI nic vědět, je tedy lepší vytvořit nějakého prostředníka, který jim data bude zprostředkovávat.
grep
, sed
, Perl, Ruby, Python,...
Tvorba refaktorizačních nástrojů pro statické jazyky je jednodušší, jelikož je v compile-time známo mnohem víc informací o struktuře kódu, než u dynamických jazyků. V dynamických jazycích si místo refaktorizačních nástrojů typicky musíme vystačit s tradičními Unixovými nástroji a skriptováním.
Ale i pokud pracujeme se statickým jazykem, zdaleka ještě nemáme vyhráno – například u C++ a C# se plete do cesty podmíněná kompilace, která tvorbu nástrojů také komplikuje. Je to pravděpodobně jeden z důvodů, proč jsou dnes asi nejdál refaktorizační nástroje pro Javu, která podobné vlastnosti nemá.
Záleží na ceně chyb a jejich nápravy
V případě obyčejné webové aplikace se formalizace testování pravděpodobně nevyplatí – data v aplikaci obyčejně nebývají důležitá, chyby nebývají fatální, uživatelé je poměrně rychle odhalí a jejich oprava je snadná. Samozřejmě, situaci může ovlivnit např. fakt, že aplikace je placená (uživatelé ze sebe nenechají dělat pokusné králíky a utečou ke konkurenci) a nebo se v ní manipuluje s důležitými údaji (často peníze).
U jakékoliv komponenty, která je používána na více místech už je cena chyb větší, protože je automaticky budou obsahovat všechny programy, které komponentu používají, a po opravě nalezených chyb se všechny tyto programy budou muset opravit také. Důkladné testování se zde už pravděpodobně vyplatí.
V případě software kardiostimulátoru, nebo obecně jakéhokoliv jiného software, kde jsou v sázce zdraví či životy lidí a nebo velké finanční částky, mnohdy investice do testování přesahují investice do vývoje samotného programu. Často se používají metody z kategorie formálních specifikací a model checkingu.
Unit testy testují malé jednotky programu, typicky jednotlivé třídy a metody v nich. Testy komponent testují větší funkční celky, integrační testy pak jejich vzájemnou interakci. Systémové testy testují celý vyvíjený produkt jako celek v jeho finální konfiguraci a cílovém prostředí.
Regresní testy často vznikají implicitně, každý test spouštěný automaticky se totiž stává testem regresním.
Pokud si budete chtít unit testy opravdu vyzkoušet v praxi, a budete při tom využívat nástroj JUnit, doporučuji projít si nejdříve následující odkazy (nejlépe v uvedeném pořadí). Shromáždil jsem je při psaní našeho softwarového projektu.
K podpoře změn: Nejčastější důvod ke snaze neměnit, co funguje, je riziko zanesení chyb do programu. Pokud používáme unit testy, vytvářejí pod programem jakousi záchrannou síť a nově zanesené chyby většinou odhalí. "Většinou" samozřejmě není "vždy", ale pokud jsou testy dobře napsané, je pravděpodobnost zanesení obvykle dostatečně malá, aby změnám nebránila. Celkovým výsledkem používání unit testů jsou obvykle častější refaktorizace a tedy i čistější kód.
Testy, které toto dělají jsou také potřeba, a můžou být napsány s pomocí unit-testing frameworku, ale nejsou to unit testy.
assertEquals
assertNotEquals
assertSame
assertNotSame
assertNull
assertNotNull
assertFalse
assertTrue
K třídám kódu a testů: Toto pravidlo nemusí platit vždy, můžeme mít víc tříd testů na jednu třídu kódu. Pozor ale, je to často signál, že třída má víc různých zodpovědností. Podobné pravidlo o metodách neplatí – metody mají často víc různých aspektů, které je vhodné testovat zvlášť.
K používání správných assertů: Nebojte se si sadu assertů rozšířit, pokud zjistíte, že používáte nějaký assert spolu s pomocným kódem okolo na více místech.
Několik zajímavých odkazů k tématu:
Experimenty zde myslím vyzkoušení naprogramování více variant řešení nějakého problému, jejich zhodnocení podle nějakého kritéria a následné použití jedné z nich a zahození ostatních. TDD může omezit experimentování s kódem v tom smyslu, že pokud máme k různým variantám kódu rovnou psát i testy, zvětší se náklady na každou variantu, což může vést až k tomu, že se experimentování nevyplatí. Někdy může být tedy výhodnější metodiku porušit, ozkoušet víc variant a teprve dodatečně k nim dopsat testy.
Pro inteligentního vývojáře může být také nepřirozené přemýšlet v tak malých úsecích návrhu/kódu, jak vyžaduje TDD; lépe by se mu pracovalo s většími celky a možná i na abstraktnější úrovni, než je kód. Pro takového vývojáře může být TDD ztráta času a tedy i produktivity.
Oba předchozí odstavce lze shrnout pod obecnou poučku, že žádná metodika není dokonalá a vhodná pro všechny situace.