Qt wiki will be updated on October 12th 2023 starting at 11:30 AM (EEST) and the maintenance will last around 2-3 hours. During the maintenance the site will be unavailable.
API Design Principles/ru
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine. Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean. |
Русском English
Принципы проектирования API
Одно из достоинств Qt — это его логичный, легкий для изучения и мощный API. С помощью этой статьи мы попытаемся подытожить все те знания, которые мы получили, проектируя API для Qt. Часть рекомендаций являются универсальными, а другие — более традиционными, и мы следуем им в основном для совместимости с уже существующими API.
Эти рекомендации в первую очередь касаются внешних API, но их так же можно использовать и для проектирования внутренних, тем самым помогая вашим коллегам разработчикам.
Если вам интересна эта тема, то можно почитать "Маленькая инструкция по проектированию API" Жасмин Бланшет.
Шесть характеристик хорошего API
API для программиста — это как GUI для пользователя. Буква "P" в аббревиатуре API означает "Программист", а не "Программа", что бы подчеркунуть тот факт, что API пользуются программисты.
В статье Qt Quarterly 13 article about API design Маттиас считает, что API должен быть минимальным и полным, иметь прозрачную и простую семантику, быть интуитивно понятным и легко запоминаться, а также способствовать написанию читаемого кода.
- Быть минимальным: Минимальный API содержит как можно меньше открытых методов на класс и как можно меньше классов. Благодаря этому можно легко понять, запомнить, тестировать и изменять API.
- Быть полным: Полное API подразумевает покрытие всей функциональности. Правда, это мешает сохранению минимальности. А еще, если член класса, находится в неправильном классе, то это приведет к тому, что пользователи не смогут найти эту функцию.
- Обладать прозрачной и просто семантикой: Необходимо следовать принципу наименьшего удивления: сделать общие задачи проще. Должна быть возможность выполнять редкие задачи, но не стоит заострять внимание на них. Надо решать конкретные проблемы, а не делать общие решения, когда в этом нет необходимости. (Например, QMimeSourceFactory в Qt 3 можно было бы назвать QImageLoader с иным API.)
- Интуитивно понятный: API, как и все остальное в компьютере, должно быть интуитивно понятным. Разные условия и опыт приводят к разным взглядам на то, что является интуитивным, а что — нет. Интуитивно понятное API, это такое API, которым может пользоваться среднестатистический пользователь, не читая документацию и программист, который не знает API, может понять код, в котором он используется.
- Легко запоминающийся: Что бы сделать API легко запоминающимся, необходимо использовать прямые и точные названия. Использовать наглядные шаблоны и концепции, избегая сокращений.
- Способствовать написанию читаемого кода: Код пишется один раз, но затем читается (и тестируется, и изменяется) множество раз. Написание читаемого кода может потребовать больше времени, чем обычно, но сэкономит время на протяжении всего жизненного цикла продукта.
Ну и наконец, имейте ввиду, что разные пользователи будут пользоваться разными частями API. Даже если пользоваться экземпляром класса Qt легко, пользователь все же прочтет документацию, прежде чем наследовать этот класс.
Статический полиморфизм
У похожих классов должно быть cхожее API. Это можно реализовать с помощью наследования, когда в этом есть смысл (при условии, что используется полиморфизм времени выполнения). Но полиморфизм всречается и во время проектирования. Например, решив заменить QProgressBar на QSlider или QString на QByteArray, можно обнаружить, что схожесть API позволяет очень легко выполнить эту замену. Мы называем это "статический полиморфизм".
Благодаря статическому полиморфизму легко запоминать API и шаблоны проектирования. Как следствие, схожее API для множества связанных классов иногда лучше, чем безупречные разные API для каждого класса.
В основном, в Qt, мы предпочитаем использовать статический полиморфизм, а не наследование, если на то нет никаких веских причин. Благодаря этому сохраняется набор открытых классов в Qt, а новички в Qt легко находят необходимые функции в документации.
Хорошо: QDialogButtonBox и QMessageBox обладают схожим API для работы с кнопками (addButton(), setStandardButtons(), и др.), не наследуя какой нибудь "QAbstractButtonBox" класс.
Плохо: QAbstractSocket наследуют и QTcpSocket и QUdpSocket: два класса с разными видами взаимодействия. Никто, кажется, никогда не пользовался указателем на QAbstractSocket универсальным или полезным способом.
Сомнительно: QBoxLayout — базовый класс для QHBoxLayout и QVBoxLayout. Преимущество: можно использовать QBoxLayout и вызывать метод setOrientation() в панели инструментов, чтобы сделать его вертикальным/горизонтальным. Недостатки: Один лишний класс, и пользователи могут писать ((QBoxLayout*)hbox)->setOrientation(Qt::Vertical), что лишено смысла.
API, основанное на свойствах
Новые классы Qt, как правило, используют API, основанное на свойствах. Например:
QTimer timer;
timer.setInterval(1000);
timer.setSingleShot(true);
timer.start();
Под свойством подразумевается любой атрибут, который является частью состояния объекта, пусть даже это не настоящий Q_PROPERTY. По-возможности, пользователь должен уметь устанавливать свойства в любом порядке, т.е. свойства должны быть ортогональными. Например, предыдущий код можно записать так
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(1000);
timer.start();
Для удобства мы можем написать так timer.start(1000).
Аналогично для QRegExp
QRegExp regExp;
regExp.setCaseSensitive(Qt::CaseInsensitive);
regExp.setPattern("'''.'''");
regExp.setPatternSyntax(Qt::WildcardSyntax);
Платой, за реализацию такого API, является позднее конструирование объекта. Например, в случае с QRegExp, нет возможности выполнить преждевременную компиляцию шаблона "." в setPattern(), потому что мы не знаем, какой стиль шаблона будем использовать.
Зачастую надо быть осторожным, потому что свойства часто устанавливаются каскадно. Обратите внимание на "размер иконки по-умолчанию" в текущем стиле и свойство "iconSize" QToolButton:
toolButton->iconSize(); // возвращает значение по-умолчанию для текущего стиля
toolButton->setStyle(otherStyle);
toolButton->iconSize(); // возвращает значение по-умолчанию для otherStyle
toolButton->setIconSize(QSize(52, 52));
toolButton->iconSize(); // возвращает (52, 52)
toolButton->setStyle(yetAnotherStyle);
toolButton->iconSize(); // возвращает (52, 52)
Обратите внимание, что единожды установив iconSize, это свойство больше не изменяется, даже если изменить текущий стиль. Это очень удобно. Иногда, этим можно воспользоваться для сброса свойства. Отсюда вытекает два подхода:
- для "сброса", воспользоваться специальным значением (например QSize(), –1, Qt::Alignment(0))
- иметь специальную функцию resetFoo() или unsetFoo()
Для "сброса" свойства iconSize достаточно установить ему значение QSize() (что эквивалентно, QSize(–1, -1))
Иногда, геттеры возвращают не те значения, которые мы записали перед этим. Например, если вызвать widget->setEnabled(true), то widget->isEnabled() может вернуть false, при условие, что родительский виджет будет в выключенном состоянии. И это нормально, потому что нас интересует действительное состояние (виджет, родитель которого находится в выключенном состоянии, должен быть тоже неактивным и вести себя, как будто он тоже выключен. В то же время, где-то внутри, он все же "включен" и просто ждет, пока родитель снова перейдет во включенное состояние), но такие ситуации должны быть документированы.
Особенности С++
Значение против Объекта
Указатели против Ссылок
Какой тип для выходных параметров лучше, указатели или ссылки?
void getHsv(int *h, int *s, int *v) const
void getHsv(int &h, int &s, int &v) const
В книгах по С++ рекомендуют использовать ссылки, где только есть возможность, аргументируя это тем, что в основном, ссылки "приятнее и безопаснее" указателей. А мы, в Qt Software предпочитаем чаще использовать указатели, потому что код пользователя становиться более читабельным. Сравните:
color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);
Только в первой строчке сразу становится понятно, что h, s, и v с высокой долей вероятности, будут модифицированы функцией.
Виртуальные функции
Когда метод класса объявлен, как виртуальный, то напрямую можно изменить поведение функции, если перегрузить ее в производном классе. Делать функции виртуальными надо в том случае, если есть необходимость вызвать ваш код, вместо кода функции. Нужно хорошо подумать, прежде чем объявить функцию виртуальной, если извне класса никто не может ее вызвать.
// QTextEdit в Qt 3: методы, которые нет смысла делать виртуальными
virtual void resetFormat();
virtual void setUndoDepth( int d );
virtual void setFormat( QTextFormat '''f, int flags );
virtual void ensureCursorVisible();
virtual void placeCursor( const QPoint &pos;, QTextCursorc = 0 );
virtual void moveCursor( CursorAction action, bool select );
virtual void doKeyboardAction( KeyboardAction action );
virtual void removeSelectedText( int selNum = 0 );
virtual void removeSelection( int selNum = 0 );
virtual void setCurrentFont( const QFont &f );
virtual void setOverwriteMode( bool b ) { overWrite = b; }
Когда мы портировали QTextEdit с Qt 3 на Qt 4, мы удалили практически все виртуальные функции. Интересно (но в то же время неожиданно), что никто на это не жаловался. Почему? Потому что в Qt 3 не использовался полиморфизм. В Qt 3 не вызывались эти функции, их вызывали вы. Проще говоря, не было причин делать производный класс от QTextEdit и переопределять эти функции, если только вы самостоятельно не вызвали эти функции. Если вам нужен полиморфизм в вашем приложении вне Qt, можно добавить полиморфизм самостоятельно.
Как избежать виртуальных функций
В Qt мы стараемся минимизировать колличество виртуальных функций по ряду причин. Каждый виртуальный вызов усложняет исправление ошибок, вставляя неконтролируемые узлы в список вызовов (результат получается непредсказуемым). Люди, внутри перегруженных функций, делают страшные вещи, например:
посылают события
- посылают сигналы
- повторно входят в цикл обработки событий (например, открывают диалог выбора файла)
- удаляют объекты (что иногда приводит к "удалению this")
Есть и другие причины, по которым стоит избегать чрезмерного использования виртуальных функций:
- невозможно добавлять, перемещать или удалять виртуальные функции, не ломая то, что уже написано
- тяжело перегружать виртуальные функции
- компиляторы практически никогда не делают оптимизацию или встраиваемые вызовы виртуальных функций
- для вызова виртуальной функции необходим поиск по таблице виртуальных функций, что замедляет работу, по сравнению с нормальным вызовом, в 2-3 раза
- из-за виртуальных функций тяжело копировать класс по значению (возможно, но грубо, что делать не рекомендуется)
Опыт показал, что в классах без виртуальных функций как правило проявляется меньше ошибок и для их поддержки нужно меньше ресурсов.
Общее правило гласит, если в основном пользователи будут вызывать метод класса, то он скорее всего должен быть не виртуальным.
Виртуальность против возможности копировать
Полиморфные объекьты и классы, передающиеся по значению — плохие друзья.
В классах, с виртуальными функциям должны быть объявлены виртуальные деструкторы, чтобы избежать утечек памяти, когда удаляется базовый класс и не чистится память производного класса.
Если необходимо копировать и присваивать класс, или сравнивать по значению, скорее всего необходим определить конструктор копирования, оператор присваивания и операторы сравнения.
class CopyClass {
public:
CopyClass();
CopyClass(const CopyClass &other;);
~CopyClass();
CopyClass &operator;=(const CopyClass &other;);
bool operator==(const CopyClass &other;) const;
bool operator!=(const CopyClass &other;) const;
virtual void setValue(int v);
};
Если создать производный класс от этого класса, ваш код может начать вести себя неожиданно. В обычной ситуации, когда нет виртуальных функций и виртуальных деструкторов, люди не могут создать производный класс и полагаться на полиморфизм. Но если добавить виртуальные функции или виртуальный деструктор, могут возникнуть причины, для создания производного класса, и все становится сложнее. На первый взгляд все выглядит просто, ведь можно объявить виртуальные операторы. Но этот путь может привести к хаосу и падению (не читаемому коду). Взгляните на этот пример:
class OtherClass {
public:
const CopyClass &instance;() const; // что тут возвращается? Что я могу ему присвоить?
};
(этот раздел находится в разработке)
Константность
В С++ есть ключевое слово "const", которым можно пометить то, что нельзя менять или то, что имеет побочный эффект. Его можно применить к простым значениям, указателям, тому, на что они указывают и как атрибут к функциям, которые не должны менять состояние объекта.
Стоит отметить что "const", само по себе, не несет никакого смысла — во множестве языков нет эквивалента ключевому слову "const", но это не делает их функционально не полными. На самом деле, если удалить перегруженные функции и, используя поиск и замену, удалить все вхождения слова "const" из С++ кода, с большой вероятностью он откомпилируется и будет нормально работать. Очень важно сохранять прагматичный подход к использованию "const".
Давайте пройдемся по областям, где используется "const" и которые относятся к проектированию API в Qt:
Входящие аргументы: константные указатели
Константные функции, которые принимают в качестве входящих параметров указатели, практически всегда должны принимать константные указатели.
Если функция определена, как const, это значит, что она никогда не будет подвержена побочному эффекту, т.е. не изменит видимого состояния объекта. Так зачем же тогда передавать не константные входящие аргументы? Давайте вспомним, что константные функции часто вызываются из других константных функций, в которых тяжело использовать не константные параметры (без const_cast и, там, где это возможно, мы действительно стараемся избежать const_cast).
До:
bool QWidget::isVisibleTo(QWidget *ancestor) const;
bool QWidget::isEnabledTo(QWidget *ancestor) const;
QPoint QWidget::mapFrom(QWidget *ancestor, const QPoint &pos;) const;
В QWidget определено много константных функций, которые в качестве входных параметров принимают не константный указатель. Напомним, что такие функции могут модифицировать виджет, но не могут модифицировать объект, которому принадлежат. Такие функции часто сопровождаются const_casts. Было бы неплохо, если бы эти функции принимали константный указатель в качестве аргументов.
После:
bool QWidget::isVisibleTo(const QWidget *ancestor) const;
bool QWidget::isEnabledTo(const QWidget *ancestor) const;
QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &pos;) const;
Хочется отметить, что для QGraphicsItem мы это исправили, а вот QWidget должен подождать до Qt 5:
bool isVisibleTo(const QGraphicsItem *parent) const;
QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point;) const;
Возвращаемые значения: константные значения
Результатом вызова функции, которая не возвращает ссылку будет R-value(то, что может стоять справа в операции присвоения).
Даже если синтаксически возможно добавить "const" к встроенным типам данных, в этом нет никакого смысла, т.к. это ничего не изменит в плане прав доступа. Современные компиляторы даже выведут предупреждение, встретив подобный код.
Если добавить "const" к классовому R-value, то доступ к не константным методам будет закрыт, так же, как и прямая работа с его членами.
Если не добавить "const", то такой доступ возможен, но он редко необходим, потому что время жизни R-value объекта ограничено и равно времени выполнения выражения.
Пример:
struct Foo
{
void setValue(int v) { value = v; }
int value;
};
Foo foo()
{
return Foo();
}
const Foo cfoo()
{
return Foo();
}
int main()
{
// Следующий код компилируется, foo() не константное R-value значение, которому нельзя ничего
// присвоить, но можно получить доступ к L-value членам:
foo().value = 1; // Ok, но значение временное и будет удалено после того, как выражение полностью выполнится.
// Следующий код компилируется, foo() не константное R-value значение, которому нельзя ничего
// присвоить, но можно получить доступ к его методам (даже не константным):
foo().setValue(1); // Ok, но значение временное и будет удалено после того, как выражение полностью выполнится.
// Следующий код НЕ компилируется, foo() константное R-value,
// с константными членами, к которым нет доступа:
cfoo().value = 1; // Not ok.
// Следующий код НЕ компилируется, foo() константное R-value,
// у которого нельзя вызвать не константный метод:
cfoo().setValue(1); // Not ok
}
Возвращаемые значения: указатели против константных указателей
Тема о том, когда константная функция должна возвращать указатели, а когда константные указатели, склоняет большинство людей к тому, что концепция "правильной константности" в С++ не работает. Проблемы начинаются, когда константная функция, которая не должна модифицировать внутреннее состояние объекта, возвращает не константный указатель на член. Просто действие, по возврату указателя, не меняет ни видимого состояния объекта, ни его возможностей. Но дает программисту косвенный доступ, для модификации данных объекта.
Этот пример демонстрирует один из нескольких способов обойти константность используя константную функцию, которая возвращает не константный указатель:
QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const
{
moveBy(10, 10); // doesn't compile!
window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // компилируется!
}
Функции, которые возвращают константный указатель, действительно защищают от подобной ситуации (возможно нежелательной\неожиданной), по крайней мере до определенной степени. Но какие функции предпочли бы вы? Те, которые возвращают константный указатель или те, которые возвращают их список? Если следовать концепции "правильной константности", каждая константная функция, которая возвращает указатель на член объекта (или список указателей на члены), должна возвращать константный указатель. На практике, такая концепция приводит к негодному API:
QGraphicsScene scene;
// … populate scene
foreach (const QGraphicsItem '''item, scene.items()) {
item->setPos(qrand() % 500, qrand() % 500); // не компилируется! Элемент - константный указатель
}
QGraphicsScene::items() — константная функция, и вы наверно подумаете, что она должна возвращать только константный указатель.
В Qt мы используем не константные шаблоны практически повсеместно. Мы выбрали прагматический подход: результат возврата константного указателя приведет к злоупотреблению const_cast, что создаст больше проблем, чем возврат не константного указателя.
Возвращаемые значения: по значению или константной ссылке?
Если у нас есть копия возвращаемого объекта, быстрее будет работать возврат константной ссылки. Однако, это может помешать нам в будущем, если, например, мы захотим переписать класс. (Используя идеологию d-указателей, мы можем менять представление класса в памяти, но одновременно, мы не можем поменять сигнатуру функций с "const QFoo &" на "QFoo" и при этом не потерять бинарную совместимость.) Именно из-за этого, мы в основном возвращаем "QFoo" вместо "const QFoo &", кроме тех случаев, когда критична скорость, а рефакторинг — не проблема (например, QList::at()).
Константность против состояния объекта
Константная корректность в С++ активно обсуждается, потому что в некоторых областях она не работает.
Основное правило гласит, что константные функции не меняют видимого состояния класса. Под состоянием подразумевается "себя и свои возможности". Это не значит, что не константные функции, в отличии от константных, меняют приватные члены класса. А значит, что эти функции активные, и могут иметь видимые побочные эффекты. Константные функции в общем случае не обладают обратными эффектами. К примеру:
QSize size = widget->sizeHint(); // const
widget->move(10, 10); // not const
Делегат отвечает за рисование на чем-то. Его состояние включает и его возможности, а значит, и состояние того, на чем он рисует. Вызов рисования влечет за собой побочный эффект. Оно меняет внешний вид устройства (а значит и состояние), рисуя на нем. Поэтому, нет смысла делать paint() константным методом, как и paint() диалогов или QIcon. Никто не будет вызывать QIcon::paint() из константной функции. А если уж очень нужно, можно явно снять константность функции, поэтому в таких случая проще воспользоваться const_cast.
// QAbstractItemDelegate::paint is const
void QAbstractItemDelegate::paint(QPainterpainter, const QStyleOptionViewItem &option;, const QModelIndex &index;) const
// QGraphicsItem::paint is not const
void QGraphicsItem::paint(QPainter''' painter, const QStyleOptionGraphicsItem '''option, QWidget '''widget = 0)
.
Семантика и документация API
Что необходимо делать, когда функция возвращает –1? и т.д.
Предупреждения/ошибки/и т.д.
API должно быть качественным. Первая версия всегда будет с ошибками, поэтому ее нужно протестировать. Взгляните на код, использующий это API и убедитесть в том, что он читабелен.
Еще один прием — попросить кого нибудь воспользоваться вашим API с/без документацией(-ии) и документировать класс (как в общем весь класс, так и каждый метод).
Ключевое слово const не должно "работать" на вас. Попробуйте избавиться от него, чтобы исключить две перегруженные версии функции (константная и не константная).
Искусство присваивания имен
Одна из самых больших проблем проектирования API — присваивание имен. Как должен называться класс? Как должны называться его методы?
Главные правила присваивания имен
Несколько правил можно отнести ко всем видам имен. Во-первых, как уже было сказано, не пользуйтесь аббревиатурами. Даже если "previous" вы замените на "prev", в будущем это только сыграет против вас, потому что пользователь должен помнить, какие слова используются в виде аббревиатур.
Еще хуже, если в API встречаются двойственные варианты. Например, в Qt 3 есть activatePreviousWindow() и fetchPrev(). Следуя правилу "никаких аббревиатур" легче сделать непротиворечивое API.
Другое, важное, но более тонкое правило: когда проектируются классы, необходимо попытаться сохранить пространство имен для производных классов чистым. В Qt 3, не всегда соблюдается этот принцип. Что бы продемонстрировать это, приведем в пример QToolButton. Если назвать name(), caption(), text(), или textLabel() в QToolButton в Qt 3, к чему это приведет? Попробуйте поиграться с QToolButton в дизайнере:
- Свойство name наследуется от QObject и ссылается на внутреннее имя объекта, которое можно использовать для отладки и тестирования.
- Свойство caption наследуется от QWidget и ссылается на заголовок окна, который никак не влияет на объекты QToolButton, потому что у них очень часто есть родитель.
- Свойство text наследуется от QButton и используется в кнопке, если только useTextLabel не установленно в true.
- Свойство textLabel определено в QToolButton и отображается на кнопке, если useTextLabel установленно в true.
Для облегчения читабельности в Qt 4 в классе QToolButton name преименовано на objectName, caption на windowTitle, а свойство textLabel вообще удалено.
В поиске имен можно воспользоваться хитростью и просто попробовать написать документацию: задокументировать элементы (классы, функции, перечисления и др.) и использовать первое предложения, как основу для создания имени. Если трудно найти подходящее имя, это первый сигнал, который ставит под сомнение необходимость данного элемента. Если не получается найти подходящее имя, но вы уверенны, что такой элемент должен существовать, нужно придумать абсолютно новое имя. Таким образом появились имена "widget", "event", "focus", "buddy".
Присвоение имен классам
Нужно искать группы классов, вместо того, что бы подбирать каждому классу индивидуальное имя. Например, в Qt 4 классы для отображения моделей имеют суффикс View (QListView, QTableView, и QTreeView), а классы, для отображения элементов — Widget (QListWidget, QTableWidget, and QTreeWidget).
Имена для перечисляемых типов и значений
Когда мы определяем перечисляемый тип, нужно держать во внимание, что в С++ (в отличии от Java или С#), значения перечисляемого типа используются без названия самого типа. Следующий пример показывает, на сколько опасно использовать общие имена в значениях перечисляемого типа:
namespace Qt
{
enum Corner { TopLeft, BottomRight, … };
enum CaseSensitivity { Insensitive, Sensitive };
…
};
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);
В последней строчке что может значить Insensitive? Одна из рекомендаций: хотя бы один элемент перечислимого типа должен повторяться во всех значениях этого типа:
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, … };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
…
};
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
Если перечислимые значения могут подвергаться операции OR и использоваться, как флажки, то результат операции будет храниться в переменной типа int, что не так уж и хорошо. В Qt 4 есть шаблонный класс QFlags, где T — перечислимый тип. Для удобства в Qt определяется новый тип для имен флага, и можно просто писать Qt::Alignment вместо QFlags<Qt::AlignmentFlag>.
Было решено, для перечислимого типа использовать имена в единственном числе (потому что в один момент времени может содержать только один флаг), а для "флажков" — в множественном. Например:
enum RectangleEdge { LeftEdge, RightEdge, … };
typedef QFlags<RectangleEdge> RectangleEdges;
В некоторых случаях, имена "флажков" могут быть в единственном числе. Тогда, имя перечислимого типа сдержит суффикс Flag:
enum AlignmentFlag { AlignLeft, AlignTop, … };
typedef QFlags<AlignmentFlag> Alignment;
Имена функций и параметров
Первое правило для присвоения имен функциям — из названия функции должно быть понятно, имеет она побочные эффекты или нет. В Qt 3, константная функция QString::simplifyWhiteSpace() игнорирует это правило, потому что не модифицирует строку, для которой она вызвана, как можно было бы подумать, исходя из имени, а возвращает QString. В Qt 4 функция была переименована в QString::simplified().
Имена параметров — важный источник информации для программиста, даже если он не смотрит в код, который относится к API. С тех пор, как современные IDE научились показывать их, во время написания кода, есть смысл присваивать имена параметрам в заголовочных файлах и использовать эти же имена в документации.
Присваивание имен логическим геттерам, сеттерам и свойствам
Особую головную боль доставляет поиск имен, для геттеров и сеттеров bool параметров. Геттер должен быть назван checked() или isChecked()? scrollBarsEnabled() или areScrollBarEnabled()?
В Qt 4 мы используем следующий подход при именовании геттер функций:
Прилагательные с префиксом is-. Примеры: isChecked() ' isDown() ' isEmpty() ' isMovingEnabled()
- Но, прилагательные применяемые к множественному числу используются без префикса:
' scrollBarsEnabled(), не areScrollBarsEnabled()
- Глаголы не имеют никакого префикса и не используют третье лицо (-s):
' acceptDrops(), не acceptsDrops() ' allColumnsShowFocus()
- Существительные обычно без префикса:
' autoCompletion(), не isAutoCompletion() ' boundaryChecking()
- Иногда, отсутсвие префикса вводит в заблуждение, поэтому мы используем префикс is-:
' isOpenGLAvailable(), не openGL() ' isDialog(), не dialog()
(Исходя из имени функции dialog() мы легко можем догадаться, что она возвращает QDialog .)
Имя сеттера можно получить из имени геттера, просто удалив все префиксы и в начало имени поставить "set". Например, setDown() и setScrollBarsEnabled(). Имя свойства будет таким же, как и геттер, только без префикса.
Как избежать распространенных ошибок
Невидимая ловушка
Популярное заблуждение, что чем меньше кода необходимо для реализации чего-то, тем лучше 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");
Ловушка с булевыми параметрами
Булевы параметры часто приводят к не читаемому коду. Идея добавить bool параметр в функцию чаще всего является ошибочной. В Qt стандартный пример — функция repaint(), которая может принимать bool в качестве необязательного параметра, который определяет, будет ли стираться задний фон (по-умолчанию) или нет. Поэтому мы можем писать так
widget->repaint(false);
а новичок такой код может прочитать как "Не перерисовывать!"
Размышления приводят к тому, что bool параметры сохраняют одну функцию, а размер уменьшается. А на самом деле, размер только увеличивается. Как много пользователей Qt честно знают, что делают все эти три строчки?
widget->repaint();
widget->repaint(true);
widget->repaint(false);
На сколько лучше был бы такой вариант
widget->repaint();
widget->repaintWithoutErasing();
В Qt 4 мы решили проблему просто удалив возможность перерисовки без стирания виджета. Встроенная поддержка двойной буферизации в Qt 4 ликвидировала необходимость в этой возможности.
Приведем еще пример:
widget->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_''''''.c??", false, true);
Очевидное решение проблемы — заменить bool параметры перечислимыми типами. Это именно то, что мы сделали в Qt 4 с учетом регистра в QString. Сравните:
str.replace("USER", user, false); // Qt 3
str.replace("USER", user, Qt::CaseInsensitive); // Qt 4
Ловушка копировать/вставить
Примеры
QProgressBar
Чтобы продемонстрировать эти концепции на практике, давайте рассмотрим API QProgressBar в Qt 3 и сравним с Qt 4 API. В Qt3:
class QProgressBar : public QWidget
{
…
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 &progressStr;,
int progress,
int totalSteps);
…
};
API довольно сложный и не логичный. Например, по именам не совсем понятно, что reset(), setTotalSteps(), and setProgress() связанны между собой.
Для улучшения API нужно было просто заметить сходство QProgressBar и класса QAbstractSpinBox в Qt 4, а так же его наследников, QSpinBox, QSlider и QDial. Какое будет решение? Заменить progress и totalSteps на minimum, maximum и value. Добавить сигнал valueChanged(). Добавить вспомогательную функцию setRange().
Следующее замечание по поводу progressString, проценты и индикатор преследуют один и тот же смысл: текст, который отобрадается на прогресс баре. Очень часто текст — это проценты, но его можно поменять, используя setIndicator(). Вот новое API:
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
По-умолчанию, текст — индикатор процентов. Но его можно изменить, самостоятельно реализовав text().
В API Qt 3 есть две функции, setCenterIndicator() и setIndicatorFollowsStyle(), которые отвечают за выравнивание. Их легко можно заменить на одну, setAlignment():
void setAlignment(Qt::Alignment alignment);
Если программист не вызовет функцию setAlignment(), выравнивание будет основываться на стиле. Для стиля Motif, текст будет располагаться посередине. Для других стилей, он будет отображаться с правой стороны.
Посмотрим на улучшенное API QProgressBar:
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);
…
};
QAbstractPrintDialog & QAbstractPageSizeDialog
В Qt 4 появились два класса QAbstractPrintDialog и QAbstractPageSizeDialog, которые служат базовыми классами для QPrintDialog и QPageSizeDialog. Но это глупое решение, потому что в Qt API нигде не используются в качестве аргументов указатели на QAbstractPrint- или -PageSizeDialog и над ними не выполняются никакие операции. Это яркий пример, когда нет никакой необходимости в абстрактных классах.
Но нельзя говорить, что хорошая абстракция — это не правильно. Ведь правда, QPrintDialog может использовать фабрику или другой механизм для его модификации, что подтверждает определение #ifdef QTOPIA_PRINTDIALOG.
QAbstractItemModel
Детально проблемы модели\представления в Qt 4 хорошо описаны и в других местах, но главный вывод, который можно сделать: "QAbstractFoo" не должен объединять все производные классы, и заострять на нем внимание во время написания кода. Потому что модель "объединить все" для базового класса практически всегда является плохим решением. И QAbstractItemModel совершает эту ошибку. На самом деле он представляет из себя всего лишь QTreeOfTablesModel, но со значительно усложненным API, которое затем еще и наследуется производными классами.
Добавление абстракции автоматически не делает API лучше.
QLayoutIterator & QGLayoutIterator
В Qt 3, для создания своего класса размещения, необходимо реализовать классы QLayout и QGLayoutIterator ("G" от слова "generic"). Экземпляр QGLayoutIterator используется классом QLayoutIterator, который пользователи могут использовать, как обычный итератор. С помощью QLayoutIterator можно писать такой код:
QLayoutIterator it = layout()->iterator();
QLayoutItem **child;
while ((child = it.current()) != 0) {
if (child->widget() == myWidget) {
it.takeCurrent();
return;
}
++it;
}
В Qt 4 мы удалили класс QGLayoutIterator (и другие подобные классы для разный размещений) и вместо него, реализуя свой QLayout нужно реализовать функции itemAt(), takeAt(), и count().
QImageSink
В Qt 3 был целый набор классов для последовательного чтения изображений и вывода их как анимации — классы QImageSource/Sink/QASyncIO/QASyncImageIO. Т.к. все они были необходимы только для анимации, то мы их просто удалили, а на замену им пришел QLabel.
Мораль сей басни такова, что не стоит добавлять абстракцию на туманное будущее. Лучше сделать проще. Когда настанет это будущее, будет намного проще внедрить новшество в простую систему, чем в сложную.
=== другие Qt3 против Qt4?
QWidget::setWindowModified(bool)
Q3Url vs. QUrl
Q3TextEdit vs. QTextEdit ===
Как все эти виртуальные функции умирают.
Qt's Clipping Story (naming of clipping fns)
When you set the clip rect, you actually set a region (should be setClipRegion(QRect) instead of setClipRect()).
(on the right, how it should have been…)