|
漁師プログラマ 山崎敏 YAMAZAKI Satoshi
vol.09 |
|
|
●●ある日 |
|
|
10年ほど昔,筆者は男ばかり4人で裏磐梯猫魔スキー場に行き,つかの間の休日を楽しんでいました.
さて,そんなスキー場からの帰りの出来事です.山道は雪が多いのでタイヤにはチェーンを巻いて走ります.筆者の車は前輪駆動なので前輪にチェーンを巻いてます.山をだいぶ降りたころ,物凄い大雪が降ってきました.
「速度を落して走らないと危ないよなー」と話をしていると,筆者の車を物凄い勢いで追い越していく車があるのです.雪が積もらないうちに山のふもとまで降りてしまおうというのでしょうか.「この新雪は危ないぞー」と言ったか言わないかの瞬間,前方でその車がくるくると回っているのです.そのまま路肩に激突.筆者の前方を塞ぎました.「あららー.自業自得だよ.早くどいてねー」などと悪態をつきながら筆者たち4人は誰も手伝おうとはしません.反対車線は車が来るし,雪で見通しが悪いので前の事故車が自力でどかない限り進めません.
後ろの車の人たちは見かねて事故処理を手伝いに行きました.しょうがない,我々も手伝いに行こうか…と思った瞬間,反対車線からワゴン車がまっすぐこっちに向かってくるではないですか!前輪は元の斜線に戻ろうと左に切られているのですが,雪で滑りタイヤの方向とは無関係にこちらにまっすぐ向かってきます.この道は若干の坂(左カーブのゆるい上り)になっていてチェーンのない車はタイヤがロックしたら自然にこちらの斜線に滑って来るのです.
「マジかよ!」筆者はすかさず後ろに下がろうとしますが,後ろにはドライバーのいない車があります.下がるにも限度があります.「ダメだ.動けない!」相変わらず,前方からせまるワゴン車.「ひー!!ひー!!」ゴンッ!正面衝突!速度がゆっくりだったので,たいした衝撃はありませんでしたが車の損傷が心配です.サイドブレーキを引き外に出て,ドアを勢いよくバンッと閉めた瞬間,車が後ろに下がり始めるではないですか!!「うを!!」必死に手で押さえる筆者.しかし,ひと1人の力で1トンを超える車が止まるわけがありません.押さえた筆者も一緒に滑ります.ズルズルズル…ゴン!後ろの車に激突!もう散々.
そうです.サイドブレーキは後輪を止めるのですが,チェーンは前輪に巻かれています.考えてみればすごくあたりまえのことなのですが,そのときはそこまで考えられませんでした.あーあー.楽しいスキーのはずだったのに….不幸中の幸いと言いますか,誰も怪我をしませんでしたし,車はバンパーに傷が入っただけで修理が必要なほどのダメージはありませんでした.
皆さん(とくにこれからスキーに行かれる方)も雪道には気をつけてください.
|
|
|
●●バグは予測できない |
|
|
この失敗談から何を言いたいのかというと「マーフィーはどこにでもいる,誰も予測することはできない」ということです.簡単に言うと「失敗は誰にでもある」ということ.もっと言うと「失敗を楽しめたらいいね」ってことです(注1).
「絶対にバグはない.完璧だ」と言い切ることが難しいことは,プログラマであれば誰でも体験からわかると思うのです.おそらく,すべてのプログラムにバグがあるでしょう.バグとはプログラマの意図しなかった動きであり,同時にプログラマの創造力の限界でもあります.未来が予測できないのと同じように,創造力の限界の先は予測できないわけです.
ここでよく言われている重要な一言を書いておきます.
プログラムは思ったとおりに動くのではなく,書いたとおりに動く
です.プログラミングの真理をよく言い当てていると思うので,皆さんもよーく覚えておくと良いでしょう.
注1)「ポストイット」のはがせる接着剤が,本来強力な接着剤を作ろうとしてできた失敗作だったことは有名な話です.
|
|
|
●●ピサの斜塔 |
|
|
ここで簡単な実験をしてみましょう.未来を予測する実験です.ペンがあるといいのですが,ペンでなくても何か円筒形の形をした乾電池のようなものを1つ用意してください.そして,机の上かどこか平らな所にそのペンを立ててください.立てたら今度は,そのペンがどちらに倒れるか予測してみてください.予測が終わったら机を揺らしてペンを倒してみてください.机が揺れない場合は,第三者にお願いして倒してもらってください.さて,予想は当たったでしょうか.
多くの場合,予想は外れる思います.予想が当たった方は,もしかしたら予知能力があるのかもしれません.しかし,多くの場合は偶然だと思われます.
もちろん中には,まわりの状況や,机の動かし方,ペンの重心などの細かい情報を読みとり綿密に計算したうえで,予想を当てた方もいらっしゃるかもしれません.
では,「ピサの斜塔」のように傾いている塔の倒れる方向を予測するのはどうでしょうか.これなら多くの人が予測を当てることができると思います.
多くの場合,重心の影響でそのまま傾いている方向に倒れるからです.
ここで,何が言いたいのかというと,未来は予測できないのだけど,ピサの斜塔が倒れる方向は予測できるということです.同じように,バグは予測できないのだけど,バグが出てもいいようにあらかじめ傾けておくことはできるということです.
データ指向とは,この「あらかじめ傾けておく作業」に似ています.わかりやすさや可読性を追求したり,アクセス権限を設けたり,アクセス可能範囲を狭めたりとあれこれ手間をかけるのは,いつバグが出てもいいようにセーフティネットを張るようなイメージです.きちんと整理されて分類された本棚から,目的の本を探すようなものです.ぐちゃくちゃに散らかった部屋から本を探すのとは,労力が違うわけです.
「オレはプロだから,どこに何があるかすべて覚えている」というのは妄想であることに早く気付いてください.3年も4年も覚えていられるわけがないのですから.むしろ不必要な情報は早く忘れるべきです.早く忘れて,新しい情報を入れる脳内の空間を積極的に作るべきだと筆者は思います.
|
|
|
●●データ指向とは |
|
|
今回のテーマは「データ指向」です.あまり聞きなれない言葉であると思います.なぜなら,筆者が勝手にそう名づけてそう呼んでいる考え方だからです.データ指向とは,主に次の点に注意を置いた考え方です.
@ データのアクセス範囲を狭くする
A データのアクセス権限を厳しくする
B データの親子関係(全体の流れ)を意識する
C 上記すべてを用いて,可読性(わかりやすさ,読みやすさ)を追求しバグを減らし保守を楽にする
プログラミングの全体の言葉の中で,データ指向という言葉がどういう位置付けなのかを図1に示します.

