「良いプログラムコード」を書くには
●参考書籍、記事
0. はじめに
この記事では、参考書籍や記事を読みつつ、私がプログラミングを学び、実際にアプリなどを製作してきた中で、本当に大切だと感じた事を書いていきたいと思う。
また、本記事の中で登場するコードは、基本的にC++によって記述される。
1. 「良いプログラムコード」とは、どんなものか?
結論から先に書くと、「書いた本人以外が見ても、スッと理解できるもの」こそが、良いプログラムコードである。
例えば、初心者が書きそうな、以下のようなコードはどうだろうか?
int main()
{
string s;
std::cout << "あなたの名前を入力してください:";
std::cin >> s;
std::cout << "あなたの名前は [ " << s << " ] ですね" << std::endl;
return 0;
}
ユーザーの名前を入力させる、単純なコードである。
これだけ短く、簡単なコードならすぐに理解可能だが、業務や製作としての観点から見ると、問題点がある。
string s;
入力されたユーザー名の扱われそうな方法としては、データーベースに登録したり、ゲームのリザルト画面に出したり、など色々あると思う。
そんなとき、ユーザー名が「s」の一文字で定義されていたらどうだろうか?
また、ユーザー名以外にも様々なデータやステータスを設定するだろうと思うが、それらが数文字で定義されていたら?
後から見たとき、書いた人以外が見たとき、分からないのは当然である。
2. 良い名前の付け方
さて、ユーザー名をsだとかの数文字で定義するのは良くない、ということは分かっていただけたと思う。
では、次のような変数の名前ならどうだろうか?
string input;
パッと見、さっきのsよりは良さそうではある。
入力されたデータかな?と感じるだろう。
しかしそれが、数値かもしれないし、文字列かもしれない。
また、整数値か、小数値か...。
名前を単語にすることで、ある程度の意味を伝えることには成功したが、何のデータなのか、どういうデータなのか、そういうことは不明瞭だ。
もしも、「入力されたユーザー名」というデータに名前を付けるなら、
string input_user_name;
素直に、入力されたユーザー名、という英単語にすればいい。
●関数(メソッド)やクラスの名前
変数名と同様、関数やクラスの名前も意味がわかりやすく定義するべきである。
クラス名について、抽象クラスの名前は抽象的でもいいと思う。
抽象クラス:GameObject, Manager, Singleton, ...etc
具象クラス:Player, (EnemyName), ...etc
抽象化、継承を覚え、数が増えてきたオブジェクトを管理したくなってくる中級者くらいになってくると、「〇〇Manager」というクラスを作りがち。
3. 関数化、クラス化する
#include <random>
int main()
{
std::mt19937 mt; // メルセンヌ・ツイスタの32bit版
std::random_device rnd; // 非決定的な乱数生成器
mt.seed(rnd()); // シード値を指定
int num_arr[10];
int arr_size = 10;
// ランダムな数値0~9を代入
for (int index = 0; index < arr_size; index++)
{
num_arr[index] = mt() / 10;
}
// 配列の中身を表示
for (int index = 0; index < arr_size; index++)
{
std::cout << num_arr[index] << ", ";
}
return 0;
}
ランダムな値(0~9)を配列に代入し、その中身を出力する、というプログラムである。
ここで注目したいのは、
・ランダムな数値の生成
・ランダムな数値を配列に代入する
・配列の中身を表示/出力する
という、主に3つの工程が行われていることだ。
今回はこれだけだが、もしも配列が複数あったり、配列ではない通常の変数にランダムな値を代入したかったり、...様々な状況が想定できるだろう。
そんなとき、毎回ランダムな数値を生成するためのコードを書いたり、というのは面倒くさいし非効率的だ。
そのために、一部を関数化して、別のプロジェクトで使いたいときに流用できるようにしておくことも、プログラムコードの書き手にとって必要だ。
また、書いた本人以外が見たとき、分かりやすい名前の関数にまとめてあると、理解もしやすいだろう。
#include <random>
#include <iostream>
// ランダムな数値を生成するためのクラス
class Random
{
private:
std::mt19937 __mt; // メルセンヌ・ツイスタの32bit版
std::random_device __rnd; // 非決定的な乱数生成器
public:
// コンストラクタ
Random()
{
mt.seed(rnd()); // シード値を指定
}
// min~maxのランダムな数値生成
int Range(int min, int max)
{
int result = (mt() / ((max + 1) - min)) + min;
return result;
}
}
// 配列をランダムな数値で埋める
void SetArrayToRandomNumber(int arr[], int arr_size)
{
Random random;
for (int index = 0; index < arr_size; index++)
{
arr[index] = random.Range(0, arr_size);
}
}
// 配列の中身を表示する
void ShowArray(int arr[], int arr_size)
{
for (int index = 0; index < arr_size; index++)
{
std::cout << num_arr[index] << ", ";
}
}
int main()
{
int number_array[20];
int array_size = 20;
SetArrayToRandomNumber(number_array, array_size);
ShowArray(number_array, array_size);
return 0;
}
main関数内はかなりスッキリした。
また、プログラムの順番もわかりやすくなった。
①整数値の配列を準備
②配列の中身をランダムな数値で設定
③配列の中身を表示
4. コメントを適所に書く / 不必要なコメントを減らす
3. のプログラムコードを再び利用する。
#include <random>
int main()
{
std::mt19937 mt; // メルセンヌ・ツイスタの32bit版
std::random_device rnd; // 非決定的な乱数生成器
mt.seed(rnd()); // シード値を指定
int num_arr[10];
int arr_size = 10;
// ランダムな数値0~9を代入
for (int index = 0; index < arr_size; index++)
{
num_arr[index] = mt() / 10;
}
// 配列の中身を表示
for (int index = 0; index < arr_size; index++)
{
std::cout << num_arr[index] << ", ";
}
return 0;
}
ランダムな数値を生成するのに必要なもの(ここでは mt と rnd)のコメントは、あっても無くても良い。(あった方が、より分かりやすくなる)
(まとめて // ランダム生成器 とかコメントで括っても良いと思う)
num_arrは数値の配列、arr_sizeは配列の大きさ、と意味が分かる。
意味が分かりやすい名前を付け、コメントが無くてもわかりやすいコードを書こう。
// ランダムな数値を代入
// 配列の中身を表示
のように、コメントを書いた後に、複数行に渡る一定の処理を行うコードを書いた場合、それは基本的に関数化するべきだ、というサインだと思う。
また、まだ実装していない処理や関数などがある場合は、
// TODO : やること/処理の内容
のように書いておくと、後で作業するときに分かりやすい。
(その部分を作ったら、TODOコメントは消す)
5. スコープ
スコープとは、変数やオブジェクトの有効範囲である。
#include <iostream>
int A = 10;
int main()
{
int B = 200;
if(A < B)
{
int temp = A;
A = B;
B = temp;
}
return 0;
}
例えばこのようなプログラムがあるとしよう。
変数A
mainなどの関数の外、グローバルな範囲で定義してある。
これを、グローバル変数といい、どこからでもアクセスができる。
どこからでもアクセスできるということは、中身をどこでも書き換えられるため、便利な反面、バグを生みやすい。
また、業務用システムやアプリでこれをやると、外部から書き換えられる可能性がある=セキュリティ的脆弱性にもなるので、気をつけよう。
変数B
main関数の中で定義してある。
このように、関数の中で定義されているものは、関数外からアクセスすることは、基本的にはできない。(ポインタや参照を利用すると可能)
グローバル変数ではない変数全般を、ローカル変数という。
変数temp
main関数内の、さらにifの{}カッコ内で定義されている。
ここで重要になってくるのが、変数の寿命(後述)である。
●変数の寿命
変数が生まれてから死ぬまで...である。
・グローバル変数
そのアプリが起動したと同時に生成&初期化され、アプリが終了する際に削除される。死ぬ。
まあ、グローバル変数は基本使わないので、覚えなくていい。
グローバル変数を使わなくてはいけない状況になってしまったら、設計を見直すことをおすすめする。
・ローカル変数
{}カッコ内で定義されている変数全般。
定義:生成され、カッコの終わり「}」で削除される。
削除された後にアクセスを試みる(ポインタや参照)と、不具合やバグ、エラーになる。(有効ではなくなるため)
・メンバ変数
クラス内で定義されている変数のこと。
クラス生成時に一緒に生成される。
そして、クラスが削除されるとき、一緒に削除される。
6. コードの集約
class Name
{
private:
char* first = nullptr;
char* last = nullptr;
public:
char* getFirstName()
{
return first;
}
char* getLastName()
{
return last;
}
~ 省略:それぞれのSetNameがある ~
}
int main()
{
Name name;
name.setFirstName("poop");
if(name.getFirstName() == nullptr)
{
// エラー
}
if(name.getLastName() == nullptr)
{
// エラー
}
return 0;
}
Nameクラスの名前のヌルチェックが2回行われています。
同じような処理は、集約:関数化しましょう。
bool IsEmpty(char* value)
{
if(value == nullptr)
return true;
return false;
}
文字列のヌルチェック用関数を作ることができます。
int main()
{
Name name;
name.setFirstName("poop");
if(IsEmpty(name.getFirstName()))
{
// エラー
}
if(IsEmpty(name.getLastName()))
{
// エラー
}
return 0;
}
また、NameクラスにもIsEmpty関数を実装しておくと、何かと便利です。
class Name
{
private:
char* first = nullptr;
char* last = nullptr;
public:
~ 省略:それぞれのsetName, getNameがある ~
bool IsEmpty()
{
if(IsEmpty(first))
return true;
if(IsEmpty(last))
return true;
return false;
}
}
7. 終わりに
私はプログラマーとして、そこそこ長く勉強をしてきた。
この、分かりやすい名前を付ける、無駄を省くといった考え方は、プログラムコードだけではなく、設計や仕様書作りなど、様々な場面でも応用ができると思う。
この記事が気に入ったらサポートをしてみませんか?