Spustit prezentaci

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

Tyto slajdy čerpají primárně ze skvělé přednášku a slajdů o návrhu API a jeho důležitosti, kterou přednesl Joshua Bloch (dříve Sun, dnes Google) na JavaPolis 2005: How to Design a Good API & Why it Matters (slajdy).

V podstatě stejné poselství lze nalézt i v článku Michi Henninga v ACM Queue: API: Design Matters.

Zajímavá je také přednáška z konference JavaOne 2006 od Tima Boudreaua a Jaroslava Tulacha: How to write API that will stand the test of time. S tím souvisí pěkný tutorial o návrhu API na webu NetBeans: How to Design a (module) API a v neposlední řadě kniha Jaroslava Tulacha, z níž také čerpám – Practical API Design: Confessions of a Java Framework Architect, APress, 2008.

K čemu je API?

API = Application Programming Interface


Rozhraní existuje mezi alespoň dvěma subjekty

Separace je hlavním důvodem pro vznik rozhraní

Rozhraní umožňuje odděleným částem komunikovat

Proč je API důležité?

API programů

API platforem

API webových služeb

K tématu rozšiřitelnosti má blízko (dlouhý a – v množství více než malém – filozofující) článek Steva Yeggea: The Pinocchio Problem.

Co je součástí API?

Co je součástí API?

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

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

Dobře navržené API je přínosem

Špatné navržené API je přítěží

Je potřeba ho trefit na poprvé

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
);

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 převzat z článku Michi Henninga API: Design Matters.

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...
  }
)
  • socketů může být mnoho, ale množiny se celkem nemění
  • chyba nás většinou zajímá u socketů pro čtení nebo zápis
    • typicky se oba seznamy slučují, tady je navíc nutné eliminovat duplicitní sockety
  • funkce nemá návratovou hodnotu a ničí předané parametry
  • timeout je v mikrosekundách, max. timeout je asi 35 minut
    • není jasné jak čekat nekonečně dlouho

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ů

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

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

Drobné nedostatky v rozhraní Select()

Vylepšené rozhraní funkce

public static bool Select (
  ISet checkRead, ISet checkWrite, Timespan timeout,
  out ISet readable, out ISet writable, out ISet error
);
Hlavním problémem je, že funkce Select() představuje pouze wrapper pro low-level funkci systému. Ale kdyby jen to – nejenže selhává v nápravě špatně navrženého API z dob minulých, navíc do všechno přidává ještě vlastní snůšku problémů.

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

Pokud programujete, navrhujete API

Je dobré myslet v intencích API

Co charakterizuje dobré API?

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

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

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

Co charakterizuje dobré API?

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

Je jednoduše rozšiřitelné

Je vhodné pro zamýšlené obecenstvo

Rozšiřitelnost API vs. SPI

API - kód poskytuje službu

SPI - kód vyžaduje službu

Nemíchat API a SPI

Životní cyklus API

API se vyvíjí...

Různá stádia vývoje API

Proces návrhu API

Proces návrhu API

1. Zjistěte požadavky na API

Proces návrhu API

2. Napište krátkou specifikaci

Proces návrhu API

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

Proces návrhu API

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

Pozor na design by comittee

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);
}

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?

Příklad je převzat z přednášky Joshuy Blocha.

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

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

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

  public static class Key { 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);
}

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

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

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

  public static class Key { 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));

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 ();
}

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é?

Pohled implementátora je druhořadý

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.

Viz také API Design: The Principle of Audience (Ben Pryor)

Obecné principy

Minimalizujte údiv uživatele.


Snažte se uživatele nepřekvapit

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

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.

Obecné principy

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


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

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

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ší

"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

Obecné principy

Implementace by neměla ovlivňovat API

Minimalizujte přístupnost všeho

Obecné principy

Snažte se o typovou a běhovou konzistenci

Obecné principy

Snažte se o typovou a běhovou konzistenci

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