簡単に言えば「プロシジャ指向」と反対側の位置付けになります.「アルゴリズム+データ構造=プログラミング」の式でたとえると,アルゴリズムがプロシジャ指向で,データ構造がデータ指向にあてはまると思います.
では,データ指向とはどういう考え方なのか,オブジェクト指向と何が違うのか,を料理にたとえて説明します.
|
|
|
●●データ指向とオブジェクト指向の違い |
|
|
●オブジェクト指向
まず,オブジェクト指向から説明します.一般に料理は食材を調理して作ります.まず食材を用意して,その食材に対して調理します.食材がなければ調理できないのでこの順番は変えられません.
オブジェクト指向とは,この食材と調理(注2)を1つにまとめる考え方です.屋台を連想してみてください.ラーメン屋,たこ焼き屋,お好み焼き屋など,それぞれの屋台は1つのオブジェクト(クラス)のように食材と調理人それに調理道具まで一式そろってまとまっています.お客は欲しい食べ物の屋台に行き注文するだけで食べ物(完成品)が手に入ります.
屋台の中でどのような食材がどのように調理されているのかは隠蔽されていて,お客は知る必要がないということなのです.この詳細を知らなくても良いということは,食材(データ)と調理人(機能)がまとまっていることによるメリットであると言えるでしょう.
このオブジェクト指向のメリットは「屋台に行って注文すれば食べ物が手に入る」という共通の手順(インターフェース)を持っているということです.
注2)調理人と考えるとわかりやすいでしょう.
●データ指向
データ指向とは,食材と調理のうち食材について優先的に意識しましょうという考え方です.より具体的には「食材は必要になってから用意し,必要のなくなった食材は素早く片付けましょう」という考え方です.たとえば,食べたいモノが決まる前に食材を用意するのはムダであるし,余計な食材があるために調理の邪魔になったり混乱を招いたりすることを危惧しているわけです.1週間分の食材を買ってきてから調理をはじめるのではなく,その時に食べる分を用意して,調理したらすぐに食べたほうが新鮮で美味しいものが食べられるし,食材のムダも省けるという考え方なのです.
一番のメリットは,食材が少ないので何がどこにあるのか把握しやすく,調理中に混乱しにくいことでしょう.バーゲンで安かったからという理由だけで必要以上に食材を買い貯めて,結局食べきれずに腐らせてしまったり,買っておいたはずの食材が実は切れていたりする失敗を避けましょうと言いたいのです.必要なものは直前に用意すべきということなのです.
もっと簡単に説明するなら,いくらお腹が空いているからといって「カレー」と「スパゲッティミートソース」を同時に作りはじめるのは無謀だということです.やはり別々に,順番に作るべきであると言いたいわけです.そのほうがわかりやすいし,ミスも入りにくいということです.
何をどう作るか,どう工夫したら最速で作れるか,などを考えるのではなくて,そのときに必要になる食材(データ)は何かを考えるわけです.安全確実な方法を考えるのです.それには,食材をできるかぎり直前に用意し,調理したあとはすぐに食べる(片づける)のです.新鮮な食材を手早く調理しアツアツのできたてを食べる.そんなイメージです.
あらゆる食材を一度に用意して,同時に作り始めてはダメです.場所が広いからとか,そのほうが早いからとか,自分はプロだからそんな失敗はしないとか,そんなふうに考えてはダメです.人間は必ずミスをします.ミスをしないと思うことがミスなのです.ミスをするという前提で行動すべきだと思うのです.そうです,未来を予測することは不可能なのです.
|
|
|
●●スパゲティか幕の内弁当か |
|
|
●スパゲッティ
「スパゲッティなコード」という言葉があります.最近はあまり聞かなくなりましたが.要するに,コードが複雑に絡み合っていて,追いかけることができない(メンテナンスができない)コードのことを示しています.構造化設計や,オブジェクト指向設計という概念が定着する以前は,このようなメンテナンスのできないコードは多かったようです.最近は,構造化設計や,オブジェクト指向設計の概念が定着してきたため「スパゲッティなコード」は見かけなくなりました(注3).
注3)オブジェクト指向言語を使えば必ずしもスパゲッティなコードから離れられるとは限りませんが.
●幕の内弁当
筆者にとってメンテナンスしやすい(バグの取りやすい)コードとは,やはり「幕の内弁当」のように開けた瞬間に,何がどこにあるのかわかるように,きちんと見やすく分けられているものです.分けて盛り付けるのはそれなりに手間のかかる作業ですが,見た目がきれいでわかりやすいというのは大きなメリットです.
なぜなら自分の好きな食材から自由に選んで食べることができるのですから.「玉ねぎが嫌い」などと偏食の多い人にはスパゲッティは食べにくいのでしょうね.もっとも,偏食のない筆者にはあまり関係のない話ですけれど.
また,食べ物の味がほかの食品に移らないというメリットもあるでしょう.あるコードがほかのコードに影響を与えにくいということです.
世界中を捜しても,日本の幕の内弁当ほど見た目を重視した食べ物はないのではないでしょうか.きっと日本人はモノをきちんと整理することが上手なのでしょう.もし,あなたが日本人であるなら「幕の内弁当なコード」を書くことが上手であると思い
●センスも必要
しかし,これは最終的にはセンスの問題なのかもしれません.構造化設計さえすればキレイになるとは思えないのです.goto文を排除することが必ずしもメンテナンスしやすいコードになるとも限りませんし,現にgoto文で書いたほうがメンテナンスしやすいコードも存在します.
また,オブジェクト指向を使えば必ずしも整頓されたバグの取りやすいコードになるのかと聞かれたら,この答えも「ノー」です.「やはりセンスは必要」と答えてしまうでしょう.データ指向的なセンスを磨くべきとも言えるでしょう.
経験と言ってしまえば簡単ですが,ある一定のルールが存在して,それを守りさえすれば良いということではないと思うのです.周りの環境や状況が変化してしまうことで,いままで正しいと思われてきたルールも変化してしまうだろうし,すべてのケースが必ずあらかじめルールに想定されたケースに収まるとは限らないとも言いたいわけです.
|
|
|
●●データ指向と変数名 |
|
|
●変数名の長さに注目
データ指向では変数名の長さにもこだわります.単純に長い変数名は間違えにくいと思うのです.iとjの違いは間違えやすいけれど,abcとxyzは間違えにくいと思うのです.間違えにくければ,バグも入りにくいであろうという単純な考え方です.たとえば,パスワードなどの文字列は,単純に長いほうが破られにくいという考え方と同じです.長さによって隠蔽度を上げるという考え方です.
プログラミング言語などの書籍や雑誌などに載っているサンプルコードを見てよく思うのは,変数名が短いということです.それはやはり本や雑誌のスペースの関係で,長い変数名で書くとすぐに改行が入ってしまい,余計に見難くなってしまうので,しかたなく短い変数名で記述しているところがあると思うのです.中には気にせず1文字の変数を使っているコードも見かけますが,これはやはりサンプルコードという短く限定された特別なコードであるから許されていると思うのです.
実際のコードを書くときにも書籍のサンプルを真似て1文字の変数を使ったりすると,保守のしにくいバグの取りにくい可読性の悪いコードになってしまいます.変数名の付け方に関しても慎重に選ぶべきであると思います.
●変数名の長さはスコープの範囲で
筆者の変数名の長さの基準は,アクセス可能なスコープの範囲に比例して変数名も長くすべきであり,逆にアクセス可能な範囲が狭い変数名に関しては,あえて長い名前を付けるべきではないと思っています.次のようなイメージです.
int aaaaaaa ; // グローバル変数
static int bbbbb ; // ファイルローカルな変数
int main()
{
int ccc ; // 関数ローカルな変数
{
int d ; // ローカル変数
}
}
可読性のためにとにかく長い変数名を付けろ!というのではなく,アクセス範囲の広さに優先度を置き,すべてをローカルな変数にして短い変数名のみで構成できれば,それがベストであると思うのです.できる限りアクセス範囲の狭い変数を使い,できる限りアクセス権限の厳しい変数を使ってください.C++であればconstを使った更新不可である変数の宣言もバグに対して有効だと思います.
よく「スカートとスピーチは短いほうが良い」という話を聞きますが「スコープ」も短いほうが良いのです.
|
|
|
●●データ指向の実践 |
|
|
データ指向のメリットは「わかりやすいのでミスをしにくい」もしくは「可読性が高いのでミスをしにくい」と言えると思います.
●実際のコード
では,実際のコードではどのように書くのでしょうか.具体的に見てみましょう.たとえば,面積を計算して求めるプログラムを書くとします.何も考えずに簡単に書いてしまうとこうなるでしょう(リスト1).
| リスト1 面積の計算 |
int main()
{
printf( "area A :%d\n", 3 * 4 ) ;
printf( "area B :%d\n", 5 * 2 ) ;
return 0 ;
}
|
この状態では数値にどんな意味があるのかよくわかりません.長方形の面積の計算のようですが,どっちが縦でどっちが横なのでしょうか?もう少し保守のことを考えて,変数を使いデータに名前(意味)を持たせてみます(リスト2).
| リスト2 変数名を変更 |
int main()
{
int a_width = 3 ;
int a_height = 4 ;
int b_width = 5 ;
int b_height = 2 ;
printf( "area A :%d\n", a_width * a_height ) ;
printf( "area B :%d\n", b_width * b_height ) ;
return 0 ;
}
|
こうなると,数値の意味が理解できるようになります.そして,機能がまとめられることに気が付いた人もいるでしょう.では,機能でまとめてみましょう(リスト3).
| リスト3 変数を機能でまとめる |
int area( int _width, int _height )
{
return _width * _height ;
}
int main()
{
printf( "area A :%d\n", area( 3, 4 ) ) ;
printf( "area B :%d\n", area( 5, 2 ) ) ;
return 0 ;
}
|
わかりやすくなりましたか?筆者としては,ここで「はて?」と思ってほしいわけです.リスト2とリスト3を比べてみて,どちらがわかりやすいか,保守しやすいか,をもう一度悩んでみてほしいのです.
いよいよここで,データ指向の登場です.データ指向はデータの流れに着目した考え方です.機能よりも先にデータの流れ注4について優先して考えます.リスト2に対してデータ指向を適用するとこうなります(リスト4).
| リスト4 データ指向の適用 |
int main()
{
{
int a_width = 3 ;
int a_height = 4 ;
printf( "area A :%d\n", a_width * a_height ) ;
}
{
int b_width = 5 ;
int b_height = 2 ;
printf( "area B :%d\n", b_width * b_height ) ;
}
return 0 ;
} |
●比較してみよう
まず,リスト2とリスト4を比べてみてください.
何が違うのでしょうか.違いはデータのアクセス可能範囲です.a_widthのアクセス可能範囲が狭くなっています.これがデータ指向の考え方の1つです.
アクセス可能範囲のことを「スコープ」とか「ブロック」と呼びます.ここではスコープと呼ぶことにします.C言語でのスコープは“{”と“}”で囲まれた範囲になります.
料理のたとえで説明すると,お米を用意→スパゲティを用意→ご飯を炊く→スパゲティを茹でる→カレー完成→スパゲティ完成→食べるではなくてお米を用意→ご飯を炊く→カレー完成→食べるスパゲティを用意→スパゲティを茹でる→スパゲティ完成食べるとするわけです.
●機能を追加した場合
では,ここで機能追加をしてみます.リスト3,リスト4それぞれに同じ機能「総面積も表示する」を追加します.リスト3への追加結果はリスト5になります.そして,リスト4への追加結果はリスト6になります.
| リスト5 リスト3 に総面積表示機能を追加 |
int area( int _width, int _height )
{
return _width * _height ;
}
int main()
{
printf( "area A :%d\n", area( 3, 4 ) ) ;
printf( "area B :%d\n", area( 5, 2 ) ) ;
printf( "all area :%d\n", area( 3, 4 ) + area( 5, 2 ) ) ;
return 0 ;
} |
| リスト6 リスト4 に総面積表示機能を追加 |
int main()
{
int all_area = 0 ;//総面積
{
int a_width = 3 ;
int a_height = 4 ;
printf( "area A :%d\n", a_width * a_height ) ;
all_area += a_width * a_height ;
}
{
int b_width = 5 ;
int b_height = 2 ;
printf( "area B :%d\n", b_width * b_height ) ;
all_area += b_width * b_height ;
}
printf( "all area :%d\n", all_area ) ;
return 0 ;
}
|
どうでしょう.何を感じますか?リスト5より,リスト6のほうが長くて保守しにくいように感じまでは,もし,area
Aの横のサイズが3から1に変更になった場合,リスト5,リスト6ではそれぞれ何ヵ所の修正が必要でしょうか?また,リスト5,リスト6のどちらがオブジェクト(クラス)を見つけやすいでしょうか?
●つまり…
ここで言いたいことは,なんでもすぐに関数として切り出すよりも,もう少し全体の流れというかデータの流れに着目してみてほしいわけです.少しおおげさに言えば「マクロな視点を持ってほしい」のです.ある程度のデータの流れが見えたところで関数化したり,オブジェクト化しても遅くはないと思うわけです.TOC(制約理論)の言葉を借りるなら,「部分最適化よりも全体最適化を優先しろ!」となまず,データの流れに注目してみてください.そして,データの親子関係,どのデータがよりローカルで,どのデータがよりグローバルなのか,といった点に着目してほしいわけです.
さらに,アクセス範囲(スコープ)を短くできないか,アクセス権限を厳しくできないか,をつねに心掛けてほしいのです.変数宣言の位置を変えたり,変数名の長さを変えたり,グローバルからローカルへ変えたり,オブジェクトのメンバにしたり,パブリックなメンバからプライベートなメンバにしたり,できることはたくさんあると思います.
これらのデータ指向の考え方を少しずつ習慣づけることで,バグは確実に減らせると筆者は思っています.最初にお話した雪道での事故にたとえるなら,無茶をするなとか,自分の力を過信するなとか,「だろう運転」をしないとか,余裕をもって行動しろとか,もっと言うとハンドルから手を離すなとか,前を見て運転しろなどと,今まで散々言われてきたことだったりします.
|
|
|
●●GUIの話 |
|
|
皆さんは家で料理をするときに,どちらの行動パターンをとりますか?
@ 先に食べたい物を決めてから冷蔵庫を見て,足りない食材を買いに行く
A 先に冷蔵庫の中身を見て,今ある食材でできる食べ物に決める
おそらく多くの人がAであると思います.先に冷蔵庫内の現状を知りたいと思うわけです.ほかにも銀行のATMでお金を引き出すとき,引き出す前に残金を確認しますよね.記憶力の良い人なら覚えているとは思いますが,それでも頭の中で現在の残金を先に計算するはずです.
では,こんなGUIの体験はありませんか?画面に「ファイル名を入力してください」というメッセージが出て,ファイル名を入力するのですが,よくわからずに適当に入れて「OK」ボタンを押す….
すると「ファイルが存在しません.入力しなおしてください」というメッセージが出る.入力しなおせと言われても,そのファイル名がわからない….
何?どうすればいいの?と途方にくれる.しかたなく「キャンセル」する.
こんな経験で,パソコンが嫌いになる人も多いようですが,この場合パソコンが悪いのではなく,このプログラムを作ったプログラマが悪いのです.ファイル名を入力させる場合は,存在するファイル一覧を最初に出すべきでしょう.その一覧の中から選んでもらうのが本来の流れだと思うのです.
ここで,プログラマには「機能が先にあるのではなくてデータが先にある」ということに気が付いてほしいのです.説明が難しいところなのですが,プログラミング作業は機能を追求することであるといった機能主体の考え方が定着してしまっている感じがするのです.そのようなアンバランスな状態に対して「もっとデータに対しても注目しましょう」と言いたいわけで,データ指向が注目されることでプログラミングのバランスが取れるのでは,とも思っているわけです.
|
|
|
●●最後に |
|
|
オブジェクト指向やデザインパターンなどの新しい(高度な?)技術も大事だと思いますが,もう少し足元にある基本的な考え方に対しても光を当ててみてほしいと思います.
次回は,アジャイル開発について書いてみたいと思います.お楽しみに.最後に.
|
|
|
●コラム 傾ける |
|
|
次ようなwhile文をときどき見かけます.
int i = 0 ;
while ( i != 10 )
{
...
++i ;
}
|
たしかにこのコードは正しく動きます.ですが,
int i = 0 ;
while ( i != 10 )
{
...
if ( foo()==false ) ++i ; // 1 行追加
++i ;
}
|
という変更が起こっても,このコードは正しく動くでしょうか?そんな変更を予測して傾けておきます.
int i = 0 ;
while ( i < 10 ) // ここを傾けた
{
...
if ( foo()==false ) ++i ;
++i ;
}
|
このように傾けておけば,iが10を一気に越えても正しく動作しますね.
このほかにも,引数をassert()でチェックしたり,例外を投げるコードを書くこともできます.そのようなちょっとした気配りでバグの発生を少なく抑えられたりするものです.
|
|
|
●コラム デストラクタ |
|
|
「明日の約束を忘れないでね.」「え?明日の約束って何?」これと同じように,コーディングをするときにはたくさんの約束をまもらないといけません.
・オープンしたら,クローズしてください.
・クリエイトしたら,デストロイしてください.
・アロケートしたら,フリーしてください.
・コネクトしたら,ディスコネクトしてください.
しかし,この約束を忘れずに実行できるでしょうか?人間は忘れる生物です.とくに自分に都合の悪いことは優先して忘れるしくみになってるようです.
C++のクラスには,コンストラクタとデストラクタという機能があります.クラスのインスタンスが宣言されたときにコンストラクタが実行され,インスタンスがスコープ(有効範囲)から抜けて不要になったときに自動的にデストラクタが呼ばれるのです.
よって,コンストラクタでオープン処理をして,デストラクタでクローズ処理をしておくことで,クローズ処理を実装しなくても,不要になった時点で(少なくともプログラムが終了する前に)デストラクタが呼び出されてクローズ処理が走ります.
この機能を使うと,うっかり眠ってしまっても小人の靴屋さんがしっかり後片付けをしてくれます.とっても便利な機能なんです.
|
|