Doporučené postupy v programování

Návrh API

Proces návrhu · Obecné principy · Návrh tříd · Návrh metod

Lubomír Bulej

KDSS MFF UK

API = Application Programming Interface

Viditelné "uživatelské" rozhraní k abstrakcím

Definuje rozhraní mezi alespoň dvěma subjekty

  • projekty, subsystémy, třídy, ...
  • tvůrci API a jeho uživatelé

Odděluje části, které se nějakým zásadním způsobem liší

  • oddělený překlad, oddělený vývoj, nezávislý vývoj ...
  • oddělené úrovně abstrakce, oddělené zodpovědnosti, ...
  • "galvanické" oddělení zúčastněných stran

Umožňuje odděleným částem koexistovat a komunikovat

  • poskytuje stabilní kontrakt, který komunikaci umožňuje

Proč je API důležité?

API programů

  • rozšiřitelnost, schopnost evoluce
  • Firefox, Eclipse, ..., Emacs, Quake, ...

API platforem

  • operační systémy
  • běhová prostředí (Java, .NET,...)
  • jazyky samotné

API webových služeb

  • Google Search, Google Maps, ...

Co je součástí API?

Vše, na co se může druhá strana spolehnout

  • signatury metod, atributy tříd
  • soubory, jejich umístění a obsah
  • proměnné prostředí
  • protokoly
    • přenos dat, clipboard, drag & drop
  • chování
    • pořadí pro volání, zamykání, multithreading...
  • lokalizace

Proč je návrh API důležitý?

Navrhnout dobré API je těžké

  • intuitivní, zapamatovatelné, nabízí vhodné abstrakce a nepřekáží
  • správně řeší okrajové situace, dobře dokumentované
  • může přitáhnout a udržet uživatele (intelektuální investice)

Navrhnout špatné API je lehké

  • těžce pochopitelné, špatně se používá, vyžaduje boilerplate kód
  • snižuje efektivitu (všeho), chybami v návrhu "trpí" mnoho uživatelů API
  • může uživatele odradit a je přítěží (vyžaduje neustálou podporu)

Je potřeba ho trefit napoprvé

  • špatná rozhodnutí nelze jednoduše změnit (hlavně u veřejných API)

Příklad: rozhraní Select() v C#

.NET funkce pro čekání na socket


                public static void Select(
                  IList checkRead, IList checkWrite, IList checkError,
                  int microseconds
                );
            

C funkce pro čekání na socket


                    int select(
                      int nfds,
                      fd_set * readfds, fd_set * writefds, fd_set * exceptfds,
                      struct timeval * timeout
                    );
                

Příklad: rozhraní Select() v C#

Představa použití C# rozhraní v kódu serveru


                int timeout = ...;
                ArrayList readList = ...; // sockets to monitor for reading
                ArrayList writeList = ...; // sockets to monitor for writing
                ArrayList errorList = ...; // sockets to monitor for errors

                while (!done) {
                  ArrayList checkRead = readList.Clone();
                  ArrayList checkWrite = writeList.Clone();
                  ArrayList checkError = errorList.Clone();
                  Select(checkRead, checkWrite, checkError, timeout);

                  foreach (Socket socket in checkRead) {
                    // deal with each socket ready for reading
                  }

                  foreach (Socket socket in checkWrite) {
                    // deal with each socket ready for writing
                  }

                  foreach (Socket socket in checkError) {
                    // deal with each socket that encountered an error
                  }

                  if (checkRead.Count == 0 && checkWrite.Count == 0 && checkError.Count == 0) {
                    // no sockets are ready -- timed out...
                  }
                )
            

Příklad: rozhraní Select() v C#

Pomocná funkce pro test seznamů


                private static boolean hasActiveSocket(
                  IList readList, IList writeList, IList errorList
                ) {
                  bool readListEmpty = (readList == null || readList.Count == 0);
                  bool writeListEmpty = (writeList == null || writeList.Count == 0);
                  bool errorListEmpty = (errorList == null || errorList.Count == 0);

                  return !readListEmpty || !writeListEmpty || !errorListEmpty;
                }
            

