API Design for CPP Notes - yszheda/wiki GitHub Wiki

Chap.1 Introduction

1.1.2 APIs in C++

  1. Headers
  2. Libraries
  3. Documentation

Tip: An API is a logical interface to a software component that hides the internal details required to implement it.

1.2 WHAT’S DIFFERENT ABOUT API DESIGN?

  • An API is an interface designed for developers
  • Multiple applications can share the same API.
  • You must strive for backward compatibility whenever you change an API.
  • Due to the backward compatibility requirement, it is critical to have a change control process in place.
  • APIs tend to live for a long time.
  • The need for good documentation is paramount when writing an API, particularly if you do not provide the source code for your implementation.
  • The need for automated testing is similarly very high.

Tip: An API describes software used by other engineers to build their applications. As such, it must be well-designed, documented, regression tested, and stable between releases.

1.3 WHY SHOULD YOU USE APIs?

1.3.1 More Robust Code

// TODO

Chap.2 Qualities

2.1 MODEL THE PROBLEM DOMAIN

2.1.1 Provide a Good Abstraction

2.1.2 Model the Key Objects

2.2 HIDE IMPLEMENTATION DETAILS

2.2.1 Physical Hiding: Declaration versus Definition

Tip: A declaration introduces the name and type of a symbol to the compiler. A definition provides the full details for that symbol, be it a function body or a region of memory. Tip: Physical hiding means storing internal details in a separate file (.cpp) from the public interface (.h).

2.2.2 Logical Hiding: Encapsulation

Tip: Encapsulation is the process of separating the public interface of an API from its underlying implementation. Tip: Logical hiding means using the C ++ language features of protected and private to restrict access to internal details.

2.2.3 Hide Member Variables

2.2.4 Hide Implementation Methods

Tip: Never return non-const pointers or references to private data members. This breaks encapsulation. Tip: Prefer declaring private functionality as static functions within the .cpp file rather than exposing them in public headers as private methods. (Using the Pimpl idiom is even better though.)

2.2.5 Hide Implementation Classes

2.3 MINIMALLY COMPLETE

2.3.1 Don’t Overpromise

Tip: When in doubt, leave it out! Minimize the number of public classes and functions in your API.

2.3.2 Add Virtual Functions Judiciously

potential pitfalls:

  • You can implement seemingly innocuous changes to your base classes that have a detrimental impact on your clients. “fragile base class problem”
  • Your clients may use your API in ways that you never intended or imagined. This can result in the call graph for your API executing code that you do not control and that may produce unexpected behavior.
  • Clients may extend your API in incorrect or error-prone ways.
  • Overridden functions may break the internal integrity of your class.

Herb Sutter: Non-Virtual Interface idiom (NVI)

Tip: Avoid declaring functions as overridable (virtual) until you have a valid and compelling need to do so.

2.3.3 Convenience APIs

Tip: Add convenience APIs as separate modules or libraries that sit on top of your minimal core API.

2.4 EASY TO USE

2.4.1 Discoverable

A discoverable API is one where users are able to work out how to use the API on their own, without any accompanying explanation or documentation.

2.4.2 Difficult to Misuse

Tip: Prefer enums to booleans to improve code readability. Tip: Avoid functions with multiple parameters of the same type.

2.4.3 Consistent

Tip: Use consistent function naming and parameter ordering.

  • static polymorphism

2.4.4 Orthogonal

Tip: An orthogonal API means that functions do not have side effects.

  1. Reduce redundancy. Ensure that the same information is not represented in more than one way. There should be a single authoritative source for each piece of knowledge.
  2. Increase independence. Ensure that there is no overlapping of meaning in the concepts that are exposed. Any overlapping concepts should be decomposed into their basal components.

2.4.5 Robust Resource Allocation

Tip: Return a dynamically allocated object using a smart pointer if the client is responsible for deallocating it.

  • RAII (Resource Acquisition Is Initialization)

