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.
Viditelné "uživatelské" rozhraní k abstrakcím
Select()
v C#public static void Select ( IList checkRead, IList checkWrite, IList checkError, int microseconds );
Select()
v C#public static void Select ( IList checkRead, IList checkWrite, IList checkError, int microseconds );
int select ( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout );
Select()
v C#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... } )
Select()
v C#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; }
IList
ani top-level objekt nemá Clone()
Clone()
, musí implementovat ICloneable
Select()
v C#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 }
Select()
v C#Select()
public static int Select ( ISet checkRead, ISet checkWrite, Timespan timeout, out ISet readable, out ISet writable, out ISet error );
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ů.
Vylepšené rozhraní má na vstupu množiny socketů pro čtení/zápis, u kterých jsou automaticky sledovány i chyby. Timeout umožňuje specifikovat velký rozsah hodnot a jeho nepřítomnost (null) může pokrýt potřebu nekonečného čekání. Místo přepisování vstupu vrací tři výstupní množiny socketů a návratová hodnota reprezentuje počet "ready" socketů, nebo 0 v případě timeoutu.
Přestože se nejedná o nejlepší API pro to, co se s pomocí funkce Select() dělá, ukazuje to, jak množství drobných nedostatků způsobuje nutnost psát složitý a těžce udržovatelný kód, který nemusel vůbec vzniknout.
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);
}
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); }
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);
}
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); }
static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey (); ThreadLocal.set (serialNumberKey, nextSerialNumber ()); System.out.println (ThreadLocal.get (serialNumberKey));
public final class ThreadLocal <T> { public ThreadLocal () { } public void set (T value); public T get (); }
public final class ThreadLocal <T> { public ThreadLocal () { } public void set (T value); public T get (); }
static ThreadLocal <Long> serialNumber = new ThreadLocal <> (); serialNumber.set (nextSerialNumber ()); System.out.println (serialNumber.get ());
Myslete na uživatele.
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)
Minimalizujte údiv uživatele.
"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.
V případě API je to podobně. Uživatel je "happy", když má věci pod kontrolou a dělají, co od nich očekává. Autor API (stejně jako autor UI) musí odhadnout "model uživatele" a přizpůsobit mu model programu (API).
Dělejte jen jednu věc a dělejte ji dobře.
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
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
"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
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 (); } }
if (car.speed () > 2 * SPEED_LIMIT) { generateAlert ("Watch out for cops!"); }
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.
String
byl interface?)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.
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é. |
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 () { } |
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++; } }
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++; } } ...
... 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; } }
@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 ()); }
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 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é.
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);
}