web-dev-qa-db-ja.com

すべての列挙型を1つのファイルに含めて、それを複数のクラスで使用することは悪い習慣ですか?

私は意欲的なゲーム開発者であり、時々インディーゲームに取り組んでいます。しばらくの間、最初は悪い習慣のように思われることをしていましたが、ここで経験豊富なプログラマから回答を得たいと思っています。

ゲームで使用するすべての列挙型を宣言するenumList.hというファイルがあるとします。

// enumList.h

enum materials_t { WOOD, STONE, ETC };
enum entity_t { PLAYER, MONSTER };
enum map_t { 2D, 3D };
// and so on.

// Tile.h
#include "enumList.h"
#include <vector>

class tile
{
    // stuff
};

主なアイデアは、ゲーム内のすべての列挙型を1 fileで宣言し、特定の列挙型を使用する必要がある場合は、必要なファイルで宣言するのではなく、その列挙型をインポートすることです。これを使って。これは物事をきれいにするので、1つの列挙型にアクセスするためだけにページを開くのではなく、1 placeですべての列挙型にアクセスできるためです。

これは悪い習慣であり、パフォーマンスに何らかの影響を与える可能性がありますか?

12
Bugster

本当に悪い習慣だと思います。コードの品質を測定する場合、「粒度」と呼ばれるものがあります。これらの列挙型をすべて1つのファイルに配置することにより、粒度が大幅に低下し、保守性も低下します。

Enumごとに1つのファイルを用意して、ファイルをすばやく見つけ、特定の機能の動作コードでグループ化します(たとえば、マテリアルの動作があるフォルダー内のマテリアルenum)。

主なアイデアは、ゲーム内のすべての列挙型を1つのファイルで宣言し、特定の列挙型を使用する必要がある場合は、使用する必要があるファイルで宣言するのではなく、その列挙型をインポートすることです。これは物事をきれいにするので、1つの列挙型にアクセスするためだけにページを開くのではなく、すべての列挙型に1か所でアクセスできるためです。

きれいだと思うかもしれませんが、実際はそうではありません。これは、機能的にもモジュール的にも属さないものを結合し、アプリケーションのモジュール性を低下させます。コードベースのサイズと、コードをどのようにモジュール化したいかに応じて、これはより大きな問題に発展し、システムの他の部分のコード/依存関係が不明確になる可能性があります。ただし、小さなモノリシックシステムを作成するだけの場合、これは必ずしも当てはまりません。しかし、小さなモノリシックシステムであっても、このようにはしません。

35
Falcon

はい、それは悪い習慣です。パフォーマンスのためではなく、保守性のためです。

OCDが「類似のものを一緒に収集する」方法でのみ、物事を「クリーン」にします。しかし、それは実際には有用ではなく、「クリーン」の良い種類ではありません

コードエンティティはグループ化して cohesion を最大化し、最小化 coupling にする必要があります。それらを技術的な基準(すべての列挙型をまとめるなど)でグループ化すると、反対のことが行われます。これは、機能的な関係がまったくないコードを結合し、ある場所でのみ使用される列挙型を別のファイルに配置します。

21

まあ、これは単なるdataです(動作ではありません)。おそらく/理論的に発生する可能性のある最悪の事態は、同じコードが2回以上含まれてコンパイルされ、比較的大きなプログラムが生成されることです。

内部に動作/手続きコードがない(ループやifなどがない)場合、そのようなインクルードが操作にサイクルを追加することは不可能です。

(比較的)より大きなプログラムは、パフォーマンス(実行速度)にほとんど影響を与えることはなく、いずれにしても、理論上の問題にすぎません。ほとんどの(多分すべての)コンパイラは、このような問題を防ぐ方法でインクルードを管理します。

私見、単一のファイル(より読みやすく、管理しやすいコード)で得られる利点は、考えられるあらゆる欠点を大幅に上回ります。

8
AlexBottoni

私はC++開発者ではないので、この回答はOOA&Dに対してより一般的です。

通常、コードオブジェクトは、必ずしも言語固有の構成要素の観点からではなく、機能の関連性の観点からグループ化する必要があります。常に尋ねられる重要な「はい」と「いいえ」の質問は、「エンドコーダーは、ライブラリーを使用するときに取得するオブジェクトのほとんどまたはすべてを使用することを期待されますか?」です。はいの場合は、グループ分けしてください。そうでない場合は、コードオブジェクトを分割し、それらを必要とする他のオブジェクトの近くに配置することを検討してください(これにより、コンシューマーが実際にアクセスするすべてのものを必要とする可能性が高くなります)。