Tip: Think of resource allocation and deallocation as object construction and destruction.

2.4.6 Platform Independent

Tip: Never put platform-specific #if or #ifdef statements into your public APIs. It exposes implementation details and makes your API appear different on different platforms.

2.5 LOOSELY COUPLED

  • Coupling. A measure of the strength of interconnection between software components, that is, the degree to which each component depends on other components in the system.
  • Cohesion. A measure of how coherent or strongly related the various functions of a single software component are.

Tip: Good APIs exhibit loose coupling and high cohesion.

2.5.1 Coupling by Name Only

Tip: Use a forward declaration for a class unless you actually need to #include its full definition.

2.5.2 Reducing Class Coupling

Scott Meyers recommends that whenever you have a choice, you should prefer declaring a function as a non-member non-friend function rather than as a member function. Tip: Prefer using non-member non-friend functions instead of member functions to reduce coupling.

2.5.3 Intentional Redundancy

Tip: Data redundancy can sometimes be justified to reduce coupling between classes.

2.5.4 Manager Classes

2.5.5 Callbacks, Observers, and Notifications

2.6 STABLE, DOCUMENTED, AND TESTED

Chap. 3 Patterns

3.2.3 Singleton versus Dependency Injection

Dependency injection is a technique where an object is passed into a class (injected) instead of having the class create and store the object itself.

3.2.4 Singleton versus Monostate

Tip: Consider using Monostate instead of Singleton if you don’t need lazy initialization of global data or if you want the singular nature of the class to be transparent.

3.3 FACTORY METHODS

Constructors in C ++ have several limitations, such as the following.

  1. No return result.
  2. Constrained naming.
  3. Statically bound creation.
  4. No virtual constructors.

Tip: Use Factory Methods to provide more powerful class construction semantics and to hide subclass details.

3.4 API WRAPPING PATTERNS

3.4.1 The Proxy Pattern

  • a single class in the proxy API maps to a single class in the original API
  • This pattern is often implemented by making the proxy class store a copy of, or more likely a pointer to, the original class.
  • An alternative solution is to augment this approach by using an abstract interface that is shared by both the proxy and original APIs. This is done to try and better keep the two APIs synchronized, although it requires you to be able to modify the original API.

Tip: A Proxy provides an interface that forwards function calls to another interface of the same form.

A Proxy pattern is useful to modify the behavior of the Original class while still preserving its interface. This is particularly useful if the Original class is in a third-party library and hence not easily modifiable directly.

  1. Implement lazy instantiation of the Original object.
  2. Implement access control to the Original object.
  3. Support debug or “dry run” modes.
  4. Make the Original class be thread safe.
  5. Support resource sharing. Flyweight pattern, where multiple objects share the same underlying data to minimize memory footprint.
  1. Protect against future changes in the Original class.

3.4.2 The Adapter Pattern

Tip: An Adapter translates one interface into a compatible but different interface.

It should be noted that adapters can be implemented using composition (object adapters) or inheritance (class adapters). (This could be done using public inheritance if you wanted to also expose the interface of Rectangle in your adapter API, although it is more likely that you would use private inheritance so that only your new interface is made public.)

  1. Enforce consistency across your API.
  2. Wrap a dependent library of your API.
  3. Transform data types.
  4. Expose a different calling convention for your API.

3.4.3 The Façade Pattern

In effect, it defines a higher-level interface that makes the underlying subsystem easier to use. To use Lakos’ categorization, the Façade pattern is an example of a multicomponent wrapper (Lakos, 1996). Façade is therefore different from Adapter because Façade simplifies a class structure, whereas Adapter maintains the same class structure.

Tip: A Façade provides a simplified interface to a collection of other classes. In an encapsulating façade, the underlying classes are not accessible.

  1. Hide legacy code.
  2. Create convenience APIs.
  3. Support reduced- or alternate-functionality APIs.

