2018-01-19

Cプログラマのための、
C++オブジェクト指向簡単裏口入門(8)

オブジェクトはどのように対象を写像するのか?(2)

まずは、前回の最終コードを見てみましょう。

コード1

// 名刺クラス
class NameCard
{
    string  m_name;                // 氏名
    string  m_mobTel;              // 携帯番号
    string  m_email;               // メアド
    string  m_address;             // 住所(通常は自宅)
    string  m_title;               // 肩書き
    string  m_date;                // 名刺交換日
    string  m_memo;                // メモ
    vector<Company *> m_pCompanys; // 所属会社 0~n
};

// 会社クラス
class Company
{
    string  m_name;                   // 会社名
    string  m_department;             // 部署
    string  m_address;                // 住所
    string  m_tel;                    // 電話番号
    vector<NameCard *> m_pNameCards;  // 社員 0~n
};
このコードを見ながら、印刷機能について考えていきます。

印刷処理を1から作るのは大変なので、何かのライブラリを使うことにします。
クラスに print() 関数を追加しますが、中身は主に自分のクラスにある情報を集め、ライブラリに渡して印刷を依頼するだけと考えてください。コード自体は示しません。

また、印刷関数自体は外部から呼ばれますが、印刷するかしないか、というのはクラスの内部にフラグを持たせることにします。
では、これらを追加してみましょう。

コード2

// 名刺クラス
class NameCard
{
    string  m_name;                // 氏名
    string  m_mobTel;              // 携帯番号
    string  m_email;               // メアド
    string  m_address;             // 住所(通常は自宅)
    string  m_title;               // 肩書き
    string  m_date;                // 名刺交換日
    string  m_memo;                // メモ
    vector<Company *> m_pCompanys; // 所属会社 0~n
    bool    m_isPrint;             // ★印刷フラグ
public:
    // ★印刷フラグのセット/リセット(パラメータを指定しない場合はtrue)
    void setPrintFlag(bool isPrint = true){ m_isPrint = isPrint; }
    // ★印刷フラグの取得
    bool isPrint(){ return m_isPrint; }
    // ★印刷処理
    void print();
};

// 会社クラス
class Company
{
    string  m_name;                   // 会社名
    string  m_department;             // 部署
    string  m_address;                // 住所
    string  m_tel;                    // 電話番号
    vector<NameCard *> m_pNameCards;  // 社員 0~n
    bool    m_isPrint;                // ★印刷フラグ
public:
    // ★印刷フラグのセット/リセット(パラメータを指定しない場合はtrue)
    void setPrintFlag(bool isPrint = true){ m_isPrint = isPrint; }
    // ★印刷フラグの取得
    bool isPrint(){ return m_isPrint; }
    // ★印刷処理
    void print();
};
まず、印刷フラグなのですが、わざわざフラグの実体(bool m_isPrint)を非公開にして、公開した関数で間接的にアクセスするようにしています。最初から m_isPrint を公開しておけばいいのでは? と思われるかも知れません。しかし、これがオブジェクト指向の流儀なのです、ではおそらく納得できないでしょう。実効速度が若干おちる可能性も無いわけではありません。

しかし、こうしておくことで実際にフラグをセット、あるいはリセットする際に、別な処理をあとで追加する事が容易くなります。今回の場合はそういった処理が追加になる可能性がある、と考えてこうしています。(他にも、デバッグ用のコードを埋め込む際にもこの方が都合がいいので、直接メンバ変数を公開するのはむしろ例外的かも知れません)

ところで、setPrintFlag() 関数はクラスの定義の中に実装まで行っています。通常はクラスのメンバ関数はプロトタイプだけを宣言し、実装は別の箇所(別のソースファイル)で行います。このように実装コードをクラス定義中に記述すると、関数はインライン展開されるはずで、関数呼び出しのオーバヘッドがキャンセルされることになります(ただし、その場合同じコードが複数展開されることになります)。
また、C言語にはない、引数のデフォルト値を使用しています。呼出で引数を省略するとデフォルト値が設定されます。


名刺、会社のそれぞれのクラスに print() 関数を追加しました。

名刺のオブジェクトや会社のオブジェクト自身に印刷させるというのは、悪い考え方ではありません。では、それを何処で呼出すのでしょうか。この print() 関数を呼ぶと、すぐに印刷処理が実行されてしまいます。
複数の宛先を一気に印刷する、という機能も必要です。

