Friday, November 8, 2013

How to design API headers

Library headers (API) should be minimal and not change very often. If this directive is followed, then the API is more direct, and any internal changes do not reflect to recompiling the whole user project.

The following article describes what steps to take to achieve this goal on an C++ API header.

Consider you want to export an API to create a file manager window, that opens up with a preselected file filter (eg *.cpp), and you want to know what file has been selected after exiting (via Ok, or Cancel).

presumably, the user would like to write something like:
 
string filter = "*.cpp";
FileManager *window = FileManager::create_window( filter );

if( window.answer() == FileManager::OK ) {
     string selected_file = window.get_selected_path();
     .....  // open the file
}

so he expects to see
class FileManager {
   public:
     static FileManager* create_window( string filter );   // factory method

     string get_selected_path() const;

     enum ExitState { Ok, Cancel };
     enum ExitState answer() const;

   private:
     FileManager();  // private, so user has to create via factory method
};

but what you've probably come up while writing it, is at best:
#include <qwindow.h>
#include <qlistview.h>
#include <qlayout.h>
#include <qbutton.h>
#include <qlineedit.h>
#include <qcombobox.h>
#include <string>

class FileManager  :  public QWindow {
   public:
     static FileManager* create_window (string filter);

     string get_selected_path() const { return m_file_path.string(); }
     string get_filter(); // for debugging

     enum ExitState { Ok, Cancel };
     enum ExitState answer() const   { return m_button_pressed_state; }

   private:
     string m_filter;
     QWindow *m_window;
     QButton *m_ok_btn;
     QButton *m_cancel_btn;
     QLineEdit *m_file_path;
     QCombobox *m_filter_combo;
     QListview *m_file_list;
     QLayout *m_layout;
     ExitState  m_button_pressed_state;
  private:
     FileManager();  // private, so user has to create via factory method
     void set_filter(string filter) { m_filter = filter; }
  slots:
     void on_ok_pressed (QButton* sender);
     void on_cancel_pressed (QButton* sender);
};

What is wrong with this header ?

Even though this header delivers the output expected, and its public interface fully matches the expected user code, still this header is not a good API, for the following reasons:
  • it is bloated -- the reader can't understand what he's supposed to do. At best, he'll look at other people's code and copy it.
  • it takes too long to compile because of the external dependencies (too many header files). 
  • every time you change its internal representation (eg. state variables) the user has to recompile.

How can I make it better ?

Here are some tips how to reduce the burden to the user, without changing any lines in your code (which you don't want to touch, because it works):

  1. copy original header to another header, which is going to be the API users will look at.
  2. remove all derivations that are not part of the API
  3. remove any non-public functions and variables
  4. remove public functions that you used for debugging purposes
  5. remove any unnecessary inlined function implementation from public interface
  6. remove any unnecessary headers
  7. have only one private variable, as a pointer to implementation class (aka pimpl)
Look at how many lines you can remove:

class FileManager  :  public QWindow {                // step 2
   public:
     static FileManager* create_window (string filter);

     string get_selected_path() const { return m_file_path.string(); }         // step 5
     string get_filter(); // for debugging           // step 4

     enum ExitState { Ok, Cancel };
     enum ExitState answer() const   { return m_button_pressed_state; }       // step 5

   private:
     string m_filter;                                // step 3
     QWindow *m_window;                              // step 3
     QButton *m_ok_btn;                              // step 3
     QButton *m_cancel_btn;                          // step 3
     QLineEdit *m_file_path;                         // step 3
     QCombobox *m_filter_combo;                      // step 3
     QListview *m_file_list;                         // step 3
     QLayout *m_layout;                              // step 3
     ExitState  m_button_pressed_state;              // step 3
  private:
     FileManager();  // private, so user has to create via factory method
     void set_filter(string filter) { m_filter = filter; }                 // step 3 
  slots:
     void on_ok_pressed (QButton* sender);            // step 3
     void on_cancel_pressed (QButton* sender);        // 
step 3

     FileManagerImpl *p_impl;                         // step 7
};

These steps should reduce the header that is targeted for the user to merely:
#include <string>
class FileManagerImpl; // fwd decl

struct FileManager {

     static FileManager* create_window (string filter);

     string get_selected_path() const

     enum ExitState { Ok, Cancel };
     enum ExitState answer() const;

   private:
     FileManager(FileManagerImpl *);
     FileManagerImpl *p_impl;
};

Then, just forward each call to the implementation object:
#include "file_manager_impl.hpp"

// call forwarding
string FileManager::get_selected_path() const {
    return m_pimpl->get_selected_path();
}

enum ExitState FileManager::answer() const {
    return m_pimpl->answer();
}
// creation
FileManager* FileManager::create_window (string filter) {
    FileMngrImpl *impl = FileMngrImpl::create_window(filter);
    return FileManager( impl );
}

FileManager::FileManager( FileManagerImpl *impl )
    : m_pimpl(impl)
{
}

and the class in your original header should be renamed to FileManagerImpl:
#include <qwindow.h>
#include <qlistview.h>
#include <qlayout.h>
#include <qbutton.h>
#include <qlineedit.h>
#include <qcombobox.h>
#include <string>

class FileManagerImpl  :  public QWindow {
   public:
     static FileManagerImpl* create_window (string filter);

     string get_selected_path() const { return m_file_path.string(); }
     string get_filter(); // for debugging

     enum ExitState { Ok, Cancel };
     enum ExitState answer() const   { return m_button_pressed_state; }

   private:
     string m_filter;
     QWindow *m_window;
     QButton *m_ok_btn;
     QButton *m_cancel_btn;
     QLineEdit *m_file_path;
     QCombobox *m_filter_combo;
     QListview *m_file_list;
     QLayout *m_layout;
     ExitState  m_button_pressed_state;
  private:
     FileManagerImpl();  // private, so user has to create via factory method
     void set_filter(string filter) { m_filter = filter; }
  slots:
     void on_ok_pressed (QButton* sender);
     void on_cancel_pressed (QButton* sender);
};







No comments:

Post a Comment