ラベル システム開発 の投稿を表示しています。 すべての投稿を表示
ラベル システム開発 の投稿を表示しています。 すべての投稿を表示

2018-02-14

身につけるべきプログラマの習慣 その7

本当の問題を探る

人は最初に入ってきた情報を優先しがちです。先入観もそういったことの1つです。
だから、何か問題が起こったとき、それを見たまま聞いたまま受け取ってしまいます。
更に悪いことに、問題の本質にたどり着けないまま我先にと夢中になって解決策を考案してしまうのです。

日常茶飯事の光景ではないでしょうか?

本当の問題は何か、一度咀嚼して考えなければなりません。
まずは、それが誰にとっての問題であるか、という点が大きなヒントになります。立場を変えれば、それは問題ではないかも知れません。誰かにとって問題でも、それが誰かにとってはメリットかも知れないのです。

そして、問題の本質を探るときに大切なことは、客観的視点です。問題の渦中にいる感覚でいるうちは、本質的な解決策を見出すことはできません。

そして、候補に挙げた解決策が、どの問題をどのように解決するのかを予測します。1つの問題を解決することで、別の新たな問題を発生させてしまうことにも注意を払います。
そのような副作用の問題を含め、一気に解決できることが理想かも知れません。そのような理想的な解決には、発想力、創造力、想像力が欠かせません。

創造力が大切

矛盾する要素を1つ高い次元で解決して融和させることは、創造的脳活動の本質だと考えています。
脳のこの能力を引き出させるためには、記憶依存の思考を行っていてはいけません。また、スケジュールなどに追われ、尻を叩かれながらの作業でも決して行えません。

精神的なゆとりや前向きの気持ちをもつことが大切ですが、それにはそういった環境が必須になります。
残念ながら、日本の企業の会社勤めではそういった環境はほとんど望むことはできません。日本人は記憶に頼る教育しか受けていないからかも知れません。

僕自身は長年在宅で、自分の体調と向き合いながらその能力を自分なりに引き出してきました。そのことで、大技、小技を繰り出してきました。もし、部屋に監視カメラが取り付けてあれば、そんな技を繰り出す前は、まるでサボっているように見えることでしょう。

発想力を必要としない、コツコツとした非創造的な作業においては、とにかく手を動かしなさい、という製造業的なやり方でも世界に通用していたかも知れませんが、ソフトウエア開発というのは、言わば創造的思考の連続でもあります。少なくとも、そういった作業フェーズの時だけは在宅での作業を認めるなど、創造性を引き出す環境作りに真剣に取り組まなければ、ますます日本の技術力は地に落ちていく一方になると思います。


2018-02-13

身につけるべきプログラマの習慣 その6

試験でのバグ出しは誉れ

バグ出しでたくさん発見されると成績を下げるという評価があります。これは最低です。
また、バグ出しなのに、自分のコードにバグが見つかるといちいちがっかりして落ち込む人を見かけますが、それもおかしいですね。

バグというのは、人間なのだから必ず入り込むものです。その率は人によって異なりますが、多かれ少なかれ必ず入り込むのです。

最終的にバグを取り去れば良くて、途中でのバグの件数自体は問題ではありません。

試験の局面においては極力バグを洗い出すことが重要で、次の行程に問題を残さないようにしなければならなりません。
だから、既にできたコードに対しては、極力バグを残さないようにできる限り多く発見することが誉れであって、恥ではないのです。


もちろん、バグにも質があります。単純なミスが原因なものもありますし、ロジックをよく理解しないまま当てずっぽうに書いたコードが動かない、という最低レベルのものまであります。
後者の場合、プログラマの資質にまで問題の範囲が及ぶことになります。動くはず、という確証のないままコーディング行程を終了させたのは深刻な問題かも知れませんね。
また、単純なミスであっても、あまり多発するようではデバッグを含め、余分に工数を要してしまうことにはなります。

プログラミング時にいかにバグを入れないようにするか、それもソフトウエア工学の1つです。そういったことをちゃんと守った上で発生するバグは、通常さほど深刻なものにはならないはずです。

もし、バグを恥じるのであれば、納品後に発生すること、あるいは、バグの量ではなく、悪質なバグに対してなのではないでしょうか。

笑い話?

30年位前の、大手企業での実話です。
バグの発生率は統計的に決まっているという点に着目し、ある人の出したバグがその範囲に収まっていない場合、試験を通過させないというものでした。
これはバグが多発した場合もそうですが、バグが少なすぎても試験をパスさせないのです。
そこで、あるグループのリーダーは、バグが少なすぎるプログラマに対して、わざとバグを入れ込めという命令を下しました。

プログラミングの本質を理解していない、あるいは、その内容を理解しないまま評価しようとすると、こういう馬鹿げたことになるのでしょうね。無能な管理者が前提の方法論なのです。

2018-02-11

身につけるべきプログラマの習慣 その5

表と裏を区別する

もしもこれが人間性の話だとしたら、もちろん裏表はない方がいいですよね?
でも、プログラミングにおいては、いつも表と裏をはっきりと区別しなければなりません。外面と、内面を意識すると言い換えてもいいです。

外面、それはインタフェースの部分に相当します。
アプリケーションの外面ならユーザインタフェースであったり、公開API等の仕様であったりしますね。これはしっかりと仕様を決めなければ話にならない部分でもあります。

ライブラリプログラムだった場合はどうでしょうか。
C言語のような関数だけでできているライブラリもあれば、C++のクラスライブラリのようなものもあります。
通常、こういったプログラムはインタフェース仕様書を書いてライブラリのユーザに提供する機能や動作、使用方法や手順をしっかりと定義します。
同じ機能を提供するのであれば、よりシンプルなインタフェースが求められます。

ところが、仲の良い同一プロジェクトチーム内でプログラムを分担するような場合、ついついそれを使う側も作る側も同じチームだから、内々で済ませてしまえるという甘えが出てしまいがちです。
インタフェース仕様をきっちり決めずに作り始めてしまったり、決めても作る時の身勝手な都合でそれを守らずにどんどん崩してしまうのです。
その結果、インタフェースの内側と外側の区別がなくなってしまい、ぐだぐだになってしまうのですね。これはプログラムの品質が格段に落ちることを意味しています。とても深刻な事態なのです。

もし、ぐだぐだになってしまっても誰にも見せない内々の話なんだから構わないだろう、と考えたあなたはかなりやばいです。

ライブラリに限らず、プログラムには使う側と作る側があります(一番外側のプログラムは使う側の立場のみとも言えますが、たとえばmain()関数はOSから使われると考えることもできます)。そこをしっかりと区別しましょう、ということですね。これはむしろ本能的な感覚として身につけておくべき基本的な習慣だと、僕は感じています。

プログラムを関数やクラスといった単位、あるいはもっと小さな単位かも知れませんが、そのまとまりごとに内側と外側を意識して作ることが大切です。

オブジェクト指向プログラムの醍醐味

オブジェクト指向プログラミングで、クラスのユーザ側から見える仕様を設計することはとても楽しいことです。単なる関数と違って、設計方法を工夫することで非常にスマートでセンスの良いクラスインタフェースを実現できるからです。言わば、外面を格好よくできるのですね。

1人の担当者がそのユーザにもなるし、制作者にもなることは普通のことですが、他の章でも書いたように、明日の自分は他人ですから、その他人に向けてよいプログラムを作る、という意識を持つことができるわけです。
そして、実際に明日そのプログラムを使ってみたときに感じる快感は、ある意味独りよがりなのかも知れませんが、それもプログラミングにおける醍醐味の1つだと考えています。


2018-02-10

身につけるべきプログラマの習慣 その4

全ての選択には理由がある

どんな些細なことでも、選択肢があればそこに優劣があります。
多くの場合、論理的に優劣の説明ができ、普遍的な選択というものが決まってきます。
そのことを無視して、何も考えずに自分独自の選択をするほど、その仕事の結果はチープになっていくのです。

もちろん、本質的な問題は個々の事情によって異なる場合も意外に多いと言えます。
問題の焦点の当て方によって、解決すべき問題が異なり、同じ選択肢であってもいつも正しい答えが1つとは限らないこともあります。

普遍的で絶対的な答えがあったとしても、それさえも揺るがすような事情が本当にあるかも知れません。そのことを理解していれば、闇雲に他人の書いたコードを尊大に否定することはできないはずです。


しかし、数々の問題を検討した結果ではなく、何も考えずにあなたの個人的な経験だけを全ての世界だと信じ、最初に見たたままの習慣を間違っていてもそのままにしておくことはみっともないばかりか、能力の低いエンジニアであると、密かに、しかし確実に評価されてしまいます。
増して、その理由に自分のセンスだとか、美学だとかを持ち出しようものなら、挽回できないほどに信用は失墜します。

どちらでもいいということは、特にこのICTの世界ではほぼあり得ません。しかし、それを知った上、理解した上で妥協をした方がいいことも多々ありますので、そこは誤解のないように。

事例

C言語系のプログラミングソース、中括弧「{}」の始まりをどこに書きますか。
これは、始まりと終りの括弧を同じインデントカラム位置に書くのが正解です。しかし、中括弧の始まりをその上にあるifだとかwhileだとかと同じ行の一番後ろに書く人が多いですね。これは、昔々、大昔、ソースコード印刷時の行数を減らすための習慣があった頃にできたC言語の教科書の書き方に習っている人が多いことが原因だと思われます。
多くの人が正しさよりも最初に見たすりこみに支配されているのです。

僕自身も、C言語を始めて5年位は、なんとなく疑問に感じながらもそのようなスタイルで書いていました。しかし、ある日ある開発環境でソースコード整形ツールを使ってみると、全て始まりの中括弧はカラム位置を揃えられてしまいました。最初は戸惑いましたが、その書き方の方がつまらないバグやミスを減らせることを実感できたので、以来その描き方を貫いています。(同じ行で中括弧を閉じる場合は別です)

その選択の理由は体験そのものではありません。体験を元にした普遍的な理屈です。
それは・・・

人は図形を素早く感覚的に認識する能力があります。
中括弧の始まりと終りが同じインデントのカラム位置に存在することで、図形的に囲まれていることを直感的に認識することができるのです。そのことで、コードの視認性が格段に高まります。
一般の文章でも、罫線に囲まれていると直感的にそこがまとまっていることを認識しやすいでしょ?(日本人は特に好きなはず)
それと一緒です。