機能としては、ユーザの操作で一覧から印刷対象のデータにチェックをして、最後に印刷開始を指示する、という手順になります。
プログラムとしては各クラスの印刷フラグを立てて(setPrintFlag()の呼出)、次にチェックされているものだけを実際に印刷する(print()の呼出)、ということになりそうです。

しかし、print()を呼出す際、印刷の対象とするクラスが2種類あるため、わざわざ2つのクラスに対応した印刷呼出処理をしなければなりません。

コード3は印刷を呼出す処理のイメージです。

コード3

// どこかにある全印刷関数
void printAll()
{
    // 名刺の宛名印刷
    for(int i = 0 i < nameCards.size() ; ++ i)
    {
        if(nameCards[i].isPrint())   
            nameCards[i].print();
    }

    // 会社の宛名印刷
    for(int i = 0 i < companys.size() ; ++ i)
    {
        if(companys[i].isPrint())
            companys[i].print();
    }
}
そっくりな処理が2つあります。どのように合理化すればいいでしょうか。

似たような処理は1つにしたいし、2つのクラスはとてもよく似ていて、違いは僅かです。
共通部分をうまくまとめることができれば、この2つの問題は一気に解決できます。

大切な継承という考え方

ここでとても重要な考え方が登場します。それは、クラスの継承です。クラスに、言わば親子に似た関係を持たせることができるのです。
より一般的なクラスから、より特殊なクラスに対して継承する、という関係です。

たとえば、「人」というクラスを継承して「男」や「女」というクラスを定義します。
このとき、「男」は「人」である、「女」は「人」であるという言い方が成立します。
もし、継承関係を逆にしてしまうと、「人」は「男」である、という間違った関係になってしまいますね。継承元のクラスの方がより一般的、継承先はより特殊である、ということになります。(継承は1階層だけでなく、何階層にも行うことが可能です)

言葉がいくつかありますので、なんとなく覚えておいてください。
「スーパークラス」を継承して「サブクラス」を作る。
「基底クラス」(=スーパークラス)を継承して「派生クラス」(=サブクラス)を作る。
「汎化」は派生クラスから基底クラスを導き出すこと。
逆に、「特化」は基底クラスから派生クラスを導き出すこと(=継承)。

