プログラミングテクニック「大で小を兼ねる」
大は小を兼ねるという“ことわざ”がありますが、プログラミングの世界でも大で小を兼ねるテクニックはよく使われます。
今回はゲームのアイテムを例に、大で小を兼ねる設計を解説します。
(初心者でも読めるようにC言語で解説しました。)
その後で業務用アプリの話も少しします。
HPとMPの回復アイテム
HPを回復するアイテム、MPを回復するアイテム、HPとMPの両方を回復するアイテムの3種類があったとします。
これらのデータを個別に定義すると、以下のような設計になると思います。
struct ItemHP {
int hp; // HPの回復量
};
struct ItemMP {
int mp; // MPの回復量
};
struct ItemBoth {
int hp; // HPの回復量
int mp; // MPの回復量
};
| アイテム | データ | |||
|---|---|---|---|---|
| ItemHP | ItemMP | ItemBoth | ||
| HP | MP | HP | MP | |
| 赤ポーション | 50 | |||
| 青ポーション | 30 | |||
| 紫ポーション | 30 | 20 | ||
上記の設計は、それぞれのアイテムで必要最小限のデータだけを持っており、シンプルで無駄がありません。一見するといい設計のようにも見えます。
しかし、データの種類が多いので、それに伴う分岐が増えたり、似たようなコードが何度も出てきたりする可能性があります。そのような問題をシンプルなコードのまま解決するのは難しいです。
また、それとは別の問題として、パラメーターの追加による組み合わせの増加の問題があります。今回はHPとMPの2つのパラメーターだけで考えましたが、さらに多くのパラメーターを扱う場合、組み合わせは膨大になります。上記のように組み合わせを個別に定義していては“組合わせ爆発”には対処できません。
そこで、次のようなルールを設けてみます。
HPだけを回復するアイテムは hp > 0, mp = 0
MPだけを回復するアイテムは hp = 0, mp > 0
HPとMPを回復するアイテムは hp > 0, mp > 0
これらのルールで上記の3種類のデータは1種類のデータにまとめることができます。
struct Item {
int hp; // HPの回復量
int mp; // MPの回復量
};
| アイテム | データ (Item) | |
|---|---|---|
| HP | MP | |
| 赤ポーション | 50 | 0 |
| 青ポーション | 0 | 30 |
| 紫ポーション | 30 | 20 |
この設計だとデータの定義は楽になります。また、この設計はデータベースでも扱いやすいです。使う時に分岐が必要になる可能性はありますが、組み合わせ毎に分岐するのではなく、それぞれの効果で分岐すると分岐回数を減らせます。
int min(int a, int b) {
return a < b ? a : b;
}
void useItem(struct Item const* item) {
int value;
value = item->hp;
if (value > 0) {
value = min(value, hpMax - hp);
hp += value;
pushMessage("HPが", value, "回復した");
}
value = item->mp;
if (value > 0) {
value = min(value, mpMax - mp);
mp += value;
pushMessage("MPが", value, "回復した");
}
}
上記のコードは「1.HPを回復するのか」「2.MPを回復するのか」の2つの分岐しかありません。「1.HPだけ回復」、「2.MPだけ回復」、「3.HPとMPの両方回復」の3つの分岐は必要ありません。ただし、HPとMPの両方回復するアイテムで pushMessage が複数回呼ばれるので、複数回呼べる仕様にする必要があります。それ以外では複雑になる部分は無さそうです。
そして、この設計は機能の追加に強いです。HPの最大値アップ、MPの最大値アップ、攻撃力アップ、防御力アップなどもそのまま追加できます。
struct Item {
int hp; // HPの回復量
int mp; // MPの回復量
int hpMax; // HPの最大値の増加量
int mpMax; // MPの最大値の増加量
int str; // 攻撃力の増加量
int def; // 防御力の増加量
};
int min(int a, int b) {
return a < b ? a : b;
}
void useItem(struct Item const* item) {
int value;
value = item->hp;
if (value > 0) {
value = min(value, hpMax - hp);
hp += value;
pushMessage("HPが", value, "回復した");
}
value = item->mp;
if (value > 0) {
value = min(value, mpMax - mp);
mp += value;
pushMessage("MPが", value, "回復した");
}
value = item->hpMax;
if (value > 0) {
hpMax += value;
pushMessage("HPの最大値が", value, "上がった");
}
value = item->mpMax;
if (value > 0) {
mpMax += value;
pushMessage("MPの最大値が", value, "上がった");
}
value = item->str;
if (value > 0) {
str += value;
pushMessage("攻撃力が", value, "上がった");
}
value = item->def;
if (value > 0) {
def += value;
pushMessage("防御力が", value, "上がった");
}
}
| アイテム | データ (Item) | |||||
|---|---|---|---|---|---|---|
| HP | MP | HPMAX | MPMAX | STR | DEF | |
| 赤ポーション | 50 | 0 | 0 | 0 | 0 | 0 |
| 青ポーション | 0 | 30 | 0 | 0 | 0 | 0 |
| 紫ポーション | 30 | 20 | 0 | 0 | 0 | 0 |
| 体力の種 | 0 | 0 | 10 | 0 | 0 | 0 |
| 知恵の種 | 0 | 0 | 0 | 10 | 0 | 0 |
| 力の種 | 0 | 0 | 0 | 0 | 5 | 0 |
| 守りの種 | 0 | 0 | 0 | 0 | 0 | 5 |
気を付ける点として、効果の優先順位があります。上記のコードではHPを回復してからHPMAXを上げるせいで、両方の効果があるアイテムで完全回復が出来ません。MPも同様です。それ以外ではうまく動きます。
ところで、上記の表を見ていると、無駄が多いことに気付きます。アイテム毎に必要なデータだけを持たせた場合と比べてデータ量が約5倍になっています。よく見るとHPとMP以外は同時に使用していないので、データの種類を分ければ効率が上がりそうです。効率を上げるためにデータを分けるべきでしょうか。
実はゲームの仕様をデータ構造に反映させるのは良くないとされています。ゲームの仕様が変わる度にデータの構造を変えなければならないからです。
今回の場合、HPMAX, MPMAX, STR, DEF の全てをアップさせるアイテムが追加されても、データの分け直しをする必要が無い設計が無難なのです。ですから無駄がある設計でも間違いではないのです。
もし、どうしてもデータの無駄を無くしたいのであれば、配列を使って次のような設計にすることでゲームの仕様を利用せずに無駄を無くせます。
enum Status { HP, MP, HPMAX, MPMAX, STR, DEF };
struct ItemEffect {
enum Status status;
int power;
};
struct Item {
int numEffect;
struct ItemEffect const* effects;
};
int min(int a, int b) {
return a < b ? a : b;
}
void useItem(struct Item const* item) {
int i;
int n = item->numEffect;
struct ItemEffect const* p = item->effects;
int value;
for (i = 0; i < n; i++, p++) {
value = p->power;
switch (p->status) {
case HP:
value = min(value, hpMax - hp);
hp += value;
pushMessage("HPが", value, "回復した");
break;
case MP:
value = min(value, mpMax - mp);
mp += value;
pushMessage("MPが", value, "回復した");
break;
case HPMAX:
hpMax += value;
pushMessage("HPの最大値が", value, "上がった");
break;
case MPMAX:
mpMax += value;
pushMessage("MPの最大値が", value, "上がった");
break;
case STR:
str += value;
pushMessage("攻撃力が", value, "上がった");
break;
case DEF:
def += value;
pushMessage("防御力が", value, "上がった");
break;
}
}
}
これならパラメーターが増えても必要最低限のデータだけを持つことになります。
また、文字列やステータスを配列にすることで更にコードを減らすことも出来ます。
enum Status {
HP, MP, HPMAX, MPMAX, STR, DEF,
NUM_STATUS
};
char const* const STATUS_MESSAGE[NUM_STATUS] = {
"HPが",
"MPが",
"HPの最大値が",
"MPの最大値が",
"攻撃力が",
"防御力が"
};
int status[NUM_STATUS];
struct ItemEffect {
enum Status status;
int power;
};
struct Item {
int numEffect;
struct ItemEffect const* effects;
};
int min(int a, int b) {
return a < b ? a : b;
}
void useItem(struct Item const* item) {
int i;
int n = item->numEffect;
struct ItemEffect const* p = item->effects;
int value;
char const* message;
for (i = 0; i < n; i++, p++) {
int statusIndex = p->status;
value = p->power;
message = "上がった";
switch (statusIndex) {
case HP:
value = min(value, status[HPMAX] - status[HP]);
message = "回復した";
break;
case MP:
value = min(value, status[MPMAX] - status[MP]);
message = "回復した";
break;
default: break;
}
status[statusIndex] += value;
pushMessage(STATUS_MESSAGE[statusIndex], value, message);
}
}
enum 型は 0 から順に数値が割り振られるので、最後の NUM_STATUS は Status の数を表しています。
ここまで効率を求めると機能の追加や修正が難しくなるかもしれないので、ほどほどがいいと思います。
ここでゲームの話は終わりにして、業務用アプリの話に入ります。
データベース層とビジネスロジック層の分離
業務用アプリではデータベースを扱うことが多く、データベース層、ビジネスロジック層、プレゼンテーション層の三層に分離する設計を行うことが多いです。
プレゼンテーション層はユーザーが操作するための機能を提供し、ビジネスロジック層はそのアプリ固有の機能を提供し、データベース層はデータを永続化します。
そして、データベースは一度稼働してしまうと構造の変更は難しいので、将来を見据えた設計にする必要があります。そのためには、アプリケーション固有の仕様を極力排除しなければなりません。
例えば、「このデータは10個も入れば十分だろう。」といった仕様はデータベースの構造には絶対に反映させません。もし10個に制限したいときはビジネスロジック層で制限します。データベース層では制限無く入れられる構造にしておきます。もし、将来20個まで入れられる必要性が出てきても、ビジネスロジック層の修正で済みます。
そのため、データベース層ではアプリ固有の最適化を行うことはできず、大で小を兼ねる設計のオンパレードとなります。