Pomocná funkce pro kopírování seznamů

  • rozhraní IList ani top-level objekt nemá Clone()
  • aby měl objekt Clone(), musí implementovat ICloneable

Pomocná funkce pro slučování seznamů

  • sloučí dva seznamy a eliminuje duplicity
  • použití pro množinu socketů testovaných na chybu

Příklad: rozhraní Select() v C#

Wrapper funkce doSelect()


                public static void doSelect(
                  IList checkRead, IList checkWrite, IList checkError, int milliseconds
                ) {
                  ArrayList readCopy;  // copies of the three parameters
                  ArrayList writeCopy; // because Select() clobbers them
                  ArrayList errorCopy;

                  if (milliseconds <= 0) {
                    // simulate waiting forever
                    do {
                      ... // copy socket lists
                      Select(readCopy, writeCopy, errorCopy, Int32.MaxValue);
                    } while (!hasActiveSocket(readCopy, writeCopy, errorCopy));

                  } else {
                    // handle finite timeouts
                    int maxMilliseconds = Int32.MaxValue / 1000;

                    int remaining = milliseconds;
                    while ((remaining > 0) && !hasActiveSocket(readCopy, writeCopy, errorCopy)) {
                      int timeout = milliseconds > maxMilliseconds ? maxMilliseconds : milliseconds;
                      ... // copy socket lists
                      Select(readCopy, writeCopy, errorCopy, timeout * 1000);
                      remaining -= timeout;
                    }
                    ... // copy the three lists back to original parameters
                }
            

Příklad: rozhraní Select() v C#

50-100 řádků boilerplate kódu

  • wrapper + pomocné funkce
  • důsledek drobných nedostatků v rozhraní

Drobné nedostatky v rozhraní Select()

  • funkce přepisuje argumenty
  • neumožňuje rozlišit timeout od změny stavu socketu
  • neumožňuje čekat déle než 35 minut
  • používá seznamy místo množin socketů

Vylepšené rozhraní funkce


public static int Select (
  ISet checkRead, ISet checkWrite, Timespan timeout,
  out ISet readable, out ISet writable, out ISet error
);
            

Jak se vás týká návrh API?

Pokud programujete, navrhujete API

  • hranice mezi moduly, abstrakcemi, ...
  • užitečné moduly jsou používány opakovaně
  • jakmile máte uživatele, nemůžete API jen tak měnit

Je dobré myslet v intencích API

  • vede k modularizaci a lepší architektuře
  • i když ne všechna API jsou nutně veřejná

Co charakterizuje dobré API?

Je snadné se ho naučit i používat

  • i bez dokumentace – intuitivní, regulární

Je těžké ho používat nesprávně

  • nenechá uživatele dělat špatné věci

Je snadné porozumět klientskému kódu

  • snižuje složitost a zvyšuje udržovatelnost kódu

Co charakterizuje dobré API?

Je dostatečně mocné na uspokojení požadavků

  • ne však všemocné

Je jednoduše rozšiřitelné

  • i když nebude na poprvé špatně, nebude úplně dobře
  • pokud bude úspěšné, bude potřeba ho rozšiřovat

Je vhodné pro zamýšlené obecenstvo

  • nelze udělat jedno správné API pro všechny
  • uživatelé z různých domén mluví různými jazyky
  • API představuje malý jazyk – musí odpovídat doméně

Rozšiřitelnost API vs. SPI

API - kód poskytuje službu

  • rozšíření API spočívá v přidávání nových metod
  • zpětně kompatibilní s kódem klientů

SPI - kód vyžaduje službu

  • Service Provider Interface
  • pluginové rozhraní pro různé implementace
  • nelze přidávat metody, maximálně ignorovat staré

Nemíchat API a SPI

  • různé způsoby použití, různí uživatelé
  • komplikuje další vývoj rozhraní

Životní cyklus API

API se vyvíjí...

  • spontánně
    • někdo něco vyvine, jiný to začně používat (a chtít jiné věci)
    • kontrakt se časem stabilizuje a vznikne API
  • při návrhu
    • existuje potřeba stabilního kontraktu mezi dvěma subsystémy
    • API vznikne v důsledku procesu návrhu, po čase se stabilizuje

Různá stádia vývoje API

  • private, friend
    • typicky počáteční stav spontánního vývoje
  • under development, stable, official
    • začátek řízeného návrhu
  • deprecated

 

Proces návrhu API

Proces návrhu API

1. Zjistěte požadavky na API

  • skeptický přístup – uživatelé neví, co chtějí
  • místo požadavků na vás mohou chrlit řešení
    • tedy jak místo co
  • cílem je dobrat se ke skutečným požadavkům
  • ze skutečných požadavků zjistíte, co má rozhraní dělat
    • use cases, množina úloh, které má rozhraní vykonávat
  • někdy může být jednodušší poskytnout obecnější rozhraní
    • pozor na přílišné zobecňování, viz. doporučení později

Proces návrhu API

2. Napište krátkou specifikaci

  • 1 stránka stačí – úplnost specifikace není podstatná
    • důležitá je schopnost rychle dělat potřebné změny
    • krátká specifikace se mění snadno
  • nechte specifikace zhodnotit co nejvíce lidem
    • snažte se brát vážně co říkají – naslouchejte
  • až získáte jistotu, můžete dotáhnout specifikaci k úplnosti
    • bez psaní kódu to nepůjde

Proces návrhu API

3. Pište testovací kód proti API

  • co nejdříve, co nejčastěji
  • před tím, než API implementujete
    • nebudete muset zahazovat kód
  • před tím, než API přesně specifikujete
    • nebudete muset zahazovat specifikaci
  • během finalizace
    • ušetříte si nepříjemná překvapení
    • kód vám zůstane – příklady a unit testy
  • ještě důležitější v případě SPI
    • alespoň 3 různé pluginy před zveřejněním SPI

Proces návrhu API

4. Buďte realisté a počítejte s iterací

  • nemůžete vyhovět všem
    • můžete nevyhovět všem stejnou měrou
  • počítejte s tím, že budete dělat chyby
    • pár let reálného provozu si s nimi poradí
    • za předpokladu, že jste připraveni API rozvíjet

Pozor na design by committee

  • určete jednoho člověka zodpovědného za výsledný návrh
    • vzhledem k množství vstupů je lépe schopen udržet konzistenci
    • málokdy je rozhraní tak velké, aby to jeden člověk nezvládl

Příklad: thread-local proměnné

Pomocná třída pro thread-local proměnné


                public final class ThreadLocal {
                  private ThreadLocal() { /* non-instantiable */ }

                  // Sets current thread's value for named variable.
                  public static void set(String key, Object value);

                  // Returns current thread's value for named variable.
                  public static <T> T get(String key, Class<T> type);
                }
            

Co je na rozhraní špatného?

  • klíče představují sdílený globální prosto jmen
  • možnost kolizí nebo falšování klíčů

Příklad: thread-local proměnné

Pomocná třída pro thread-local proměnné


                public final class ThreadLocal {
                  private ThreadLocal() { /* non-instantiable */ }

                  public static final class Key { private Key() { } };

                  // Generates unique, unforgeable key
                  public static Key getKey() { return new Key(); }

                  public static void set(Key key, Object value);
                  public static <T> T get(Key key, Class<T> type);
                }
            

Funguje, ale vyžaduje boilerplate kód


                    static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey();
                    ThreadLocal.set(serialNumberKey, nextSerialNumber());
                    System.out.println(ThreadLocal.get(serialNumberKey, int.class));
                

Příklad: thread-local proměnné

Třída reprezentující thread-local proměnnou


                public final class ThreadLocal<T> {
                  public ThreadLocal() { }

                  public void set(T value);
                  public T get();
                }
            

Thread-local proměnná je klíčem


                    static ThreadLocal<Long> serialNumber = new ThreadLocal<>();
                    serialNumber.set(nextSerialNumber());
                    System.out.println(serialNumber.get());
                

 

Obecné principy

Obecné principy

Myslete na uživatele.

Kdo jsou mí uživatelé?

  • Co budou chtít s API dělat?
  • Bude se jim API snadno používat? I bez dokumentace?
  • Bude kód uživatelů dobře čitelný a spravovatelný?
  • Je API interní nebo externí? Je potřeba více různých API?
  • Je potřeba SPI?

Pohled implementátora je druhořadý

  • je lepší investovat čas do jednoduchosti použití než implementace
  • analogie k psaní kódu primárně pro pohodlí čtenáře, až poté písaře

Obecné principy

Minimalizujte údiv uživatele.

Snažte se uživatele nepřekvapit

  • očekávání se těžko odhadují
  • očekávání různých uživatelů konfliktní

"A user interface is well-designed when the program behaves exactly how the user thought it would." — Joel Spolsky

Obecné principy

Dělejte jen jednu věc a dělejte ji dobře.

Funkce API by měla jít snadno vysvětlit

  • pokud to nejde pojmenovat, něco je špatně
  • počítejte s nutností rozdělovat a spojovat moduly

Obecné principy

Usilujte o co nejjednodušší možné řešení.

"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

API by mělo být co nejmenší, ale ne menší

  • důležité je, aby API splňovalo požadavky
  • do rozhraní se dá snadno přidávat, ale nikdy odebírat
  • konceptuální náročnost – jak dlouho potrvá se API naučit
  • hledejte dobrý poměr síla/náročnost

Obecné principy

Implementace by neměla ovlivňovat API

  • implementační detaily jsou matoucí a omezují možnost změny
  • ujasněte si, co jsou v daném kontextu implementační detaily
    • pozor na příliš detailní specifikaci metod
  • nenechte implementační detaily prosakovat do API

Minimalizujte přístupnost všeho

  • maximalizuje skrývání informací, rozvolňuje vazby
  • usnadňuje pochopení, testování a ladění

Obecné principy

Snažte se o typovou a běhovou konzistenci

  • vše co jde zapsat by mělo správně fungovat
    • ne vždy je možné toho dosáhnout
  • omezuje možnost nesprávného použití API

Příklad: java.sql


                        public interface Connection {
                          ...
                          public Savepoint setSavepoint();
                          public void rollback(Savepoint sp);
                          ...
                        }

                        public interface Savepoint {
                          public String getSavepointId();
                          public String getSavepointName();
                        }
                    

                        public interface Connection {
                          ...
                          public Savepoint setSavepoint();
                          ...

                          public interface Savepoint {
                            public void rollback();
                            public String getSavepointId();
                            public String getSavepointName();
                          }
                        }
                    

Obecné principy

Na názvech záleží – API je malý jazyk

  • samovysvětlující názvy, bez kryptických zkratek
  • konzistentní používání stejných slov pro stejné věci
    • v rámci API, v rámci platformy
  • usilujte o regularitu a symetrii
    • pozor, ne vždy dává smysl
  • usilujte o dobrou čitelnost klientského kódu
    
                            if (car.speed () > 2 * SPEED_LIMIT) {
                              generateAlert ("Watch out for cops!");
                            }
                        
  • pozor na změnu sémantiky při evoluci

Buďte otevření možnostem refaktoringu

  • rozdělování/slučování modulů

Obecné principy

Dokumentujte ve velkém

  • tutoriál, FAQ, reference

Dokumentujte v malém

  • úplně každý jednotlivý prvek rozhraní
  • kontrakty, vedlejší efekty, vlastnictví

Na dokumentaci záleží – pokud chybí

  • uživatelé budou hádat nebo koukat do zdrojového kódu
  • omezujete prostor pro opětovné použití kódu

Obecné principy

Přizpůsobte API cílové platformě

  • využívejte idiomů cílové platformy
    • názvové konvence, standardní knihovny
    • napodobujte kód standardních knihoven
    • vyhněte se obsolete/deprecated věcem
  • využívejte syntaktické prvky jazyka
    • parametry – defaulty, variabilní počet, "keyword params"
    • šablony/generiky, výčtové typy, ...

Pozor na multiplatformní/portovaná API

  • pokud není nutná striktní kompatibilita, použijte idiomy cílové platformy

 

Návrh tříd (pro API)

Doporučení pro návrh tříd

Obecné zásady – detaily později

  • omezte mutability, dědičnost používejte jen když to dává smysl
  • navrhujte a dokumentujte pro dědičnost nebo ji zakažte

Usilujte o flexibilitu při zachování jednoduchosti

  • žádné veřejné atributy – gettery/settery pro přístup k atributům
    • pozdní inicializace, synchronizace, přesun metody do nadtřídy, ...
  • zvažte použití factory metody či builderu vs. konstruktoru
    • konstruktor lze jen přetěžovat, factory metody lze vhodně pojmenovat
    • možnost vytvářet instance podtříd, cachování instancí, synchronizace
    • Pozor: statické factory metody omezují testovatelnost

Doporučení pro návrh tříd

Preferujte immutable třídy

  • mutable objekty komplikují design a při sdílení se na ně nelze spolehnout
  • 
                        HttpRequest req = new HttpRequest();
                        req.setUrl("https://api.example.com");
                        req.setMethod("POST");
    
                        sendAsync(req);
                        req.setUrl("https://another.example.com");
                    
  • pokud objekt představuje konfiguraci nebo výsledek, neměl by jít po vytvoření změnit

Pozor na "teleskopické" konstruktory

  • vytváření immutable objektů s mnoha volitelnými parametry
  • 
                            HttpRequest req = new HttpRequest(
                                "https://api.com", "POST", 5000, true, null, "Bearer token"
                            );
                        
  • v místě volání nepřehledné, náchylné k chybám

Doporučení pro návrh tříd

Alternativa ke konstruktoru: fluent builder


                HttpRequest req = HttpRequest.newBuilder("https://api.com")
                    .method("POST")
                    .timeout(5000)
                    .followRedirects(true)
                    .header("Authorization", "Bearer token")
                    .build();
            
  • kód je čitelný v místě volání, IDE může napovídat
  • volání build() umožňuje explicitní kontrolu konzistence parametrů
  • výsledkem je immutable objekt, který lze libovolně sdílet

Doporučení pro návrh tříd

Zvažte použití vhodného nositele rozhraní

  • interface – Java, ...
    • vícenásobná dědičnost, oddělení rozhraní a implementace, oddělení konceptů
    • problém s rozšiřováním pokud rozhraní implementuje klient – SPI
    • nemá konstruktor ani factory, může ho implementovat kdokoliv
    • není možné vynucovat sémantiku (co kdyby String byl interface?)
  • final třída
    • podporuje různé úrovně přístupu, může poskytnout statické metody
    • lepší předpoklady pro evoluci – možno přidávat metody
  • abstraktní třída?
    • může mít statické metody (ale to už interface také)
    • nepodporuje vícenásobnou dědičnost (signatur)
    • omezení přístupových práv (včetně konstruktoru)
      • poskytuje kontrolu nad tím, kdo může implementovat abstraktní metody

Doporučení pro návrh tříd

Vyhněte se dědičnosti v SPI

  • třídy a abstraktní třídy bohaté na virtuální metody
  • málo dokumentované sémantické závislosti – implementační detail
  • problém odpadá při použití final tříd a interfaces

Nahraďte třídy s virtuálními metodami kompozicí

  • pomocí kombinace final tříd a (Java) interfaces
  • dejte jednotlivým metodám v API jasný účel
    • metoda je určena k volání klientem
    • metoda představuje slot pro implementaci
    • metoda je určena k volání odvozenou třídou
  • dejte jednotlivým typům v API jasný účel

Doporučení pro návrh tříd

Význam modifikátorů u metod v API

Modifikátory Primární význam Vedlejší významy
public Metoda určena k volání externími klienty API. Může být předefinována v odvozených třídách.
Může být volána z odvozených tříd.
public abstract Metoda musí být implementována v odvozených třídách. Může být volána externími klienty.
public final Metoda určená pouze k volání. Žádné.
protected Metoda může být volána z odvozených tříd. Může být předefinována v odvozených třídách.
protected abstract Metoda musí být implementována v odvozených třídách. Žádné.
protected final Metoda může být volána z odvozených tříd. Žádné.

Doporučení pro návrh tříd

Transformace metod s vedlejšími významy

Původní kód Transformace

                        public abstract void method ();
                    

                        public final void method () {
                          methodImpl ();
                        }

                        protected abstract void methodImpl ();
                    

                        public void method () {
                          someCode ();
                        }
                    

                        public final void method () {
                          methodImpl ();
                        }

                        protected abstract void methodImpl ();
                        protected final void someCode () {
                        }
                    

                        protected void method () {
                          someCode ();
                        }
                    

                        protected abstract void method ();
                        protected final void someCode () {
                        }
                    

Příklad: Kompozice jednoúčelových typů

Problém: klientské API, SPI, a API pro podtřídu v jedné třídě


                public abstract class AbstractSensor {
                  private final int i2cAddress;

                  protected AbstractSensor (int address) {
                    this.i2cAddress = address;
                  }

                  // 1. API pro klienty
                  public final double readTemperature () {
                    return readFromHardware ();
                  }

                  // 2. SPI poskytované podtřídou (implementace čtení)
                  protected abstract double readFromHardware ();

                  // 3. Služba pro podtřídu (konfigurace)
                  protected final int getI2cAddress () {
                    return i2cAddress;
                  }
                }
            

Příklad: Kompozice jednoúčelových typů

Řešení: samostatné třídy pro SPI a API pro pluginy (část 1)


                // Služba pro SPI (konfigurace)
                public final class SensorContext {
                  private final int i2cAddress;

                  SensorContext (int address) {
                    this.i2cAddress = address;
                  }

                  public int getI2cAddress () {
                    return i2cAddress;
                  }
                }

                // SPI - rozhraní pro tvůrce pluginu.
                public interface SensorProvider {
                  // Při inicializaci získá kontext s parametry.
                  void initialize (SensorContext context);

                  double readFromHardware ();
                }
            

Příklad: Kompozice jednoúčelových typů

Řešení: samostatná třída pro klienty (část 2)


                public final class Sensor {
                  private final SensorProvider provider;

                  private Sensor (SensorProvider provider) {
                    this.provider = provider;
                  }

                  // Factory metoda pro vytvoření senzoru.
                  public static Sensor create (int address, SensorProvider provider) {
                    SensorContext context = new SensorContext (address);
                    provider.initialize (context);
                    return new Sensor (provider);
                  }

                  // API určené pouze pro klienty.
                  public double readTemperature () {
                    return provider.readFromHardware ();
                  }
                }
            

Příklad: Kompozice jednoúčelových typů

Použití odděleného API a SPI v testech

  • Vytvoříme "dummy" implementaci SPI

                class DummyI2cSensor implements SensorProvider {
                    private int address;

                    @Override
                    public void initialize(SensorContext context) {
                        this.address = context.getI2cAddress();
                    }

                    @Override
                    public double readFromHardware() {
                        if (this.address == 0x42) {
                            // Čtení ze známé adresy.
                            return 22.5;
                        }

                        // Chyba při čtení z "nefuknční" adresy.
                        return -99.0;
                    }
                }
            

Příklad: Kompozice jednoúčelových typů

Použití odděleného API a SPI v testech

  • SPI můžeme testovat odděleně od API

                public class SensorTest {
                    @Test
                    public void sensorWorksInIsolation() {
                        DummyI2cSensor provider = new DummyI2cSensor();
                        SensorContext context = new SensorContext(0x42);
                        provider.initialize(context);

                        assertEquals(22.5, provider.readFromHardware());
                    }

                    @Test
                    public void sensorWorksThroughClientApi() {
                        Sensor sensor = Sensor.create(0x42, new DummyI2cSensor());

                        assertEquals(22.5, sensors.readTemperature());
                    }
                }
            

 

Návrh metod (pro API)

Doporučení pro návrh metod

Obecné zásady – detaily později

  • parametry a datové typy, počet a pořadí parametrů
  • návratové hodnoty a výjimky
  • přetěžujte opatrně – pozor na nejednoznačnosti
    • stejný počet parametrů, více parametrů stejného typu

Vynucujte dodržení kontraktu na rozhraní

  • odolnost, robustnost, bezpečnost
    • uživatel nesmí uvést váš kód do nekonzistentního stavu
  • více u defenzivního programování

Nenuťte uživatele dělat něco, co můžete sami

  • snižuje potřebu psát boilerplate kód
    • otravné, cut & paste, náchylné k chybám

Příklad: nenuťte uživatele dělat zbytečnosti

Java: serializace XML dokumentu


                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!
                  }
                }
            