C++には更に高度な?継承が可能です。多重継承と言って、複数の親を持つことができるのです。そうすることによって、複数の親の特徴を子が継承することになります。
(C++以外のプログラミング言語では多重継承ができないものも多いです。C#もできませんが、それでは困るので別な方法が用意されています。)

今回は「男」と「女」の共通点から「人」を導出する、という継承とは逆の手順(汎化)で、より一般的なクラスを導き出してみましょう。しかも、それは印刷機能にとって都合の良いものでなければなりません。

コード4

// ★印刷住所クラス
class PrintAddress
{
    string  m_name;                // 宛名
    string  m_honorificTitle;      // ★敬称
    string  m_address;             // 住所
    string  m_tel;                 // 電話番号
    string  m_email;               // メアド
protected:
    bool    m_isPrint;             // 印刷フラグ
public:
    // 印刷フラグのセット/リセット(パラメータを指定しない場合はtrue)
    void setPrintFlag(bool isPrint = true){ m_isPrint = isPrint; }
    // 印刷フラグの取得
    bool isPrint(){ return m_isPrint; }
    // 印刷処理
    virtual void print() = 0;
};

// 名刺クラス
class NameCard : public PrintAddress // ★継承
{
    string  m_mobTel;              // 携帯番号
    string  m_title;               // 肩書き
    string  m_date;                // 名刺交換日
    string  m_memo;                // メモ
    vector<Company *> m_pCompanys; // 所属会社 0~n
public:
    // 印刷処理
    void print();
};

// 会社クラス
class Company : public PrintAddress  // ★継承
{
    string  m_department;             // 部署
    vector<NameCard *> m_pNameCards;  // 社員 0~n
public:
    // 印刷処理
    void print();
};
基底クラスとして印刷住所クラス(PrintAddress)を追加し、名刺クラスと会社クラスはそれを継承するようにしてあります。

新たに登場する文法もいくつかありますが、このクラス構成にしたときのメリットを先に示しましょう。
次のコードは print() を呼出す部分と、印刷用データ配列の作成のイメージです。

コード5

// 印刷データポインタの配列
vector<PrintAddress *> printAddresses;

// 名詞クラス、会社クラスのインスタンスを印刷データ配列に登録する処理の例
void func()
{
    NameCard nc;

    // NameCardのポインタをPrintAddressのポインタへキャストして登録
    printAddresses.push_back((PrintAddress *)&dc);


    Company cp;

    // CompanyのポインタをPrintAddressのポインタへキャストして登録
    printAddresses.push_back((PrintAddress *)&cp);
}

// どこかにある全印刷関数
void printAll()
{
    // 宛名印刷
    for(int i = 0 ; i < printAddresses.size() ; ++ i)
    {
        if(printAddresses[i]->isPrint())   
            printAddresses[i]->print();
    }
}
printAll() 関数は1つのループだけになっています。何故こんなことができるのでしょうか。

1つ目の理由は、同じ1つの配列に NameCard と Company のインスタンス(のポインタ)が登録されているからです。その配列は printAddresses ですが、これは PrintAddressクラスのポインタを要素とする配列として定義されています。この場合、PrintAddressを継承したクラスのポインタを PrintAddressポインタにキャストすることができるので無理なく登録ができるのです。

では、PrintAddresses[i]->print() は何を呼出すのでしょうか。普通に考えると、PrintAddressクラスの print() が呼ばれます。しかし、PrintAddressクラスの定義を見ると分かるのですが、virtual というキーワードで修飾されています。これは仮想関数と言って、継承先のクラスに同じ関数があった場合、そちらが優先して呼ばれる仕組みになっているのです。ただし、これはポインタを経由したときだけこの機能が働くことに注意してください。だから printAddresses の要素はポインタになっています。

ここでまた是非覚えて欲しい言葉が出てきます。それはポリモフィズムです。日本語で多態とも言います。外から同じ処理を呼んでも、実体に合わせて異なる処理が実行されるという意味になります。C++ のポリモフィズムは、このようにポインタを使用して実現します。

それから、基底クラスである PrintAddressクラスの print() 関数には = 0 と付いていて、これは実装がないことを意味しています。純粋仮想関数とも言います。この場合、これはこういう関数が継承先にありますよ(または、継承先で実装してください)、という宣言ということになります。
そして、この純粋仮想関数が1つでもあるクラスは、それ自身をインスタンス化することができません。このようなクラスのことを抽象クラスと言います。

PrintAddressクラスに protected というキーワードがあります。これは継承したクラスのメンバからはアクセスできるが、外部からはアクセスできないという意味のキーワードです。private だと継承したクラスからもアクセスできませんし、public では外部から無制限にアクセスされてしまいます。

継承の書き方はこの通りに覚えておけば問題ありません。何故、public というキーワードが必要なのか、今は気にしないでください。

今回は複数の似たクラスから汎化したクラスを導き出しました。これは、2つのクラスに印刷するための共通のインタフェースを追加する、という意味合いを含んでいます。この考え方はクラス設計では大切な考え方、概念ですので、是非、理解してください。

コラム:クラス設計のアプローチ(重要)

クラスの設計をオブジェクト指向のべき論から入る場合もあります。実装のことはひとまず後回しにするのですね。

今回のような場合だと、名刺クラスではなく自然人クラス、会社クラスは法人クラス。そして、それらの基底クラスを人クラスにする、という風に、コンピュータ内部ではなく、リアル世界に即したクラス構成にして、ある意味大風呂敷を広げてしまう訳です。

もちろん、アプローチとしては特に間違っていませんが、やや遠回りをしすぎてしまう可能性もあります。この記事の説明は、名刺管理の名刺や、機能に着目したアプローチで、現場のプログラマにはとっつきやすいものだと考えています。

いずれのアプローチでも、最終的にたどり着くところが同じであれば、僕は構わないのかな、と思っています。ただし、名刺管理のような比較的シンプルなアプリケーションではなく、多種のデータ種類を扱ったり、ユーザインタフェースも複数の系統があるようような、比較的大規模な場合、特に外側からのアプローチは大切になってきます。ただし、そのアプローチは機能設計の段階を経る、と言うことが前提にはなってきます。オブジェクト指向は必ずしもプログラミングだけのものではないのです。

いずれにしても、オブジェクト指向の考え方に慣れてくれば、両方のアプローチで考え、双方の落としどころを探る、という技も身についてくるはずです。


0 件のコメント:

コメントを投稿