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, x);
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. U kratších funkcí (zvlášť pokud jsou statické a používají se z jednoho místa) se dá očekávat, že překladač kód funkce zainlinuje a k žádnému volání nedojde. Když už k němu dojde, dá se očekávat, že parametry funkce (pokud jich není příliš) budou předány v registrech.
Volání funkce není pro procesor tak "nepříjemné" jako podmíněný skok, takže i když je volání nepřímé, překladač může adresu skoku "spočítat" (tj. natáhnout z virtuální tabulky metod do registru) ve vhodný okamžik, takže v okamžiku zpracování instrukce volání (resp. nepodmíněného skoku na hodnotu v registru) 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 pro software typické.
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 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. V modernějších jazycích
(Scala, Kotlin) naleznete klíčová slova val
resp.
var
, která slouží k deklaraci konstatních resp.
modifikovatelných proměnných (instančních i lokálních, včetně
parametrů funkcí a metod).
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
je implementační detailnull
hodnotou: HttpServletRequest.getCookies()
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<T>
, což je (immutable) kontejner na 1 hodnotu.
Ve Scale je k tomuto účelu typ Option<T>
.
public final class StatusHolder<S> {
public S status;
}
QueryResult DB.execute(Query query, StatusHolder<QueryStatus> statusHolder);
...
StatusHolder<QueryStatus> queryStatusHolder = new StatusHolder<>();
QueryResult queryResult = db.execute(findInactiveUsersQuery, queryStatusHolder);
if (queryStatusHolder.status == QueryStatus.SUCCESS) {
...
for (Row row : queryResult.rows()) {
...
}
}
public final class Response<S, R> {
public final S status;
public final R result;
public Response(S status, R result) {
this.status = status;
this.result = result;
}
}
Response<QueryStatus, QueryResult> DB.execute(Query query);
...
Response<QueryStatus, QueryResult>
queryResponse = db.execute(findInactiveUsersQuery);
if (queryResponse.status == QueryStatus.SUCCESS) {
...
for (Row row : queryResponse.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í):
Ned Batchelder má hezký model toho, jak vypadá software v článku "Exceptions in the rainforest". Podle něj má software 3 vrstvy, shora C-B-A. Nejnižší (A = Adapting software beneath) přizpůsobuje jiný kód našim potřebám. Někdy jsou to low-level volání, pak se často používají chybové kódy, které je dobré převádět na výjimky.
Prostřední vrstva (B = Building pieces of your system, někdy také Business logic :-) slouží k vytvoření částí, ze kterých se skládá náš svět. Tady je prostor pro problem-specific koncepty, algoritmy a datové struktury. V prostřední vrstvě chceme být maximálně produktivní a chceme tady mít dobře čitelný kód.
Nejvyšší vrstva (C = Combining it all together) ví co se děje, takže typicky ví, co dělat s výjimkami. Výjimky neznamenají, že error handling bude najednou snadnější, ale znamenají, že chyby z vrstvy A se neztratí, a že nemusíme "špinit" vrstvu B tím, že bude předávat chyby výše, do vrstvy C.
Typicky pak vrstva A vesměs vyhazuje výjimky, vrstva B také, ale méně, a vrstva C primarne chytá výjimky a potenciálně něco užitečného dělá.
A protože software je fraktální, dá se tenhle model embeddovat do různých vrstvev.
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);
}
}
...
}
java.lang.*
, java.util.*
, ...
IllegalArgumentException
, IllegalStateException
NullPointerException
IndexOutOfBoundsException
ConcurrentModificationException
UnsupportedOperationException
K standardním výjimkám: Viz Effective Java: Programming Language Guide, Item 42.
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 mohou být užitečné primárně ve vlastním kódu, kde si tím mohu zajistit, že nezapomenu na obsluhu nějakého chybového stavu. Vně svého kódu bych kvůli minimalizaci obtěžování konzumentů mého kódu vyhazoval výjimky nekontrolované. Ty je pak nutné velmi dobře zdokumentovat. Pro situace, kdy je možné provést test před voláním výkonné metody (a kdy se situace po volání testu nemůže změnit), bych se snažil mít k výkonným metodám vždy testovací metody. Výkonné metody by pak mohly vyhazovat nekontrolované výjimky (bylo by chybou programátora, že nezkontroluje, zda může výkonnou metodu volat). Pokud není možné test a vykonání operace rozumně oddělit, volil bych spíše nekontrolovanou výjimku, abych nezatěžoval konzumenta. Dá se očekávat, že konzument nejspíš bude můj kód nějak adaptovat a kontrolované výjimky si může na vlastním území zavést sám.
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 EOFException, IOException
public final double readDouble ()
throws EOFException, 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
Zde stojí za zmínku, že obvykle pokud něco hledáme, tak počítáme s možností, že to nenajdeme. Proč je to v případě RMI lookupu jiné? Stejně tak pokud víme, že zadané URL je správně, tak proč muset řešit checked exception?