Редактирование: Designing Qt-Style C++ APIs
Материал из Wiki.crossplatform.ru
Внимание: Вы не представились системе. Ваш IP-адрес будет записан в историю изменений этой страницы.
Правка может быть отменена. Пожалуйста, просмотрите сравнение версий, чтобы убедиться, что это именно те изменения, которые вас интересуют, и нажмите «Записать страницу», чтобы изменения вступили в силу.
Текущая версия | Ваш текст | ||
Строка 2: | Строка 2: | ||
<b>от Матиаса Этриха</b> | <b>от Матиаса Этриха</b> | ||
- | <b>Мы | + | <b> Мы сделали существенное исследование в Trolltech в плане улучшении качества разработки Qt. В этой статье я хочу поделиться некоторыми нашими выводами и представить принципы, которые мы использовали при разработке Qt4, и показать вам, как применять их в вашем коде.</b> |
__TOC__ | __TOC__ | ||
- | + | Designing application programmer interfaces, APIs, is hard. It is an art as difficult as designing programming languages. There are many different principles to choose from, many of which tend to contradict each other. | |
- | + | Computer science education today puts a lot of emphasis on algorithms and data structures, with less focus on the principles behind designing programming languages and frameworks. This leaves application programmers unprepared for an increasingly important task: the creation of reusable components. | |
- | + | Before the rise of object-oriented languages, reusable generic code was mostly written by library vendors rather than by application developers. In the Qt world, this situation has changed significantly. Programming with Qt is writing new components all the time. A typical Qt application has at least some customized components that are reused throughout the application. Often the same components are deployed as part of other applications. KDE, the K Desktop Environment, goes even further and extends Qt with many add-on libraries that implement hundreds of additional classes. | |
- | + | ||
- | + | ||
+ | But what constitutes a good, efficient C++ API? What is good or bad depends on many factors -- for example, the task at hand and the specific target group. A good API has a number of features, some of which are generally desirable, and some of which are more specific to certain problem domains. | ||
<div id="sixcharacteristicsofgoodapis"></div> | <div id="sixcharacteristicsofgoodapis"></div> | ||
== Шесть характеристик хорошего API == | == Шесть характеристик хорошего API == | ||
- | API | + | API программисту, что GUI конечному пользователю. Буква 'P' в API означает "Программист", а не "Программа", с тем чтобы подчеркнуть тот факт, что API, используются программистами, которые являются людьми. |
- | Мы считаем, что API должны быть | + | Мы считаем, что API должны быть минимальными и полными, иметь чёткую и простую семантику, быть интуитивными, легко запоминаться, и приводить к читаемому коду. |
- | * <b>Быть | + | * <b>Быть минимальным:</b> Минимальный API - тот, который имеет настолько мало открытых членов класса и настолько мало классов наколько это возможно. Это делает его лёгким для понимания, запоминания, отладки, и изменения API. |
- | * <b>Быть | + | * <b>Быть полным:</b> Полный API означает, что ожидаемая функциональность должна быть в нём. Это может привести к конфликту с сохранением его минимальным. Кроме того, Если функция-член находится в ошибочном классе, многие потенциальные пользователи этой функции не смогут найти её. |
- | * <b>Иметь | + | * <b>Иметь чёткую и простую семантику:</b> As with other design work, you should apply the principle of least surprise. Make common tasks easy. Rare tasks should be possible but not the focus. Solve the specific problem; don't make the solution overly general when this is not needed. (For example, [[Qt:Документация_4.3.2/q3mimesourcefactory | QMimeSourceFactory]] in Qt 3 could have been called QImageLoader and have a different API.) |
- | * <b>Быть интуитивным:</b> | + | * <b>Быть интуитивным:</b> As with anything else on a computer, an API should be intuitive. Different experience and background leads to different perceptions on what is intuitive and what isn't. An API is intuitive if a semi-experienced user gets away without reading the documentation, and if a programmer who doesn't know the API can understand code written using it. |
- | * <b>Быть легко запоминаемым:</b> Чтобы сделать API | + | * <b>Быть легко запоминаемым:</b> Чтобы сделать API лёгким для запоминания, выберите последовательное и точное соглашение об именовании. Используйте узнаваемые шаблоны и концепции, и избегайте сокращений. |
- | * <b> | + | * <b>Приводить к читаемому коду:</b> Code is written once, but read (and debugged and changed) many times. Readable code may sometimes take longer to write, but saves time throughout the product's life cycle. |
- | Наконец, имейте в виду, что разные пользователи будут использовать различные части | + | Наконец, имейте в виду, что разные пользователи будут использовать различные части API. Просто использование экземпляра класса Qt должно быть интуитивным, разумно ожидать, что пользователь будет читать документацию прежде, чем попытается унаследоваться от него. |
Строка 33: | Строка 32: | ||
== Ловушки удобства == | == Ловушки удобства == | ||
- | + | Общее заблуждение - чем меньшим количеством кода вы добиваетесь чего-то, тем лучше API. Помните, что код написаный уже не раз, должен быть понятен снова и снова. Например, | |
{{code|qt|code= | {{code|qt|code= | ||
Строка 39: | Строка 38: | ||
0, "volume"); | 0, "volume"); | ||
}} | }} | ||
- | + | является более трудным для чтения (и даже для написания) чем | |
{{code|qt|code= | {{code|qt|code= | ||
Строка 52: | Строка 51: | ||
== Ловушки булевых параметров == | == Ловушки булевых параметров == | ||
- | Булевые параметры часто приводят к | + | Булевые параметры часто приводят к нечитаемому коду. В частности, почти всегда является ошибкой добавлять <tt>булевый</tt> параметр к существующей функции. В Qt, традиционным примером является функция <tt>repaint()</tt>, которая принимает не обязательный <tt>булевый</tt> параметр определяющий должен ли фон быть стерт (по умолчанию) или нет. Это приводит к такому коду: |
{{code|qt|code= | {{code|qt|code= | ||
widget->repaint(false); | widget->repaint(false); | ||
}} | }} | ||
- | который новички могут прочитать | + | который новички могут прочитать со смыслом: "Не перерисовывать!" |
- | + | Размышление состоит, очевидно, в том, что <tt>булевый</tt> параметр сохраняет одну функцию, тем самым способствуя снижению раздувания кода. В действительности, это увеличивает раздувание кода; как много пользователей Qt знаю наизусть, что делает каждая из следующих трех строк? | |
{{code|qt|code= | {{code|qt|code= | ||
widget->repaint(); | widget->repaint(); | ||
Строка 64: | Строка 63: | ||
widget->repaint(false); | widget->repaint(false); | ||
}} | }} | ||
- | + | Несколько лучшее API, могло бы быть таким: | |
{{code|qt|code= | {{code|qt|code= | ||
widget->repaint(); | widget->repaint(); | ||
widget->repaintWithoutErasing(); | widget->repaintWithoutErasing(); | ||
}} | }} | ||
- | В | + | В Qt 4, мы решили проблему просто удалив возможность перерисовки без стирания виджета. Естественная поддержка двойной буферизации в Qt4 делает эту особенность устаревшей. |
- | + | Здесь даются несколько примеров: | |
{{code|qt|code= | {{code|qt|code= | ||
widget->setSizePolicy(QSizePolicy::Fixed, | widget->setSizePolicy(QSizePolicy::Fixed, | ||
Строка 78: | Строка 77: | ||
QRegExp rx("moc_*.c??", false, true); | QRegExp rx("moc_*.c??", false, true); | ||
}} | }} | ||
- | + | Очевидное решение состоит в том, чтобы заменить <tt>булевый</tt> параметр перечислением. Это то, что мы сделали в Qt4, с регистрозависимостью в [[Qt:Документация_4.3.2/qstring | QString]]. Сравните: | |
{{code|qt|code= | {{code|qt|code= | ||
Строка 89: | Строка 88: | ||
== Статический полиморфизм == | == Статический полиморфизм == | ||
- | + | 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 [[Qt:Документация_4.3.2/qcombobox | QComboBox]], or a QSlider with a [[Qt:Документация_4.3.2/qspinbox | 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. | |
<div id="theartofnaming"></div> | <div id="theartofnaming"></div> | ||
- | + | == 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 <tt>activatePreviousWindow()</tt> and <tt>fetchPrev()</tt>. 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 [[Qt:Документация_4.3.2/qtoolbutton | QToolButton]]. If you call <tt>name()</tt>, <tt>caption()</tt>, <tt>text()</tt>, or <tt>textLabel()</tt> on a [[Qt:Документация_4.3.2/qtoolbutton | QToolButton]] in Qt 3, what do you expect? Just try playing around with a [[Qt:Документация_4.3.2/qtoolbutton | QToolButton]] in <i>Qt Designer</i>: | |
+ | * The <tt>name</tt> property is inherited from [[Qt:Документация_4.3.2/qobject | QObject]] and refers to an internal object name that can be used for debugging and testing. | ||
+ | * The <tt>caption</tt> property is inherited from [[Qt:Документация_4.3.2/qwidget | QWidget]] and refers to the window title, which has virtually no meaning for [[Qt:Документация_4.3.2/qtoolbutton | QToolButton]]s, since they usually are created with a parent. | ||
+ | * The <tt>text</tt> property is inherited from QButton and is normally used on the button, unless <tt>useTextLabel</tt> is <tt>true</tt>. | ||
+ | * The <tt>textLabel</tt> property is declared in [[Qt:Документация_4.3.2/qtoolbutton | QToolButton]] and is shown on the button if <tt>useTextLabel</tt> is <tt>true</tt>. | ||
- | + | In the interest of readability, <tt>name</tt> is called <tt>objectName</tt> in Qt 4, <tt>caption</tt> has become <tt>windowTitle</tt>, and there is no longer any <tt>textLabel</tt> property distinct from <tt>text</tt> in [[Qt:Документация_4.3.2/qtoolbutton | 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 <tt>View</tt> ([[Qt:Документация_4.3.2/qlistview | QListView]], [[Qt:Документация_4.3.2/qtableview | QTableView]], and [[Qt:Документация_4.3.2/qtreeview | QTreeView]]), and the corresponding item-based classes are suffixed with <tt>Widget</tt> instead ([[Qt:Документация_4.3.2/qlistwidget | QListWidget]], [[Qt:Документация_4.3.2/qtablewidget | QTableWidget]], and [[Qt:Документация_4.3.2/qtreewidget | QTreeWidget]]). | ||
- | |||
- | |||
- | === | + | === Именование перечисление и значений === |
- | + | When declaring enums, we must keep in mind that in C++ (unlike in Java or C#), the enum values are used <i>without</i> the type. The following example shows illustrates the dangers of giving too general names to the enum values: | |
{{code|qt|code= | {{code|qt|code= | ||
Строка 132: | Строка 129: | ||
str.indexOf("$(QTDIR)", Qt::Insensitive); | str.indexOf("$(QTDIR)", Qt::Insensitive); | ||
}} | }} | ||
- | + | In the last line, what does <tt>Insensitive</tt> 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: | |
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
Строка 147: | Строка 143: | ||
str.indexOf("$(QTDIR)", Qt::CaseInsensitive); | 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 <tt>int</tt>, which isn't type-safe. Qt 4 offers a template class QFlags<T>, where <tt>T</tt> is the enum type. For convenience, Qt provides typedefs for the flag type names, so you can type <tt>Qt::Alignment</tt> 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: | |
- | + | ||
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
Строка 156: | Строка 151: | ||
typedef QFlags<RectangleEdge> RectangleEdges; | typedef QFlags<RectangleEdge> RectangleEdges; | ||
}} | }} | ||
- | + | In some cases, the "flags" type has a singular name. In that case, the enum type is suffixed with <tt>Flag</tt>: | |
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
Строка 164: | Строка 158: | ||
}} | }} | ||
- | === | + | === Именование функций и параметров === |
- | + | 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 [[Qt:Документация_4.3.2/qstring | QString]]::simplifyWhiteSpace() violated this rule, since it returned a [[Qt:Документация_4.3.2/qstring | QString]] instead of modifying the string on which it is called, as the name suggests. In Qt 4, the function has been renamed [[Qt:Документация_4.3.2/qstring | 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. | ||
- | |||
- | === | + | === Именование булевых геттеров, сеттеров, и свойств === |
- | Поиск хороших имен для геттера и сеттера <tt>булевого</tt> параметра | + | Поиск хороших имен для геттера и сеттера <tt>булевого</tt> параметра - всегда головная боль. Должен ли геттер называться <tt>checked()</tt> или <tt>isChecked()</tt>? <tt>scrollBarsEnabled()</tt> или <tt>areScrollBarEnabled()</tt>? |
- | В Qt4 | + | В Qt4, мы используем следующие руководящие принципы для именования геттерных функций: |
- | * | + | * Прилагательные имеют приставку <tt>is</tt>-. Например: |
** <tt>isChecked()</tt> | ** <tt>isChecked()</tt> | ||
** <tt>isDown()</tt> | ** <tt>isDown()</tt> | ||
Строка 179: | Строка 174: | ||
** <tt>isMovingEnabled()</tt> | ** <tt>isMovingEnabled()</tt> | ||
- | * | + | *Однако, у прилагательных, относящихся к существительному во множественном числе, нет никакой приставки: |
** <tt>scrollBarsEnabled()</tt>, вместо <tt>'''are'''ScrollBarsEnabled()</tt> | ** <tt>scrollBarsEnabled()</tt>, вместо <tt>'''are'''ScrollBarsEnabled()</tt> | ||
- | *Глаголы не имеют | + | * Глаголы не имеют никакой приставки и не используют третье лицо (-<tt>s</tt>): |
** <tt>acceptDrops()</tt>, вместо <tt>accept'''s'''Drops()</tt> | ** <tt>acceptDrops()</tt>, вместо <tt>accept'''s'''Drops()</tt> | ||
** <tt>allColumnsShowFocus()</tt> | ** <tt>allColumnsShowFocus()</tt> | ||
- | * | + | * У существительных вообще нет никакой приставки: |
** <tt>autoCompletion()</tt>, вместо <tt>'''is'''AutoCompletion()</tt> | ** <tt>autoCompletion()</tt>, вместо <tt>'''is'''AutoCompletion()</tt> | ||
** <tt>boundaryChecking()</tt> | ** <tt>boundaryChecking()</tt> | ||
- | * Иногда, отсутствие приставки вводит в заблуждение, | + | * Иногда, отсутствие приставки вводит в заблуждение, в этом случае мы добавляем приставку <tt>is</tt>-: |
** <tt>isOpenGLAvailable()</tt>, вместо <tt>openGL()</tt> | ** <tt>isOpenGLAvailable()</tt>, вместо <tt>openGL()</tt> | ||
- | ** <tt>isDialog()</tt>, вместо <tt>dialog()</tt> | + | ** <tt>isDialog()</tt>, вместо <tt>dialog()</tt> |
- | Название сеттера получается из имени геттера, | + | *(От функции называемой <tt>dialog()</tt>, мы обычно ожидаем, что она вернет [[Qt:Документация_4.3.2/qdialog | QDialog]] <tt>*</tt>.) |
+ | |||
+ | Название сеттера получается из имени геттера, удаляя все приставки, и помещая <tt>set</tt> перед именем, например, <tt>'''set'''Down()</tt> и <tt>'''set'''ScrollBarsEnabled()</tt>. Название свойства - такое же как и у геттера, но без приставки <tt>is</tt>. | ||
Строка 204: | Строка 201: | ||
{{code|qt|code= | {{code|qt|code= | ||
void getHsv(int *h, int *s, int *v) const | void getHsv(int *h, int *s, int *v) const | ||
- | void getHsv(int &h, int &s, int &v) const | + | void getHsv(int &h, int &s, int &v) const |
}} | }} | ||
- | + | Большинство книг по C++ рекомендуют ссылки, когда это возможно, в соответствии с общим мнением о том, что ссылки являются "безопасными и лучше", чем указатели. В противоположность этому, в Trolltech, мы, как правило, предпочитаем указатели, поскольку они делают пользовательский код более читаемым. Сравните: | |
- | Большинство книг по C++ рекомендуют | + | |
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
- | color.getHsv(&h, &s, &v); | + | color.getHsv(&h, &s, &v); |
color.getHsv(h, s, v); | color.getHsv(h, s, v); | ||
}} | }} | ||
- | + | Только первая строка дает понять, что существует высокая вероятность того, что <tt>h</tt>, <tt>s</tt> и <tt>v</tt> будут изменены вызовающей функцией. | |
- | Только | + | |
<div id="casestudyqprogressbar"></div> | <div id="casestudyqprogressbar"></div> | ||
== Наглядный пример: QProgressBar == | == Наглядный пример: QProgressBar == | ||
- | Чтобы | + | Чтобы показать некоторые из этих концепций на практике, мы рассмотрим [[Qt:Документация_4.3.2/qprogressbar | QProgressBar]] API в Qt 3 и сравним его с API в Qt 4. В Qt 3: |
{{code|qt|code= | {{code|qt|code= | ||
Строка 229: | Строка 223: | ||
int progress() const; | int progress() const; | ||
- | const QString &progressString() const; | + | const QString &progressString() const; |
bool percentageVisible() const; | bool percentageVisible() const; | ||
void setPercentageVisible(bool); | void setPercentageVisible(bool); | ||
Строка 246: | Строка 240: | ||
protected: | protected: | ||
- | virtual bool setIndicator(QString &progressStr, | + | virtual bool setIndicator(QString &progressStr, |
int progress, | int progress, | ||
int totalSteps); | int totalSteps); | ||
Строка 252: | Строка 246: | ||
}; | }; | ||
}} | }} | ||
- | API | + | API является довольно сложным и непоследовательным; например, из именования не ясно, что <tt>reset()</tt>, <tt>setTotalSteps()</tt>, и <tt>setProgress()</tt> тесно связанны между собой. |
- | + | Ключ к улучшению API заключается в том, чтобы заметить, что класс [[Qt:Документация_4.3.2/qprogressbar | QProgressBar]] подобен [[Qt:Документация_4.3.2/qabstractspinbox | QAbstractSpinBox]] и его наследникам, [[Qt:Документация_4.3.2/qspinbox | QSpinBox]], QSlider and [[Qt:Документация_4.3.2/qdial | QDial]] в Qt 4. Решение? Заменить <tt>progress</tt> и <tt>totalSteps</tt> на <tt>minimum</tt>, <tt>maximum</tt> и <tt>value</tt>. Добавить сигнал <tt>valueChanged()</tt>. Добавить вспомогательную функцию <tt>setRange()</tt>. | |
- | + | The next observation is that <tt>progressString</tt>, <tt>percentage</tt> and <tt>indicator</tt> 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 <tt>setIndicator()</tt> function. Here's the new API: | |
{{code|qt|code= | {{code|qt|code= | ||
Строка 263: | Строка 257: | ||
bool isTextVisible() const; | bool isTextVisible() const; | ||
}} | }} | ||
+ | By default, the text is a percentage indicator. This can be changed by reimplementing <tt>text()</tt>. | ||
- | + | The <tt>setCenterIndicator()</tt> and <tt>setIndicatorFollowsStyle()</tt> functions in the Qt 3 API are two functions that influence alignment. They can advantageously be replaced by one function, <tt>setAlignment()</tt>: | |
- | + | ||
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
void setAlignment(Qt::Alignment alignment); | void setAlignment(Qt::Alignment alignment); | ||
}} | }} | ||
+ | If the programmer doesn't call <tt>setAlignment()</tt>, 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 [[Qt:Документация_4.3.2/qprogressbar | QProgressBar]] API: | |
- | + | ||
- | + | ||
{{code|qt|code= | {{code|qt|code= | ||
Строка 306: | Строка 298: | ||
<div id="howtogetapisright"></div> | <div id="howtogetapisright"></div> | ||
- | == | + | == 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 <i>and</i> 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. |