440 likes | 622 Views
CSCE 431: Application Programming Interface (API) Design. Outline. Introduction About a Good API Interface Resources Error Handling Inheritance and API API and Performance. Library vs. Application. So far have discussed models for single applications
E N D
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Library vs. Application • So far have discussed models for single applications • Transforming models to code for applications • Discussion of contracts • Today • Considerations when building libraries • Contracts for libraries: APIs • Practical guidance • References • Kernighan and Pike, The Practice of Programming • Joshua Bloch, Library-Centric Software Design 2005 Keynote: “How to Design a Good API and Why It Matters” CSCE 431 API Design
Application Programming Interface (API) • Source code interface • For a library or an OS • Provides services to a program • At its base, like a header file (list of function signatures) • But more than just function signatures • Call protocols, contracts, resource handling, error handling CSCE 431 API Design
Why API Design Matters • Company View • Can be an asset • Big user investment in learning and using • Bad design can be source of long-term support problems • Cost to discontinue an API high, customers will not abandon an API easily • Once in use, API difficult to change • Especially if there are several users • Public APIs – One chance to get it right CSCE 431 API Design
Why API Design Matters • Individual programmer view • Every programmer is an API designer • APIs exist between modular pieces of code • Useful modules get clients, API can no longer be changed without affecting those clients CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Characteristics of a Good API • Easy to learn • Easy to use even without documentation • Hard to misuse • Easy to read and maintain code that uses it • Sufficiently powerful to satisfy requirements • Easy to extend • Appropriate to audience CSCE 431 API Design
Designing an API: “Eat your own dog food” • Write to your API early and often • Even if not yet implemented • Even if not yet fully specified • These avoid wasted implementation and specification effort • Code you write lives on as unit tests and example uses (documentation) • Expect the API to evolve • “The rule of Threes” (Will Tracz:, Confessions of A Used Program Salesman, Addison-Wesley, 1995) • Write one client before release, will not support another • Write two clients before release, will support more with difficulty • Write three clients before release, will work fine CSCE 431 API Design
Broad Issues to Consider in API Design • Interface • Classes, methods, parameters, names • Resource Management • How is memory, other resources dealt with • Error Handling • What errors are caught and what is done to handle them • Information Hiding • How much detail is exposed • Impacts all three of the above CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Interface • Simple • General • Regular • Predictable • Robust • Adaptable CSCE 431 API Design
Interface | Simple • Users have to understand API features • Can you come up with logical, concise name for each function? • If it is hard to name, likely hard to understand • Good names guide development • Do one thing and do it well • Functionality should be easy to explain • As small as possible — orthogonal primitives • But be mindful of performance • (Stepanov’s notion of a computational basis) • When in doubt, leave a feature out (can add later, but cannot remove!) • Do not provide three ways to do the same thing! CSCE 431 API Design
Interface | Simple | Long Parameter Lists // Eleven parameters including four consecutive ints CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, intx, inty, intnWidth, intnHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam); • Three or fewer parameters (ideally) • Long lists of parameters with the same type especially bad • Invitations to easy errors that go undetected by the compiler • To shorten parameter lists • Break up methods • Group parameters into helper classes CSCE 431 API Design
Interface | General • Implementation can change, API cannot • Hide Information • Do not let implementation detail leak into API • E.g. on-disk formats, implementation related exceptions • Goal: Modules can be used, understood, built, tested, and debugged independently • Be aware of what is implementation • There are subtle ways to over-specify behavior of modules • Tuning parameters are suspect • hash table size, load factor • capacityIncrement for Java’s Vector • By setting capacityIncrement, one can go from O(n) to O(n2) insertion cost Vector(intinitialCapacity, intcapacityIncrement); CSCE 431 API Design
Interface | General Joshua Bloch: . . . I believe that we erred by allowing implementation details (such as hash table size and load factor) into our APIs. The client should perhaps tell us the maximum expected size of a collection, and we should take it from there. Clients can easily do more harm than good by choosing values for these parameters. As an extreme example, consider Vector’s capacityIncrement. No one should ever set this, and we shouldn’t have provided it. If you set it to any non-zero value, the asymptotic cost of a sequence of appends goes from linear to quadratic. In other words, it destroys your performance. Over time, we’re beginning to wise up about this sort of thing. If you look at IdentityHashMap, you’ll see that it has no low-level tuning parameters… CSCE 431 API Design
Interface | Regular • Names matter • API is a Domain-Specific Language (DSL) • Aim for self-explanatory names if (file.size() > MAX_SIZE) throw oversized_file_exception(file.name()); • Same word means the same thing everywhere • Same thing named with the same word everywhere • Same form everywhere: nbytes, nfiles, . . . • Consistency beyond names • Prime example: STL • Do the same thing the same way everywhere • Related things should be achieved by related means • Consistent parameter ordering, required inputs char* strcpy (char *dest, char *src); void bcopy (void *src, void *dst, intn); • Return types, error handling, resource management • Do what is customary (core APIs, language idioms, conventions) CSCE 431 API Design
Interface | Predictable • Follow the Principle of Least Astonishment • User should not be surprised by behavior • Even if this costs performance public class Thread implements Runnable { // Tests whether current thread has been interrupted. // Clears the interrupted status of current thread. public static booleaninterrupted(); // .... } • Do not reach behind the user’s back • Accessing and modifying global variables • Secret files or information written • Static variables • Minimize use of other interfaces, self-contained • Be explicit about external services required CSCE 431 API Design
Interface | Robust • Able to deal with unexpected input • Error Handling (see later) CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Resource Management • Determine which side is responsible for • Initialization • Maintaining state • Sharing and copying • Cleaning up • Various resources • Memory • Files • Global variables • GUI handles • Database handles CSCE 431 API Design
Resource Management • Generally, free resources where they were allocated • Use RAII in C++, emulate RAII in other languages • RAII – Resource Allocation is Iniitalization • Allocate resources on initialization, release with destruction • Return references or copies? • Impacts performance, ability to reason • C++ move semantics(?) • Multi-threaded code places its own restrictions • Reentrant: works regardless of number of simultaneous executions • Reentrancy destroyed by using or exposing static data (globals, static locals) that other threads could also use CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Error Handling • Catch errors, do not ignore them • “Print message and fail” is not always good • E.g. airplane autopilot • Especially in APIs, as calling context not known • Need to allow programs to recover or save data • Detect at low level, but handle at high level • Generally, error should be handled by the caller • The callee should leave things in a “nice” state for recovery • Keep things usable in case the caller can recover • Resources deallocated, invariants maintained CSCE 431 API Design
Indicating Errors • Return values • Should be in form caller can use • Return as much useful information as possible • Define pairs, or return another parameter to indicate errors • Sentinel values (such as -1 to indicate error) only work if function cannot return all possible values of that type • Drawback: Signifying an error in the return type forces one to check after each call (contrast to exceptions) • Global variables as error codes? (No) • Breaks under concurrency. Example: while (write(fd, buffer, size) == -1) { if (errno != EINTR) { fprintf(myerrlog, "%s\n", strerror(errno)); exit(1); } } • errno is magic (it’s a thread-local variable), myerrlog is not • Special treatment of errno is a showcase of a distortion that is considered necessary to adapt an API that really cannot be adapted CSCE 431 API Design
Exceptions • Operates by unwinding call stack until handler found • Exceptions should include information about failure for repair and debugging • Exceptions should generally be unchecked • Process globally, rather than require explicit checks over and over • Can be significantly slower than returning from a function • Some compilers optimize for the non-exception case, at the expense of the exception case • Exceptions thus best if reserved for exceptional cases, instead of using as just another control flow mechanism • E.g., invalid file name passed to a library is probably normal, not an exception CSCE 431 API Design
Exception Safety in Generic Components • Exception safety of non-generic components can be inspected in isolation • Exception safety of a generic components is more complex • Depends on behavior of its parameters • Dave Abraham’s Exception Safety Guarantees gives a clear characterization of exception safety, esp. for generic components • Contractbetween a component and its clients/parameters: • Component places assumptions on the behavior of its parameters • If assumptions hold, component’s exception safety properties guaranteed • Dave Abrahams: Exception Safety in Generic Components, www.boost.org/community/exception_safety.html • For example, assuming that ~T() does not throw, operations of X<T> can offer certain safety guarantees CSCE 431 API Design
Abrahams’ Categorization • The basic guarantee: invariants of component are preserved, no resources are leaked • Basically, an object can be assigned to • The strong guarantee: that the operation has either • Completed successfully, or • Thrown an exception, leaving the program state exactly as it was before the operation started • The no-throw guarantee: that the operation will not throw an exception http://www.boost.org/community/exception_safety.html CSCE 431 API Design
Basic Guarantee template <class X> intrandom_number_generator(intn) { { vector<X> v(n); try { // insert provides the basic guarantee v.insert(v.begin(), X()); } catch(...) {} // ignore any exceptions above return v.size(); } • “Safe” == not crashing, but results undefined CSCE 431 API Design
Strong Guarantee • “Commit-or-rollback” semantics • For operations of e.g. C++ standard containers, this means: • If exception is thrown, all iterators remain valid • Container has exactly the same elements as before the exception was thrown • Strong guarantee has a big benefit: • The program state is simple and predictable in case of an exception • In the C++ standard library, nearly all of the operations on the node-based containers list, set, multiset, map, and multimapprovide the strong guarantee CSCE 431 API Design
No-Throw Guarantee • Means that an operation is guaranteed to not throw an exception • Necessary for most destructors • Destructors of C++ standard library components are all guaranteed not to throw exceptions CSCE 431 API Design
Guarantees Can Be Provisional • std::vector<T>::erase() gives • basic guarantee for any T • no-throw guarantee, if T’s copy constructor and copy assignment operator do not throw CSCE 431 API Design
“Natural” Exception Safety Guarantee • Strongest guarantee ideal to the client, but • Stronger guarantee may sacrifice performance • Abraham’s observation on STL: tends to be a “natural” level of guarantee for most (all) operations CSCE 431 API Design
Example • Can vector<T>::insert have the strong guarantee? • Inserting in the middle requires copying • If copying of an element fails, must roll back • But “undoing” copies already done seems likely to fail again • Strong guarantee thus not plausible • Another way: • Build new array every time, only destroy the old one after all copying has succeeded • Increases cost, since full array copy required each time • More failure possibilities as more elements copied • The most “natural” level of safety for vector<T>::insert seems to be the basic guarantee • Too strong a guarantee may restrict a library to an unnecessarily inefficient implementation CSCE 431 API Design
From Basic to Strong • General pattern for strengthening the exception safety guarantee of some operation op: template <class Container, class BasicOp> void make_op_strong( Container& c, constBasicOp& op ) { Container tmp(c); // Copy c op(tmp); // Work on the copy c.swap(tmp); // Cannot fail } CSCE 431 API Design
Determining Exception Safety Guarantees // SearchableStack - A stack which can be efficiently searched for any value. template <class T> class SearchableStack { public: voidpush(constT& t); // O(log n) void pop(); // O(log n) boolcontains(constT& t) const; // O(log n) constT& top() const; // O(1) private: std::set<T> set_impl; std::list<std::set<T>::iterator> list_impl; }; template <class T> void SearchableStack<T>::push(constT& t) { set<T>::iterator i = set_impl.insert(t); // 2 try // 3 { // 4 list_impl.push_back(i); // 5 } // 6 catch(...) // 7 { // 8 set_impl.erase(i); // 9 throw; // 10 } // 11 } • Can pushhave the strong guarantee? CSCE 431 API Design
Example Continued template <class T> void SearchableStack<T>::push(constT& t) { set<T>::iterator i = set_impl.insert(t); // 2 try // 3 { // 4 list_impl.push_back(i); // 5 } // 6 catch(...) // 7 { // 8 set_impl.erase(i); // 9 throw; // 10 } // 11 } • Invariant: the set and list should always have the same number of elements; each of the set’s elements should be referenced by an element of the list. • line 2: if insertion fails and set_impl is modified, invariant violated • Must be able to rely on strong guarantee of set<T>::insert • line 5: if push_back fails, but list_impl is modified, invariant violated • Must be able to rely on strong guarantee of list<T>::push_back • line 9: rolling back the insertion to the set • Roll-back cannot fail; erase must be no-throw CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Inheritance • Inheriting from a library class is also use of an API • Inheritance easily conflicts with encapsulation, see e.g.: • Snyder: Encapsulation and Inheritance in Object-Oriented Programming Languages, OOPSLA’86 • Steyaer, Lucas, Mens, D’Hondt: Reuse Contracts: Managing the Evolution of Reusable Assets, OOPSLA’96 • Mikhajlov, Sekerinski: A Study of the Fragile Base Class Problem, ECOOP’98 • Derived class can break the invariant of the base class by modifying member variables directly • Modification in a base (i.e., when providing a new version of a library) can break the invariant of a derived class CSCE 431 API Design
Open Recursion • The problem arises because of open recursion • One method can invoke another method in the same class, and the invocation uses dynamic binding class A { virtual m() { ... n(); ... } virtual n() { ... } }; class B : public A { virtual n() { ... } }; • Calling A::m()ends up as a call to B::n(), which breaks A’s encapsulation • Changing the implementation of A::m(), e.g., to not call n, may break B CSCE 431 API Design
Fragile Base Class Problem, example public class InstrumentedHashSetextends HashSet { // The number of attempted element insertions private intaddCount = 0; public InstrumentedHashSet () {} public InstrumentedHashSet(Collection c) { super(c); } public booleanadd(Object o) { addCount++; return super.add(o); } public booleanaddAll(Collection c) { addCount+= c.size(); return super.addAll(c); } public intgetAddCount() { return addCount; } } InstrumentedHashSet s = new InstrumentedHashSet(); s.addAll(Arrays.asList(new String[] {"Java", "C++", "Python"})); s.getAddCount(); • Should we have relied on HashSet.addAll()calling add()? • Based on the API documentation, perhaps Item 16 of Joshua Bloch: Effective Java, Addison-Wesley, 2001 CSCE 431 API Design
Java Documentation Suggestion, But Not Definite addAll public booleanaddAll(Collection c) Adds all of the elements in the specified collection to this collection (optional operation). The behavior of this operation is undefined if the specified collection is modified while the operation is in progress. (This implies that the behavior of this call is undefined if the specified collection is this collection, and this collection is nonempty.) This implementation iterates over the specified collection, and adds each object returned by the iterator to this collection, in turn. Note that this implementation will throw an UnsupportedOperationExceptionunless addis overridden (assuming the specified collection is non-empty). CSCE 431 API Design
Outline • Introduction • About a Good API • Interface • Resources • Error Handling • Inheritance and API • API and Performance CSCE 431 API Design
Performance • API design decisions can limit performance • Example of a performance-wise bad decision in Java Libraries API • Dimension Component.getSize() • Dimension is a mutable class (most Java classes are) • A component cannot just return a reference to the internal Dimension object Dimension dim = comp.getSize(); dim.height *= 2; • Every getSize() call will allocate/construct a new Dimensionobject • The above example is known as defensive copying CSCE 431 API Design