3.5 OBSERVER PATTERN

Tip: An Observer lets you decouple components and avoid cyclic dependencies.

3.5.1 Model–View–Controller

  1. Segregation of Model and View components makes it possible to implement several user interfaces that reuse the common business logic core.
  2. Duplication of low-level Model code is eliminated across multiple UI implementations.
  3. Decoupling of Model and View code results in an improved ability to write unit tests for the core business logic code.
  4. Modularity of components allows core logic developers and GUI developers to work simultaneously without affecting the other.

Tip: The MVC architectural pattern promotes the separation of core business logic, or the Model, from the user interface, or View. It also isolates the Controller logic that affects changes in the Model and updates the View.

3.5.2 Implementing the Observer Pattern

3.5.3 Push versus Pull Observers

CHAPTER 4 Design

4.2 GATHERING FUNCTIONAL REQUIREMENTS

  • Business requirements: describe the value of the software in business terms, that is, how it advances the needs of the organization.
  • Functional requirements: describe the behavior of the software, that is, what the software is supposed to accomplish.
  • Non-functional requirements: describe the quality standards that the software must achieve, that is, how well the software works for users.

4.3.4 Requirements and Agile Development

4.4 ELEMENTS OF API DESIGN

Grady Booch suggests that there are two important hierarchical views of any complex system (Booch et al., 2007):

  1. Object Hierarchy: Describes how different objects cooperate in the system. This represents a structural grouping based on a “part of” relationship between objects.
  2. Class Hierarchy: Describes the common structure and behavior shared between related objects. It deals with the generalization and specialization of object properties. This can be thought of as an “is a” hierarchy between objects.

4.6.4 Liskov Substitution Principle

The LSP states that if S is a subclass of T, then objects of type T can be replaced by objects of type S without any change in behavior.

Tip: The LSP states that it should always be possible to substitute a base class for a derived class without any change in behavior.

Private Inheritance

Private inheritance lets you inherit the functionality, but not the public interface, of another class. In essence, all public members of the base class become private members of the derived class.

Composition

instead of class S inheriting from T, S declares T as a private data member (“has-a”) or S declares a pointer or reference to T as a member variable (“holds-a”).

Tip: Prefer composition to inheritance. (Sutter and Alexandrescu, 2004)

4.6.5 The Open/Closed Principle

Bertrand Meyer introduced the Open/Closed Principle (OCP) to state the goal that a class should be open for extension but closed for modification (Meyer, 1997).

The principal idea behind the OCP is that once a class has been completed and released to users, it should only be modified to fix bugs. However, new features or changed functionality should be implemented by creating a new class.

Tip: Your API should be closed to incompatible changes in its interface, but open to extensibility of its functionality.

4.6.6 The Law of Demeter

The Law of Demeter (LoD), also known as the Principle of Least Knowledge, is a guideline for producing loosely coupled designs.

When applied to object-oriented design, the LoD means that a function can:

  • Call other functions in the same class.
  • Call functions on data members of the same class.
  • Call functions on any parameters that it accepts.
  • Call functions on any local objects that it creates.
  • Call functions on a global object (but you should never have globals).

Tip: The Law of Demeter (LoD) states that you should only call functions in your own class or on immediately related objects.

4.6.7 Class Naming

4.7 FUNCTION DESIGN

4.7.2 Function Naming

  • Functions that answer yes or no queries should use an appropriate prefix to indicate this behavior, such as Is, Are, or Has, and should return a bool result
  • Functions that form natural pairs should use the correct complementary terminology.

4.7.3 Function Parameters

Named Parameter Idiom (NPI)

4.7.4 Error Handling

Ken Pugh’s Three Laws of Interfaces (Pugh, 2006):

  1. An interface’s implementation shall do what its methods say it does.
  2. An interface’s implementation shall do no harm.
  3. If an interface’s implementation is unable to perform its responsibilities, it shall notify its caller.

