プログラミングのセオリー(2)

プログラミングのセオリー

プログラミングのセオリー


どうしても我慢できないところとどうでもいいところをごっちゃまぜにつついてみる。
きむら(K)さんが指摘した部分とはかぶってないはず。たぶん。
以下無駄に長いので注意。

命名規約

もしも「この命名規約では嫌だ」と言う人がいたら、リーダーが説得して命名規約を守らせるようにするべきです。
「プロなのだから自分勝手はいけない。チーム内の取り決めを受け入れられる柔軟性がなければプロとして失格である。
あなたしか作れないような天才的なプログラムを作れるというなら話は別だが、そうでないならルールを守って欲しい」と言い含めて、聞き分けのないプログラマを説得してください。

こんな「説得」、いや、「押しつけ」で納得する人なんているんだろうか。
こういう場合は「なぜその規約が嫌なのか」をはっきりさせておくことが最低限必要だと思うけどなぁ。
その結果どうなるか、ってのは別の話だし。

日本人だけのチームであれば、日本語の名前やgetKyuyo(get+給与)のように、英語と日本語が混在した名前でも構いません。

個人的な感覚だけど、ローマ字表記にするならそれが明確になるように工夫して欲しいんだよなー。
getXxxのXxxの部分が全部ローマ字表記だったらいいんだけど、絶対そんなことはない。入り交じってる。
今のチームに提案して採用された規約で、ローマ字部分の前にJP_を付ける、と言うものがある。
上の例では「getJP_kyuyo」となる。

多くのプログラミング言語で採用されているのが、定数の名前をすべて大文字にすることです。

.NET Frameworkは違いますよね。String.Emptyやら、int.MaxValueやら。
この本はVB.NETも使ってるんだから、その点は補足しとくべきじゃないかな。

複合語を区切るCamel形式とPascal形式

全部小文字にしてアンダーバー区切りは?

最近ではほとんど使われませんが、「ハンガリアン記法」と呼ばれる命名規約がありました。
マイクロソフトのプログラミング部門のリーダーであるチャールズ・シモニーが好んで使った命名規約です。


ハンガリアン記法では、変数名の先頭に、変数のデータ型を示す略語を付けます。

間違ったコードは間違って見えるようにする - The Joel on Software Translation Projectを読めばいいと思うよ。

本書では、ソースコードの表記を統一してわかりやすくするために、プログラミング言語の種類に関わらず、多くの場合にJavaの命名規約を採用しています。

このChapterの最初の方で「プログラミング言語には名前付けの慣例がある」って言ってたのにこれですか・・・
しかも多くの場合ってなんだよ・・・
それより、複数言語使ってる意味がわかんない。CならCに統一した方が絶対わかりやすくなるでしょ。この本ほとんどCのコードばっかだし。

ソースコードの書式

なんかこれと同じにおいがしないでもないな。
命名規約は「慣例がある」とか言ってたのに、スタイルに関してはうるさく言わないのは片手落ちじゃないかな。

どんなプログラムにも最低限必要なコメントがあります。
それは、作者の名前です。 〜略〜

さらに、プログラムのタイトル、レビジョン、最終更新日時があれば、よりよいでしょう。

最終更新日時はバージョン管理システムに任せたいところ。
ここで言ってるレビジョンは、バージョン管理システムが採番するものじゃないからまぁ許す。
そういえば、バージョン管理システムやらバグ追跡システムやら、プログラムそのものとは関係ない話は全くないな。

コメントの書き方、3つのテクニック

〜略〜
1つの目アイディアは、ソースコードの先頭に、ドキュメントのようにまとめてコメントを書くと言うことです。

否定はしないけど、doxygenの様なツールの存在には全く触れないのね。

3つ目のアイディアは、for文やif文の閉じカッコ}の後ろに、それが何の閉じカッコかをコメントで書くことです。
〜略〜

for (i = 0; i < MAX; i++)
{
    if (a > b)
    {
        // 何らかの処理
    } // end of if
} // end of for

これ、本当に読みやすいと思ってるのかな?
こうしないと読みにくいってことは、Python否定してる感じが。
そういえば一行コメントなんて書いてるけど、C99には見えないなー。

ハードコーディング

変更の可能性があるデータを1つのファイルにまとめておく

こんな事言ってる人がいるからこんなことになるんですよ。
なんでもかんでも1つにまとめようとしないでくれよ。
ちゃんと分類してくれおねがいだから。

特定の環境だけで動作するプログラムも、ハードコーディングの一種だと言えるでしょう。
〜略〜
特定の種類のDBMSでないと動作しないようなプログラムです。
このようなプログラムは、「行儀の悪いプログラム」とも呼ばれます。
さまざまな環境で動作するプログラムを「行儀のよいプログラム」と呼ぶので、その反意語です。

それはハードコーディングが問題の場合もあるけど、「一種」は言い過ぎじゃないか?
それより、特定の環境だけで動作するプログラムを「行儀の悪いプログラム」だなんてどこで言われてるんだ?
行儀が悪いプログラムというと、メモリリークしてるだとか、リソース解放してないだとか、そんなプログラムが真っ先に思いつくけどなぁ。