Doporučení pro návrh metod

Nechystejte žádná překvapení

  • uživatele by nemělo překvapit chování metod
  • stojí za větší úsilí při implementaci
  • stojí za snížení výkonnosti

Příklad nesplněného očekávání v Javě


                public class Thread implements Runnable {
                  // Tests whether current thread has been interrupted.
                  // Clears the interrupted status of current thread
                  public static boolean interrupted();
                }
            

Doporučení pro návrh metod

Selžete rychle

  • o chybách dejte vědět co nejdříve po jejich vzniku
  • pokud to jde tak při překladu
  • za běhu pokud možno při prvním chybném volání

Příklad rychlého selhání


                public class Properties extends Hashtable {
                  public Object put(Object key, Object value);

                  // Throws ClassCastException if this properties
                  // contains any keys or values that are not Strings
                  public void save(OutputStream out, String comments);
                }
            

Doporučení pro návrh metod

Rozlišujte chyby programátora...

  • Špatné použití API — nedodržení kontraktu (preconditions)
    • např. předání null tam, kde nesmí být
  • Cílem je rychlé selhání (fail-fast) a oprava kódu, nikoliv obsluha chyb
    • např. výjimky jako IllegalArgumentException nebo IllegalStateException

... a chyby uživatele

  • Očekávatelné doménové chyby a problémy
    • např. platba zamítnuta, soubor neexistuje, uživatel zadal text místo čísla
  • Cílem je komunikace s uživatelem (případně opakování operace)
    • vraťte chybový objekt, nebo použijte specifické (dokumentované) doménové výjimky

Doporučení pro návrh metod

Příklad: různé chyby při zpracování platby


                public class PaymentProcessor {

                  public PaymentResult charge(CreditCard card, double amount) {
                    // 1. Chyba programátora: fail-fast
                    if (card == null) {
                      throw new IllegalArgumentException("Card cannot be null");
                    }
                    if (amount <= 0) {
                      throw new IllegalArgumentException("Amount must be positive");
                    }

                    // ... komunikace s bankou ...

                    // 2. Očekávatelná doménová chyba: výsledek, nikoliv výjimka
                    if (bankResponse.isDeclined()) {
                      return PaymentResult.declined(bankResponse.getReason());
                    }

                    return PaymentResult.success(bankResponse.getTransactionId());
                  }
                }