逆に、他の某言語のように begin と end という単語で囲まれているとあまり直感的に見ることができなくなり、視認性が悪くなりますね。
同様に、中括弧の位置がずれていれば、結局その分余計な情報処理が脳で起こってしまうので、始まりと終りの位置を揃えたコードに比べてやや視認性に劣ってしまいます。

せっかくC系の言語は図形的な表現をして視認性を高めるデザインになっているのに、わざわざそのメリットを削ぐことは正しくないと断言できます。

ですが、長年そのスタイルで書いてきたプログラマに文句は言いません。スタイルが統一されていれば、そこまで読み辛いということもありませんし。
でも、一番正しい正解というわけでもありません。
改善する力が残っているならば、よりよい選択をすべきです。


身につけるべきプログラマの習慣 その3

明日の自分は他人と思う

プログラマであれば、特にコメントに気を遣いましょう。コメントは後で書くのではなく、コードと同時に書かなければいけません。明日どころか、10分後にはなぜそのようなコードを書いたのか判らなくなることさえあるからです。

しかし、プログラミングでコメントに依存したコードを書くのは完全にNGです。コメントの内容は必ずしも信用できないからです。特に、修正を繰り返したコードは、コメントがメンテナンスされていないことが多々あります。
なので、第一義的にはコメントを読まなくても理解しやすいコードを目指します。
そして、コメントはコード自体では何をしているか判りにくい箇所を捕捉し、コードの可読性を高めることを目的とします。

このようにすることで、多大な複雑系で疲弊し、記憶することを放棄した脳にも優しくなり、明日他人になった自分を取り戻すことができるようになります。

そして、それは本当の他人に対しても優しくなり、保守性、つまりソフトウエア制作における「質」の要素の1つ、を改善することができるのです。


自分を含めた他人に対して優しく作ること、それは善意のあるソフトウエア制作であり、技術者の善意、という最近では忘れられてしまった大切な精神を思い出させる行為でもありますね。

これなくして、技術者は成立しない「何か」の1つなのではないでしょうか。


身につけるべきプログラマの習慣 その2

継続的にメンテナンスできないドキュメントは書かない

もしあなたに作成ドキュメントの選択権があるならば、まず第一に新規作成より修正のことを念頭に無駄を排除すべきです。

大きなプロジェクトほど無駄なドキュメントを作成する傾向が感じられます。最初に作成したはいいが、結局それが何の為に存在しているのか誰にも理由がわからず、そして役に立たない。役に立たない一番の原因は、メンテナンスが行き届いていないため、内容をどこまで信用していいのかさえ判らなくなっているからです。

少なくとも、システムの内容に変更が生じたときは、修正すべきところを全て洗い出すことができなくてはなりません。そして、修正すべきであることを直接ドキュメントに印を付けることが重要です。修正後の内容を書き込むのは後でもいいので、とにかくその内容が違ってしまったことがはっきりと判るようにだけはしておきましょう。その際、修正IDを併記しておくと良いでしょう。
そのようにしておけば、もしそのままの状態で放置されてしまったとしても、印の付いていないところはまだ信用できることが判るので、ドキュメント全体が死んでしまうことはなくなります。

同じ内容は1箇所だけに書くようにしろ、という教えもありますが、それは無理があります。もちろん、極力重複は排除すべきですが、概要と詳細に分かれていたり、図面と文章に分けて表現する等、どうしても複数箇所に同一内容が記載されてしまいます。

これらのことを踏まえて、確実にメンテナンスできるドキュメントを作成するように心がけましょう。

とは言え、ドキュメントは必ず書かなければなりません。どうせメンテナンスできないのだから、という理由でドキュメントの作成を放棄するのは大間違いです。
それは、ひょっとするとコンパイルしてバイナリが生成できたら、ソースファイルを廃棄するのと同じかも知れませんよ。


2018-02-09

身につけるべきプログラマの習慣 その1

上流工程の結果を鵜呑みにしない

特に、ウォーターホール開発で上流から流れてきた結果を盲信すると痛い目に遭うことが(よく)あります。少なくとも、1工程分は遡って、どうしてその結果が導き出されたのかを理解し、検証する必要があります。

怠って一番痛い目に遭うのは自分自身です。

問題の有無さえ考えずに上流工程の結果のまま作業をして、最後にそれが動かないことが判明しても自分には無関係のふりをして自己保身をする人をよく見かけます。

そんな人はいったい何の為に仕事をしているのでしょうか? お金のためだけならもっと儲かる仕事は他に沢山あると思います。
良いものを作って誰かの役に立たないなら、その仕事に意味や価値はありませんし、そんなエンジニアは害悪になるだけ、必要とされなくなります。

体験談

割と最近のことです。

某大手ソフトウエア会社でお手伝いさせていただいたとき、詳細設計以後を担うグループに入りました。既存のシステムや改造点について学んでいく段階で致命的な問題を発見しました。工程の初期段階での発見だったために大事には至らず、設計の一部修正を行って頂くことで回避することができました。
問題はそれを指摘するだけではなく、最善と思われる解決策も併せて上流工程担当に伝えました。このとき、自分はまだ全てを把握&理解しているわけではなかったので、想定される条件によっていくつかの案を提案していました。

僕自身はこれまでにほぼ全行程の経験をしています。しかし、そのこととは関係なく、ICTエンジニアであればそれがどのように具体的に動作をするのか、理解、確認しながら作業を進めることが大切なのだと思います。

言われたとおりにコーディングだけをするプログラマ(コーダ?)は必要とされません。


2018-01-31

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

クラス設計お悩みポイント?

今日はC++のコーディングから少し離れて、コーヒーブレイクのような記事にします。手を休めて、気軽に考えて見てください。

簡単に試すことが難しい?

僕がオブジェクト指向プログラミングを始めてみてそれが短所だと感じることがあったのは、コーディングをして、その動作確認が出来るようになるまでの時間が長くなったことでした。ちょっとだけ辛抱が必要なのです。

原因の1つは、プログラムの依存サイクルの違いです。

C言語では、プログラミングの屋台骨は関数しかありません。関数が、他の関数を呼び、またその関数が他の関数を呼ぶ。

呼ぶ方も呼ばれる方も自分で作る場合、意味的により要約されているのは呼ぶ側であることが一般的です。とりあえず、要約されている部分を実行したいとなった場合、そこから呼ばれる関数をダミーで作っておくことはさほど難しくありません。実行時間やそのタイミング等に依存しないのであれば、戻り値だけを気にすればいいからです。

なので、とりあえず通して動かしてみる、ということが比較的簡単にでき、徐々に詳細部分を作り込んでいく、ということが可能です。

オブジェクト指向プログラミングで中心となるのはクラスです。
通常、クラスは他のクラスのユーザになります。関数が他の関数のユーザになるのと同じです。

クラスを使う、というのはどういうことでしょうか。
関数を使うというのは、引数があって、その結果としての戻り値しかありません。
クラスを使うのは、多くはまずインスタンス化をして、そして、そのクラス独自の使用方法があり、最後に廃棄する。
問題はその独自の使用方法です。ファイルを扱うなら、オープンがあってクローズをしなければならないかも知れません。クラスのインスタンス化の段階から例外をキャッチしてエラー処理を行う必要もあるかも知れません。
クラスを使うというのは、いくつかの手続きが必要なのです。

クラスというのは機能を使うための手続きの集約でもあり、つまりインタフェースそのものと言えます。クラスというインタフェースと、関数という手続きがセットになっているので、依存のサイクルは関数だけの場合と比較すると以下のようになります。

矢印を依存方向だとすると、以下のような依存サイクルになります。

C言語の場合:
[関数] → [関数] → ・・・

C++のオブジェクト指向プログラミングの場合:
[クラスインタフェース]+[関数] →  [クラスインタフェース]+[関数] → ・・・

つまり、関数が関数だけに依存しているC言語の場合は、使用する関数のダミーを作るだけで仮の実行が可能です。しかも、それは比較的少数のダミー関数を作ればいいだけになります。

クラスに依存している場合、依存されるクラスのダミーを作る必要があるのですが、その中で使用するメソッド(メンバー関数)のダミーを全て(通常は複数)作っておいたり、プロパティ(メンバ変数)に外部から直接アクセスさせるのであれば、そこにダミーのデータを設定するなどの仮の処理が必要になるかも知れません。
これは本当に仮なのでしょうか。仮であっても、やや本番に近いプログラミングをする必要があると言えるのではないでしょうか。

以上のように、ダミーのクラスを作ることが二度手間になる部分もあります。
その無駄を避けるために、クラス中心のプログラミングではトップダウンよりもボトムアップで、依存される側のクラスから実装を完了させていく手順になることが自然な流れになります。
つまり、全体的な動きを試してみる、というのがどうしても後半に回されてしまいがちなのですね。

このことは、最近注目され、導入事例も増えてきているアジャイル開発とは矛盾してしまうような気もするのです。残念ながら、僕にはアジャイル開発のスキルが不足しています。こういったオブジェクト指向開発と、アジャイル開発をどのように折り合いを付けていくのか、ある程度の想像はできますが、現実を知りません。

プロパティ or 継承の問題

最右翼のオブジェクト指向であれば、あまり迷うことはなく、全て継承して解決するのかも知れませんが、現実的な解を求められるC++では悩むことがあります。

たとえば、[人]クラスを作りますが、男女を区別する必要があるとします。
[人]クラスの中にプロパティ(変数)を追加して、そこに男性か女性かを設定する、というのが1つの方法です。
男性は男子トイレを使う、女性は女性トイレを使う、という判定がクラスのメンバ関数の中で必要になったとき、このプロパティを見て判断する訳ですね。

もう1つの解は[人]クラスを継承して[男性]クラス、[女性]クラスを作ることです。継承したこれらのクラスは、自分の性別を最初から知っているので、どのトイレを使うかは最初から決まっています。なので、判定処理が不要になるメリットがあります。

最近では、戸籍上の性別が本人の本質的な性と一致しないケースも無視できなくなっています。だから、戸籍上の男性が、女性用のトイレを使うことだってあるわけです。

プロパティで対応していたクラスであれば、戸籍上の性別だけではなく、本人が実際にはどちらの性で生きているかを示すプロパティを更に追加することになりそうです。

継承の場合はどうでしょうか。
[男性]クラスと[女性]クラスを更に継承し、両方の特徴を兼ね備えた[男女]クラスを作った上で、更に戸籍上の性と実生活上の性の情報を持って処理を分岐させなければならなくなりそうです。

