API Design Principles/ja
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. |
API 設計の原理原則
Qt が好評を得ている理由の1つが、首尾一貫した、簡単に学べる、非常に強力な API です。Qt の API を設計する上でこれまで蓄積してきたノウハウをこの記事で解説したいと思います。多くのガイドラインは普遍的なもので、中には慣例的なものもあります。我々がこのガイドラインに従う第一の理由は、既存の API の一貫性を保つためです。
基本的にこのガイドラインはパブリックな API に対するものですが、同様のテクニックをプライベートな API に対しても適用することを推奨しています。
Jasmin Blanchette による Little Manual of API Design (PDF) も合わせて読むと良いでしょう。
良い API の6つの特徴
プログラマーにとって API とは、エンドユーザーにとっての GUI のようなものです。API が人間のプログラマーによって使われるという事実を強調する場合、API の 'P' は "Program" ではなく "Programmer" を意味します。
Qt Quarterly 13 の API design に関する記事 で、Matthias は API は最小限かつ完全で、明確でシンプルなセマンティックを持ち、直感的で、簡単に覚ることができ、可読なコードを導くべきだという彼の考えを示しました。
- 最小限であれ: 最小限の API とは、クラス数を可能な限り少なくし、クラスのパブリックなメンバも可能な限り少なくするということです。これにより、理解が容易になり、覚えやすく、デバッグがしやすく、API の変更もしやすくなります。
- 完全であれ: 完全な API とは必要な機能を備えているということです。最小限であることとは相反することがあります。メンバ関数が誤ったクラスにある場合には、その関数にたどり着く可能性が低くなります。
- 明確でシンプルなセマンティックであれ: 一般的な設計と同様に、「驚き最小の原則」に従うべきです。一般的なものは簡単に対応できるようにします。一般的ではないものも対応できるようにすべきですが、それにはフォーカスしません。個々の問題への対処: 解決策を必要以上に一般化しないようにしましょう (例えば、Qt 3の QMimeSourceFactory は、QImageLoader と呼ばれる異なるAPI を持つものにすることもできました)。
- 直感的であれ: コンピューター上の全てのものと同様に、API も直感的であるべきです。何が直感的であるかは経験やバックグランドによって異なります。それなりに経験のあるユーザーがドキュメントを読まずともはじめることができ、API を知らないプログラマーでも書かれたコードを理解することができる場合は API は直感的と言えるでしょう。
- 覚えやすくあれ: API を簡単に覚えられるようにするには、首尾一貫した正確な名前をつけることです。理解できるパターンやコンセプトを採用し、省略はさけましょう。
- 可読なコードを導け: コードは1度しか書かれませんが、何度も読まれ(デバッグされ、変更され)ます。読みやすいコードは書くのには時間がかかることもありますが、プロダクトのライフサイクルにおいては時間の節約になります。
最後に、様々なユーザーが様々な部分の API を使用することを心に留めてください。Qt のクラスのインスタンスを単に使用するのも直感的であるべきですが、ユーザーが派生クラス化をする前にそのドキュメントを読むことを想定してください。
静的なポリモーフィズム
同じようなクラスは同じような API を持つべきです。これは、実行時のポリモーフィズムが利用できる場所では、継承により実現することができます。しかし、ポリモーフィズムは設計時にも起こります。例えば QProgressBar の QSlider への変更や QString の QByteArray への変更など、API が似ていることでこれらの変更がとても簡単にできることがわかるでしょう。我々はこれを "静的なポリモーフィズム" と呼んでいます。
静的なポリモーフィズムにより API やプログラミングのパターンを覚えるのも簡単になります。関係するクラス群が同じような API を持つということは、それぞれのクラスが完璧な API をそれぞれ持つよりも結果として良い場合があります。
Qt ではやむを得ない場合を除き、実際の継承よりもこの静的なポリモーフィズムを採用することを好んでいます。これにより、Qt のパブリックなクラス数を少なく保つことができ、Qt の初心者でもドキュメントを見て使用方法を簡単に理解することができるようになります。
良い例: QDialogButtonBox と QMessageBox は "QAbstractButtonBox" のようなクラスを継承することなくボタンに関する同じような API (addButton(), setStandardButtons() など)を持ちます。
悪い例: QAbstractSocket は QTcpSocket と QUdpSocket により継承されていますが、この2つのクラスでは動作が大きく異なります。QAbstractSocket のポインタを有効に使用した(できた)例は見たことがありません。
難しい例: QBoxLayout は QHBoxLayout と QVBoxLayout の基底クラスです。利点: QBoxLayout を使用し、ツールバーで setOrientation() を呼ぶことで Horizontal/Vertical の設定ができる。欠点: 余計なクラスで、ユーザーは ((QBoxLayout *)hbox)->setOrientation(Qt::Vertical) とする事もできるが、あまり意味がない。
プロパティに基づく API
新しい Qt のクラスは "プロパティに基づく API" を持つ傾向にあります。QTimer を例に見てみましょう。
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() で指定された "."のパターンをコンパイルしてしまうのは時期尚早です。
プロパティは連続して指定されることがあります。この場合は注意深く先に進める必要があります。現在のスタイルによって決まる "デフォルトのアイコンサイズ" と QToolButton の "iconSize" プロパティの例を見てみましょう。
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 を設定すると、この設定が有効になり、スタイルの変更では設定が変わりません。これは 良いことです 。プロパティをリセットできるようにしておくと便利な場合もあり、これには2つのアプローチがあります。
- 特別な値 (QSize() や –1、Qt::Alignment(0) など) を "リセット" の意味で渡す
- resetFoo() や unsetFoo() などの関数を明示的に用意する
iconSize の場合は (QSize(–1, -1) を意味する) QSize() を "リセット" の意味とすることで十分でしょう。
取得関数が設定されたものと異なるものを返す場合があります。例えば widget->setEnabled(true) を実行した場合でも widget->isEnabled() が false を返すことがあり得ます。これは親が無効な場合です。通常はこれがチェックしたい点 (親が無効な場合は子ウィジェットもグレーアウトされるべきで、そのウィジェット自身も無効として振る舞うべきであると同時に、この設定は内部では保持されており、実際は "有効" であり、親が再度有効になるのを待っているということを) なので、問題はありません。ただし、この動作はドキュメントに正確に記載されるべきですが。
C++ 固有の問題
値かオブジェクトか
ポインタか参照か
出力パラメーターには、ポインタと参照ではどちらがベストでしょうか?
void getHsv(int *h, int *s, int *v) const
void getHsv(int &h, int &s, int &v) const
ほとんどの C++ の本では可能な場合は参照が推奨されています。一般的な観点では、ポインタよりも参照の方が "より安全でより適切" とされています。これに反して、Qt ではポインタを選択する傾向にあります。これはユーザーのコードがより読みやすくなるからです。それでは比較してみましょう。
color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);
h、s、v がこの関数呼び出しで変更される可能性が高いことが明らかなのは最初の行だけでしょう。
バーチャル関数
C++ ではメンバ関数をバーチャルで宣言する基本的な目的は、派生クラスでその関数をオーバーロードし、その振る舞いをカスタマイズできるようにすることです。その関数をバーチャルにする目的はその関数の既にある呼び出しで、代わりに自分のコードを実行するためです。ある関数を外部から呼び出すコードがどこにもない場合には、その関数をバーチャルとして宣言することに慎重になるべきです。
// Qt 3 の QTextEdit: バーチャルである理由がないメンバ関数の一覧
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 では QTextEdit のためにはポリモーフィズムが使われていなかったからです。Qt 3 の中ではこれらの関数は呼び出していませんでした。呼び出していたのはユーザーです。端的に言うと、QTextEdit の派生クラスを作成する理由は無く、ユーザーがこれらの関数を呼び出さない限り、これらの関数を再実装する理由も無かったのです。Qt の外のアプリケーションでポリモーフィズムが必要な場合は、自分でポリモーフィズムを追加することができたのです。
バーチャル関数を避ける
Qt では我々は様々な理由によりバーチャル関数の数を最小限にするように努めました。バーチャル関数の呼び出しは、制御不能なノードが呼び出しグラフの中に入るため(出力が予測不可能になり)、バグの修正を困難にします。以下のように、再実装したバーチャル関数の中でとんでもない処理をする人もいます。
イベントの送信
- シグナルの発生
- (例えばモーダルなファイルダイアログを開くなどによる) イベントループの実行
- (例えば "delete this" となるような) オブジェクト自体の破棄
これ以外にも、バーチャル関数の過度の使用を避ける理由は山ほどあります。
- バイナリコンパチビリティを保ちながらのバーチャル関数の追加、移動、削除ができない
- バーチャル関数は簡単にはオーバーロードできない
- ほとんど全てのバーチャル関数の呼び出しはコンパイラによる最適化やインライン化ができない
- 関数呼び出しに v-table のルックアップが必要になり、通常の関数に比べ2〜3倍遅くなる
- バーチャル関数を持つクラスでは値としてのコピーが難しくなる(可能だが、とても厄介で非推奨)
バーチャル関数を持たないクラスほどバグが少なく、一般的にメンテナンス性が高いことが経験的に分かっています。
経験則では、我々がツールキットとして、そして主なユーザーとして、関数を呼び出すような場合でない限り、その関数はおそらく非バーチャルであるべきです。
バーチャル vs コピー可能
ポリモーフィックなオブジェクトと値型のクラスは良いお友達ではありません。
バーチャル関数を持つクラスのデストラクタはバーチャルでなければなりません。これは派生クラスのデータが破棄されることなしに、基底クラスが破棄されることによって起こるメモリリークを防ぐためです。
あるクラスのコピーや代入を可能にしたり、値による比較をしたい場合には、コピーコンストラクタと代入演算子、等価演算子が必要でしょう。
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 について
C++ にはあるものが変わらない/副作用が無いことを表すための "const" というキーワードがあります。これは単純な値、ポインタ、ポインタが示すものに対して有効で、さらにオブジェクトの状態を変えない関数の特別な属性として使われます。
const がついたもの自体にはそれほど大きな価値がありません。"const" キーワードさえ持っていないプログラミング言語もたくさんあります。しかしこのことがそのまま不要であるという理由にはなりません。実際、C++ のソースコードから、関数の const のオーバーロードを消し、"const" キーワードを全て検索して削除してみてもほとんどのものは、コンパイル、動作ともに問題ないでしょう。しかし、実用性を考えると、"const" の使用はとても重要です。
Qt の API 設計の中で、"const" が適切に使用されているものをいくつか見てみましょう。
引き数の const ポインタ
ポインタを引き数として取る const 関数はほぼ全て const ポインタを引き数に取ります。
ある関数が実際に const と宣言されている場合、副作用やそのオブジェクトの目に見える状態を変えることはありません。それにも関わらず const ではない引き数が必要な場合はあるのでしょうか。const 関数は他の const 関数から呼ばれることが多く、それを考えると、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 ではないポインタを引き数に取る const 関数がたくさんあります。これらの関数は引き数で渡された widget を変更することは可能ですが、自分自身は変更できません。このような関数は const_cast と一緒になっています。これらの関数は const ポインタを引き数に取る方がいいでしょう。
変更後:
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;
戻り値の const
関数呼び出しの戻り値で、参照を返さないものは R-value と呼ばれます。
クラスではない R-value は常に cv 非修飾型です。文法的には "const" をつけることが可能であっても、アクセス権に関するものは何も変更しないため、あまり意味はありません。
最近のほとんどのコンパイラではこのようなコードのコンパイル時に警告が表示されるでしょう。
クラス型の R-value に "const" を追加した場合、const ではないメンバ関数へアクセスすることや、そのメンバを直接操作することは禁止されています。
"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() は const ではない R-value のため
// (一般的に L-value を必要とする)代入はできませんが、
// メンバへのアクセスは L-value となります。
foo().value = 1; // OK だが、一時オブジェクトは文の最後で破棄されます。
// 以下の文はコンパイルは通ります。foo() は const ではない R-value のため
// 代入はできないが、(const でなくても) メンバ関数を呼ぶことはできます。
foo().setValue(1); // OK だが、一時オブジェクトは文の最後で破棄されます。
// 以下の文はコンパイルが_通りません_。foo() は const なメンバを持つ
// const な R-value のため、メンバアクセスでの代入はできません。
cfoo().value = 1; // NG
// 以下の文はコンパイルが_通りません_。foo() は const なメンバを持つ
// const な R-value のため、const ではないメンバ関数を呼び出せません。
cfoo().setValue(1); // NG
}
戻り値: ポインタ vs const ポインタ
const 関数がポインタを返すか、const ポインタを返すかについてですが、ほとんどの人が C++ での "const の正しさ" のコンセプトが破綻していると感じるところです。問題は自分の状態を変更しない const 関数が const ではないメンバのポインタを返すことで起こります。this ポインタを返す単純な例でもオブジェクトの目に見える状態は変わりませんし、その影響範囲の状態も変わりません。しかし、プログラマーは間接的にオブジェクトのデータを変更することができます。
以下のサンプルで const ではないポインタを返す const 関数を使用した、数ある const の抜け道の1つを見てみましょう。
QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const
{
moveBy(10, 10); // コンパイルできない!
window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // コンパイルできる!
}
const ポインタを返す関数では(期待どおりかどうかは別として) this に対する副作用を少なくともある程度の範囲において防いでいます。しかし、const ポインタやそのリストを返したいと思うのはどのような関数でしょうか?const の正しいアプローチを取った場合、メンバの1つへのポインタ(もしくはその配列)を返す全ての const 関数は、const ポインタを返さなければいけません。しかし残念なことに現実的には API が使いづらくなります。
QGraphicsScene scene;
// … シーンの構築
foreach (const QGraphicsItem '''item, scene.items()) {
item->setPos(qrand() % 500, qrand() % 500); // item が const ポインタなのでコンパイルできない!
}
QGraphicsScene::items() は const 関数のため、const ポインタの配列を返すべきだということになるかもしれません。
Qt では、我々はほとんど全てで const ではないパターンを使用しています。つまり、実用的なアプローチを選択しました。const ではないポインタを返し、その不正使用により起こりうる問題よりも、const ポインタを返すことによって起こりうる const_cast の過度の利用の方が深刻だと考えたのです。
戻り値: 値か const 参照か
戻り値のためのオブジェクトのコピーを保持している場合、const 参照を返すのが最も速いアプローチです。しかし、これは我々が後でそのクラスのリファクタリングをする際には足かせとなります。 (d-ポインタを使用する方法により、Qt のクラスのメモリ表現をいつでも変えることができます。しかし、バイナリ互換を保ったまま関数のシグネチャを "const QFoo &" から "QFoo" へ変更することはできません。) このため、処理速度が本当に求められる場合でリファクタリングの問題が無い場合(例えば QList::at())を除いて、一般的には "const QFoo &" ではなく "QFoo" を返すようにしています。
const vs オブジェクトの状態
const の正当性は C++ に置ける vi/emacs 議論であり、このトピックはいくつかの分野で破綻しています(例えばポインタベースの関数)。
しかし、const 関数はクラスの目に見える状態を変えないというのが一般的なルールです。状態とは "自分と自分の責任の範囲" を意味します。これは const ではない関数がそのプライベートなメンバデータを変えることを意味するものではありません。また、const 関数でそれをできないわけでもありません。しかし、その関数はアクティブで、目に見える副作用を持ちます。const 関数は通常は目に見える副作用は行いません。
QSize size = widget->sizeHint(); // const
widget->move(10, 10); // const ではない
デリゲートは何か別のものの上に描画をするためのものです。デリゲートの状態にはその責任が含まれるため、描画対象の状態を含みます。描画には副作用があり、描画を行う対象の見た目(とそれに伴い、状態)を変更します。このため、paint() を const とするのは間違っています。全てのビューの paint() や QIcon の paint() も同様です。ある関数の const 性を明らかに破る目的ではない限り、const 関数の中で QIcon::paint() を呼ぶ人はいないでしょう。また、その場合は const_cast による明示的なキャストの方がいいでしょう。
// QAbstractItemDelegate::paint は const
void QAbstractItemDelegate::paint(QPainterpainter, const QStyleOptionViewItem &option;, const QModelIndex &index;) const
// QGraphicsItem::paint は const ではない
void QGraphicsItem::paint(QPainter''' painter, const QStyleOptionGraphicsItem '''option, QWidget '''widget = 0)
API のセマンティックとドキュメント
-1 を関数に渡した場合にどうすべきかなど…。
警告、致命的なエラーなど
API には品質の保証が必要です。1番最初のものは決して正しくありません。API のテストをしなければなりません。この API を使用しているコードを見ることでユースケースを作成し、そのコードが可読かどうかを確認してください。
他には、他の人にその API をドキュメントのあり/なしで使ってもらい、そのクラスのドキュメント(クラスの概要と個々の関数)を書く方法があります。
const キーワードはあなたのためには "なにもしてくれません" 。1つの関数に const/const ではないバージョンのオーバーロードを持つよりは、削除することを検討して下さい。
命名の美学
命名はおそらく API の設計における最重要課題です。そのクラスはなんと呼ばれるべきですか?メンバ関数はなんと呼ばれるべきですか?
一般的な命名規則
どんな種類の名前にも上手く適用できる規則がいくつかあります。まずはじめに、前述の通り、省略はしてはいけません。これは "previous" を "prev" とするような明らかな場合でも、ユーザーがどの場合は省略形なのかを覚えなければならないため、長期的にはこれは良くありません。
API 自体に矛盾があるのは当然よくありません。例えば、Qt 3 には activatePreviousWindow() と fetchPrev() がありました。"省略は無し" というルールにより矛盾のない API の実現が単純になります。
もう1つのクラスを設計する際の重要で、それでいて繊細なルールは派生クラスのために名前空間を綺麗にするべきだということです。Qt 3 はこの原理に従っていないところもありました。分かりやすく説明するため、QToolButton を例にとります。Qt 3 の QToolButton で name()、caption()、text()、textLabel() を呼んだ場合、何を期待しますか? Qt Designer 上で QToolButton で色々試してみてください。
- name プロパティは QObject から継承したもので、デバッグやテストの際に使用される内部のオブジェクト名を表します。
- caption プロパティは QWidget から継承したもので、ウィンドウのタイトルを表しますが、QToolButton は基本的には子ウィジェットとして使われるため実質的には意味がありません。
- text プロパティは QButton から継承したもので、useTextLabel が true でない場合にボタン上に表示されます。
- textLabel プロパティは QToolButton で定義され、useTextLabel が true の場合に表示されます。
可読性の観点から、name は Qt 4 では objectName となりました。caption は windowTitle となり、QToolButton で text とは別にあった textLabel プロパティは無くなっています。
良い名前が思い浮かばない場合には、ドキュメント化はとてもいい方法になりえます。そのアイテム(クラス、関数、enum の値など)のドキュメントを作成してみて、最初にひらめいたの文章から決めるのです。正確な名前が見当たらない場合、そのアイテムがあるべきではないというサインの可能性があります。もし全ての方法に失敗し、それでもそのコンセプトに意味がある場合、新しい名前を発明しましょう。"widget" や "event"、"focus"、"buddy" などはこの結果生まれたものです。
クラスの命名
個々のクラスに完璧な名前をつけるのではなく、クラスのグループが分かるようにしましょう。例えば Qt 4 に含まれる、モデルを利用するビュークラスは全て View で終わる名前(QListView、QTableView、QTreeView)になっていて、対応するアイテムベースのクラスは Widget で終わる名前(QListWidget、QTableWidget、QTreeWidget)になっています。
Enum 型と値の命名
enum を宣言する際、C++ では(Java や C# とは異なり)、型には関係なく enum 値が使われることを心に留めておかなければなりません。以下の例で一般的すぎる名前を enum 値につけた場合の危険性を見てみましょう。
namespace Qt
{
enum Corner { TopLeft, BottomRight, … };
enum CaseSensitivity { Insensitive, Sensitive };
…
};
tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);
最後の行で、Insensitive は何を意味するのでしょう?我々の enum 型の命名のガイドラインではそれぞれの enum 値で、 enum の型名の少なくとも1つの部分を繰り返すことになっています。
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, … };
enum CaseSensitivity { CaseInsensitive,
CaseSensitive };
…
};
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
enum の値を OR でまとめてフラグとして使用できる場合、その結果は int に格納するのが伝統的な方法ですが、これは型安全ではありません。Qt 4 では T が enum 型である QFlags<T> というテンプレートクラスを導入しました。Qt では利便性を考え、このようなフラグ型の名前を typedef してあるため、QFlags<Qt::AlignmentFlag> の代わりに Qt::Alignment と書くことができます。
慣例的に、enum 型の名前には(一度に1つのフラグしか持たないため)単数形を使用していて、"flags" 型は複数形の名前にしてあります。例:
enum RectangleEdge { LeftEdge, RightEdge, … };
typedef QFlags<RectangleEdge> RectangleEdges;
"flags" 型の名前が単数形の場合もあります。この場合、enum 型は Flag で終わる名前にしています。
enum AlignmentFlag { AlignLeft, AlignTop, … };
typedef QFlags<AlignmentFlag> Alignment;
関数と引数の命名方法
関数の命名の第一のルールは名前から副作用の有無が分かるようにすることです。Qt 3 では const 関数 QString::simplifyWhiteSpace() がこの規則を違反していました。この関数は名前の通り呼び出された文字列自体を変更するのではなく、QString を返していたからです。この関数は Qt 4 では QString::simplified() という名前に変更されています。
引数の名前はその API を使用するコードには現れませんが、プログラマーにとっては重要な意味を持ちます。最近の IDE ではプログラマーがコードを書いている際に表示されることが多いため、引き数に適切な名前をつけ、ヘッダファイルと同じ名前をドキュメントでも使用することはとても大事なことです。
bool 型の取得関数、設定関数、プロパティの命名方法
bool 型のプロパティに対する取得関数と設定関数の良い名前を探すことは常に難しい問題です。取得関数は checked() とすべきでしょうか?それとも isChecked() とすべきでしょうか。scrollBarsEnabled() と areScrollBarEnabled() ではどうでしょうか。
Qt 4 では取得関数の命名に以下のガイドラインを使用しました。
形容詞は接頭語 is をつける isChecked() ' isDown() ' isEmpty() ' isMovingEnabled()
- 複数形の名詞に対する形容詞の場合は is をつけない
- scrollBarsEnabled() であり、areScrollBarsEnabled() ではない
- 動詞の場合は接頭語をつけず、原型にする(三単現の s はつけない)
- acceptDrops() であり、acceptsDrops() ではない
- allColumnsShowFocus()
- 名詞の場合は通常は接頭語をつけない
- autoCompletion() であり、isAutoCompletion() ではない
- boundaryChecking()
- 接頭語をつけないことが適切ではない場合には、is をつける
- isOpenGLAvailable() であり、openGL() ではない
- isDialog() であり、dialog() ではない
(dialog() という名前の関数があった場合 何かしらの QDialog を返すと思うでしょう。)
設定関数の名前は、取得関数から接頭語を除いて、名前の最初に set をつけています。(例: setDown() や setScrollBarsEnabled()) プロパティの名前は取得関数から接頭語を除いたものです。
一般的な罠を回避する
利便性の罠
1つの間違ったコンセプトは、何かをするために必要なコードが短ければ短いほど、良い API であるということです。コードが書かれるが1度であったとしても、そのコードは何度も何度も読まれます。
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 型の引数の罠
bool 型の引数は頻繁に理解しづらいコードにつながります。特に既存の関数に bool 型の引数を追加するのはほぼ例外なく間違いです。これにあてはまる Qt での例は repaint() で、この関数は背景を消去するかどうかの指定のための bool 型の引数をオプションでとります(デフォルトは true)。これにより、以下のようなコードが書かれます。
widget->repaint(false);
初心者はこれを見て "再描画はされない" と思うかもしれません。
無駄な拡張を防ぐために、関数を1つ追加する代わりに bool 型の引数を追加したということは明らかですが、逆に無駄な拡張をもたらしています。Qt ユーザーの中のどのくらいの人が以下の3つの行の本当の意味での違いを知っているでしょうか。
widget->repaint();
widget->repaint(true);
widget->repaint(false);
API を以下のようにすることでいくらか改善されるでしょう。
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);
1つの代替案は bool 型の引数を enum 型に置き換えることです。Qt 4 では QString の大文字小文字の区別に対してこれを適用しました。以下の2行を比較してみて下さい。
str.replace("USER", user, false); // Qt 3
str.replace("USER", user, Qt::CaseInsensitive); // Qt 4
ものまねの罠
ケーススタディ
QProgressBar
このコンセプトを実際の例で見てみましょう。QProgressBar の API を Qt 3 と Qt 4 で比較してみます。Qt 3 では以下のようになっていました。
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()、setProgress() が緊密に連携していることは読み取れません。
この API を改善する上でカギとなる点は、QProgressBar が Qt 4 の QAbstractSpinBox クラスやその派生クラスの QSpinBox、QSlider、QDialog に似ているということです。解決方法は progress や totalSteps を minimum や maximum、value で置き換え、valueChanged() シグナルを追加し、setRange() 関数を利便性を考えて追加することです。
次のポイントは、progressString と percentage、indicator が同じものを指していることです。これは実際はプログレスバー上に表示されているテキストになります。通常はこのテキストはパーセントですが、setIndicator() 関数を使用することで様々なものが設定できます。新しい API は以下のようになります。
virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;
デフォルトでは、このテキストはパーセントの表示です。これは text() を再実装することで変更可能です。
Qt 3 の API にあった setCenterIndicator() と setIndicatorFollowsStyle() 関数はアライメントに関するものです。これらは setAlignment() という1つの関数に変更しました。
void setAlignment(Qt::Alignment alignment);
プログラマーが setAlignment() を実行しない場合は、アライメントはスタイルによって決まります。Motif をベースにしたスタイルではテキストは中央に表示され、その他のスタイルでは右側に表示されます。
改善後の 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);
…
};
QAbstractPrintDialog と QAbstractPageSizeDialog
Qt 4.0 では QAbstractPrintDialog と QAbstractPageSizeDialog の2つのクラスが登場し、それぞれ QPrintDialog と QPageSizeDialog の基底クラスとなっています。これは、QAbstractPrint- や -PageSizeDialog のポインタを引数で取り処理を行う Qt の API がないため完全に無意味でした。qdoc をトリッキーに使用し、我々はこれらを隠しましたが、これらは不必要な抽象クラスの典型的な例でした。
これは 良い 抽象化が間違っているということではありませんが、ファクトリなどの仕組みで 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 クラス(と内部の Box と Grid の派生クラス)をなくし、QLayout の派生クラスで itemAt()、takeAt()、count() を実装するようにしました。
QImageSink
Qt 3 には画像を順番に読み込みアニメーションをするための一連のクラスがありました。QImageSource/Sink/QASyncIO/QASyncImageIO クラスです。しかし、これらを使用するのは QLabel でのアニメーションのみであったため、これらは結局やりすぎでした。
ここから学んだことは、なんらかの漠然とした将来の可能性のために抽象化をするのは良くないということでした。はじめはシンプルにしましょう。そのような未来が来た場合でも、変更するシステムは複雑なものよりシンプルな方が対応も簡単でしょう。
Qt 3 vs. Qt 4 その他?
QWidget::setWindowModified(bool)
Q3URL vs. QUrl
Q3TextEdit vs. QTextEdit
どのように仮想関数を無くしたのか
Qt のクリッピングの物語
クリップの矩形を設定した場合、実際は領域(setClipRect() の代わりに setClipRegion(QRect) であるべき)を設定していること。