基本的なコンセプトは「高い結束力」です。コードメンバー(クラスのメソッドから名前空間またはDLLのクラスまで、およびDLL自体)は、コーダーが必要なすべてを含み、必要がないものを含むことができるように編成する必要があります。これにより、全体的な設計の変更に対する耐性が高まります。変更する必要のないものは、変更する必要のない他のコードオブジェクトに影響を与えることなく、変更できます。多くの状況では、アプリケーションのメモリ効率も向上します。 a DLLは、プロセスによって実行される命令の量に関係なく、全体がメモリにロードされます。したがって、「リーン」アプリケーションを設計するには、メモリに読み込まれるコードの量に注意する必要があります。 。

この概念は、実質的にすべてのレベルのコード編成に適用され、保守性、メモリ効率、パフォーマンス、ビルド速度などにさまざまな程度の影響を与えます。コーダーが1つのオブジェクトにアクセスするだけでかなりモノリシックサイズのヘッダー/ DLLを参照する必要がある場合、これは、そのDLL内の他のオブジェクトに依存しないため、DLLにそのオブジェクトを含めることを再考する必要があります。ただし、他の方法で行うこともできます。a= DLLすべてのクラスについて、ビルド速度が遅くなり(関連するオーバーヘッドで再ビルドするDLLが増える))バージョン管理が悪夢になるため、これは悪い考えです。

適例:コードライブラリを実際に使用する際に、この「enumerations.h」ファイルに入れる列挙型のほとんどまたはすべてを使用する必要がある場合は、必ずそれらをグループ化してください。あなたはそれらを見つけるためにどこを探すべきかを知るでしょう。しかし、使用しているコーダーがヘッダーで提供している12個の列挙型の1つまたは2つだけを必要とする可能性がある場合は、それらを別のライブラリーに入れて、より大きなものと残りの列挙型の依存関係にすることをお勧めします。これにより、プログラマは、よりモノリシックなDLLにリンクせずに、必要な1つまたは2つだけを取得できます。

3
KeithS

私にとってそれはすべてあなたのプロジェクトの範囲に依存します。たとえば、10個の構造体を含むファイルが1つあり、それがこれまでに使用された唯一のものである場合、1つの.hファイルを使用することで完全に快適になります。ユニット、経済、建物など、いくつかの異なるタイプの機能がある場合、私は間違いなく物事を分割します。ユニットを扱うすべての構造体が存在するunits.hを作成します。どこかでユニットを使用したい場合は、units.hを含める必要がありますが、このファイルでユニットを使用した処理が行われる可能性があることも、ニースの「識別子」です。

このように見てください、あなたはコーラの缶が必要なのでスーパーマーケットを購入しません;)

3
hoppa

複数の開発者が同じコードベースで作業している場合、このようなことが問題になります。

私は確かに同様のグローバルファイルがマージの衝突と(より大きな)プロジェクトでのあらゆる種類の悲しみのつながりになることを見てきました。

ただし、プロジェクトで作業しているのがあなただけの場合は、最も快適な方法を使用し、その背後にある動機を理解(および同意)した場合にのみ「ベストプラクティス」を採用してください。

残りの人生でカーゴカルトプログラミングの実践に行き詰まるリスクを負うよりも、いくつかの間違いを犯してそれらから学ぶ方が良いです。

2
William Payne

この場合、私は理想的な答えは列挙型がどのように消費されるかに依存するということですが、ほとんどの場合それはおそらくすべての列挙型を個別に定義するのが最善ですが、それらのいずれかがすでに結合されている場合設計上、上記の結合された列挙型をまとめて導入する手段を提供する必要があります。実際には、すでに存在する意図的なカップリングの量までのカップリング許容差がありますが、それ以上はありません。

これを考慮すると、最も柔軟なソリューションは、各列挙型を個別のファイルで定義する可能性があります。butは、そうすることが合理的である場合(関連する列挙型の意図する使用法によって決定される)、結合パッケージを提供します。


同じファイルですべての列挙型を定義すると、それらが結合されます。また、拡張機能により、コードが実際にses他の列挙型であるかどうかに関係なく、1つ以上の列挙型に依存するコードはすべての列挙型に依存します。

_#include "enumList.h"

// Draw map texture.  Requires map_t.
// Not responsible for rendering entities, so doesn't require other enums.
// Introduces two unnecessary couplings.
void renderMap(map_t, mapIndex);
_

renderMap()は、_map_t_についてのみ知っているのではなく、実際に他のユーザーと対話していなくても、他の変更が影響を与えるためです。

_#include "mapEnum.h" // Theoretical file defining map_t.

void renderMap(map_t, mapIndex);
_

ただし、コンポーネントがすでに結合されている場合、単一のパッケージで複数の列挙型を提供すると、列挙型が結合される明確な論理的理由がある場合、それらの列挙型の使用法も結合されている場合、追加の明快さと単純さを簡単に提供できます。また、それらを提供しても、追加のカップリングは導入されません。