トイレに行くという処理は架空の話ではなくて、たとえばある会社での社員が、どちらのトイレを使うのか統計を取り、トイレの設計を行うといった場合に応用する話です。
他にも、更衣室、場合によっては銭湯のようなお風呂、制服、きっと、それぞれによってどちらを使うのかが異なる人もいるだろう、ということを考えると、一概に決めることはできなくなります。

もし、何かを利用する場合にどちらの性別用のものを選ぶか、という視点で継承を考えた場合、それぞれの性別、たとえばトイレ用の性別クラス、更衣室用の性別クラス、制服用の性別クラス、そんな基底クラスから全ての性質を多重に受け継いだクラスが必要になりそうです。
こうなってくると、やはり人間にとって性別とは単なるプロパティであって、しかも、それは1種類ではないと考えた方が現実にもマッチして自然に思えてきますね。

継承で表現するクラスは、もう少し普遍性のあるものの方が適しているかも知れません。
仮に継承を使ったとしても、判定が必要になるため、結局プロパティを持つことにもなります。

じゃあ、何でもかんでも継承はできるだけ避けて、プロパティで分岐させればいいじゃないか、というのもまた違うのです。それを追求すると、クラスはアプリケーションにたった1つで良くなってしまう可能性すら出てきてしまいます。
それは明らかな間違いである、と判りますね(例示は割愛します)。

考えること、議論することが大切

ああでもない、こうでもないと考えてきましたが、継承とするのか、プロパティで区別するのか、迷うケースは意外にあるものです。しかし、多くの場合最初に思いついた方、あるいは、考えたくないのでプロパティだらけになってオブジェクト指向的ではない設計になってしまうことも多々あります。

どうか、これを考えることから逃げないで、より合理性の高い解を見つける労を惜しまないようにお願いしたいと思うのです。それは、頭でっかちな理想論を追求することではなく、現実的な解を求めるという行為です。
できれば、いろいろな考え方に気づくため、人と議論をすることをお勧めします。この議論こそが、オブジェクト指向のメリットを活かす力のために必要と考えていますし、醍醐味の1つでもあると感じるのです。

オブジェクト指向を「やっている」開発現場では、この議論が活発に行われているはずです。多くの解から、何故それを選ぶのか。いつも、他の可能性があることを知りながら、最初に思いついたものを解としているようでは前に進むこと(上達すること)はありません。

2018-01-28

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

C++のクラスは魔法の箱?(2)

前回、オブジェクト指向プログラミングの醍醐味の1つは、自分が提供するクラスに高い機能をスマートで格好よく組み込めることであると書きました。
しかし、それは演算子をクラスに組み込むことだけではありません。

ギミックと言える要素は他にもあります。

初期化と後片付け

クラスの設計では、それが1つの独立したアプリケーションの様に動作させることも多々あります。当然、クラス内部の初期化や終了処理を行うことも多いわけです。

クラスのユーザ側にそれを意識させて、使い始めるときには必ず init() を、使い終わったら term() を呼んでくださいね、さもないとエラーやメモリリークが発生してしまいますよ、と仕様書に注意書きを記すのも1つの方法です。

しかし、初期化と終了処理はもはや普遍的な定型処理と言えるものですから、C++ではこれを自動化できるようになっています。

では、それを組み込んだクラスの例を見てください。

コード1

// 巨大データクラス
class LargeData
{
    size_t m_size;
    char * m_pData;
public:
    // ★コンストラクタ
    LargeData(size_t size)
    {
        m_size = size;
        m_pData = (char *)malloc(size); // エラー処理は省略
    }

    // ★デストラクタ
    ~LargeData()
    {
        free(m_pData);
    }

    // データの設定
    const char * setData(const unsigned char * pData, size_t size)
    {
        return (const char *)memcpy(m_pData, pData,
                                 m_size < size ? m_size : size);
    }
    // データの取得
    const char * getData()
    {
        return m_pData;
    }
}

// クラスを使う
int main()
{
    char myData = "abcdefg";
    LargeData ld(1000);    // 内部で1000バイト取得

    ld.setData(myData);

    return 0;
}
この LargeData クラスには目新しい関数が2種類追加されています。クラス名と同じ名前の関数と、それにチルダ(~)が頭に付いたものです。それぞれ、コンストラクタ、デストラクタと言われるメンバ関数で、クラスがインスタンス化されるとき、および、破棄されるときに自動で呼ばれます。

ここでは main() 関数でクラスを使用しています。自動変数として LargeData をインスタンス化する際にパラメータを渡しています。これがコンストラクタ関数のパラメータの引数になります。dl(1000) という書式は、関数を読んで居ることを強調するものですが、この場合引数が1つなので、dl = 1000 という書き方も可能です。(初期化の書式は他にもありますが、ここでは割愛します)
デストラクタの方はいつ呼ばれるのでしょうか。ld は自動変数ですから、main() 関数を抜ける際に破棄されます。破棄されるタイミングでデストラクタが呼ばれます。

このような作りにしておけば、特に後処理を忘れてメモリリークを起させてしまう心配がとても少なくなるわけです。便利ですね!

ところで、このコードではメモリの取得にCの標準関数でもある malloc() を使用しています。実は、C++では malloc() を使うことは滅多にありません。なぜなら、代りとなる言語組込みの演算子 new が用意されているからです。free() に相当する delete もあります。 早速書き換えてみましょう。

コード2

// 巨大データクラス
class LargeData
{
    size_t          m_size;
    unsigned char * m_pData;
public:
    // コンストラクタ
    LargeData(size_t size)
    {
        m_size = size;
        m_pData = new char [size];  // ★
    }

    // デストラクタ
    ~LargeData()
    {
        delete m_pData; // ★
    }

    // データの設定
    const unsigned char * setData(const unsigned char * pData)
    {
        return (const char *)memcpy(m_pData, pData, m_size);
    }
    // データの取得
    const unsigned char * getData()
    {
        return m_pData;
    }
}
new は確保したメモリのポインタを返します。また、delete には開放するメモリのポインタを渡します。
malloc() や free() の代わりに使うような説明をしましたが、互換性はありませんので、new で取得して free() で開放する、といった使い方はできません。
また、malloc() はメモリが確保できない場合 NULL を返しますが、通常 new はメモリが確保できない場合は例外を発生します。(処理系の設定によって、NULLを返すようにもできる場合があります)ので、直後にいちいちNULLチェックをする必要はありません。(最新のC++ではNULLではなく、nullptr キーワードを使用します)
必要なところで例外をキャッチしてエラー処理を行うようにします。

更に、new と delete は malloc() や free() 関数とは本質的に異なることがあります。
次のコードを見てください。

コード3

// クラスを使う
int main()
{
    char myData = "abcdefg";

    LargeData * pLD = new LargeData(2000);
    pLD->setData(myData);

    delete pLD;

    return 0;
}
これは先ほどの LargeData クラスを使用するところです。今度は自動変数としてインスタンス化しているのではありません。new 演算子を使って LargeData クラスをインスタンス化しているのです。

malloc() は単にメモリを確保するだけでしたが、new は指定したクラスのメモリ領域を確保すると共に、インスタンス化まで行います(厳密には、インスタンス化という言い方は必要なメモリ領域の確保までを含めます)。もちろんコンストラクタも呼ばれます。
また、同様に delete 演算子はメモリを解放するだけではなく、後処理も行い、デストラクタも呼ばれます。

new や delete はC++では頻繁に使用されます。その際気をつけなければならないのは、delete を忘れてしまうことです。
Java や C# ではその必要が無く、言語のランタイム処理によって自動的に不要になったオブジェクトが判断され、開放される仕組みになっています。C++ ではプログラマの責任で開放しなければなりません。
しかし、最新の C++ では、そういった新しい言語の仕様の影響を受けたためか、ある程度の自動化が考慮されるようになっています。標準のライブラリを使うことで実現します。

アイディア

さて、コンストラクタやデストラクタを使って何をすればいいのでしょうか。単に、クラス内部で使用する領域の確保をしたり、開放したり、あるいはメンバ変数の初期化だけでしょうか。
もし、あなたがファイルをリードライトするクラスを設計するとしたら、どうしますか?

コンストラクタでファイルをオープンし、デストラクタでクローズする、といったアイディアが思いつきましたか?
しかし、コンストラクタでファイルをオープンしたら、戻り値が使えないのでエラーになったときどうすればいいのでしょうか。そんな心配をされたなら、むしろ良く理解されているということでもありますね。こういうときのためにも、C++では例外処理機構が用意されています。コンストラクタでのエラーは例外を発生させればいいのです。

コンストラクタで必ずファイルをオープンしなければならないとしたら、場合によっては都合が悪いこともあるでしょう。クラスをインスタンス化だけしておき、あとでオープンすることもできるように、fileOpen() メンバ関数も追加します。また、デストラクタが呼ばれる前にクローズしたい場合も考えて fileClose() も追加します。

なんだかとても便利そうなファイルIOクラスができそうな気がしてきませんか?

更に、テキストファイル用に特化されたクラスを作るなら、元のファイルクラスを継承させて設計すればきっと理に適っているはずです。更に発展させて、UNIX系の考え方にならって、コンソールIOとか、ネットワークのIOもファイルとして扱うなら、どのような継承関係にすればいいのか、ワクワクしてきませんか。


ここまでにご紹介してきた演算子の再定義や、コンストラクタ、デストラクタ、そして継承。センスの善し悪しはこれらの使い方だけが問題ではありません。実は、それは本質ではありません。
これらを駆使したとしても、クラスのユーザには更に複雑な使用方法をお願いしなければならないことも現実には沢山あります。それを、必要以上に複雑にせず、あるいはブラックボックスにしすぎず、様々なバランスを取りながら設計することが大切です。
クラスを作ることは、1つのアプリケーションを作ることに近い作業で、単に関数を作ることよりもずっとやりがいのある作業なのです。

2018-01-27

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

C++のクラスは魔法の箱?(1)

オブジェクト指向プログラミングの醍醐味の1つは、自分が提供するクラスに高い機能をスマートで格好いいインタフェース仕様で組み込めることだと思います。
優れたものは適切にギミックを組込み、あたかもマジックボックスのようでもあります。プログラマのセンスや腕の見せ所、重要なアピールポイントになります。

演算子関数

