1
Help us understand the problem. What are the problem?

posted at

updated at

だからボクはオブジェクト指向が使いこなせない Qt/C++編

はじめに

本記事は、Qt/C++開発において、オブジェクト指向を使いこなしたい、という方に向けた記事です。

「だからボクはオブジェクト指向が使いこなせない」シリーズ

私が15年以上オブジェクト指向系のプログラム言語でアプリ開発に関わってきて思うのが、開発現場でオブジェクト指向らしいコードを書けるプログラマーは、残念ながら全体の2割にも満たない、ということです。(あくまで私個人の感想です)。
逆に言えば、オブジェクト指向らしいコードを書けるようになるだけで、開発現場で重宝される人材になれます。開発現場で、より即効性があり、かつ、Qt以外のプログラミングにも応用できる、潰しのきく技術、それがオブジェクト指向らしいコードを書く技術です。

どうしたらオブジェクト指向らしいコードを書く技術を身につけられる?

※具体的な技術的な話をすぐに読みたい方は「開発現場」の段落までスキップしてください。
昔、先輩社員に「どうしたらオブジェクト指向らしいコードを書く技術を身につけられるか」を聞いたところ「開発経験を積むこと」というあいまいな答えをもらい悩んだことがあります。この「開発経験」は自分にとって余程都合が良いプロジェクトでないと意味がありません。テスターとしてプロジェクトに参画するのはもちろん、他者が書いたコードの拡張や改修する役割で参画してもあまりよい経験にはなりません(フレームワークのAPIの使い方を知るという意味では意味がありますが)。自分でソフト構成を設計できるような新規案件や大きな機能ブロックを作成できる案件でないとなかなか「オブジェクト指向らしいコードを書く技術」を磨くチャンスがないのです。しかし、多くの仕事内容は、他者が書いたコードの拡張や改修する作業です。
オブジェクト指向らしいコードを書く技術を磨くには、自分で技術の本や記事を積極的に読み、実際に自分でコードを書き、頭をトレーニングする必要があります。

おすすめのトレーニング方法

「C++はオブジェクト指向言語」-> 「C++の入門書をがっつり読めばオブジェクト指向をマスターできる」わけではありません。プログラミング言語の入門書はあくまで文法などの「ルール」を説明するもので、「オブジェクト指向」という考え方を説明することにフォーカスされているわけではないのです。
「オブジェクト指向らしいコードを書く技術」を身につける為におすすめなのが「デザインパターン」と「リファクタリング」の技術を身につけることです。どちらもオブジェクト指向らしいコードを身につける為の要素がたくさん詰まっています。
残念ながら、C++はデザインパターンやリファクタリングに関する書籍があまりありません。
私自身はJavaでオブジェクト指向らしいコードの書き方を身に着けた為、Javaで書いたコードをC++ではどのように書くのだろう、という形で試してきました。書籍の充実という観点からすると最初にJavaを覚えることはとても良いことだと思いますが、やっぱりC++を使いたい、使いこなしたい、という方もいるでしょう。そこで、Qt Widgetsのアプリをベースに本稿を書きました。C++だけにしないのは、画面という視覚情報がある方がイメージしやすいからです。初見でオブジェクト指向に触れる場合はイメージも大切だと思います。

デザインパターン

デザインパターンについて、もう少し掘り下げます。デザインパターンはGoFの23パターンが有名です。私個人の意見としては、最初は23種全部覚えるよりも、重要ないくつかのパターンを使いこなすのに時間を掛けた方がよいと思っています。次のパターンを優先的にマスターするのがおすすめです。

  • Template Methodパターン
  • Factory Methodパターン
  • Facadeパターン
  • Stateパターン
  • Builderパターン
  • Observerパターン

この中で最初に覚えた方がよいパターンは「Template Method パターン」です。プログラミングのできる方だと、わざわざデザインパターンの名前も付ける必要もないくらいオブジェクト指向の性質そのものだ、と言う方もいると思います。それくらい「基本でかつ奥義」みたいなパターンです。この「Template Method パターン」について本記事を通して学んでいきましょう。

開発現場

※筆者注:導入部は物語風会話形式で書いてみました。
とあるソフト開発会社「エゥーゴソフト」開発チームの二人の会話から始まります。

登場人物説明

  • ブライト(リーダー):社会人(&開発経験)5年目。新人の頃からQt一筋で、厳しい先輩に教わったこともあり、現在では新規アプリ開発プロジェクトの立ち上げを任されている。
  • アムロ(新人女性社員):大学生の頃は主にC言語でプログラミングしていた。C++言語は入門書の7割くらいは読んだが、正直オブジェクト指向と言われてもピンとこない。新人研修のときに初めてQt Widgetsアプリの作り方を学んだ。

ブライトさんの作成依頼

ブライトリーダー:アムロさん、こんな感じのQt Widgetsアプリを作ってほしいんだ。(といって下記のイメージを見せる)