トリッキーなコード

プロならどちらを選ぶか迷うことはない

迷わず、標準ライブラリの関数を使いますね。なんだよmyStrlen関数って。トリッキーかどうか以前の問題だよそれは。

リスト3 breakを省略したswitch文【C言語

switch (a)
{
    case 1:
        printf("apple\n");
        break;
    case 2:
        printf("orange\n");
------------------------------ここで裏のページに改ページ
    case 3:
        printf("banana\n");
        break;
    default:
        printf("lemon\n");
        break;
}

これは矢沢さんがどうのって問題じゃない気もするけど、一瞬どこにbreakが抜けてるのかわからなかったw

?:のことを「三項演算子」または「条件演算子」と呼びます。

お、おしい*1・・・

恐怖の一本ソース

1つの関数の処理があまりにも長いものを、俗に「恐怖の一本ソース」と呼びます。

呼びます・・・?呼んでいますじゃなくて?

もしも、誰かのソースコードを引き継ぐとしたら、1つの関数の処理の長さはどのくらいまで我慢できますか。
きっと「できればプリンタ用紙2枚まで、長くても3枚ぐらいにしてほしい」という答えになるのではないでしょうか。

そういう答えにはならないのではないでしょうか。

データ型

C言語オブジェクト指向プログラミングの機能を追加したプログラミング言語C++です。

それだけじゃねーよ。

浮動小数点数の誤差

誤差が生じないという点では、固定小数点数にメリットがあります。

マテ、固定小数点数といえど、誤差は出るぞ。浮動小数点数表現よりも起こりうる誤差の種類が少ないってだけで。

2人のプログラムを結合する際に、変数aとbを比較する部分があったとしたらどうなるでしょう。
この場合は、異なる値だと判断されてしまいます。これは、リスト4に示すプログラムで確認できます。
〜略〜
この問題の対策は、データ型を統一することです。
〜略〜

〜略〜
double a = 0.1;
float b = 0.1F;