C++標準ライブラリでさえそうなのです。たとえばこれまでの記事で扱ってきた vector  クラスは、まるで言語組込みの機能のように、添え字([])を使って配列の要素にアクセスできる、というインタフェースを提供しています。
更に、map クラスに至っては、添え字の中に文字列等を指定できる連想配列を実現しています。

もし、クラスが提供するファンクションが全て普通の関数タイプだけだとしたら味気ありません。演算子を再定義できればコードの視認性を格段に高められますし、使う側に楽しさを提供することさえ可能なのです。

これは、一見オブジェクト指向そのものとは無関係に思われるかも知れません。
しかし、実は間接的に関係があるのです。
組込みの型、たとえば int であるとか char であるとか、これらもC++では型であると同時にクラスでもある、と考えるのです。型=クラスと言ってもいいのです。
つまり、クラスを作ることはユーザ定義型を作ることでもあるのです。であるならば、組込み型と同様にユーザ定義型にも演算子が使えないと、それを使用する箇所での見た目が全く違うものになってしまい、ユーザ定義型と言うには少々無理がある、ということになってしまうのです。(ただし、一般的にはクラスを完全な組込み『型』のように綺麗に設計できるケースは少ないと言えます。)

演算子を使う場合と関数をコールする場合の違いを標準ライブラリの string クラスの場合で見てみましょう。

コード1

void func1()
{
    string a = "abcd";
    string b = "1234";
    a += b;    // "abcd1234"
}

void func2()
{
    string a = "abcd";
    string b = "1234";
    a.append(b);    // "abcd1234"
}
func1()では演算子を使っているので、文字列の接続を直感的に理解できるコードになっています。func2()は、+=と同じ動作をする append() メンバ関数を使っていますが、append という単語の意味を知っていたとしても、やはり演算子の視認性に比べれば劣ります。ぱっと見が大切なのです。

なお、この append() は実際に存在している関数です。なぜ、+= 演算子が使えるのに、と思われるかも知れませんが、使用する側の都合で演算子よりも関数を使う方が適切と判断される場合があるからかも知れません。
たとえば、append() には引数を複数指定する機能の詳細が異なるバージョンが多重に定義されています。そういった他の append() と並列に使用する場合、1カ所だけ += 演算子を使うことはせず、append() で揃えたい、といった場合が考えられます。

この記事ではC++の文法自体はあまり説明してきませんでしたが、演算子を再定義する方法はここでご紹介しておきます。演算子を関数を使って再定義するには operator というキーワードを使います。
+= 演算子を再定義する例を示します。

コード2

// 座標クラス
class Point
{
    int  m_x;
    int  m_y;
public:
    // + 演算子の定義
    Point & operator += (const Point & rPoint)
    {
        m_x += rPoint.m_x;
        m_y += rPoint.m_y;

        return * this; // 自分自身を返す
    }
}


int main()
{
    Point a, b;
    a += b;

    return 0;
}
このように、operator というキーワードの次に再定義する演算子を記述し、合わせて関数名のようになります(ただし、呼出す方は関数呼出の書式は使えません)。再定義できる演算子は限られていて、全てを書き換えられるわけではありません。演算子の優先順位を変えることもできません。
それと、再定義と言っていますが、コード2の場合、Point クラス同士の演算の場合に限られます。それ以外の組み合わせには影響しません。

また、演算子は + なのに、中身では引き算をする等、使用者を混乱させるような定義は避けなければなりません。これは、故意に行わなくても、特に複雑な処理になる場合など、結果的に演算子の意味をこじつけてしまい、直感的に理解しにくい動作を定義してしまうことはありがちです。そのような場合は無理をせず、通常の関数として実装した方が好ましいかも知れません。この辺りのさじ加減はプログラマのセンスが問われるところでもあります。

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つのクラスに印刷するための共通のインタフェースを追加する、という意味合いを含んでいます。この考え方はクラス設計では大切な考え方、概念ですので、是非、理解してください。

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

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

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

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

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

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


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

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

名刺管理のアプリケーションの設計で考えていきましょう。
要点以外の詳細はできるだけ省きます。

オブジェクト指向の設計では、何がオブジェクトか、その全てを洗い出すところから始める、というのが常套手段です。でも、今回は設計手順を学ぶ記事ではないので、自由に考えていきます。

名刺管理ですから、当然名刺がオブジェクトになると考えてみます。では、名刺クラスを作りましょう。

コード1

// 名刺クラス定義
class NameCard
{
    string  m_name;         // 氏名
    string  m_mobTel;       // 携帯番号
    string  m_email;        // メアド
    string  m_title;        // 肩書き
    string  m_company;      // 所属会社
    string  m_department;   // 部署
    string  m_address;      // 会社住所
    string  m_tel;          // 電話番号
    string  m_date;         // 名刺交換日
    string  m_memo;         // メモ
};
とりあえず、名刺に記載されている情報と、名刺交換日とメモを加えてみました。
処理についてはまだ当分は後回しにします。

ところで、この名刺管理アプリにはどのような機能があるのでしょうか。(もちろん、本来であれば機能要件は先に決まっているはずです)

  • 検索機能(入力項目を条件にした絞り込み検索)
  • 宛名印刷(宛名ラベルやはがきの宛名印刷)

今回はこの2つの機能があると仮定します。

このうち宛名印刷は、個別の名刺宛に書類の送付等用以外に、年賀状等の一斉送付用も含めます。

さて、ここで機能上の問題が考えられます。

このクラスだと名刺1枚ずつをそのまま入力することになります。しかし、同じ会社に所属している人の名刺もたくさんあります。同じ会社なのに、毎回入力することになります。
とすると、年賀状を各所属会社に一通ずつ送付したいと思っても、所属が一括管理されていないので難しいというのはご理解いただけるでしょうか。
入力が正確で、一言一句、スペースの入力や全角半角の文字まで全て一致していて、なおかつ入力ミスが無ければ印刷するときに動的に名寄せする(重複を1つにまとめる)ことも可能ですが、人が入力する以上正確性は当てにできません。

この問題には印刷時だけでなく、所属会社の事務所が移転する等修正が必要になった場合等にもぶつかります。

そこで、所属する会社を分けて管理することが思いつきます。

コード2

// 名刺クラス
class NameCard
{
    string  m_name;         // 氏名
    string  m_mobTel;       // 携帯番号
    string  m_email;        // メアド
    string  m_title;        // 肩書き
    string  m_date;         // 名刺交換日
    string  m_memo;         // メモ
    Company * m_pCompany;   // 所属会社(NULLで所属無し)
};

// 会社クラス
class Company
{
    string  m_name;         // 会社名
    string  m_department;   // 部署
    string  m_address;      // 住所
    string  m_tel;          // 電話番号
};
会社クラスが新たにできました。そして、名刺クラスには会社クラスのオブジェクトを1つ所有できるようにしています。
これで概ね良さそうですか?

でも、もしこの名刺管理アプリを僕が使うのだとしたら困ることがあります。
同一人物が複数の会社やサークルなどに属していて、その人から所属の異なる名刺を複数枚もらうことがあるからです。
会社は名寄せした状態で管理できるようになりましたが、今度は人物です。

次のように解決しましょう。
名刺クラスと会社クラスの関係が、名刺クラス側から見た場合、今は1:0or1ですが、これを1:0~nとすることです。

ここで、ついでにもう1つ解決しておきたいことがあります。所属無しの名刺の場合(自由業など、個人名刺の場合)、連絡先住所などの入力がなくなってしまうので、名刺クラスの方にも住所などが必要になるということです。
この項目は個人の自宅住所になりますが、通常名刺には書かれていませんね。しかし、親交が深まる等で個人宅当てにお歳暮を贈る、ということは良くあることなので、会社などに所属している場合でも利用できます。。

では、コードを修正してみましょう。

コード3

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

// 会社クラス
class Company
{
    string  m_name;         // 会社名
    string  m_department;   // 部署
    string  m_address;      // 住所
    string  m_tel;          // 電話番号
};
★1の部分、名刺クラスに住所項目を追加しました。
★2は、vector という動的配列で所属会社を複数登録できるようにしました。もちろん、要素数0も可能です。

ところで、ここまであえて書かなかったのですが、クラスをどのように実体化してプログラムの中に持つのでしょうか。疑問に思われた方もいるかも知れません。

個人も所属先の会社も名寄せした状態(重複させない)のですから、それぞれが配列のように実体を保持する状態だ、というところまではよろしいでしょうか。今はデータベースのことは考えず、メモリ上に全てのデータがあると考えてください。

コード4

vector<NameCard>    nameCards;
vector<Company>     companys;
単純に、それぞれは vector 配列でこんな感じで保持されていることになります。

NameCardクラスには Company クラスのポインタを要素とする配列の項目がありますね(コード3)。ここに、コード4の companys の要素のうち、所属するもののポインタを保持することになるわけです。

これで、とりあえず多対多が成立はするのですが、会社クラスには関連付けの項目が無いので、ある会社に着目して、所属社員全てを見たいときは、このままだと nameCards を全て洗い出さなければなりません。これでは非常に処理に時間を要してしまいますので、やはり、会社クラスの方にも関連付けのための項目を持たせておいた方が都合がいいですね。

コード5

// 名刺クラス
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
};
会社クラスに社員項目を追加しました。

これで、NameCard 側からも Company 側からも所属が瞬時に判る訳です。もちろん、しっかりと整合性を持たせて、どちらかの片思いの状態にしてはいけません。

ただし、データの保持にデータベースエンジンを使用する場合、これらのクラスは一時的な使用に留まるため、関連付けの項目自体不要になる可能性もあります。関連付けはDB上で行われていて、関連のあるデータのみを読み出す場合が多いかもしれないからです。つまり、両方とも関連のあるデータしかメモリ上に存在しない状態であれば、関連付け情報自体必要が無いというわけです。
あるいは、もし関連情報が必要になった場合でも、その時点でデータベースに検索させる、ということも十分考えられます。

ところで、そろそろ説明に窮屈さを覚えてきましたので、1つ重要な用語を覚えてください。
今まで、漠然とオブジェクト、という言葉を使ってきましたが、「インスタンス」という言葉を登場させます。

インスタンスとは、クラスを実体化したオブジェクトのことです。クラスがハンコだとすると、それで押した印影がインスタンスです。クラスからインスタンスを作ることをインスタンス化すると言います。つまり、インスタンスは狭義の意味でのオブジェクトと言えます。
お堅いオブジェクト指向用語の1つですが、現場でもよく登場する言葉なので是非、覚えておいてください。