<アプリ仕様>
ObjectOrientationQt.png

  • アプリを起動するとダイアログを表示する
  • ダイアログの上の各ボタンを押すと文字に対応した背景色のダイアログを表示する

ブライトリーダー:アムロさんはQtアプリ開発を新人研修で教わったみたいだけど、いきなり全部作れって言うのは厳しいと思うから、途中((青いダイアログを表示するところ)まで作ってあるから、これをベースに黄色のダイアログと赤色のダイアログも表示できるようにしてほしい。
アムロ:わかりました。作ってみます。

ベースとなるコード

サンプル:ObjectOrientationQtCpp_base
※サンプルはGithubに格納してあります。

dialog.cpp
#include "dialog.h"
#include "./ui_dialog.h"
#include "bluedialog.h"

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::Dialog)
{
    ui->setupUi(this);
}

Dialog::~Dialog()
{
    delete ui;
}


void Dialog::on_pushButtonBlue_clicked()
{
    showDialog(new BlueDialog);
}


void Dialog::on_pushButtonYellow_clicked()
{
    //TODO: showDialog(new YellowDialog);
}

void Dialog::on_pushButtonRed_clicked()
{
    //TODO: showDialog(new RedDialog);
}

void Dialog::showDialog(QDialog *dialog)
{
    connect(dialog, &QDialog::finished, this,
            [=](){
        delete dialog;
    });
    dialog->exec();
}
bluedialog.cpp
#include "bluedialog.h"

BlueDialog::BlueDialog(QWidget *parent) : QDialog(parent)
{
}

void BlueDialog::paintEvent(QPaintEvent *event)
{
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, Qt::blue);
    this->setAutoFillBackground(true);
    this->setPalette(palette);
}

。。。。
アムロさんはQt Creatorの操作に苦戦しつつも、アプリを作り上げました。
アムロ:できました。こんな感じでどうでしょう。(ブライトさんにパソコンの画面を見せながら操作する)
ブライトリーダー:いいね。動作は完璧だね。ソースコードの方を見せてもらえる?

アムロさんが作ったコード
サンプル:ObjectOrientationQtCpp_amuro

bluedialog.cpp
#include "bluedialog.h"

BlueDialog::BlueDialog(QWidget *parent) : QDialog(parent)
{
}

void BlueDialog::paintEvent(QPaintEvent *event)
{
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, Qt::blue);
    this->setAutoFillBackground(true);
    this->setPalette(palette);
}
yellowdialog.cpp
#include "yellowdialog.h"

YellowDialog::YellowDialog(QWidget *parent) : QDialog(parent)
{
}

void YellowDialog::paintEvent(QPaintEvent *event)
{
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, Qt::yellow);
    this->setAutoFillBackground(true);
    this->setPalette(palette);
}
reddialog.cpp
#include "reddialog.h"

RedDialog::RedDialog(QWidget *parent) : QDialog(parent)
{
}

void RedDialog::paintEvent(QPaintEvent *event)
{
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, Qt::red);
    this->setAutoFillBackground(true);
    this->setPalette(palette);
}

ブライトリーダー:(そうきたか。。。)うーん。残念だけどお仕事の成果物としては、このままのコードではダメだね。
アムロ:えー。ボクのコードの何がダメなんですか。ちゃんと仕様通り動いているじゃないですか。
ブライトリーダー:bluedialog.cppとyellowdialog.cppとreddialog.cppってなんか「とてもよく似ている」よね。
アムロ:まあ、bluedialog.cppをコピペしてYellowDialogクラスとRedDialogクラスを作って、画面の色の部分だけ修正したので。
ブライトリーダー:(コピペしたって言っちゃった。正直というか。。。)似たようなコードがあるってことはもっとコードを少なくして、スッキリさせることができるはずなんだ。コードで説明するとわかりずらいから、日本語に置き換えて説明しよう。


3つのクラスの動作を日本語で表現すると、次のようになります。
スライド1.PNG

この中で、クラス毎に異なる部分を赤字にします。
スライド2.PNG

(1)、(2)、(4)は3つのクラスとも同じですね。(2)は赤字以外の部分は同じです。
この同じ部分(重複部分)が「無駄」と言えます。無駄な部分を取り消し線で書きます。
スライド3.PNG
この取り消し線の部分がなくなったら、スッキリしたコードになると思いませんか。そこで次のように、赤字の部分を〇に置き換えます。

スライド4.PNG

3つのクラスとも同じ内容になりました。
同じ内容なら1つのクラスで表現すればいいですよね。そこで3つのクラスの親クラスというものを考えてみます。
スライド5.PNG

これをソースコードで表現すれば、スッキリしたコードになります(親クラスのクラス名をColorDialogとします)。
黄色のような情報を〇に置き換えることを「抽象化」と言います。抽象化した情報を具体化するのは子クラス側に任せ、親クラスは決めません。ソースコードを見ていきましょう。

サンプル:ObjectOrientationQtCpp_final

colordialog.h
#ifndef COLORDIALOG_H
#define COLORDIALOG_H

