aKademy 2004
Designing Qt-style APIs
Matthias Ettrich
APIs
- API stands for Application Programmer Interface
- "Programmer", not "Program". Programmers are humans
- The API is to the programmer what the GUI is to the end user.
Implications
APIs should:
- be minimal
- be complete
- have clear and simple semantics
- be intuitive
- be easy to memorize
- guide to readable code
Minimal
- Minimize the number of members per class
- Minimize the number of classes
Rationale:
- Easier to understand.
- Easier to remember.
- Easier to debug.
- Easier to change.
Note: overloads are conceptually cheaper than other members
Complete
- If most users of a class miss a certain feature, the API is not complete
- BUT: remember minimization. make sure the feature really belongs into the class.
Rationale: if a member function is misplaced in a class, this can lead to two effects:
- it leads to even more functions (nasty things tend to grow on their own)
- many potential users of the function won't find it.
Clear and simple semantics
- As with any other design work: apply the principle of least surprise.
- Avoid modality.
- Make common tasks easy, and rare tasks possible.
- Be specific when you solve a specific problem. Beware pseudo generalization unless you really really really want to and must solve the general case (bad example: QMimeSourceFactory)
- If you are not sure about something, remove it. Add it later, when you fully understand it.
Intuitive
Intuition is based on experience, different experience and background leads to different perception on what is and is not intuitive.
Definition:
A Qt-style API is intuitive...
- if a semi-experienced Qt-user gets away without reading the docs.
- if a programmer who does not know Qt can understand code written against it.
Easy to memorize
- Choose consistent and presise naming.
- Avoid words that are unknown to non-native speakers.
- Do not abbreviate.
- Use recognizable patterns and concepts.
Guidance to readable code
Rationale: Code is written once, but read/debugged/understood many many times.
=> Readable code may sometimes take longer to write, but safes time throughout the code lifecycle.
Mindset: Picture your target user
- Different types of users use different parts of an API:
- using a component
- subclassing a component
- Usage should be "intuitive", subclassing may require reading the docs.
- Component authors are expected to have a higher level of conceptual understanding.
- Normal usage should not require subclassing. If it does, mark the class (e.g., with "abstract" in the name)
Mindset: The convenience trap
- Missconception: The fewer code necessary to achieve something, the better the API.
- Remember: Code is written once, but read/debugged/understood many many times.
Example:
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "Convenience level");
QSlider *slider = new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("Convenience level");
Also: Consider bindings to dynamically typed languages
Mindset: The boolean trap
Pattern:
- void doSomething();
- void doSomething(bool doSomethingElse = false);
- void doSomething(bool doSomethingElse = false, bool orSomethingEntirelyDifferent = false;
Broken rationale: Adds no extra function thus no extra bloat, no need to document another function, no need to adjust the class documentation, simple.
Truth: Adds bloat (to a function, not a class), hard to memorize, leads in non-intuitive code.
Examples:
QWidget: widget->repaint(false);
QWidget: widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding, true);
QTextEdit: editor->insert("what does it mean?", true, true, false);
Mindset: Mind the inheritance structure
Bad example: Good old QToolButton
In Qt 4:
QObject::objectName
QWidget::windowTitle
QAbstractButton::text
Another example: QWidget::icon QButton::pixmap QToolButton::iconSet
In Qt 4: QWidget::windowIcon QAbstractButton::icon
Mindset: static polymorphism
- Similar classes should have a similar to identical API.
- Extrem position: Similar classes should implement the same interface or a common base class. This allows runtime polymorphism.
- But: A similar to identical API also makes sense without. Rationale: Polymorphism also happens at design time.
Examples:
- exchanging a list box with a combo box
- exchanging a slider with a spin box
- simply remembering APIs and programming patterns
- A similar API for a set of related classes is better than perfect individual APIs for each class.
Mindset: QA
- The first revision of an API is never right, you must test APIs.
- Make use cases, carefully study code that uses the API (an API is beautiful if the code written against it is beautiful).
- Have somebody else use it with/without documentation.
- Document it. For classes, don't just write functional documentation, but a class overview that covers the entire API. Basic rule: if this is really really hard, then the class has problems.
- Get your names right, names are as important as good documentation is.
Naming
- General considerations
- Naming classes
- Naming members
- Naming parameters
- Naming enums
- Naming boolean properties
Naming: General
- Do not abbreviate, unless the term is commonly used.
- Use more elobarate names for specific features of classes that are intended for subclassing.
Example: windowTitle as opposed to title or caption.
Consider what caption or title might mean for group box or menu.
- Keep the namespace for subclasses clean.
Ways of finding good names
- Document the item and use your first sentence as inspiration.
- Talk to a friend or your dog and explain what the item does.
- If you cannot find a precise name, that might be because the item should not exist. (warning: Names with 'or' and 'and' are typically not considered precise)
- Invent a new name. Examples: widget, focus, buddy (but only if everything else fails and you are convinced that the concept makes sense).
Naming: Classes
- Don't state the obvious, remove redundant parts Example: QLabel instead of QLabelWidgetObject.
- Identify groups of classes instead of finding the perfect name for each individual class.
Q{VBox, HBox, Grid, Stacked}Layout
Q{VBox, HBox, Grid, Stacked}Widget
Q{List, Table, Icon, Widget, Graphic}View
(instead of QListBox, QTable, QIconView, QScrollView in AutoOneFit mode, QCanvas)
- Leverage knowledge and experience that potential users have from other toolkits.
Naming: Members
- Parameter names are an important source of information to the programmer, almost as important as member names.
- Modern IDEs show them in edit mode (put the names in headerfiles!)
- Describe a parameter's meaning rather than a parameter's type.
- As with everything: avoid using mystical abbreviations
Example:
should be:
Naming: Enums
- Keep in mind that enumerator values are used without the type:
enum Corner { TopLeft, BottomRight, ... }
enum CaseSensitive { Insensitive, Sensitive }
w->setCornerWidget(cw, Qt::TopLeft);
s.indexOf(needle, Qt::Insensitive);
- Therefore duplicate parts of the type name in each value:
enum Corner {
TopLeftCorner,
BottomRightCorner,
... }
enum CaseSensitivity {
CaseInsensitive,
CaseSensitive }
w->setCornerWidget(cw, Qt::TopLeftCorner);
s.indexOf(needle, Qt::CaseInsensitive);
- When enumerators are used as flags, Qt 4 offers a template class QFlags<T>. Main advantage over using uint is type-safety through the compiler's static type-checking
- Convention: We give the enum a singular name (since it can hold one single flag only) and the flags-type itself a plural name:
enum RectangleEdge { LeftEdge, RightEdge, ... };
Q_DECLARE_FLAGS(RectangleEdges, RectangleEdge);
Other examples:
WFlags, WFlag
ImageConversionFlags, ImageConversionFlag
DockWindowAreas, DockWindowArea
ToolBarAreas, ToolBarArea
Where the name itself indicates a complex state, we don't need plural. In those cases we mark the single flag explicitly:
Alignment, AlignmentFlag
WState, WStateFlag
AutoFormatting, AutoFormattingFlag
Naming: Boolean properties
- Rule of thumb: "is" for singular adjectives, and no prefix otherwise.
- Never use the third person.
- Never prefix with "has".
Singular adjectives use "is":
Plural adjectives have no prefix:
scrollBarsEnabled(), not areScrollBarsEnabled()
Verbs have no prefix and don't use the third person ("s"):
Nouns generally have no prefix:
In some cases, having no prefix is misleading, in which case we prefix with "is":
Example: Naming a boolean property
Task: HTML-rendering widget should have a property that indicates whether it underlines links or not.
Obvious start: setLinkUnderline():
good, but getters don't work:
linkUnderline()- bad
isLinkUnderline() - bad
Start with getter:
linkUnderlineEnabled() - clumsy
underlineLinks() - sounds like a once-off action
linksUnderlined() - works
Result: setLinksUnderlined()and linksUnderlined()
Misc
- Objects belong on the heap, values on the stack. Pointers to values should not normally happen (QString*, QPixmap*).
- Const references are good, non-const references are bad for readability. Use pointers for out-parameters instead and mark the function with get.
Example:
QRect::getCoords(int *x1, int *y1, int *x2, int *y2);
Remember what we said about abbreviations? Coords arguably isn't a common term:
QRect::getCoordinates(int *x1, int *y1, int *x2, int *y2);
Case study: QProgressBar
Current QProgressBar API in Qt3
public:
int totalSteps() const;
int progress() const;
const QString &progressString() const;
bool percentageVisible() const;
void setPercentageVisible(bool);
void setCenterIndicator(bool on);
bool centerIndicator() const;
void setIndicatorFollowsStyle(bool);
bool indicatorFollowsStyle() const;
public slots:
void reset();
virtual void setTotalSteps(int totalSteps);
virtual void setProgress(int progress);
void setProgress(int progress, int totalSteps);
protected:
virtual bool setIndicator(QString & progress_str, int progress, int totalSteps);
Case study: QProgressBar
- Discovery: QProgressBar is similar to QAbstractSpinBox and its subclasses QSpinBox, QSlider and QDial.
- Replace
int progress, int totalSteps
with
int minimum, int maximum, int value
- Add a signal valueChanged().
- Add convenience function setRange().
Case study: QProgressBar
- progressString, percentage and indicator really refer to one thing: text. It can be a percentage, but also something else (see virtual function setIndicator)
QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
- centerIndicator and indicatorFollowsStyle are two boolean value to define alignment. Qt uses this on many places: we have AlignLeft, AlignCenter and AlignAuto. AlignRight must be supported anyway for right-to-left locales.
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment alignment);
Case study: QProgressBar
- QProgressBar makes it possible for subclasses to change the text.
virtual bool setIndicator(QString & progress_str, int progress, int totalSteps);
- Simpler solution: make text() virtual:
Case study: QProgressBar
New and improved QProgressBar API
public:
void setMinimum(int minimum);
int minimum() const;
void setMaximum(int maximum);
int maximum() const;
void setRange(int minimum, int maximum);
int value() const;
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment alignment);
public slots:
void reset();
void setValue(int value);
signals:
void valueChanged(int value);