先ほどのデータベースの話に戻りますと、データベースから特定のデータを読み出してきたものをクラスのインスタンスに保持する、という言い方になります。


今回はここまでにします。
しかし、実はまだまだこれらのクラスには合理化できる余地がたくさん残されています。次の記事以後説明していきます。

2018-01-16

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

変化に寄り添えるオブジェクト指向

大前提として、システム開発というものは一度の開発サイクルで終わることは希である、という点は確認しておきましょう。
バグのないシステムを構築したとしても、ユーザが使っているうちに問題点や改善点が見えてきます。そして機能の修正や追加が繰り返されて完成度が増していきます。これは悪いことではないし、成功しているシステムはそういうものです。

ソフトウエアはハードウェアに比べて変更しやすいというのは、これはもう抗えない宿命です。つまり、システム開発は潜在的に変化に追従していく義務があるのだと思うのです。

そこで、ソフトウエアの品質を計る要素の1つ、「保守性」が登場するわけです。

ここで言う保守とは、機能を維持する、という消極的なものだけではなく、システムの改築までを視野に入れたより広い範囲を指します。

保守性に問題があると

初版のプログラムは概ね設計通りにできたとしても、何度も改造を繰り返すうちにプログラムコードは時間つぶしには最適の迷路パズルゲームと化し、その難易度は高くなる一方で、最後には手が付けられなくなって開発サイクルはすぐに終焉を迎えてしまいます。

ありがちですが、どうしてそうなってしまうのでしょうか。

1つには予算の問題が挙げられます。
システムの改修にかかる費用が割高に思えるのは、新車の購入金額の割に修理の部品代が妙に高く感じられるのと似ています。車1台分の部品をバラバラに購入したら新車が何台買えることか・・・?
車の部品には定価があるので、工賃を少し負けてもらうくらいしかできません。
しかし、ソフトウエアの改修は基本は全てが人的工数ですから、どうしても無理をさせられてしまいがちです。
無理をした結果、改修の繰り返しは悪循環と化し、地獄へ落ちていくのですね。

しかし、実際にプログラムの改修というのは、100%自由に作ることができる新規開発に比べて工数がかかるのは仕方がない面もあります。

なので、これをいかに軽減できるか、そこが焦点になります。

オブジェクト指向でどのようにこの問題を解決するのか

理想はプログラム改修の工数が、その機能改修によって生み出される価値と一致するか、更にそれを下回るようにすることです。

しかし、オブジェクト指向は決して魔法ではありません。これを取り入れたからといって、一気に理想に近づける訳ではありません。しかし、そのポテンシャルは十分に備えていますから、習得の為の労は必ず報われるはずです。少なくとも、僕にはそう感じられるので、これまで楽しくオブジェクト指向プログラミングに取り組んで来ることができました。

蛇足ながら、「楽をするための労は惜しまない。」という僕のスタンスにぴったりの技術でもあります(BLOG記事『忘れられた合理性の追求』を参照のこと)。

こんなことを書くと、ここ以降は茨の道になるように誤解されるかも知れませんが、そんなことはないのでご安心ください。

オブジェクト指向の神髄を紐解いていきましょう。

肝となる写像という考え方

オブジェクト指向の聖書(偉い人が書いた書籍)には、よく現実をモデル化したものをそのままクラス(オブジェクト)にする、といったような迷言が書かれています。
我々プログラマはその持って回った言い方によく惑わされてしまいます。
技術者ではないマネージャがこの話を聞くと、オブジェクト指向がまるで魔法のツールのような誤解をして我々技術者を困らせることもあります(実話)。

現実にプログラムを対応させる、と言っても、コンピュータの中にはそれとは無関係なオブジェクトと呼べるものがたくさんあります。ファイルシステムやディレクトリ、ファイル、データベース、メモリやネットワーク等々。プログラムが意識する対象の多くはそんなものが中心だったりしますね。

現実をモデル化したもの、それはたとえば「人」。人というオブジェクトが、コンピュータ内部に特有のオブジェクトと同列に存在することに、違和感を覚えずにはいられませんね。

でも実は、そこは階層を分けて考えればいいだけなのです。簡単な話でした。
簡単だけど、この説明を直接書いてある書籍に出会ったことはありません。


ただし、この聖書に書かれている言葉にはとても大切なことが隠されています。
それは、オブジェクトが対象を良く表したものであることが求められるという点です。対象が現実社会の何かをモデル化したものであっても、あるいはコンピュータの内部のことであったとしても同じです。
オブジェクトはプログラムが扱う「もの」であって、対象そのものではありません。対象に対して、オブジェクトは写像であると考えます。

写像であれば、その元になる対象に変化があっても、写像も同じように変化させればよく、回りくどい修正は必要無いはずである、と考えるのですね。

従来型のプログラムの問題点

逆に、オブジェクト指向ではない従来のプログラムではどうだったかを思い出してみましょう。

まとまりのあるオブジェクトという構造は存在せず、対象を操作する関数だけがありました。ひょっとしたら関数の操作対象は1つだけではなく、同時にいくつかの対象を操作するものかも知れません。その方が都合が良かったのでしょうね。

そこで、1つの対象に変化があり、プログラムがその変化に対応しなければならなくなったとします。

まず、その対象の変化により、プログラムのどの部分に影響があるか、その全てを調べるのに大きな労力が必要です。あちこちにそれを操作する関数が分散しているからです。
そして、操作対象が複数になっている関数やプログラムの場合、無関係な対象に影響が出ないように慎重に行う必要があり、これまた膨大な時間がかかってしまうことになります。

オブジェクト指向という筋の通った考え方に沿ったプログラミングから見ると、従来型のプログラムには節操がないように見えてしまいます。


今回は具体的なプログラミングの例を示すことができませんでしたが、次回からは具体的なオブジェクトのプログラミングに入っていきたいと思います。

コラム:オブジェクト指向の習得

2018-01-14

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

プライバシー問題!?

とてもソフトウエア工学とは直接関係なさそうなタイトルですね。でも、実際にはプログラミングにとって非常に重要な考え方で、是非、これは覚えるだけではなく、身につけて今後に活かしていただきたいと願います。
特に、C言語だけを何の問題も感じずに長年やってきた人にとっては、ひょっとすると面倒臭い余計なものに感じられるかも知れませんので、注意が必要です。

実社会のプライバシー問題

実社会でも、以前の日本ではプライバシー問題という言葉すらほとんど聞くことはありませんでした。
ところが、時代が新しくなるにつれ、人間関係が疎になっていったし、習慣の違ういろんな人が集まるようになってくると、赤の他人に必要以上の個人情報を知られて悪用されることに、社会全体が敏感になっていったのです。

プログラミングのプライバシー問題?

プログラミングも昔はプライバシー問題に鈍感でした。パーソナルコンピュータが一般に普及し始めた頃、おまけで付いていたインタプリタ言語(N88BASIC)にはグローバル変数しかありませんでした。

しかし、多機能、高機能が求められ、プログラムが複雑、かつ大きくなってくると限界が見え始めます。そんなときに構造化プログラミングの必要性が浸透すると共に、C言語が普及し始めました。関数という単位でプログラミングをし、関数の中だけで使える変数がある。それだけでもプログラマにとってとてもありがたく、助かりました。

オブジェクトのプライバシー

ハードウェアの進化と共に、ソフトウエアに求められる機能はより多く、より高度になっていきました。当然プログラムは複雑、かつ巨大にならざるを得ません。

この問題解決の一助となったのがオブジェクト指向プログラミングです。

これまで、プログラミングにおけるオブジェクトとは、ただのデータ構造でしかありませんでした。関数などの処理コードがまずあって、そのコードが操作する対象がデータ構造でした。
オブジェクト指向ではこのデータ構造を主体に考えます。そして、コードはデータ構造に従属するという考え方に変わります。つまり、データ構造とコードが非常に密接に関連付けられることになったのです。

その、オブジェクト指向で言うところのオブジェクト、つまり、そのデータ構造に従属する専用の処理コードもひっくるめてのオブジェクトは、これまでの関数という単位よりも高い独立性を確保することができます。

独立性が高いということは、逆に依存度は下がります。その結果、より汎用的で応用範囲も広がり、プログラムの価値も高くなります。

そんな独立性を高めるための機能をご紹介します。(やっとですね!)
C++では、オブジェクトの独立性をどうやって確保するのでしょうか。前回のサンプルコードをもう一度見てみましょう。

コード1

struct Point  // データと処理が一体となっている構造体
{
    int m_x, m_y;
    void set(int x, int y);
};

// Pointのメンバ関数
void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.set( 100, -200 );
    reutrn 0;
}
実はこのコードには問題があります。それは、次のようにメンバ変数を直接どこからでもアクセスできてしまうところです。

コード2

struct Point  // データと処理が一体となっている構造体
{
    int m_x, m_y;
    void set(int x, int y);
};

// Pointのメンバ関数
void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.m_x = 100;  // C言語と同じように直接メンバ変数を変更できてしまう。
    p.m_y = -200;
    reutrn 0;
}
現実的にこのような単純な構造体でしたら直接的な問題はないのかも知れません。
しかし、たとえば分数を表す構造体で、分母に0を設定できないというルールがあった場合はどうでしょうか。

コード3

struct Fraction  // 分数
{
    int      m_numerator;    // 分子
    unsigned m_denominator;  // 分母
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};

// メンバ関数(戻り値は成功/不成功)
bool Fraction::set(int numerator, unsigned denominator)
{
    if(denominator == 0) // 分母に0は設定できない
        return false;    // エラーを返す

    m_numerator   = numerator;
    m_denominator = denominator;

    return true;
}

// これもメンバ関数
double Fraction::getDouble()
{
    // これは分母が0だと困ります
    return (double)m_numerator / (double)m_denominator;
}

int main()
{
    Fraction frt;
    if(! frt.set( -2, 3 ))
        return 1;

    frt.m_denominator = 0;  // ★やばい!

    printf("%f\n", frt.getDouble());  // ここで0除算で落ちるね(;;)

    reutrn 0;
}
せっかく、構造体専用の関数set()で分母が0の場合をエラーにして設定できないようにしていたのに、直接メンバ変数を操作されてしまえば意味がありません。
さて、どうしましょうか。コーディング規約に「メンバ変数を直接操作しないこと!」の一行を追加しましょうか。でも、デバッグ用の特例コードで変数を操作して、うっかりそのコードを本番に残してしまったら? 大量のコードからどうやって見つけますか? ひょっとしたら、m_denominator という変数名は他でも使っているかも知れません。すると、grep機能を使ってもなかなか見つけられませんね。