if (a == b)
{
〜略〜

いやいやいや、データ型を統一するのはもちろん大切だけど、浮動小数点数同士を一点比較するのはどうなのよ。
せめてaとbの差を丸めて0と比較するとかやろうよ。

変数の初期化

ここだけの話じゃないけど、矢沢さんは戻り値用の変数を用意して、関数の最後でreturnするスタイルが好きみたい。
個人的にはばんばん途中でreturnする派*2で、この本のやり方は好きじゃない。
だって、インデントを必要以上に深くしたくないし。

変数を適切に初期化することで、ソースコードを短く記述できる場合があります。
例をお見せしましょう。リスト3は、うるう年を判定する関数です。
〜略〜
現状では、else文で戻り値を0としていますが、変数ansの初期値を0にすればelse文が不要となり、ソースコードを短く記述できます(リスト4)。

int isLeapYear(int year)
{
    int ans = 0;    // 変数を初期化する
    
    if (year % 400 == 0)
    {
        ans = -1;
    }
    else if ((year % 4 == 0) && (year % 100 != 0))
    {
        ans = -1;
    }
    
    return ans;
}

標準ライブラリを適切に使用することで、ソースコードを短く記述できる場合があります。

// Java
public static boolean isLeapYear(int year) {
    return new GregorianCalendar(year, 0, 1).isLeapYear();
}
' VB.NET
Function IsLeapYear(ByVal year As Integer) As Boolean
    Return DateTime.IsLeapYear(year)
End Function

元の例はC言語だけど、これパソコン甲子園の問題であったから、パソコン甲子園で使えるJavaVB.NETで書いてみた。
VB.NETなんてもはや関数化する意味なし。


というか、いちいちansに入れなければ元のプログラムも十分短くなるのにね。

int isLeapYear(int year)
{
    if (year % 400 == 0) return -1;
    if ((year % 4 == 0) && (year % 100 != 0)) return -1;
    return 0;
}

#defineとconst

定数として値を持つということに関して、#defineとconstに違いはありません。

C言語ではconstでは配列の要素数として指定できなかったんじゃなかったっけ?
実は定数じゃなくて読み取り専用変数だとかで。

マクロを定義するときの注意点

異常なほどの括弧の事を注意してるんだけど、++やらの話がないな。
あくまで定義するときの注意点。

演算子の優先順位

四則演算以外はカッコで囲むのがわかりやすい

それはやり過ぎ。(a)[i]とかさすがにない。

カッコで囲んでも思いどおりの結果にならないことがある

それでは、b = a++;をb = (a++);と書いたらどうなるでしょう。
変数aのインクリメントが優先されて、変数bに101が代入されると考えるのではないでしょうか。

・・・。

0除算とオーバーフロー

0除算だけじゃなくて、0による剰余のこともたまには思い出してあげてくださいね。

goto文

マシン語アセンブリ言語のレベルでは、プログラムの流れの制御はジャンプ命令によって実現されているのです。
この事実を知れば、やみくもに「goto文を使ってはいけない」と思い込んでいるプログラマも、少しは考えが変わるでしょう。

マシン語アセンブリ言語のレベルでは、マジックナンバーは堂々と使われているのです。
この事実を知れば、やみくもに「マジックナンバーを使ってはいけない」と思い込んでいるプログラマも、少しは考えが・・・変わるわけねーだろ。
ちょっとぶっ飛びすぎな考え方が多いな。

状態遷移

図1は、ヘビかどうかを判定する状態遷移図です。

状態遷移の考え方は便利だしいろいろ使えるけど、こんな問題は正規表現使えという話でしかないだろ。
ちなみにこの本のヘビの判定は、.*>=+-.*とかそんな感じで可能。

引数の数を減らす

summary

  • 引数の数が多い関数が使いにくく、バグを生む可能性がある
  • グローバル変数にデフォルト値を持たせれば、引数を減らすことができる
  • 引数を減らす手段として他に、メソッドのオーバーロード、デフォルト引数、ラッパ関数がある

引数を構造体かなにかにする、ってのはなしですか。
グローバル変数なんかよりよっぽど使われてる方法だと思うんだけどなぁ。

再帰呼び出し

summary

〜略〜

今まで一緒に仕事してきた人たちは再帰呼び出し読みにくい、って人が多いけどなぁ。
実行速度云々は、末尾最適化がされるかどうかによっても違うから、遅いと言い切ってしまうのはちょっとなぁ。
あと、スタックオーバーフローに関しても、末尾最適化されるなら問題ないし、そもそも階乗だとかの「オモチャ」でしかない例じゃなくて、実際に使われるような処理でスタック食いつぶすほど呼び出し階層が深くなったりしないような。

関数の構造

int checkNumber(int a)
〜略〜

checkとかやめてくれ。
JavaだとMath.signum*3、.NETだとMath.Signで、どっちもそのまま。
このくらい素直な名前にしてくれればわかりやすいのに。

もしも、100行の関数の中に5つreturn文があったら、処理の流れを追うことを苦痛に感じるはずです。

100行の関数をどうにかしろよ、というのは置いとくとして。
もし均等に並んでいたとして、20行ごとにreturnなら結構読みやすいかもしれないなぁ。
流れって言うのが、フローチャートのような図にした流れなら確かに追いにくくなるかもしれないけど、戻り値用の変数を用意する方法だと、プログラムのコードを直接追うときにいちいちいろいろな状態を考慮する必要が出てくるから、逆に追いにくいと思うし。

どのようにすれば、モジュール間結合度を弱くできるのでしょう。
目安として、表1に示すモジュール間結合の分類があります。

結合度 分類 モジュール間の結合
弱い データ結合 引数でデータの値を渡す
スタンプ結合 引数でデータの構造を渡す
制御結合 引数で制御構造を渡す
外部結合 グローバル変数でデータの値を渡す
共有結合 グローバル変数でデータの構造を渡す
強い 内部結合 処理の一部だけを呼び出す

懐かしいなー、この分類。もはや目安にすらならないんじゃないかな。
同じように、凝集度*4の表もね。

リファクタリング

ソースコードの内容を書き直すわけですから、「そんなことをして動作がおかしくなってしまったら困る」と思う人が多いでしょう。
書き直しをすれば、再度テストする必要もあります。
それを時間の無駄だと感じるでしょう。
そうであっても、リファクタリングした方が最終的によい結果となることを、私たちプログラマは長年の経験から学んだのです。
〜略〜
リファクタリングを実践するのに必要なのは、「勇気」と「余裕」です。

これはひどい・・・
リファクタリング―プログラムの体質改善テクニック (Object Technology Series)によると、

リファクタリングを開始するとき、最初にすることは常に同じです。
対象となるコードについてきちんとしたテスト群を作り上げることです。
リファクタリングは非常に秩序だっていて、新たなバグを生み出しにくくなっていますが、人間が作業する以上、間違いを犯す可能性があります。
このためテストは大切で、きちっとした一連のテストを用意するべきなのです。

リファクタリング 第一章 リファクタリング―最初の例 リファクタリングの第一歩

98ページにはこんな記述もありますね。

不完全なテストでも、書いて実行する方が、実行できない完全なテストよりもましだ。

リファクタリング 第4章 テストの構築 テストの追加


リファクタリングに必要なのは勇気と余裕」とか言っちゃってますけど、勇気と無謀は違いますよ?
リファクタリングに必要なのは「自動化されたテスト群」、あるといいのが「IDEによるサポート」、かな。


動作がおかしくー、だとか、再度テストする必要がー、だとか言ってるような人は、テストしやすい設計が出来ていない人だと思うな。
それか、テストの自動化なんて出来ると思ってないとか、トラウマがあるとか。

インターフェイス

public static int getAverage(IntIterator obj)
{
    int sum = 0;
    while (obj.hasNext() == true)
    {
        sum += obj.next();
    }
    return sum;
}

get・・・Average?

*1:ちなみにここ以外ではすべて三項演算子で統一・・・

*2:関数の最初のあたりでifでフィルタリングするようなやり方が多い。forの中でも平気でreturnするけど、頻度としては前者の方が圧倒的に多いな

*3:intを引数にとるものはないっぽい

*4:本ではモジュール強度