Designing Qt-Style C++ APIs

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 13 | Документация

от Матиаса Этриха

Мы закончили важное исследование в Trolltech, связанное с улучшением процесса разработки Qt. В этой статье я хочу поделиться некоторыми нашими находками и представить принципы, которые мы использовали при проектировании Qt4, а также показать вам, как использовать их в вашем коде.

Содержание

Проектирование интерфейса программирования - сложная задача. Это такое же сложное искусство, как проектирование языков программирования. Существует множество различных принципов построения API (Application Programming Interface, интерфейс прикладного программирования), многие из которых конфликтуют друг с другом.

В настоящее время компьютерные науки уделяют много внимания алгоритмам и структурам данных, упуская из вида принципы проектирования языков программирования и фреймворков. Это упущение является причиной неподготовленности программистов к задаче, которая встает перед ними все чаще и чаще: создание компонентов повторного использования.

Перед расцветом объектно-ориентированных языков программирования код многократного использования писался главным образом производителями библиотек, а не разработчиками ПО. В мире Qt эта ситуация основательно изменилась. Программирование на Qt - это постоянная разработка новых компонентов. Типичное приложение на Qt содержит ряд собственных компонентов, которые повторно используются во всех частях программы. Часто эти же компоненты являются частью другого приложения. В KDE пошли дальше и расширили Qt множеством библиотек, которые реализуют сотни дополнительных классов.

Но что же определяет хороший и эффективный C++ API? Хороший он или плохой, зависит от многих факторов - например, задача и специфические цели. Хороший API имеет ряд характерных свойств. Наличие некоторых, как правило, является желательным, а наличие других определяется спецификой конкретной сферы.


Шесть характеристик хорошего API

API для программиста - это тоже самое, что и пользовательский интерфейс (GUI) для конечного пользователя. Под буквой 'P' в сокращении API имеется ввиду "Программист", не "Программа", подчеркивая тот факт, что API используются программистами (которые являются людьми).

Мы считаем, что API должны быть компактным и законченным, иметь ясную и простую семантику, быть интуитивными, легко запоминаться, и приводить к читаемому коду.

  • Быть компактным: Компактный API подразумевает минимальное количество как публичных членов класса, так и самих классов. Это позволяет легче понимать, запоминать, отлаживать и изменять API.
  • Быть законченным: Законченный API подразумевает, что в нем заключен ожидаемый функционал. Это правило может конфликтовать с компактностью. Также, если функция-член находится не в том классе [в котором ожидалось], то многие потенциальные пользователи не найдут эту функцию.
  • Иметь ясную и простую семантику: Как и в других видах проектирования, вы должны идти по пути наименьшего удивления. Делайте выполнение стандартных задач простым. Выполнение редких задач также должно быть возможным, а не являться фокусом. Решайте конкретную задачу; не делайте решение слишком общим, если этого действительно не требуется. (Например, QMimeSourceFactory в Qt3 мог бы называться QImageLoader и иметь другой API)
  • Быть интуитивным: как и другие вещи в компьютере, API должен быть интуитивным. Опыт и различные предпосылки приводят к различному восприятию того, что является интуитивным, а что - нет. API является интуитивным, если пользователь среднего класса начинает работу без чтения документации, и если программист, не знающий API, может понять исходный код, написанный с использованием этого API.
  • Быть легко запоминаемым: Чтобы сделать API простым для запоминания, выберите согласованную и точную политику именования. Используйте узнаваемые шаблоны и концепции, и избегайте сокращений.
  • Способствовать читабельности кода: Код пишется однажды, но читается (и отлаживается, и изменяется) множество раз. Иногда написание читабельного кода может занять много времени, но это в результате сэкономит время в течении всего жизненного цикла продукта.

Наконец, имейте в виду, что разные пользователи будут использовать различные части вашего API. В то время как использование экземпляров классов Qt должно быть интуитивно понятным, логично предполагать, что пользователь прочтет документацию перед использованием класса.


Ловушки удобства

Существует распространенное заблуждение, что чем меньше кода вы пишите для решения задачи, тем лучше API. Имейте ввиду, что код пишется однажды, но должен быть понят много раз. Например

QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical,
                              0, "volume");

намного сложнее прочитать (и даже написать), чем

QSlider *slider = new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("volume");

Ловушки булевых параметров

Булевые параметры часто приводят к не читаемому коду. В частности, практически всегда будет ошибкой добавление булевого параметра в функцию. В Qt, традиционным примером является функция repaint(), которая принимает не обязательный булевый параметр, указывающий, олжен ли очищаться задний фон (по умолчанию) или нет. Это приводит к такому коду:

widget->repaint(false);