C++にはオブジェクト指向の隠蔽という考え方を反映させた機能が最初から組み込まれています。コード3にそれを適用したものがコード4です。

コード4

struct Fraction  // 分数
{
private:  // プライベート宣言をして、隠蔽する
    int      m_numerator;    // 分子
    unsigned m_denominator;  // 分母
public:   // 公開することを宣言する
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};

// メンバ関数(戻り値は成功/不成功)
bool Fraction::set(int numerator, unsigned denominator)
{
    if(denominator == 0) // 分母に0は設定できない
        return false;    // エラーを返す

    m_numerator   = numerator;
    m_denominator = denominator;

    return true;
}

// これもメンバ関数
double Fraction::getDouble()
{
    // これは分母が0だと困ります
    return (double)m_numerator / (double)m_denominator;
}

int main()
{
    Fraction frt;
    if(! frt.set( -2, 3 ))
        return 1;

    frt.m_denominator = 0;  // ★★コンパイルエラー!

    printf("%f\n", frt.getDouble());

    reutrn 0;
}
構造体にキーワード「private」と「public」が追加されています。private: 以後は外部からのアクセスができないメンバ(変数も関数も)になります。(もちろんメンバ関数からは自由にアクセスできます)
public: 以後は公開メンバとなります。
その結果、main()関数の中で直接プライベートメンバに値を設定しようとするところでコンパイルエラーになってしまいました。

このように外部に晒したくないメンバを隠蔽することで、不要なアクセスをされる心配がなくなります。アクセスされないことが保証されるわけです。そのことにより構造体がオブジェクトとして独立性を高められる、ということになるのです。

他人の家には、玄関を通してのみ、許可を得て出入りするのと一緒ですね。その家の裏口も窓も開けっぱなしでは、いつ誰が入ってくるか判りません。周辺の住人を信用しているかいないかの問題ではなく、そういった疑いを持たずに済む、ということが重要なのです。

クラスの登場

ここまで、C言語にもある構造体を使って説明してきました。しかし、C++では上記の例のような場合は struct ではなく、class を使うのが一般的です。クラスというのはオブジェクト指向の用語でもあります。実は、C++では struct もクラスです。違いはただ1点だけです。
それは、デフォルトが private か public かの違いだけです。class と struct、どちらがどちらでしょうか。上記のコードを見ていただければ判ると思いますが、class はデフォルトが private、つまり、public: が現れる前は private 扱いになり、struct はその逆です。

コード5

class Fraction  // 分数クラス
{
    int      m_numerator;    // 分子(プライベートメンバ)
    unsigned m_denominator;  // 分母(プライベートメンバ)
public:
    bool   set(int numerator, unsigned denominator);
    double getDouble();
};
違いはそれだけなのですが、慣例として struct を使うのは受動的なデータ構造を扱う場合が多いです。つまり、C言語で使っていたようなデータ構造を示すだけの構造体的なものの場合ですね。

オブジェクト指向の用語でもあるクラスを示す class のデフォルトが private なのは意味があります。公開は必要最小限にして、独立性を高め、プライバシーを強く意識することが信頼性を高めるために大切だからです。

更に、家の玄関を通常は1つにするように、インタフェースはできるだけシンプルにするという意味もあります。

コラム:隠蔽とは?

2018-01-12

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

指向!?

前回、C++でのオブジェクトは実際にはメモリ上の構造に過ぎないことを示しました。では、そんなオブジェクトに対する指向とはどういうことなのでしょうか。単なるメモリ上の構造にしては、少々大げさな表現に思えますよね。

しかし、本当のところ、ここにはパラダイムシフトと言える程の大きな変化が隠されています。にもかかわらず、その変化の基本は非常にシンプルです。

オブジェクト指向ではないC言語では、メモリ構造に対する処理は通常次のようになります。

コード1

// C言語でのメモリ構造操作
typedef struct
{
    int m_x, m_y;
} Point;

void setPoint(Point * pPoint, int x, int y)
{
    pPoint->m_x = x;
    pPoint->m_y = y;
}

int main()
{
    Point p;
    setPoint( &p, 100, -200 );
    return 0;
}
では、次にC++でオブジェクト指向的に書いてみます(C++でもCと同じスタイルで書けてしまうのでこういう表現をしましたが、今後特別な場合を除いてこの説明は省きます)。

コード2

// C++でのメモリ構造操作
struct Point   // 今回はclassではなく、structとしておきます
{
    int m_x, m_y;
    void setPoint(int x, int y);
};

void Point::setPoint(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.setPoint( 100, -200 );
    return 0;
}
Cにはない書き方がありますね。
1つは、構造体の中に関数 setPoint() のプロトタイプがあることです。これはこの構造体のメンバ関数と呼びます。変数をメンバ変数と言うのと一緒です。そして、そのプロトタイプの実装が Point::setPoint(){} といった関数名で書かれています。Point構造体のメンバ関数なので、「::」(コロン×2)で繋いでメンバ関数であることを表します。
main()の中では、構造体のメンバ変数にアクセスするときと同じように「.」(ドット)を使ってメンバ関数にアクセスし、呼出しています。

コード1も2も同じ機能を果たすプログラムですが、コード2のC++の方はsetPoint()の引数が1つ減っています。これは、メンバ関数の中から同じメンバに対してはグローバル変数のようにアクセスできるので、わざわざパラメータを必要としないためです。

さて、これら2つのプログラムの本質的な違いは何でしょうか。関数を動詞に喩えると、

  • Cの場合、「~をする」
  • C++は、「~が~をする」

と、なります。C++の方には主語があるのです。

あれ、たったそれだけのこと? 目先が違うだけでは? と感じられた人もいるかも知れません。今後、徐々にその意義を理解して頂ければいいのですが、今回は概要を説明しておくことにします。

~が~をする、というのは、オブジェクトと動作が強く結びつけられている、ということでもあります。そうすると何が良くなるのか、ユーザインタフェースを例に説明します。

WindowsにしてもMacにしても、スマートフォンにしてもそうなのですが、GUIにはオブジェクト指向的なコマンドの起動方法が含まれています。

たとえば、Windowsのデスクトップにワープロのドキュメントファイルがあり、それを編集するとき、皆さんはどうされますか? 多くの人はそのファイルをダブルクリックしますね。すると、結びつけられているワープロソフトがファイルを開いた状態で立ち上がります。ファイルが表計算ソフトのものであれば、表計算ソフトが同様に起動します。
ダブルクリックではなく、マウスの右クリックでコンテキストメニューを開き、「編集する」というコマンドを選んでも同じ動作が可能です。また、右クリックでは印刷コマンドとか、プロパティを表示するといった、そのファイルで可能な別のコマンドも実行できるようになっています。

この動作はユーザにとって極めて自然です。もし、GUIにおけるオブジェクト指向がなかったらどうでしょうか。編集するときは目の前にファイルが見えていても、一旦ワープロソフトを起動し、ワープロソフトからファイルを探してオープンする操作をしなければなりません。

世の中のコンピュータがGUIになる以前は、全てコマンドプロンプトからプログラムを起動しなければなりませんでした。テキストを編集するコマンドが edt で、編集対象のファイルが test.txt であるなら、

    > edt  c:\myfiles\test.txt

等と入力しなければなりませんでした。コマンドも、対象ファイルもどちらも覚えておかなくてはならなかったのです。

現在主流のGUIは単にマウスを使ってグラフィカルに操作するだけでなく、ファイルとコマンドの結びつきが定義されていて、そのファイルに対して何ができるのかといったことをユーザが考えたり、予め知らなくてもいいようになっています。
オブジェクト(この場合はファイル)と動作(コマンドやアプリケーション)が密接に関連付けられている、という意味ではオブジェクト指向的なユーザインタフェースであると言えるのです。

プログラミングに戻りましょう。

上記コード1では、Point構造体とそれを扱う関数との間に直接の関連付けはありません。Point構造体だけ見ても、それに対するどんな処理が存在するのか不明です。不便ですね。上記のCUIのときと似た不便さがあります。
逆に、コード2ではそれが明らかで、これもGUIの例になぞらえることができます。

関数の側から見てみると、コード1ではsetPoint()という関数名はグローバル定義にするしかありません。なので、他の目的でも同じ関数名が必要になった場合、名前がぶつかってしまいます。なので、汎用的で意味の広い関数名は使いにくくなります。不便ですね。
コード2の場合、同じメンバでなければ(同じメンバでも引数が異なれば可能)、同じ関数名がいくつあっても問題ありません。なので、シンプルで判りやすい関数名を使うことができます。
ところで、コード1での関数名はPoint構造体に値を設定する、という意味でsetPoint()という関数名にしてありますが、コード2は関連付けが明らかなので、あえて関数名にPointを含める必要はありません。なので、この場合は単にset()とした方が良いでしょう(コード3参照)。

コード3

// C++でのメモリ構造操作
struct Point   // 今回はclassではなく、structとしておきます
{
    int m_x, m_y;
    void set(int x, int y);
};

void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.set( 100, -200 );
    return 0;
}

ここまでをまとめると、
オブジェクトを主体に考えること(つまりオブジェクト指向)で、オブジェクトに対して何ができるのかがはっきりする。処理、という観点においても、オブジェクトが結びついていることで何に対する処理なのかが明確になる。ということになるでしょうか。

そして、この判りやすさは使うときだけではなく、設計やコーディング時にも享受できるのですが、その点については今後徐々に理解して頂けると思います。


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

オブジェクト!?

これまで、散々オブジェクト指向って聞かされてきたけれど、そのオブジェクトとは何なの? という疑問をお持ちでしょうか。それは、平たく言えば「もの」となります。C++ではこれをクラスというものを使って構築&表現します。

おっと、既に話が飛躍したかも知れません。

オブジェクト指向というのは、単にプログラミング以外にも、システムの概要的な設計の段階からその考え方を適用可能なので、一概にその「もの」が実際には何であるか、とは言い切れないところがあります。しかし、この記事はC++プログラミングに限定するので、それはメモリ上のある構造を指しているに過ぎない、と言えます。

Cでは構造体に相当する、クラスというものがC++にはあります。キーワードもそのものズバリ、classです。そして、構造体を示すstructも、実はC++ではクラスなのです。違いはほとんどありません。ある意味、全く違わないとも言えます。(小さな違いについてはいずれまた)