Buďte otevření možnostem refaktoringu

Obecné principy

Dokumentujte ve velkém

Dokumentujte v malém

Na dokumentaci záleží – pokud chybí

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ě horší, 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 – úroveň detailu by měla být právě dostačující k tomu, aby uživatel mohl s rozhraním pracovat.

Obecné principy

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

Pozor na multiplatformní/portovaná API

Návrh tříd (pro API)

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

Obecné zásady – detaily později

Usilujte o flexibilitu při zachování jednoduchosti

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

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

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

Vyhněte se dědičnosti v SPI

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

Viz. J. Tulach: Practical API Design, kapitola 10.

Velkým problémem v návrhu API stále zůstává dědičnost – specificky bohaté třídy se spoustou virtuálních metod a jejich implementacemi. Metody se mohou navzájem volat a třídy mohou tyto metody libovolně předefinovat a vytvářet sémantické závislosti, které jsou většinou považovány za implementační detail. Bez těchto detailů však téměř není možné korektně napsat odvozenou třídu a je nutné studovat zdrojový kód. Taková je např. situace v případě třídy javax.swing.JComponent. Nutnost číst zdrojový kód pro správné použití API však indikuje problematický návrh API samotného. V podstatě se dá říct, že v případě tříd s velkým množstvím různě provázaných virtuálních metod jsou problémy garantovány. Proto bývá nejlepší takové třídy z API eliminovat, cehož je možné docílit kombinací final tříd a interfaces.

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ů

Původní třída: API a SPI v jedné třídě

public abstract class MixedClass {
  private int counter;
  private int sum;
  
  protected MixedClass () {
    super ();
  }
  
  public final int apiForClients () {
    sum += toBeImplementedBySubclass ();
    return sum / counter;
  }
  
  protected abstract int toBeImplementedBySubclass ();
  
  protected final void toBeCalledBySubclass () {
    counter++;
  }
}

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

Nová třída: oddělené API a SPI

public final class NonMixed {
  private int counter;
  private int sum;
  private final Provider provider;
  
  public interface Provider {
    public void initialize (Callback cb);
    public int toBeImplementedBySubclass ();
  }
  
  public static final class Callback {
    private NonMixed api;
    
    Callback (NonMixed api) {
      api = api;
    }
    
    public final void toBeCalledBySubclass () {
      api.counter++;
    }
  }
  ...

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

Nová třída: oddělené API a SPI

  ...
  private NonMixed (Provider provider) {
    provider = provider;
  }
  
  public static NonMixed create (Provider provider) {
    NonMixed api = new NonMixed (provider);
    Callback callback = new Callback (api);
    provider.initialize (callback);
    return api;
  }
    
  public final int apiForClients () {
    sum += provider.toBeImplementedBySubclass ();
    return sum / counter;
  }
}

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

Použití nové třídy

@Test public void useWithoutMixedMeanings () {
  class AddFiveMixedCounter implements NonMixed.Provider {
    private Callback callback;

    public int toBeImplementedBySubclass () {
      callback.toBeCalledBySubclass ();
      return 5;
    }

    public void initialize (Callback callback) {
      callback = callback;
    }
  }
  
  NonMixed add5 = NonMixed.create (new AddFiveMixedCounter ());
  assertEquals ("5/1 = 5", 5, add5.apiForClients ());
  assertEquals ("10/2 = 5", 5, add5.apiForClients ());
  assertEquals ("15/3 = 5", 5, add5.apiForClients ());
}

Návrh metod (pro API)

Doporučení pro návrh metod

Obecné zásady – detaily později

Vynucujte dodržení kontraktu na rozhraní

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

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

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.

Doporučení pro návrh metod

Nechystejte žádná překvapení

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 ();
}       

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é.

Viz také API Design: The Principle of Least Surprise.

Doporučení pro návrh metod

Selžete rychle

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);
}