Accordingly, the three main ways of dealing with error conditions in your API are

  1. Returning error codes.
  2. Throwing exceptions.
  3. Aborting the program.

CHAPTER 7 Performance

Tip: Don’t warp your API to achieve high performance.

7.1 PASS INPUT ARGUMENTS BY CONST REFERENCE

7.2 MINIMIZE #INCLUDE DEPENDENCIES

7.2.1 Avoid “Winnebago” Headers

7.2.2 Forward Declarations

A forward declaration can be used when

  1. The size of the class is not required. If you include the class as a member variable or subclass from it, then the compiler will need to know the size of the class.
  2. You do not reference any member methods of the class. Doing so would require knowing the method prototype: its argument and return types.
  3. You do not reference any member variables of the class; but you already know to never make those public (or protected).

Tip: As a rule of thumb, you should only need to #include the header file for a class if you use an object of that class as a data member in your own class or if you inherit from that class.

Tip: Only forward declare symbols from your own API.

Tip: A header file should #include or forward declare all of its dependencies.

7.2.3 Redundant #include Guards

// Deprecated?

Tip: Consider adding redundant #include guards to your headers to optimize compile time for your clients.



7.3 DECLARING CONSTANTS

Tip: Declare global scope constants with extern or declare constants in classes as static const. Then define the value of the constant in the .cpp file. This reduces the size of object files for modules that include your headers. Even better, hide these constants behind a function call.

7.3.1 The New constexpr Keyword

7.4 INITIALIZATION LISTS

7.5 MEMORY OPTIMIZATION

7.6 DON’T INLINE UNTIL YOU NEED TO

implications:

  1. Exposing implementation details.
  2. Code embedded in client applications.
  3. Code bloat.
  4. Debugging complications.

Tip: Avoid using inlined code in your public headers until you have proven that your code is causing a performance problem and confirmed that inlining will fix that problem.

7.7 COPY ON WRITE

Flyweight design pattern, which describes objects that share as much memory as possible to minimize memory consumption (Gamma et al., 1994).

7.8 ITERATING OVER ELEMENTS

7.8.2 Random Access

  1. The [] operator. This is meant to simulate the array indexing syntax of C/C ++ . Normally this operator is implemented without any bounds checking so that it can be made very efficient.
  2. The at() method. This method is required to check if the supplied index is out of range and throw an exception in this case. As a result, this approach can be slower than the [] operator.

7.8.3 Array References

Tip: Adopt an iterator model for traversing simple linear data structures. If you have a linked list or tree data structure, then consider using array references if iteration performance is critical.

7.9 PERFORMANCE ANALYSIS

CHAPTER 8 Versioning

8.1.3 Creating a Version API

// version.h
#include <string>

#define API_MAJOR 1
#define API_MINOR 2
#define API_PATCH 0

class Version
{
  public:
    static int GetMajor();
    static int GetMinor();
    static int GetPatch();
    static std::string GetVersion();
    static bool IsAtLeast(int major, int minor, int patch);
    static bool HasFeature(const std::string &name);
};

8.2 SOFTWARE BRANCHING STRATEGIES

8.3 LIFE CYCLE OF AN API

8.4 LEVELS OF COMPATIBILITY

8.4.1 Backward Compatibility

  1. Functional compatibility
  2. Source compatibility
  3. Binary compatibility

8.4.2 Functional Compatibility

Functional compatibility is concerned with the run-time behavior of an implementation. An API is functionally compatible if it behaves exactly the same as a previous version of the API.

Tip: Functional compatibility means that version N + 1 of your API behaves the same as version N.

8.4.3 Source Compatibility (API compatibility)

users can recompile their programs using a newer version of the API without making any change to their code.

Tip: Source compatibility means that a user who wrote code against version N of your API can also compile that code against version N + 1 without changing their source.

8.4.4 Binary Compatibility (ABI compatibility)