#include <QDialog>

class ColorDialog : public QDialog
{
    Q_OBJECT
public:
    explicit ColorDialog(QWidget *parent = nullptr);
protected:
    virtual QColor getColor() = 0; //★
    void paintEvent(QPaintEvent *event) override;
};

#endif // COLORDIALOG_H
colordialog.cpp
#include "colordialog.h"

ColorDialog::ColorDialog(QWidget *parent) : QDialog(parent)
{
}

void ColorDialog::paintEvent(QPaintEvent *event)
{
    QPalette palette = this->palette();
    palette.setColor(QPalette::Window, getColor());
    this->setAutoFillBackground(true);
    this->setPalette(palette);
}

ColorDialog::getColor()は純粋仮想関数です。ColorDialogはどんな色を設定するかを具体的に決めません。決めるのは子クラスに任せます。色取得し、それを★で使用することだけを親クラスで書きます。
子クラスのソースコードも見ていきましょう。

bluedialog.cpp
#include "bluedialog.h"

BlueDialog::BlueDialog(QWidget *parent)
    : ColorDialog(parent)
{
}

QColor BlueDialog::getColor()
{
    return Qt::blue;
}
yellowdialog.cpp
#include "yellowdialog.h"

YellowDialog::YellowDialog(QWidget *parent)
    : ColorDialog(parent)
{
}

QColor YellowDialog::getColor()
{
    return Qt::yellow;
}
reddialog.cpp
#include "reddialog.h"

RedDialog::RedDialog(QWidget *parent)
    : ColorDialog(parent)
{
}

QColor RedDialog::getColor()
{
    return Qt::red;
}

クラス図でクラス関係を整理しておきます。
クラス図.png


アムロ:なるほど。たしかにスッキリした感じがしますね。BlueDialogとか差分の情報しかないって感じがすごくします。子クラスは、Qt固有の関数が1つも出てこないんですね。。。でもこのようなコードに改造するのは大変な気がします(というかメンドクサイ)。なんでこんなことをやる必要があるのでしょうか。
ブライトリーダー:簡単に言えば
(1)「バグの量」を減らすこと
(2)クラスの役割分担を明確する
を行うことでメンテナンスしやすくなるんだ。メンテナンスしやすければ「楽に」働ける。
(1)は、ソースコードを人が書く以上、書く量が多ければ多い程ミスが出て、バグが生まれる可能性が高くなる、という考え方から来ている。だから基本的にコード量は少ない方がいいんだ。(C++のコード量で開発工数の見積もりをしようとお客さんがいるけど、正直コード量で見積もりするのはナンセンスだよな。コピペ文化を増長するし。)
(2)は、日本語の赤字をつけた例を見ればわかると思うけど、本来BlueDialogやYelloDialogは「何色か」を決めるだけでいいはずなのに黒字と赤字が混ざっている。言い換えれば、Qt固有の関数(API)を呼ぶコードと呼ばなくてもよいコードが混ざっている。例えば、QtのVersionが上がってAPIの仕様変更があって動かなくなったときにどこを修正すればいいのか調査する必要が出てきたとする。この場合、アムロさんのコードでは、BlueDialog、YelloDialog、RedDialogの3つのクラスの調査する必要がある。
<ObjectOrientationQtCpp_amuroのソフト構成>
クラス構成1.png

それに比べてObjectOrientationQtCpp_finalサンプルは、ColorDialogだけを(基本的に)調査すればいいんだ。
<ObjectOrientationQtCpp_finalのソフト構成>
クラス構成2.png

アムロさんには、ぜひ「ObjectOrientationQtCpp_final」のようなコードが書けるようになってほしいね。
アムロ:わかりました。がんばります!

Template Method パターンの理解を深める為のQ&A

  • Q. Template Method パターンとは、何かを取得する処理を抽象化するパターンのことである。Yes or No
    • A. No。抽象化の対象は動作(動詞)、つまり関数のことです。ただし、〇〇を取得するという関数の例が、Template Method パターンを理解する上で一番わかりやすい例だとは思います。
  • Q. 抽象化する対象関数は必ず純粋仮想関数にする必要がある Yes or No
    • A. No。ColorDialogクラスの例で言えば、ColorDialogクラスでデフォルトの色を持つ、という設計でも構わないと思います。その場合は「=0」を付けずに、virtualのみを付け、ColorDialog::getColor()の実装を記述します。純粋仮想関数化するかどうかは設計によりけりです。

あとがき(2021年の総括)

今年は、書籍Qt5/Qt6入門 C++編を執筆したり、YouTubeチャンネル(ゼッツプログラTV)を立ち上げたりと意欲的に活動できたと思います。
来年は動画コンテンツをもっと充実させたり、新しいビジネスにチャレンジしたいですね。
これからも技術情報をどんどん発信していきたいと思いますので、こんな情報を扱ってほしい等ご要望がありましたら、ぜひコメントしていただければと思います。

ここまで読んでいただきありがとうございました。
よいお年を。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?