который новички могут прочитать как "Не перерисовывать!"

Идея, по-видимому, состоит в том, что применение булевого параметра избавит от создания еще одной функции, что, в свою очередь, приведет к сокращению программного кода. На деле же - наоборот; сколько пользователей Qt "спиной почувствует", что делает каждая из следующих строчек?

widget->repaint();
widget->repaint(true);
widget->repaint(false);

Более качественный API:

widget->repaint();
widget->repaintWithoutErasing();

В Qt4 мы решили эту проблему просто исключив возможность перерисовки без очистки виджета. Qt4 поддерживает двойную буферизацию, что делает это свойство не актуальным.

Еще ряд примеров:

widget->setSizePolicy(QSizePolicy::Fixed,
                      QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_*.c??", false, true);

Очевидным решением является замена булевых параметров перечислениями (enum types). Так мы поступили с регистрозависимыми строками QString. Сравните:

str.replace("%USER%", user, false);               // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4


Статический полиморфизм

Similar classes should have a similar API. This can be done using inheritance where it makes sense -- that is, when run-time polymorphism is used. But polymorphism also happens at design time. For example, if you exchange a QListBox with a QComboBox, or a QSlider with a QSpinBox, you'll find that the similarity of APIs makes this replacement very easy. This is what we call "static polymorphism".

Static polymorphism also makes it easier to memorize APIs and programming patterns. As a consequence, a similar API for a set of related classes is sometimes better than perfect individual APIs for each class.

The Art of Naming

Naming is probably the single most important issue when designing an API. What should the classes be called? What should the member functions be called?


Общие правила именования

A few rules apply equally well to all kinds of names. First, as I mentioned earlier, do not abbreviate. Even obvious abbreviations such as "prev" for "previous" don't pay off in the long run, because the user must remember which words are abbreviated.

Things naturally get worse if the API itself is inconsistent; for example, Qt 3 has activatePreviousWindow() and fetchPrev(). Sticking to the "no abbreviation" rule makes it simpler to create consistent APIs.

Another important but more subtle rule when designing classes is that you should try to keep the namespace for subclasses clean. In Qt 3, this principle wasn't always followed. To illustrate this, we will take the example of a QToolButton. If you call name(), caption(), text(), or textLabel() on a QToolButton in Qt 3, what do you expect? Just try playing around with a QToolButton in Qt Designer:

  • The name property is inherited from QObject and refers to an internal object name that can be used for debugging and testing.
  • The caption property is inherited from QWidget and refers to the window title, which has virtually no meaning for QToolButtons, since they usually are created with a parent.
  • The text property is inherited from QButton and is normally used on the button, unless useTextLabel is true.
  • The textLabel property is declared in QToolButton and is shown on the button if useTextLabel is true.

In the interest of readability, name is called objectName in Qt 4, caption has become windowTitle, and there is no longer any textLabel property distinct from text in QToolButton.


Именование классов

Identify groups of classes instead of finding the perfect name for each individual class. For example, All the Qt 4 model-aware item view classes are suffixed with View ( QListView, QTableView, and QTreeView), and the corresponding item-based classes are suffixed with Widget instead ( QListWidget, QTableWidget, and QTreeWidget).


Именование перечислений и значений

When declaring enums, we must keep in mind that in C++ (unlike in Java or C#), the enum values are used without the type. The following example shows illustrates the dangers of giving too general names to the enum values:

namespace Qt
{
    enum Corner { TopLeft, BottomRight, ... };
    enum CaseSensitivity { Insensitive, Sensitive };
    ...
};
 
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);

In the last line, what does Insensitive mean? One guideline for naming enum types is to repeat at least one element of the enum type name in each of the enum values:

namespace Qt
{
    enum Corner { TopLeftCorner, BottomRightCorner, ... };
    enum CaseSensitivity { CaseInsensitive,
                           CaseSensitive };
    ...
};
 
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

When enumerator values can be OR'd together and be used as flags, the traditional solution is to store the result of the OR in an int, which isn't type-safe. Qt 4 offers a template class QFlags<T>, where T is the enum type. For convenience, Qt provides typedefs for the flag type names, so you can type Qt::Alignment instead of QFlags<Qt::AlignmentFlag>.

By convention, we give the enum type a singular name (since it can only hold one flag at a time) and the "flags" type a plural name. For example:

enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;

In some cases, the "flags" type has a singular name. In that case, the enum type is suffixed with Flag:

enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;

Именование функций и параметров

The number one rule of function naming is that it should be clear from the name whether the function has side-effects or not. In Qt 3, the const function QString::simplifyWhiteSpace() violated this rule, since it returned a QString instead of modifying the string on which it is called, as the name suggests. In Qt 4, the function has been renamed QString::simplified().

Parameter names are an important source of information to the programmer, even though they don't show up in the code that uses the API. Since modern IDEs show them while the programmer is writing code, it's worthwhile to give decent names to parameters in the header files and to use the same names in the documentation.


Именование булевых геттеров, сеттеров, и свойств

Поиск хороших имен для геттера и сеттера булевого параметра - всегда головная боль. Должен ли геттер называться checked() или isChecked()? scrollBarsEnabled() или areScrollBarEnabled()?

В Qt4, мы используем следующие руководящие принципы для именования геттерных функций:

  • Прилагательные имеют приставку is-. Например:
    • isChecked()
    • isDown()
    • isEmpty()
    • isMovingEnabled()
  • Однако, у прилагательных, относящихся к существительному во множественном числе, нет никакой приставки:
    • scrollBarsEnabled(), вместо areScrollBarsEnabled()
  • Глаголы не имеют никакой приставки и не используют третье лицо (-s):
    • acceptDrops(), вместо acceptsDrops()
    • allColumnsShowFocus()
  • У существительных вообще нет никакой приставки:
    • autoCompletion(), вместо isAutoCompletion()
    • boundaryChecking()
  • Иногда, отсутствие приставки вводит в заблуждение, в этом случае мы добавляем приставку is-:
    • isOpenGLAvailable(), вместо openGL()
    • isDialog(), вместо dialog()
  • (От функции называемой dialog(), мы обычно ожидаем, что она вернет QDialog *.)

Название сеттера получается из имени геттера, удаляя все приставки, и помещая set перед именем, например, setDown() и setScrollBarsEnabled(). Название свойства - такое же как и у геттера, но без приставки is.


Указатели или ссылки?

Что лучше для выходных параметров, указатели или ссылки?

void getHsv(int *h, int *s, int *v) const
void getHsv(int &amp;h, int &amp;s, int &amp;v) const

Большинство книг по C++ рекомендуют ссылки, когда это возможно, в соответствии с общим мнением о том, что ссылки являются "безопасными и лучше", чем указатели. В противоположность этому, в Trolltech, мы, как правило, предпочитаем указатели, поскольку они делают пользовательский код более читаемым. Сравните:

color.getHsv(&amp;h, &amp;s, &amp;v);
color.getHsv(h, s, v);

Только первая строка дает понять, что существует высокая вероятность того, что h, s и v будут изменены вызовающей функцией.

Наглядный пример: QProgressBar

Чтобы показать некоторые из этих концепций на практике, мы рассмотрим QProgressBar API в Qt 3 и сравним его с API в Qt 4. В Qt 3:

class QProgressBar : public QWidget
{
    ...
public:
    int totalSteps() const;
    int progress() const;
 
    const QString &amp;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 &amp;progressStr,
                              int progress,
                              int totalSteps);
    ...
};

