Primárním účelem funkcí je typicky spočítat nějakou hodnotu jako funkci vstupních parametrů. Opravdu čistých funkcí se v typickém programu asi najde málo, pokud to není program ve funkcionálním jazyce. Hlavní charakteristikou funkcí je absence vedlejších efektů, což má celou řadu výhod. Nicméně z pohledu běžného programátora se čistě funkcionální programování může jevit poměrně málo intuitivní.
Procedury jsou typické pro imperativní styl programování, kdy má programátor absolutní kontrolu nad průběhem výpočtu. Procedury obecně reprezentují aktivitu a výsledkem procedury není hodnota, ale např. přechod systému do jiného stavu. Přestože řada procedur vrací nějakou hodnotu a čistě syntakticky se jedná o funkce, svou povahou jsou to stále procedury.
Nakonec jsou metody, což jsou procedury a funkce svázané s daty.
V základních kurzech programování se člověk typicky dozví, že procedury a funkce slouží hlavně k zamezení duplikace kódu, cehož důsledek je především to, že se program lépe vyvíjí, ladí, dokumentuje a udržuje. Vedle syntaktických detailů jak používat parametry a lokální proměnné se toho člověk víc moc nedozví.
Přestože takové vysvětlení v každém bodě říká pravdu, zdaleka se nedotýká např. mnohem důležitějšího aspektu a tím je zvládání složitosti s využitím abstrakce a skrývání informací. Zamezení duplikace kódu samo o sobě typicky nepřináší všechny zmiňované výhody – ty právě plynou až z dobře strukturovaného programu, přimž vhodná struktura nevznikne sama od sebe pouhým naskládáním kódu do procedur a funkcí.
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);
if (node != null) {
Node currentNode = node;
while (currentNode.next != null) {
currentnode = currentNode.next;
}
leafName = currentNode.name;
} else {
leafName = '";
}
leafName = getLeafName (node);
uintptr_t alloc_pages (...) {
lock (pgalloc_lock);
...
// do something
...
if (status = FAILURE) {
unlock (pgalloc_lock);
return;
}
...
// do something else
...
if (status = FAILURE) {
return;
}
...
unlock (pgalloc_lock);
}
static
uintptr_t __alloc_pages (...) {
...
// do something
...
if (status = FAILURE) {
return NULL;
}
...
// do something else
...
}
uintptr_t alloc_pages (...) {
uintptr_t result;
lock (pgalloc_lock);
result = __alloc_pages (...);
unlock (pgalloc_lock);
return result;
}
Častou překážkou při vytváření procedur, funkcí a metod bývá pocit, že pro triviální operace nemá cenu vytvářet funkce, zvlášť pokud se neopakují.
Pokud se opakují, není co řešit. Pokud se neopakují, dostáváme se do šedé zóny – je nutné zvážit zda se opakovat v principu mohou a jak ovlivňují soudržnost té konkrétní metody (viz. funkční soudržnost později).
Režie na volání funkce se není třeba bát. Pokud je funkce jednoduchá a překladač rozumný, předá parametry funkce v registrech, případně funkci přeloží inline. Volání funkce je něco jiného než podmíněný skok, takže i když je volání nepřímé, překladač může adresu skoku spočítat ve vhodný okamžik, takže v okamžiku zpracování instrukce skoku už je jasné, kam se bude skákat.
U moderních procesorů je navíc rozumné počítat s tím, že se snaží rozumně vykonávat operace spojené s voláním virtuálních metod, které jsou tak typické pro dnešní software.
public IdentifiedObject() {
static int nextId = 0;
this.id = nextId++;
/* ... */
}
private int getUniqueId() {
static int nextId = 0;
return nextId++;
}
public IdentifiedObject() {
this.id = getUniqueId();
/* ... */
}
points = deviceUnits * (POINTS_PER_INCH / getDeviceUnitsPerInch ());
int deviceUnitsToPoints (int deviceUnits) {
return deviceUnits * (POINTS_PER_INCH / getDeviceUnitsPerInch ());
}
...
points = deviceUnitsToPoints (deviceUnits);
int deviceUnitsToPoints (int deviceUnits) {
if (getDeviceUnitsPerInch () > 0) {
return deviceUnits * (POINTS_PER_INCH / getDeviceUnitsPerInch ());
} else {
return 0;
}
}
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];
}
Cosine()
vs. CosineAndTan()
sin(), getCustomerName(), eraseFile(), ageFromBirthday(), ...
getEmployeeData()
vs getFirstPartOfEmployeeData(), getRestOfEmployeeData()
this
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)
//
// Compute expense data. Each of the routines accesses the member
// data expenseData. DisplayExpenseSummary should be called last
// because it depends on data calculated by the other routines.
//
expenseData = initializeExpenseData (expenseData)
expenseData = computeMarketingExpense (expenseData)
expenseData = computeSalesExpense (expenseData)
expenseData = computeTravelExpense (expenseData)
expenseData = computePersonnelExpense (expenseData)
displayExpenseSummary (expenseData)
//
// The following calls must be in correct order. Inside the
// taskReachedState method, the Task Manager may decide to
// close the context which contains this task by calling the
// HostRuntimeInterface.closeContext method.
//
// If the calls are not properly ordered, the Host Runtime will not
// have been notified about the task's completion (the notification
// happens in the notifyTaskFinished call) and will refuse to close
// the context (throwing IllegalArgumentException).
//
// This would lead to 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.
Název by měl přesně a úplně popisovat, co procedura dělá nebo co funkce vrací.
getID
, computeEquationResults
,
drawWindowBorder
Enumerable.hasMoreElements
vs Iterator.hasNext
Enumerable.nextElement
vs Iterator.next
User.getUserName
vs User.getName
a, b = divmod(7, 3)
print a # => 2
print b # => 1
fprintf(stream, format,...)
fputs(str, stream)
strncpy(dst, src, len)
memcpy(dst, src, len)
final
, C/C++ – const
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.
float response (float inputSample) {
sampleHistory.store (inputSample);
inputSample = 0.0f;
for (int i = 0; i < coefficients.length; i++) {
...
inputSample += coefficients [i] * sampleHistory.previous (i);
...
}
...
return inputSample;
}
float response (final float inputSample) {
sampleHistory.store (inputSample);
float outputValue = 0.0f;
for (int i = 0; i < coefficients.length; i++) {
...
outputValue += coefficients [i] * sampleHistory.previous (i);
...
}
...
return outputValue;
}
void printNames (ArrayList <String> names);
void printNames (List <String> names);
true
nebo
false
znamená
int compareStrings (String s1, String s2, boolean caseInsensitive)
setEnabled (boolean enabled)
K interfacům a abstraktním typům: Viz Effective Java: Programming Language Guide, Item 34.
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.
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.
NaN
nebo výjimky
atoi()
, atol()
, atoll()
if (report.formatOutput (formattedReport) == FormatResult.SUCCESS) {
...
}
formatResult = report.formatOutput (formattedReport);
if (formatResult == FormatResult.SUCCESS) {
...
}
report.formatOutput (formattedReport, resultHolder);
if (resultHolder.result == FormatResult.SUCCESS) {
...
}
result
null
null
kontejnerem: HttpServletRequest.getCookies()
null
null
viz Effective Java:
Programming Language Guide, Item 34.
S podporou pro funkcionální programování přibyla v Javě od verze 8
třída Optional
, což je (immutable) kontejner na 1 hodnotu.
public final class StatusHolder <S> {
public S status;
}
QueryResult DB.execute (
Query query, StatusHolder <QueryStatus> statusHolder);
...
StatusHolder <QueryStatus> queryStatusHolder =
new StatusHolder <QueryStatus> ();
QueryResult queryResult =
db.execute (findInactiveUsersQuery, queryStatusHolder);
if (queryStatusHolder.status == QueryStatus.SUCCESS) {
...
for (Row row : queryResult.rows ()) {
...
}
}
public final class StatusResult <S, R> {
public final S status;
public final R result;
public StatusResult (S status, R result) {
this.status = status;
this.result = result;
}
}
StatusResult <QueryStatus, QueryResult> DB.execute (Query query);
...
StatusResult <QueryStatus, QueryResult>
queryStatusResult = db.execute (findInactiveUsersQuery);
if (queryStatusResult.status == QueryStatus.SUCCESS) {
...
for (Row row : queryStatusResult.result.rows ()) {
...
}
}
public class QueryException extends ... {
...
}
QueryResult DB.execute (Query query) throws QueryException;
...
try {
QueryResult queryResult = db.execute (findInactiveUsersQuery);
for (Row row : queryResult.rows ()) {
...
}
...
} catch (QueryException e) {
...
}
Problematika výjimky vs. chybové kódy je složitější – my jsme se jí tu jen zlehka dotkli. Zájemcům o motivační diskuzi lze doporučit k přečtení následující články (v uvedeném pořadí):
Co se týče toho zda výjimky používat, pro nastartování diskuze postačí již dříve zmiňovaná debata Joel Spolsky vs. Ned Batchelder. K tomu je např. zajímavá polemika na obhajobu chybových kódů od Douga Rosse:
Základní pravidlo asi jako obvykle zní – pokud použití výjimek zjednoduší a zpřehlední kód, asi není co řešit. Důležité je pak používat výjimky správně používat. Nakonec asi není až tak podstatné, zda se používá to či ono, ale zda se programátor vědomě a systematicky věnuje obsluze abnormálních stavů.
K výjimečným situacím: Viz Effective Java: Programming Language
Guide, Item 39: Use exceptions only for exceptional conditions.
To znamená nepoužívat výjimky pro řízení toku programu, jako např.
vynechání kontroly Iterator.hasNext()
a čekání
na to až Iterator.next()
vyhodí výjimku.
...
try {
while (true) {
Employee employee = employeeIterator.next ();
...
}
catch (NoSuchElementException e) {
}
...
try {
while (true) {
Employee employee = employeeIterator.next ();
...
}
catch (NoSuchElementException e) {
}
...
while (employeeIterator.hasNext ()) {
Employee employee = employeeIterator.next ();
...
}
null
?unsigned
)
catch
blokůmK úrovni abstrakce: Viz Effective Java: Programming Language Guide, Item 43: Throw exceptions appropriate to the abstraction. To znamená, že výjimky, které metoda vyvolává by měly být na úrovni abstrakce odpovídající tomu, co metoda dělé, ne jak to dělá.
K detailnímu popisu problému: Viz Effective Java: Programming Language Guide, Item 45.
K prázdným catch
blokům: Viz Effective Java: Programming
Language Guide, Item 47.
class Employee {
..
public TexId getTaxId () throws IOException {
...
}
...
}
class Employee {
...
public TexId getTaxId () throws EmployeeDataNotAvailableException {
...
try {
...
catch (IOException e) {
throw new EmployeeDataNotAvailableException (e);
}
}
...
}
throw
, slouží k vyvolání výjimkytry-catch
, slouží k odchycení třídy výjimektry-finally
, slouží k uvolnění prostředků při výjimcecatch
bloky
Mnohem těžší na rozhodnutí je dilema kolem kontrolovaných a nekontrolovaných výjimek. Řada expertů je dnes považuje za omyl a doporučují vyhazovat pouze nekontrolované výjimky a kontrolované převádět na nekontrolované. Viz např. Bruce Eckel:
Celou debatu pak ale poměrně dobře shrnuje Brian Goetz na webu IBM developerWorks:
A poměrně dobrý návod poskytuje také Barry Ruzek na serveru Dev2Dev (odkaz vede jinam, puvodní server je nedostupný):
Situace | Eventualita/Contingecy | Chyba/Fault |
---|---|---|
Považováno za | součást návrhu | ošklivé překvapení |
Očekávaný výskyt | předvídatelný, ale vzácný | nikdy |
Koho to zajímá | kód volající metodu | lidi, kteří mají odstranit problém |
Příklad | alternativní návratové hodnoty | programové chyby, selhání hardware, konfigurační chyby, chybějící soubory, nedostupné servery |
Nejlepší mapování | kontrolovaná výjimka | nekontrolovaná výjimka |
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: Viz Effective Java: Programming Language Guide, Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors.
K užívání kontrolovaných výjimek: Viz Effective Java: Programming Language Guide, Item 41: Avoid unnecessary use of checked exceptions.
Osobně se přikláním spíše k názoru, že kontrolované výjimky mnoho užitečného nepřináší. Když už, tak bych primární použití viděl v reprezentaci doménových výjimek, tj. výjimečných případů např. v use cases. Jenže proti tomu stojí zásada nepoužívat výjimky pro control flow, která je podle mého názoru silnější. Osobně bych se prostě snažil pro všechny takové výjimky vytvořit testovací a výkonné metody – pokud programátor volá výkonnou metodu a neví, jestli je to možné, tak je to programátorská chyba, která se běžně řeší nekontrolovanými výjimkami. Výjimku by se možná hodilo učinit v případě, kdy z výkonnostních nebo jiných důvodů není možné tyto metody oddělit, ale i pak prostě může metoda vyhazovat dokumentovanou výjimku nebo chybový kód.
java.io.RandomAccessFile
public int read (byte [] b, int off, int len)
throws IOException
java.io.DataInputStream
public final void readFully (byte[] b, int off, int len)
throws IOException
public final void readDouble ()
throws IOException
IOException
nebo EOFException
java.util.Arrays
public static <T>
int binarySearch (T [] a, T key, Comparator <? super T> c)
java.rmi.Naming
public static Remote lookup (String name)
throws NotBoundException, MalformedURLException, RemoteException
java.lang.*
, java.util.*
, ...
IllegalArgumentException
, IllegalStateException
NullPointerException
IndexOutOfBoundsException
ConcurrentModificationException
UnsupportedOperationException
K standardním výjimkám: Viz Effective Java: Programming Language Guide, Item 42.