Binary compatibility implies that clients only need to relink their programs with a newer version of a static library or simply drop a new shared library into the install directory of their end-user application.

Tip: Binary compatibility means that an application written against version N of your API can be upgraded to version N + 1 by simply replacing or relinking against the new dynamic library for your API.

Binary-Incompatible API Changes:

  • Removing a class, method, or function.
  • Adding, removing, or reordering member variables for a class.
  • Adding or removing base classes from a class.
  • Changing the type of any member variable.
  • Changing the signature of an existing method in any way.
  • Adding, removing, or reordering template arguments.
  • Changing a non-inlined method to be inlined.
  • Changing a non-virtual method to be virtual, and vice versa.
  • Changing the order of virtual methods.
  • Adding a virtual method to a class with no existing virtual methods.
  • Adding new virtual methods (some compilers may preserve binary compatibility if you only add new virtual methods after existing ones).
  • Overriding an existing virtual method (this may be possible in some cases, but is best avoided).

Binary-Compatible API Changes:

  • Adding new classes, non-virtual methods, or free functions.
  • Adding new static variables to a class.
  • Removing private static variables (if they are never referenced from an inline method).
  • Removing non-virtual private methods (if they are never called from an inline method).
  • Changing the implementation of an inline method (however, this requires recompilation to pick up the new implementation).
  • Changing an inline method to be non-inline (however, this requires recompilation if the implementation is also changed).
  • Changing the default arguments of a method (however, this requires recompilation to actually use the new default argument).
  • Adding or removing friend declarations from a class.
  • Adding a new enum to a class.
  • Appending new enumerations to an existing enum.
  • Using unclaimed remaining bits of a bit field.

Tips:

  • Instead of adding parameters to an existing method, you can define a new overloaded version of the method.
  • The pimpl idom can be used to help preserve binary compatibility of your interfaces because it moves all of the implementation details—those elements that are most likely to change in the future—into the .cpp file where they do not affect the public .h files.
  • Adopting a flat C style API can make it much easier to attain binary compatibility simply because C does not offer you features such as inheritance, optional parameters, overloading, exceptions, and templates.
  • If you do need to make a binary-incompatible change, then you might consider naming the new library differently so that you don’t break existing applications.

8.4.5 Forward Compatibility

8.5 HOW TO MAINTAIN BACKWARD COMPATIBILITY

8.5.1 Adding Functionality

  • adding new pure virtual member functions to an abstract base class is not backward compatible

Tip: Do not add new pure virtual member functions to an abstract base class after the initial release of your API.

8.5.2 Changing Functionality

In terms of maintaining binary compatibility, any changes you make to an existing function signature will break binary compatibility, such as changing the order, type, number, or constness of parameters, or changing the return type.

8.5.3 Deprecating Functionality

8.5.4 Removing Functionality

8.6 API REVIEWS

CHAPTER 9 Documentation

9.1.2 Documenting the Interface’s Contract

design by contract / contract programming:

  1. Preconditions: the client is obligated to meet a function’s required preconditions before calling a function. If the preconditions are not met, then the function may not operate correctly.
  2. Postconditions: the function guarantees that certain conditions will be met after it has finished its work. If a postcondition is not met, then the function did not complete its work correctly.
  3. Class invariant: constraints that every instance of the class must satisfy. This defines the state that must hold true for the class to operate according to its design.

Tip: Contract programming implies documenting the preconditions and postconditions for your functions and the invariants for your classes.

9.1.4 What to Document

Diomidis Spinellis presents a short list of qualities that all good code documentation should strive for:

  1. Complete
  2. Consistent
  3. Effortlessly accessible
  4. Non-repetitive

9.2 TYPES OF DOCUMENTATION

9.2.1 Automated API Documentation

9.2.2 Overview Documentation

9.2.3 Examples and Tutorials

  • Simple and short examples.
  • Working demos.
⚠️ **GitHub.com Fallback** ⚠️