クラスそのものはオブジェクトではありません。オブジェクトの設計図です。これをインスタンス化したものがオブジェクトです。「インスタンス化」という言葉は是非ここで覚えてしまってください。次のコードのように使用します。

コード1

// まずはクラスの定義
class MyClass
{
    int member;
};

void func()
{
    MyClass object;    // MyClassクラスをobjectという名前でインスタンス化
}
このやり方は実はCでもありましたね。Cでは構造体になりますが、そっくりなコードになります。

コード2

// 構造体の定義(C言語)
typedef struct
{
    int member;
} MyStruct;

void func()
{
    MyStruct object;  // これも実はインスタンス化
}
ちなみに、C++ではsturctもclassと同様、typedefは不要です。
是非、C++なのにstructにtypedefを使うような大人にならないでくださいね!?(何故か使う人が多い)

コード3

// 構造体の定義(C++)
struct MyStruct  // structでも実際にはクラス。構造体の上位互換性がある。
{
    int member;
};

void func()
{
    MyStruct object;
}

ここまでをまとめると、オブジェクト指向のオブジェクトの設計図がクラス、そして、そこからインスタンス化したものがオブジェクトになります。
なので、C++でオブジェクトとは、メモリ上の構造でしかない、と覚えておいてほぼ間違いはありません。

2018-01-10

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

なぜ、オブジェクト指向なのか?

ソフトウエアの品質を上げようとすると、自然にオブジェクト指向にたどり着いた、と、僕自身は感じています。
このシリーズを通じて、読者の方にも徐々に感じていただけたらと願っています。なのでこの問いの回答を最初に説明するつもりはありません。

ただし、C++の機能を知っていく中でオブジェクト指向の必然性を感じていただくためのヒントとして、ソフトウエアの品質について少し説明させてください。

ソフトウエアの品質とは


ソフトウエアの品質とは、主に
  • 機能性
  • 安定性
  • 保守性
  • コストパフォーマンス
から成っています。
もちろん、それぞれの要素は完全に独立しているわけではなく、相互に強く関連し合っています。

[機能性]
コーディング前のフェーズにはこれを大きく左右する要素が含まれますが、コーディングでは主に処理速度のことを指します。
場合によっては安定性や保守性とトレードオフの関係になります。

[安定性]
動作の安定性のことで、バグが含まれないことが求められます。
安定性と機能性は相反する場合がありますが、現在はハードウェアの性能が高くなった分を安定性に振り分ける余裕があるので、多くのプログラミングでこれがせめぎ合うケースは少ないと思われます。もちろん、リソースに余裕のないハードウェアをターゲットにしたり、処理速度に関する要求仕様が非常にシビアなジャンルもあり、全てが容易に両立できるわけではありません。

[保守性]
機能追加やバグ修正のしやすさです。他の要素に比べて見落とされがちですが、非常に重要な要素です。
機能性とはトレードオフの関係になる場合もありますが、安定性とはトレードオンの関係になる場合が多いと思われます。

[コストパフォーマンス]
主に開発効率のことです。
上記3要素が全て揃っても、システム開発に莫大なコストがかかってしまえば現実的には成立できず、システムは存在できなくなります。技術者であってもその点を意識し、開発効率を常に考慮する必要があります。

ソフトウエアの生存期間をトータルで考えた場合、特に保守性、および安定性とはトレードオンの関係と言えます。機能性とは間接的にトレードオフの関係になります。
更に、システム開発において1から10までを毎回全て新規開発する状況は好ましくありません。独立性が高く、汎用性のある部分をライブラリ化して無駄な工数を削減することはとても重要です。つまり、1つのシステムだけではなく、社内的な開発体制にまで視野を広げる必要があるということになります。

オブジェクト指向と品質の関係


ここでは具体的な説明は避けますが、概要だけお伝えしておきます。
オブジェクト指向は品質の要素のうち、特に「安定性」、「保守性」、「コストパフォーマンス」に大きく寄与します。
「機能性」については、実行速度の点でややマイナスになる可能性はありますが、少し俯瞰して見ると、その他の要素が向上することで余力が生まれ、それまでできなかったことができるようになるなどして、むしろプラスに作用する場合も多いはずです。

ソフトウエアの品質を意識する


ソフトウエアの品質を常に意識しながら読み進めてください。そして、実際のコーディング時にも常に意識することが大切です。

コーディングのしやすさは開発効率に寄与しますが、場合によってはあまり目先のことに囚われるとトータル的には効率を落としてしまうことがあります。システムの開発サイクル全体を意識して判断することが求められます。


2018-01-09

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

はじめに


「C++を学ぶ前に、まずはオブジェクト指向を学べ。」

この言葉を鵜呑みにして何やら詳しそうなオブジェクト指向の本を買ってみても、モデルがなんちゃらとか、なじみのない用語が登場していきなり疎外感に見舞われます。仕方なく章を飛ばすけれど、やはりC言語では比較的なじみのない業務系アプリケーションの設計方法が例として採り上げられていて、鬱陶しいやらめんどくさいやら。

ご立派なオブジェクト指向の本(聖書)には妙に遠回りさせられてしまうし、ひょっとしたら制御系をやっている人にはあまり関係が無いんじゃないか、等と都合の良い理由を付けて諦めてしまいます。

要は、オブジェクト指向の敷居は妙に高い。という、気にさせられてしまうわけですね。
こんな環境がオブジェクト指向ディバイド(リンク先参照)を産んでしまうのでしょうか。

きっと、その要因はもっといろいろあると思いますが、まずはそんなことは忘れて、C++という言語を使って、より効率よく、より品質の高いプログラミングをする、ということを目標に、まずはできることからやってみませんか。
もちろん、これまでオブジェクト指向を学ぼうとしたことがなく、上記のような経験さえないという方も大歓迎です。

冒頭の言葉を言い換えるなら、

「C++を学ぶなら、オブジェクト指向もついでに学べ。」

と、なります。

もし、過去BLOGの Cプログラマのための、C++簡単裏道アプローチ を読んでいないなら、できれば先に目を通しておいてください。
そちらの記事はオブジェクト指向を避けてC++のメリットをお伝えするものでしたが、やはりそれには限界がありました。C++の本領を発揮するにはやはりオブジェクト指向を避けては通れなくなり、そこで今回の記事を書くことにしたのです。

本編は(2)から始めます。

2018-01-06

Cプログラマのための、C++簡単裏道アプローチ(6)

いつも同じようなデータの入れ物を作っているならSTLが使える

裏口アプローチの最後は、データの入れ物(コンテナ)の話です。

今回も、既にC++を知っている人がCを使わざるを得ない環境で、
「ああC++のこの機能だけでも使えたらな」
と、感じるような内容です。

配列はC言語にももちろん備わっていますが、固定長です。可変長の配列が必要な場合、realloc()等での操作が必要になるなど、かなり複雑で面倒なことになってしまいます。

まずはC++での可変長配列の例を見てみましょう。

コード1

void func1()
{
    vector<int> numbers = {1,2,3,4};  // 配列の宣言と初期化

    // 要素1を200に変更->{1,200,3,4}
    numbers[1] = 200;

    // 配列の末尾に要素(10)を1つ追加->{1,200,3,4,10}
    numbers.push_back(10);

    // 要素2の前に300を挿入->{1,200,300,3,4,10}
    numbers.insert(numbers.begin() + 2,300);

    // 要素4を削除->{1,200,300,3,10)}
    numbers.erase(numbers.begin() + 4);

    // 表示
    for(int i = 0 ; i < numbers.size() ; ++ i)
    {
        printf("numbers[%d]=%d\n", i, numbers[i]);
    }
}
デモンストレーション的に無意味な操作をしていますが、末尾に要素を追加するのはもちろん、途中への挿入、削除なども行っています。
これらと同じ処理をCで行うことを想像してみれば、比較にならないほど圧倒的であることが容易に判断できますね。

もう少し、このコードについて説明します。

関数の最初で vector<int>という型でnumbersという変数(オブジェクト)を宣言し、通常の配列のように初期化しています。ここで使用している<int>の部分を変更すれば、要素にあらゆる型を適用することができます。

コード2

struct XY {int x; int y;};

vector<int>     intNums = {1,2,3};
vector<double>  doubleNums = {1.0, 2.0, 253.9991};
vector<XY>      xys = {{1,100}, {2,300}};
特に注目して欲しいのは、3番目の独自の型を適用できる点です。

まず、最初に説明しなければならないのは、ここで使用しているvectorというのは単にC++のライブラリであるということです。まるでC++という言語に組み込まれているかのように見えるところもありますが、STLというライブラリの1つです。

STLは標準テンプレートライブラリ、という名称です。テンプレートはC++の文法の1つで、型そのものをパラメータとしてコーディングすることができる機能です。

テンプレートを関数に適用した例を示します。max()はCではマクロとして実装できますが、マクロには副作用があるため、通常の関数の方が望ましいわけです。しかし、そのまま関数にしてしまうと仮引数の型指定によって1つの型に決まってしまい、それ以外の型に対応できなくなります。そこでテンプレートを使用します。

コード3

// 関数の実装
template <typename T>
T max(T var1 , T var2)
{
    if( var1 > var2 )
        return var1;
    else
        return var2;
}

// 関数の使用例1
void func2()
{
    int    iMax = max<int>( 111, 112 );     // iMax = 112
    double dMax = max<double>( 1.2, 1.1 );  // dMax = 1.2
}

// 関数の使用例2
void func3()
{
    int    iMax = max( 111, 112 );  // iMax = 112
    double dMax = max( 1.2, 1.1 );  // dMax = 1.2
}
これはテンプレート関数と言いますが、関数の使用例1の基本的な記法を見ると、<typename T>というタイプパラメータにintやdoubleが適用されることが分かりやすいと思います。
使用例2は<int>等の部分を省略した記法で、この場合maxの引数部分の型から自動的に判断されます。

テンプレート機能を使用した場合、その実装部分ではまだ実際には仮の状態であり、使用部分で型が決まった時点で実装が完結する仕組みになっています。
余談ですが、そのためライブラリと言ってもその一部、または全てがソースコードで提供されることになります。