API является довольно сложным и непоследовательным; например, из именования не ясно, что reset(), setTotalSteps(), и setProgress() тесно связанны между собой.

Ключ к улучшению API заключается в том, чтобы заметить, что класс QProgressBar подобен QAbstractSpinBox и его наследникам, QSpinBox, QSlider and QDial в Qt 4. Решение? Заменить progress и totalSteps на minimum, maximum и value. Добавить сигнал valueChanged(). Добавить вспомогательную функцию setRange().

The next observation is that progressString, percentage and indicator really refer to one thing: the text that is shown on the progress bar. Usually the text is a percentage, but it can be set to anything using the setIndicator() function. Here's the new API:

virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;

By default, the text is a percentage indicator. This can be changed by reimplementing text().

The setCenterIndicator() and setIndicatorFollowsStyle() functions in the Qt 3 API are two functions that influence alignment. They can advantageously be replaced by one function, setAlignment():

void setAlignment(Qt::Alignment alignment);

If the programmer doesn't call setAlignment(), the alignment is chosen based on the style. For Motif-based styles, the text is shown centered; for other styles, it is shown on the right hand side.

Here's the improved QProgressBar API:

class QProgressBar : public QWidget
{
    ...
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);
    ...
};

How to Get APIs Right

APIs need quality assurance. The first revision is never right; you must test it. Make use cases by looking at code which uses this API and verify that the code is readable.

Other tricks include having somebody else use the API with or without documentation and documenting the class (both the class overview and the individual functions).

Documenting is also a good way of finding good names when you get stuck: just try to document the item (class, function, enum value, etc.) and use your first sentence as inspiration. If you cannot find a precise name, this is often a sign that the item shouldn't exist. If everything else fails and you are convinced that the concept makes sense, invent a new name. This is, after all, how "widget", "event", "focus", and "buddy" came to be.