_#include "entityEnum.h"    // Theoretical file defining entity_t.
#include "materialsEnum.h" // Theoretical file defining materials_t.

// Can entity break the specified material?
bool canBreakMaterial(entity_t, materials_t);
_

この場合、エンティティタイプとマテリアルタイプの間に直接の論理的な関係はありません(エンティティが定義されたマテリアルのいずれかで作成されていない場合)。ただし、たとえば、1つの列挙型が他の列挙型に明示的に依存している場合、すべての結合された列挙型(およびその他の結合されたコンポーネント)を含む単一のパッケージを提供することは意味があり、可能な限りそのパッケージに隔離します。

_// File: "actionEnums.h"

enum action_t { ATTACK, DEFEND, SKILL, ITEM };               // Action type.
enum skill_t  { DAMAGE, HEAL, BUFF, DEBUFF, INFLICT, NONE }; // Skill subtype.

// -----

#include "actionTypes.h" // Provides action_t & skill_t from "actionEnums.h", and class Action (which couples them).
#include "entityEnum.h"  // Theoretical file defining entity_t.

// Assume ActFlags is or acts as a table of flags indicating what is and isn't allowable, based on entity_t and Action.
ImplementationDetail ActFlags;

// Indicate whether a given type of entity can perform the specified action type.
// Assume class Action provides members type() and subtype(), corresponding to action_t and skill_t respectively.
// Is only slightly aware of the coupling; knows type() and subtype() are coupled, but not how or why they're coupled.
bool canAct(entity_t e, const Action& act) {
    return ActFlags[e][act.type()][act.subtype()];
}
_

しかし、悲しいかな... 2つの列挙型が本質的に一緒に結合されている場合でも、「2番目の列挙型が最初の列挙型のサブカテゴリを提供する」ほど強力であっても、列挙型の1つだけが必要な場合があります。

_#include "actionEnums.h"

// Indicates whether a skill can be used from the menu screen, based on the skill's type.
// Isn't concerned with other action types, thus doesn't need to be coupled to them.
bool skillUsableOnMenu(skill_t);

// -----
// Or...
// -----

#include "actionEnums.h"
#include "gameModeEnum.h" // Defines enum gameMode_t, which includes MENU, CUTSCENE, FIELD, and BATTLE.

// Used to grey out blocked actions types, and render them unselectable.
// All actions are blocked in cutscene, or allowed in battle/on field.
// Skill and item usage is allowed in menu.  Individual skills will be checked on attempted use.
// Isn't concerned with specific types of skills, only with broad categories.
bool actionBlockedByGameMode(gameMode_t mode, action_t act) {
    if (mode == CUTSCENE) { return true; }
    if (mode == MENU) { return (act == SKILL || act == ITEM); }

    //assert(mode == BATTLE || mode == FIELD);
    return false;
}
_

したがって、単一のファイルで複数の列挙を定義すると不必要な結合が追加される状況が常に存在する可能性があること、および結合された列挙を単一のパッケージで提供することで意図された使用法を明確にし、実際の結合コード自体を可能な限り、理想的なソリューションは、各列挙を個別に定義し、頻繁に一緒に使用することを目的とした列挙にジョイントパッケージを提供することです。同じファイルで定義されている列挙型は、本質的に相互にリンクされている列挙型だけなので、一方を使用すると、もう一方も使用する必要があります。

_// File: "materialsEnum.h"
enum materials_t { WOOD, STONE, ETC };

// -----

// File: "entityEnum.h"
enum entity_t { PLAYER, MONSTER };

// -----

// File: "mapEnum.h"
enum map_t { 2D, 3D };

// -----

// File: "actionTypesEnum.h"
enum action_t { ATTACK, DEFEND, SKILL, ITEM };

// -----

// File: "skillTypesEnum.h"
enum skill_t  { DAMAGE, HEAL, BUFF, DEBUFF, INFLICT, NONE };

// -----

// File: "actionEnums.h"
#include "actionTypesEnum.h"
#include "skillTypesEnum.h"
_

はい、大きなプロジェクトでそうするのは悪い習慣です。接吻。

若い同僚がコア.hファイルの単純な変数の名前を変更し、100人のエンジニアがすべてのファイルが再構築されるまで45分待ったため、全員のパフォーマンスに影響がありました。 ;)

すべてのプロジェクトは、長年にわたって小さな風船で始まり、技術的な負債を生み出すために初期の近道を行った人たちを呪います。ベストプラクティスは、グローバルな.hコンテンツを常にグローバルなものに制限することです。

1
AmyInNH