さて、ここまでは比較的シンプルな配列という入れ物を見てきました。C++ライブラリの柔軟性やその可能性について感じていただきたかったからです。
しかし、データの入れ物として、単なる配列だけではその有用性は限られたものに感じられるかも知れません。STLには他にも次のような種類のコンテナが用意されています。

  • vector:動的な配列
  • map:キーと値のペアを格納する(キーはユニーク)
  • multimap:キーと値のペアを格納する(キーは重複可能)
  • set:値を持たないmapのようなもの(ユニーク)
  • multiset:multimapの値を持たないようなもの(重複可能)
  • list:双方向リスト
  • forward_list:片方向リスト
  • deque:両端キュー
  • queue:キュー
  • priority_queue:優先度付きのキュー
  • stack:スタック

以上は代表的なもので、他にも多少バリエーションが存在します。また、以前紹介したstringも“コンテナ相当”と位置づけられていて、コンテナ相当のものは他にもいくつか存在します。

キューやリスト、スタック等はなじみ深いと感じる方も多いかもしれません。毎回、新しい開発の度に少しずつ違う構造体を扱うために同じ機能の似たようなプログラムをよく作っている、という人も多いのではないでしょうか。現実的に、C言語ではそうせざるを得ません。しかし、C++を知っているとそういった労力が無駄に思えてきます。

もう1つだけ、上記の中からmapを紹介しておきます。これは連想配列を実現するものなのですが、メモリ上に簡易データベースを構築できると考えてもいいものです。ただし、本格的なデータベースに比べれば機能も限定的ですし、パフォーマンスも期待できません。
とは言え、連想配列はその使用方法が非常に明快かつ柔軟性もあり、初めて知ったときは“目から鱗”的な感慨があったと記憶しています。

コード4

void func4()
{
    map<const char *,double> populations;

    populations["東京都"] = 927.3; // 東京都は927.3万人
    populations["横浜市"] = 372.5; // 横浜市は372.5万人
    populations["北海道"] = 547.4; // 北海道は547.4万人

    printf("横浜市の人口は%f万人\n", populations["横浜市"] );
}
これは機能の説明はほとんど要らないのではないでしょうか。一目瞭然の動作ですね。

配列の添え字に数値ではなく、文字列を使用しています。そして、元々なにもないところに値を設定すると、その項目が追加されます。また、既に項目が存在すれば、データは新しいものに置き換えられます。

もし、項目がヒットしなければ(見つからなければ)値のデフォルト値(この場合doubleなので0.0)が返されます。ヒットしないことを知りたいなら[]による添え字ではなく、組込みのfind()関数を使用すればいいだけです。

添え字を使った場合、項目がヒットしないとデフォルト値が返されることには意味があります。次のコードを見てください。

コード5

void func5(const char * name)
{
    static map<const char *,long> nameCounts;

    printf("%sは%ld人\n", name, ++ nameCounts[name] );
}
nameが初めて出現した場合、longの初期値0が返されるので、インクリメントは期待通り1からカウントされます。

最後に

2018-01-05

Cプログラマのための、C++簡単裏道アプローチ(5)

増殖するエラー処理は例外処理ですっきりと

想像してみてください。もしも、エラー処理を書かなくて良かったとしたら。コードはどれだけすっきりし、本来の処理の流れが容易に見渡せるのか。
逆に言えば、コードの多くを占めるのがエラー処理ではないでしょうか。特にC言語の場合はそれが顕著です。

そのエラー処理とはいったい何なのでしょうか。まずはエラーの検出。多くの場合、関数の戻り値によって判定します。
エラーと判定された場合、何をするのでしょうか。まずはエラーのレベルの判定をして、致命的で処理を続行することができなければプロセスを終了させなければなりません。ユーザインタフェースを起因とするエラーであれば、ユーザーに通知する必要があります。あるいは、通信相手にエラーになったことを通知することもあるでしょう。
そして、共通するのはあとでメンテナンスをするためにログに書き出すことです。

ログ出力はシステム設計の時点で計画されることが普通で、1カ所にまとめて出力することで処理の分析を容易にすることができます。良くあるのは、エラーレベルという属性を伴うことです。これにより出力時にフィルタリングすることもあれば、ログを解析するときにフィルタリングする場合もあります。よくあるレベル分けには、デバッグ、ウォーニング、エラー、致命的エラー、といった分類で、レベル以外にも種類や系統のフラグ属性を付加することもあるでしょう。大規模なシステムでは、発生すると赤色灯を点灯させるようなものもあります。

一言でエラー処理と言っても、その内容は多種多様です。そして、システムの保守性、信頼性を高めるためにも決して省略することはできません。

しかし、です。このエラー処理のために、プログラムコードから本来の処理の流れが読みにくくなってしまうという大きな問題を抱えています。これは仕方がないのでしょうか。

C++ではCにはなかった例外処理という大胆な仕組みが追加されました。これをうまく使うことで、本来の処理の流れをつかみやすい、良質なコードを書くことが可能になります。

エラー処理の簡単なパターンを示します。
まずはエラー処理を省いて処理の流れを見て、つぎにCでエラー処理を追加した場合にどうなるのか見てください。

コード1

// エラー処理のない仮コード

void func()
{
    foo1();    // どこかのライブラリ関数
    foo2();    // どこかのライブラリ関数
    fooEnd();  // どこかのライブラリ後処理関数
}

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        func();
    }
}

int main()
{
    funcRepeat(10);
    return 0;
}


コード2

// エラー処理付き、Cの場合

int func()
{
    int result1 = foo1();   // どこかのライブラリ関数
    if( result1 != FOOLIB_NOERROR )   // libのエラー判定
    {
        // 関数名やコースコード行数とともにログ出力
        logout("foo1() = %d", result1);
        fooEnd();  // どこかのライブラリ後処理関数
        return MYERROR_FATAL;
    }

    int result2 = foo2();   // どこかのライブラリ関数
    if( result2 != FOOLIB_NOERROR )  // libのエラー判定
    {
        // 関数名やコースコード行数とともにログ出力
        logout("foo2() = %d", result2);
        fooEnd();  // どこかのライブラリ後処理関数
        return MYERROR_FATAL;
    }

    fooEnd();  // どこかのライブラリ後処理関数
    return NO_MYERROR;
}

int funcRepeat(in n)
{
    int myResult = NO_MYERROR;

    for(int i = 0 ; i < n ; ++ i)
    {
        if(func(i) == MYERROR_FATAL)
        {
            logout("func() %d回目でエラー", i + 1);
            myResult = MYERROR_FATAL;
            break;
        }
    }

    return myResult;
}

int main()
{
    if(funcRepeat(10) != NO_MYERROR)
    {
        logout("funcRepeat() は失敗した");
        return 1;
    }

    return 0;
}
このコードはもっと最適化することは可能かも知れませんが、現実的にはこのようにエラー処理だらけになってしまいがちです。
では、C++の例外処理ではどうなるでしょうか。

コード3

// エラー処理付き、C++の場合

void func()
{
    try
    {
        foo1();   // どこかのライブラリ関数(例外発生の可能性あり)
        foo2();   // どこかのライブラリ関数(例外発生の可能性あり)
        fooEnd(); // どこかのライブラリ後処理関数(例外は発生しない)
    }
    catch(LibExp &e)   // foo1()またはfoo2() の例外
    {
        fooEnd(); // どこかのライブラリ後処理関数

        // 例外情報を使って内容をログ出力
        logout("%s:%d", e.errstr, e.code);
        throw  MyExp("%sエラー発生!",e.errstr);  // 独自の例外を発生
    }
}

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        func(i);
    }
}

int main()
{
    try
    {
        funcRepeat(10);
    }
    catch(MyExp &e)   // 独自の例外
    {
        // 例外情報を使って内容をログ出力
        logout("%s", e.errinfo);
        return 1;
    }
    return 0;
}
tryとcatch、およびthrowというキーワードが現れました。
try{}の中だけを見ると、コード1のエラー処理がない場合とほぼ同じに見えます。正常処理の流れがCの場合に比べ、よく判るのではないでしょうか。
このtry{}の中で例外が発生し、その例外を捕捉するのがcatchです。例外はthrowで発生させますが、このときパラメータを与えます。そのパラメータは自由な型のものが指定できます(たとえばint型でもいいし、独自のクラスや構造体でも構いません)。catchはその型を指定し、関数の仮引数と同じように例外情報を取得することができます。

もう一度コード3を見てください。funcRepeat()の中にはtryもcatchもありません。throwで発生させた例外は捕捉されるまで関数コールを遡って伝わっていくので、ここではmain()で捕捉するようにしています。もし、最後まで例外が捕捉されないと、プロセスは異常終了するはずです。

例外、という言葉は語感として強いものがありますが、どこかで例外が発生したらシステムの処理を続行できなくなる、という意味ではありません。今まで、関数の貴重な戻り値を使ってエラーを通知していたものをそっくり置き換えることが可能です。
たとえば、ファイルのオープンエラーで例外が発生したとすると、再度ユーザにファイル名を入力してもらい、正常処理に戻るといったことも可能です。

また、今回ここでは示しませんが、例外処理は関数コールを跨がなくても使えます。同じ関数の中でtry、throw、catchがあってもいいのです。そう考えると応用範囲はもっと広がるように感じられますよね。

ところで、コード2と3のfuncRepeat()関数を見ておや?と思ったかも知れません。関数コールの途中で何もしなくても良いことを強調するためにこのようにしたのですが、ループの何回目でエラーになったかログを出す必要があれば、次のコード4のようにします。

コード4

void funcRepeat(in n)
{
    for(int i = 0 ; i < n ; ++ i)
    {
        try
        {
            func(i);
        }
        catch(MyExp &e)
        {
            logout("func() %d回目でエラー", i + 1);
            throw;
        }
    }
}
throwにパラメータを渡していません。catchの中でthrowする際にパラメータを指定しなければ、catchした例外を再度発生させることができます。ここではログ出力だけが目的なので、例外情報にはなにも手を加える必要がなく、このようにしました。


さて、ここまで細かいところは気にせず、C++ではこのような例外処理の仕組みを使ってエラー処理を書くことができる、という点だけ押さえておいてください。
例外処理により、エラー処理の冗長なお決まりコードを減らすことが可能です。そのことで本来の処理の流れがつかみやすく、更に、エラー処理自体の流れも判りやすくなり、高品質、かつ、無駄な工数を削減できるコードを書くことが可能になります。

例外処理を使えるだけでも、C++を使う価値があると言えます。これを知っていながらあえてC言語のみを使ってコードを書かなければならないときに感じる理不尽は、かなり強烈なものがあります。