非常に複雑なソフトウェアのコードベースを管理するにはどうすればよいですか?


8

私は、さまざまなオブジェクト指向プログラミング言語を使用して、自分自身と他の人のためのプログラムを作成することがよくあります。その場合、通常は比較的小さくなります(最大で数千行)。しかし最近、私は完全なゲームエンジンなど、より大きなプロジェクトを作成しようとしています。そうするとき、私はしばしば複雑さという障害にぶつかるように見えます。

私の小さなプロジェクトでは、プログラムのすべての部分がどのように機能するかについてのメンタルマップを覚えることは簡単です。これにより、変更がプログラムの残りの部分にどのように影響するかを十分に認識し、バグを非常に効果的に回避できるだけでなく、新機能がコードベースにどのように適合するかを正確に確認できます。しかし、より大きなプロジェクトを作成しようとすると、非常に面倒なコードと多数の意図しないバグにつながる、優れたメンタルマップを維持することが不可能であることがわかりました。

この「メンタルマップ」の問題に加えて、自分のコードを他の部分から切り離しておくことは難しいと思います。たとえば、マルチプレーヤーゲームにプレーヤーの動きの物理を処理するクラスとネットワークを処理するクラスがある場合、これらのクラスの1つが他のクラスに依存してプレーヤーの動きのデータをネットワークシステムに取得する方法はありませんネットワーク経由で送信します。このカップリングは、優れたメンタルマップを妨げる複雑さの重要な原因です。

最後に、他のクラスを調整する1つ以上の「マネージャー」クラスを考え出すことがよくあります。たとえば、ゲームでは、クラスがメインのティックループを処理し、ネットワーククラスとプレーヤークラスの更新メソッドを呼び出します。これは、各クラスは他とは独立して単体テスト可能で使用可能でなければならないという私の研究での発見の哲学に反しています。そのようなマネージャークラスは、その目的によってプロジェクトの他のほとんどのクラスに依存しているためです。さらに、プログラムの残りのマネージャークラスオーケストレーションは、メンタルマッピング不可能な複雑さの重要な原因です。

まとめると、これにより、かなりのサイズの高品質のバグのないソフトウェアを作成できなくなります。この問題を効果的に処理するために、プロの開発者は何をしますか?私は特にJavaとC ++のOOP回答ターゲットに興味がありますが、この種のアドバイスはおそらく非常に一般的です。

ノート:

  • 私はUMLダイアグラムを使用してみましたが、それは最初の問題に役立つようであり、それでも(たとえば)最初に初期化されるものに関するメソッド呼び出しの順序付けではなく、クラス構造に関する場合に限られます。

7
「株を売買するとき、どうやってできるだけ多くのお金を稼ぐのですか?」-同じ質問、異なる職業。
MaxB

1
設計とモデリングにどのくらいの時間を費やしていますか?私たちが開発中に遭遇した問題の多くは、視点の漏洩に起因します。モデリング中に私たちが焦点を当てている種類の視点。その後、このリークをコードで解決しようとします。パースペクティブ(適切な抽象化)もリークするコード。大規模なプロジェクトでは、全体像を明確にするために計画を立てる必要があります。コードベースのレベルから、ビジョンを持つのは難しいでしょう。「木があなたに森を見ることを止めさせないでください」。
Laiv

4
モジュール化と分離。しかし、あなたはすでにそうしているようですが、失敗しています。その場合、それは実際にはどのようなプロジェクトであり、どのようなテクノロジーが使用されているかによって異なります。
陶酔感

1
継続的なリファクタリングはソフトウェア開発の必要な部分です-何かを正しく行っていることを示していると考えてください。重要なシステムには、常に「未知の未知数」が含まれています。つまり、元の設計では予測も説明もできなかったものです。リファクタリングは、反復的な開発の現実であり、「良い」ソリューションに向けて段階的に進化します。その理由は、開発者がリファクタリングが容易なコードの記述を好む傾向がある理由です。これには、SOLID原則に
Ben Cottrell

2
UML IMHOには、このための最も有用な種類のダイアグラムタイプ、つまりデータフローダイアグラムがありません。ただし、これは特効薬ではありません。モジュール化と分離は、データフロー図を使用しても自動的には行われません。
Doc Brown

回答:


8

私はしばしば、複雑さという障害にぶつかるように見えます。

この主題について書かれた本は全部あります。これは、ソフトウェア開発についてこれまでに書かれた最も重要な本の1つであるSteve McConnellのコードコンプリートからの引用です。

複雑さの管理は、ソフトウェア開発における最も重要な技術トピックです。私の見解では、ソフトウェアの主要な技術的命令が複雑さを管理する必要があることは非常に重要です。

余談ですが、ソフトウェア開発に少しでも関心がある場合は、この本を読むことを強くお勧めします(あなたがこの質問をしたので、私はあなたがそうしていると思います)。少なくとも、上のリンクをクリックして、デザインコンセプトの抜粋を読んでください。


たとえば、マルチプレーヤーゲームでプレーヤーの動きの物理を処理するクラスとネットワークを処理するクラスがある場合、これらのクラスの1つが他のクラスに依存してプレーヤーの動きデータをネットワークシステムに取得する方法はありませんネットワーク経由で送信します。

この特定の場合、私はあなたのPlayerMovementCalculatorクラスとあなたのNetworkSystemクラスが互いに完全に無関係であると考えます。1つのクラスはプレーヤーの動きの計算を担当し、もう1つのクラスはネットワークI / Oを担当します。おそらく、別個の独立したモジュールでもです。

ただし、モジュール間のデータやイベント/メッセージを仲介するモジュールの外側のどこかに、少なくともいくつかの追加の配線または接着剤があることが確かに期待されます。たとえばPlayerNetworkMediatorメディエーターパターンを使用してクラスを記述できます。

別の可能なアプローチは、Event Aggregatorを使用してモジュールを分離することです。

ネットワークソケットに関連するロジックのタイプなどの非同期プログラミングの場合は、Observableを公開して、それらの通知を「リッスン」するコードを整理することができます。

非同期プログラミングは必ずしもマルチスレッドを意味するわけではありません。その詳細は、プログラムの構造とフロー制御についてです(ただし、マルチスレッドは非同期の明らかなユースケースです)。Observableは、これらのモジュールの一方または両方で、無関係なクラスが変更通知をサブスクライブできるようにするのに役立ちます。

例えば:

  • NetworkMessageReceivedEvent
  • PlayerPositionChangedEvent
  • PlayerDisconnectedEvent


最後に、他のクラスを調整する1つ以上の「マネージャー」クラスを考え出すことがよくあります。たとえば、ゲームでは、クラスがメインのティックループを処理し、ネットワーククラスとプレーヤークラスの更新メソッドを呼び出します。これは、各クラスは他とは独立して単体テスト可能で使用可能でなければならないという私の研究での発見の哲学に反しています。そのようなマネージャークラスは、その目的によってプロジェクトの他のほとんどのクラスに依存しているためです。さらに、プログラムの残りのマネージャークラスオーケストレーションは、メンタルマッピング不可能な複雑さの重要な原因です。

これのいくつかは確かに経験に帰着しますが、Managerクラスの名前は、多くの場合、デザインのにおいを示します。

クラスに名前を付けるときは、そのクラスが担当する機能を考慮し、クラス名がその機能を反映できるようにします。

コードでのマネージャーの問題は、職場でのマネージャーの問題に少し似ています。彼らの目的はあいまいで、自分自身でもよく理解されていません。ほとんどの場合、私たちはそれらなしで完全にうまくいきます。

オブジェクト指向プログラミングは主に動作に関するものです。クラスはデータエンティティではなく、コード内のいくつかの機能要件の表現です。

それが果たす機能要件に基づいてクラスに名前を付けることができれば、何らかの肥大化したGod Objectで終わる可能性が減り、プログラム内でのアイデンティティと目的が明確なクラスを持つ可能性が高くなります。

さらに、名前が間違って見え始めるので、余分なメソッドと動作が実際には属さないときに忍び寄る場合は、より明確になるはずです。つまり、その名前に反映

最後に、名前がエンティティリレーションシップモデルに属しているように見えるクラスを作成する誘惑を避けてください。以下のようなクラス名での問題PlayerMonsterCarDog、などは、彼らの行動について何を意味するものではありません、とだけ論理的に関連するデータや属性のコレクションを記述するように見えるということです。 オブジェクト指向設計は、データモデリングではなく、その動作モデリングです。

たとえば、aをモデル化して損傷MonsterPlayer計算する2つの異なる方法を考えます。

class Monster : GameEntity {
    dealDamage(...);
}

class Player : GameEntity {
    dealDamage(...);
}

ここでの問題は、あなたが合理的に期待するかもしれないということですPlayerMonster、おそらくこれらのエンティティを行う可能性がありますダメージの量(例えば運動)と全く関係のない、他の方法の全体の束を持っています。あなたは上記の神オブジェクトへの道を進んでいます。

より自然なオブジェクト指向のアプローチは、その動作に基づいてクラスの名前を特定することです。次に例を示します。

class MonsterDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

class PlayerDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

このタイプの設計では、PlayerおよびMonsterオブジェクトには、アプリケーション全体で必要なデータが含まれているため、それらに関連付けられたメソッドはおそらくありません。これらはおそらく、リポジトリ内に存在し、フィールド/プロパティのみを含む単純なデータエンティティです。

このアプローチは通常、Anemic Domain Modelとして知られており、ドメイン駆動設計(DDD)のアンチパターンと見なされますが、SOLIDの原則により、「共有」データエンティティ(おそらくリポジトリ内)を明確に分離できます。 、およびアプリケーションのオブジェクトグラフ内のモジュール式(できればステートレス)動作クラス。

SOLIDとDDDは、OO設計に対する2つの異なるアプローチです。彼らは多くの方法で交差していますが、クラスのアイデンティティとデータと動作の分離に関して反対の方向に引っ張る傾向があります。


上記のMcConnellの引用に戻ると、複雑さを管理することが、ソフトウェア開発が平凡な事務的な仕事というよりも熟練した職業である理由です。マコネルが彼の本を書く前に、フレッド・ブルックスはあなたの質問に対する答えをきちんと要約する主題についての論文を書きました- 複雑さを管理するための銀の弾丸ありません

したがって、単一の答えはありませんが、それに取り組む方法に応じて、人生を楽にしたり難しくしたりできます。

  • KISSDRYYAGNIを思い出してください。
  • OO設計/ソフトウェア開発のSOLID原則の適用方法を理解する
  • また、アプローチがSOLIDの原則と矛盾する場所がある場合でも、ドメイン駆動設計を理解します。SOLIDとDDDは、同意しないよりも同意する傾向があります。
  • コードの変更を期待する-自動テストを作成して、それらの変更の影響をキャッチする(便利な自動テストを作成するためにTDDに従う必要はありません-実際、これらのテストの一部は、「使い捨て」コンソールアプリを使用した統合テストであるか、テストハーネスアプリ)
  • 最も重要なこと-実用的であること。怠惰にガイドラインを遵守しないでください。複雑さの反対は単純さなので、疑わしい場合(再度)-KISS

確かな答え。パンは意図した。
ipaul 2017年

4
DDDとSOLIDが互いに不一致であることに同意しません。実際、それらはかなり補完的だと思います。それらが矛盾している、またはおそらく直交しているように見える理由の一部は、抽象化の異なるレベルで異なるものを説明していることです。
RibaldEddie 2017年

1
@RibaldEddie私はそれらが大部分を補完していることに同意します。私が目にする主な問題は、SOLDがSRPをクラスごとの真の単一責任という論理的な結論に導いた自然な帰結として、いわゆる「貧血」ドメインモデルを積極的に奨励しているようですが、DDDは反パターン。あるいは、ファウラーの貧血領域モデルの説明について、私が誤って解釈したものがあるのでしょうか?
Ben Cottrell 2017年

1

複雑さを管理することについてだと思います。これは、あなたの周りのものが複雑になり、現在持っているものではまだ管理できなくなると常に低下し、おそらく1人の企業から50万人のグローバル企業に成長するときに数回発生します。

一般に、プロジェクトが大きくなると複雑さが増します。そして一般的に、複雑さが非常に大きくなる現在(ITであるか、プログラム、プログラム、または企業全体を管理しているかに関係なく)、より多くのツールを必要とするか、ツールを複雑さをより適切に処理するツールで置き換えることが、人類が永遠に行ってきたことです。そして、誰かが他の誰かが取り組む必要がある何かを出力する必要があるので、ツールはより複雑なプロセスと連携します。そして、それらは特定の役割を持つ「追加の人」と協力します。たとえば、自宅での1人のプロジェクトでは不要なエンタープライズアーキテクトです。

同時に、分類法が複雑すぎて非構造化データにプッシュする瞬間が来るまで、その背後にある分類法は複雑さの背後に成長する必要があります。たとえば、ウェブサイトのリストを作成して分類することができますが、10億のウェブサイトのリストが必要な場合、妥当な時間内にこれを行う方法がないため、よりスマートなアルゴリズムに進むか、データを検索するだけです。

小さなプロジェクトでは、共有ドライブで作業できます。やや大規模なプロジェクトでは、.gitまたは.svnなどをインストールします。すべてがライブであり、すべてが異なるリリース日を持ち、すべてが他のシステムに依存している複数のプロジェクトを持つ複雑なプログラムでは、バージョン管理だけでなく、すべての構成管理を担当している場合は、クリアケースに切り替える必要があります。プロジェクトのいくつかのブランチ。

ですから、これまでの人間の最善の努力は、ツール、特定の役割、プロセスを組み合わせて、あらゆるものの複雑化を管理することだと思います。

幸いなことに、ビジネスフレームワーク、プロセスフレームワーク、分類法、データモデルなどを販売する企業があります。そのため、業界によっては、複雑さが非常に大きくなり、他の方法がないことを誰もが認めるときに、購入できるデータがあります。グローバルな保険会社などの箱から出してすぐにモデル化およびプロセスを実行し、そこから既存のアプリケーションをリファクタリングして、実証済みのデータモデルとプロセスがすでに含まれている一般的な実証済みフレームワークにすべてを再び適合させることができます。

あなたの業界の市場に何もない場合、おそらく機会があります:)


基本的に、「人を雇う」または「問題を解決する何かを購入する™」のいくつかの組み合わせを提案しているようです。私は、趣味のプロジェクトに取り組んでいる予算のない個々の開発者としての管理の複雑さに興味があります(現時点では)。
john01dav 2017年

そのレベルでは、おそらくあなたの質問は「名前100のベストプラクティスは優れたアプリケーションアーキテクトが適用する」のようなものでしょうか。私は本質的に彼が彼がするすべての背後にある複雑さを管理していると思うので 彼は複雑さと戦うための基準を設定します。
edelwater 2017年

それは私が求めていることのようなものですが、私はリストよりも一般的な哲学に興味があります。
john01dav 2017年

アプリケーションアーキテクチャプラクティス/パターン/デザインなどに関する21.000冊以上の本があるので、その幅広い質問:safaribooksonline.com/search/?query
application%

本は質問の人気を示すだけであり、良い答えの広さを示すことができませんでした。この質問に答えるのに本が必要だと思われる場合は、ぜひお勧めしてください。
john01dav 2017年

0

すべてのOOPの記述を停止し、代わりにいくつかのサービスを追加します。

たとえば、私は最近、ゲームのGOAPを書いていました。インターネット上で、OOPスタイルのゲームエンジンに対するアクションの例をいくつか示します。

Action.Process();

私の場合、エンジン内とエンジン外の両方でアクションを処理する必要がありました。プレイヤーがいない場合は、より詳細な方法でアクションをシミュレートしました。だから代わりに私は使用しました:

Interface IActionProcessor<TAction>
{
    void ProcessAction(TAction action);
}

これにより、ロジックを分離して2つのクラスを持つことができます。

ActionProcesser_InEngine.ProcessAction()


ActionProcesser_BackEnd.ProcessAction()

これはOOPではありませんが、アクションがエンジンにリンクしていないことを意味します。したがって、私のGOAPモジュールは、ゲームの他の部分から完全に分離されています。

完成したら、私はDLLを使用するだけで、内部実装については考えません。


1
アクションプロセッサはオブジェクト指向ではないのですか?
Greg Burghardt

2
「OOPは複雑さに対処するための多くの手法の1つに過ぎないため、OOPデザインを作成しようとしないでください。他の方法(たとえば、手続き型のアプローチ)は、あなたのケースでうまく機能するかもしれません。実装の詳細を抽象化する明確なモジュール境界がある限り、どのように到達するかは関係ありません。」
amon

1
問題はコードレベルではないと思います。より高いレベルにあります。コードベースの問題は、病気ではなく症状です。解決策はコードから来るものではありません。デザインとモデリングから来ると思います。
Laiv

2
私が自分自身をOPとして想像してこれを読んだ場合、この回答には詳細が欠けており、有用な情報が提供されていないことがわかります。これは、IActionProcessorが実際に値を提供する方法を説明していないため、単なる間接参照の層を追加するだけではありません。
whatsisname

このようなサービスの使用は、OOではなく手続き型です。これにより、OOと比べて責任が明確に分離されます。OPはすぐに過度に結合される可能性があることを発見しています
Ewan

0

母は質問を何と言いましたか!私はこれを自分の奇抜な考えで試して恥ずかしいかもしれません(私が本当にオフの場合は、提案を聞きたいです)。しかし、私がドメインで最近学んだ最も有用なこと(以前はゲームを含んでいたが、今はVFXです)は、抽象インターフェース間の相互作用をデカップリングメカニズムとしてのデータに置き換えることです(そして、最終的に必要な情報の量を減らします)物事やお互いについて、最低限の絶対的な極限まで)。これはまったく正気に聞こえないかもしれません(そして、私はあらゆる種類の不適切な用語を使用している可能性があります)。

それでも、私があなたに合理的に管理可能な仕事を与えるとしましょう。レンダリングするシーンとアニメーションデータを含むこのファイルがあります。ファイル形式をカバーするドキュメントがあります。あなたの唯一の仕事は、ファイルをロードし、パストレースを使用してアニメーションのきれいな画像をレンダリングし、結果を画像ファイルに出力することです。これはかなり小規模なアプリケーションであり、かなり洗練されたレンダラー(数百万ではない)であっても、LOCの数万を超えることはおそらくありません。

ここに画像の説明を入力してください

このレンダラーには、独自の小さな孤立した世界があります。外界の影響を受けません。独自の複雑さを分離します。このシーンファイルを読み取って結果を画像ファイルに出力することの懸念を超えて、レンダリングだけに集中できます。このプロセスで何か問題が発生した場合、この図には他に何も関与していないので、それはレンダラーにあり、他には何もないことがわかります。

一方、実際には数百万のLOCを含む大きなアニメーションソフトウェアのコンテキストでレンダラーを機能させる必要があるとしましょう。レンダリングに必要なデータを取得するために、合理化され、文書化されたファイル形式を読み取るだけでなく、あらゆる種類の抽象インターフェースを通過して、作業に必要なすべてのデータを取得する必要があります。

ここに画像の説明を入力してください

突然、あなたのレンダラーはもはやそれ自身の小さな孤立した世界にいなくなりました。これは、とても複雑に感じます。多くのソフトウェアの全体的な設計を、潜在的に多くの可動部品を含む1つの有機的な全体として理解する必要があります。また、ボトルネックやバグのいずれかが発生した場合、メッシュやカメラなどの実装について考える必要がある場合もあります。関数。

機能性と合理化されたデータ

理由の1つは、機能が静的データよりもはるかに複雑であるためです。また、関数呼び出しが静的データの読み取りでは失敗する可能性があるという点で、多くの原因が考えられます。概念的にはレンダリング用に読み取り専用データを取得しているだけの場合でも、これらの関数を呼び出すと発生する可能性のある非常に多くの隠れた副作用があります。変更する理由は他にもたくさんあります。数か月後、まったく同じデータをフェッチしていても、レンダラーの大量のセクションを書き直してそれらの変更に対応する必要がある方法で、メッシュまたはテクスチャインターフェースがパーツを変更または非推奨にする場合があります。レンダラーへのデータ入力はまったく変更されていません(最終的にすべてにアクセスするために必要な機能のみ)。

したがって、可能な場合、合理化されたデータは、システム全体を全体として考える必要をなくし、システムの非常に特定の部分に集中して作成できる種類の非常に優れた分離メカニズムであることがわかりました改善、新機能の追加、物事の修正などです。ソフトウェアを構成するかさばる部分については、非常にI / Oの考え方に従っています。これを入力し、あなたのことを実行し、それを出力し、何十もの抽象インターフェースを経由せずに、途中で無限の関数呼び出しを終了します。そして、それはある程度、関数型プログラミングに似始めています。

したがって、これは1つの戦略にすぎず、すべての人に適用できるとは限りません。そしてもちろん、単独で飛行している場合でも、すべて(データ自体の形式を含む)を維持する必要がありますが、違いは、そのレンダラーに改善を加えるために座っているとき、本当にレンダラーに集中できることです。ほとんどの場合、他には何もありません。それはそれ自体の小さな世界で非常に分離されます-入力が非常に合理化されるために必要なデータと同じくらい分離されます。

そして、私はファイル形式の例を使用しましたが、それは入力のために関心のある合理化されたデータを提供するファイルである必要はありません。インメモリデータベースの場合もあります。私の場合、対象のデータを格納するコンポーネントを備えたエンティティコンポーネントシステムです。それでも、合理化されたデータへのデカップリングのこの基本的な原則を見つけました(ただし、それを行ったとしても)抽象化と、たくさんの、そしてこれらすべての間で行われているたくさんの相互作用を中心とした以前に取り組んだ以前のシステムよりも、私の精神能力への負担ははるかに少ないです1つのことに腰を下ろし、それについてのみ考え、それ以外はほとんど考えられないようにする抽象的なインターフェース。私の脳は、以前のタイプのシステムで危機に瀕していて、爆発することを望んでいました。

デカップリング

大きなコードベースが脳にかかる負担を最小限に抑えたい場合は、ソフトウェアの大きな部分(レンダリングシステム全体、物理システム全体など)が可能な限り最も孤立した世界に存在するようにしてください。最も合理化されたデータを介して、最小限の最小限に至るコミュニケーションと相互作用の量を最小限に抑えます。交換がはるかに分離されたシステムであり、その作業を実行する前に他の何十ものものと通信する必要がない場合は、ある程度の冗長性(プロセッサまたはユーザー自身のための冗長な作業)を受け入れることさえできます。

そして、それを始めると、巨大なものではなく、12の小規模なアプリケーションを維持しているように感じます。そして、それはとても楽しいことでもあります。座って、外の世界を気にすることなく、1つのシステムで思いのままに作業できます。正しいデータを入力し、最後に正しいデータを他のシステムがアクセスできる場所に出力するだけです(その時点で、他のシステムが入力して処理を実行する可能性がありますが、そのことを気にする必要はありません。システムで作業する場合)。もちろん、たとえば、すべてがユーザーインターフェイスにどのように統合されるかについて考える必要があります(まだ、GUIのすべてのデザイン全体について考える必要があります)が、少なくとも、既存のシステムに座って作業するときはそうではありません。または新しいものを追加することを決定します。

おそらく私は、最新のエンジニアリング手法を最新に保つ人々に明白な何かを説明しています。知りません。しかし、それは私には明らかではありませんでした。相互に作用し合うオブジェクトや、大規模なソフトウェアに求められる機能を中心としたソフトウェアの設計に取り組みたかったのです。そして、私がもともと大規模なソフトウェア設計について読んだ本は、実装やデータなどのインターフェース設計に焦点を当てていました(当時のマントラは、実装はそれほど重要ではなく、インターフェースのみです。前者は簡単に交換または置換できるためです。 )。ソフトウェアの相互作用を、この合理化されたデータを除いて、互いにほとんど通信しない巨大なサブシステム間でのデータの入力と出力だけに煮詰めるようなものだと考えることは、最初は直感的ではありませんでした。しかし、そのコンセプトを中心に設計することに焦点を移し始めたとき、それは物事を非常に簡単にしました。頭が爆発することなく、はるかに多くのコードを追加できました。タワーの代わりにショッピングモールを建設しているような気分でした。タワーを追加しすぎた場合、または一部が破損した場合、倒れる可能性があります。

複雑な実装と複雑な相互作用

これは私が言及すべきもう1つの問題です。これは、キャリアの初期のかなりの部分を最も単純な実装を探すために費やしたためです。それで、私は保守性を改善していると考えて、物事を最も小さくて単純な断片に分解しました。

後になって、私はあるタイプの複雑さを別のタイプの交換と交換していることに気付かなかった。すべてを最も単純な部分にまで削減することで、これらの10代の部分の間で行われた相互作用は、関数呼び出しとの最も複雑な相互作用のWebになり、コールスタックの30レベルに達することもあります。そしてもちろん、1つの関数を見ると、それはとても単純で、関数の機能を簡単に理解できます。しかし、各関数はほとんど機能していないので、その時点ではあまり有用な情報は得られません。次に、あらゆる種類の機能をトレースし、あらゆる種類のフープをジャンプして、脳が2つ以上の大きなものを爆発させようとするような方法でそれらがすべて何を行うかを実際に理解する必要があります。

それは神のオブジェクトやそのようなものを示唆するものではありません。しかし、おそらくメッシュオブジェクトを頂点オブジェクト、エッジオブジェクト、面オブジェクトなどの細かいものに分割する必要はありません。たぶん、コードの相互作用を根本的に減らす代わりに、やや複雑な実装を背後に置いて "メッシュ"に保つことができます。適度に複雑な実装をあちこちで処理できます。私は、誰がどこで、どのような順番で知っているかという副作用を伴う、無数の相互作用を処理することができません。

少なくとも私は、脳への負担がはるかに少ないことを発見しました。それは、大規模なコードベースで脳を傷つける相互作用だからです。特別なことはありません。

一般性と特定性

上記に関係しているかもしれませんが、私は一般性とコードの再利用が大好きで、優れたインターフェースを設計する際の最大の課題はさまざまなニーズでさまざまなものに使用されるため、幅広いニーズを満たすことだと考えていました。そして、それを行うと、必然的に一度に100のことを考える必要があります。なぜなら、一度に100のことのニーズのバランスをとろうとしているからです。

物事を一般化するには、非常に時間がかかります。私たちの言語に付属している標準ライブラリを見てください。C ++標準ライブラリにはほとんど機能が含まれていませんが、その設計について議論し、提案を行う人々の委員会全体でチームを維持し、調整する必要があります。これは、その小さな機能が世界中のニーズ全体を処理しようとしているためです。

おそらく、これまでのところは必要ないでしょう。おそらく、インデックス付きメッシュ間の衝突検出にのみ使用される空間インデックスのみを使用しても問題ありません。多分私達は他の種類の表面のために別のものを使用し、レンダリングのために別のものを使用できます。以前はこのような冗長性を排除することに集中していたが、その理由の一部は、さまざまな人々によって実装された非常に非効率的なデータ構造を扱っていたためだった。当然、単に300kの三角形メッシュに1ギガバイトを使用するoctreeがある場合、メモリ内にさらに1つを配置したくありません。

しかし、そもそもなぜオクツリーがそれほど非効率なのでしょうか。ノードごとに4バイトしか使用せず、メガバイト未満でoctreeを作成し、ギガバイトバージョンと同じことを実行しながら、短時間で構築し、より高速な検索クエリを実行できます。その時点で、ある程度の冗長性は完全に受け入れられます。

効率

したがって、これはパフォーマンスが重要なフィールドにのみ関連しますが、メモリ効率のようなものをよりよく取得すればするほど、生産性を優先して、もう少し無駄にすることができます(一般性の低下またはデカップリングと引き換えに、冗長性をもう少し受け入れることができます)。 。また、コードがすでに非常に効率的であり、生産性と引き換えに生産性と引き換えに、効率を犠牲にするだけの余裕があるので、プロファイラーとかなりうまくやり、コンピューターアーキテクチャとメモリ階層について学ぶことができます。重要な領域でさえも、効率性はやや劣りますが、競合他社をしのいでいます。この領域を改善することで、よりシンプルな実装も回避できることがわかりました。

信頼性

これは明白なことですが、言及するかもしれません。最も信頼性の高いものには、最小限の知的オーバーヘッドが必要です。それらについて多くを考える必要はありません。彼らはただ働く。その結果、徹底的なテストを通じて「安定」している(変更する必要がない)超信頼性パーツのリストを大きくするほど、考える必要が少なくなります。

詳細

上記のすべてが私に役立ついくつかの一般的なことをカバーしていますが、あなたの地域のより具体的な側面に移りましょう:

私の小さなプロジェクトでは、プログラムのすべての部分がどのように機能するかについてのメンタルマップを覚えることは簡単です。これにより、変更がプログラムの残りの部分にどのように影響するかを十分に認識し、バグを非常に効果的に回避できるだけでなく、新機能がコードベースにどのように適合するかを正確に確認できます。しかし、より大きなプロジェクトを作成しようとすると、非常に面倒なコードと多数の意図しないバグにつながる優れたメンタルマップを維持することが不可能であることがわかりました。

私にとって、これは複雑な副作用と複雑な制御フローに関連する傾向があります。これはかなり低レベルのビューですが、見栄えの良いすべてのインターフェースと、具体的なものから抽象的なものへの分離によって、複雑な制御フローで発生する複雑な副作用について簡単に推論することはできません。

副作用を単純化/軽減し、制御フローを単純化します。理想的には両方です。そして、一般的に、はるかに大きなシステムが何をするのか、また変更に応じて何が起こるのかを推論するのは非常に簡単です。

この「メンタルマップ」の問題に加えて、自分のコードを他の部分から切り離しておくことは難しいと思います。たとえば、マルチプレーヤーゲームでプレーヤーの動きの物理を処理するクラスとネットワークを処理するクラスがある場合、これらのクラスの1つが他のクラスに依存してプレーヤーの動きデータをネットワークシステムに取得する方法はありませんネットワーク経由で送信します。このカップリングは、優れたメンタルマップを妨げる複雑さの重要な原因です。

概念的には、いくつかのカップリングが必要です。人々がデカップリングについて話すとき、それらは通常、ある種類を別のより望ましい種類に置き換えることを意味します(通常は抽象化に対して)。私にとって、私のドメイン、私の脳の働きなどを考えると、「メンタルマップ」の要件を最小限に抑えるために最も望ましい種類は、上記の合理化されたデータです。1つのブラックボックスが別のブラックボックスに送られるデータを吐き出し、両方がお互いの存在を完全に認識していません。彼らが知っているのは、データが保存されている中央の場所(例:中央のファイルシステムまたは中央のデータベース)だけであり、それらを介して入力をフェッチし、何かを実行して、他のブラックボックスが入力する可能性のある新しい出力を吐き出します。

このようにすると、物理システムは中央データベースに依存し、ネットワーキングシステムは中央データベースに依存しますが、お互いについては知りません。彼らはお互いが存在していることさえ知る必要はないでしょう。彼らは、お互いに抽象的なインターフェースが存在することを知る必要すらありません。

最後に、他のクラスを調整する1つ以上の「マネージャー」クラスを考え出すことがよくあります。たとえば、ゲームでは、クラスがメインのティックループを処理し、ネットワーククラスとプレーヤークラスの更新メソッドを呼び出します。これは、各クラスは他とは独立して単体テスト可能で使用可能でなければならないという私の研究での発見の哲学に反しています。そのようなマネージャークラスは、その目的によってプロジェクトの他のほとんどのクラスに依存しているためです。さらに、プログラムの残りのマネージャークラスオーケストレーションは、メンタルマッピング不可能な複雑さの重要な原因です。

ゲーム内のすべてのシステムを調整するために何かが必要になる傾向があります。Centralはおそらく、完了後にレンダリングシステムを呼び出す物理システムのようなものよりも、複雑さが少なく、管理が容易です。ただし、ここでは必然的にいくつかの関数を呼び出す必要があり、できればそれらは抽象的です。

したがって、抽象update関数を使用してシステムの抽象インターフェースを作成する場合があります。その後、中央エンジンに登録され、ネットワークシステムは、「私はシステムです。ここに私のアップデート機能があります。時々電話してください。」と言うことができますそして、エンジンはそのようなすべてのシステムをループして、特定のシステムへの関数呼び出しをハードコーディングすることなくそれらを更新できます。

これにより、システムは独自の孤立した世界のようにより多く生きることができます。ゲームエンジンは、それらについて具体的に(具体的に)知る必要がなくなりました。そして、物理システムは更新関数が呼び出される場合があります。その時点で、中央システムから必要なデータをすべてのモーションに入力し、物理を適用して、結果のモーションを出力します。

その後、ネットワークシステムの更新関数が呼び出されることがあります。この時点で、中央データベースから必要なデータが入力され、たとえばソケットデータがクライアントに出力されます。繰り返しになりますが、私が見ている目標は、各システムを可能な限り分離して、外の世界についての最小限の知識で独自の小さな世界に住むことができるようにすることです。これは基本的に、ECSで採用されている種類のアプローチであり、ゲームエンジンの間で人気があります。

ECS

上記の私の考えの多くはECSを中心に展開されており、このデータ指向のデカップリングへのアプローチにより、保守がオブジェクト指向およびCOMベースのシステムよりもはるかに簡単になった理由を合理化しようとしているので、ECSについて少し説明する必要があると思います過去、私がSEについて学んだ、神聖なものすべてに違反したにもかかわらず、また、大規模なゲームを作ろうとするなら、それはあなたにとって非常に理にかなっているかもしれません。したがって、ECSは次のように機能します。

ここに画像の説明を入力してください

上記の図のようにMovementSystemupdate関数の呼び出しが行われる場合があります。この時点でPosAndVelocity、入力するデータとしてコンポーネントを中央データベースに照会する場合があります(コンポーネントは単なるデータであり、機能はありません)。次に、それらをループして、位置/速度を変更し、新しい結果を効果的に出力します。次に、RenderingSystem更新関数が呼び出され、その時点でデータベースPosAndVelocitySpriteコンポーネントが照会され、そのデータに基づいて画像が画面に出力されます。

すべてのシステムは、互いの存在について完全に気づいていないため、それらが何であるかを理解する必要さえありません。 Carです。1つを表すために必要なデータを構成する各システムの関心の特定のコンポーネントを知る必要があるだけです。各システムはブラックボックスのようなものです。データの入力と出力は、外界に関する最小限の知識で行われ、外界も最小限の知識しか持っていません。あるシステムからプッシュして別のシステムからポップするイベントが発生する可能性があります。たとえば、物理システム内の2つのエンティティの衝突により、オーディオに衝突イベントが表示され、サウンドが再生されますが、システムはまだ気づいていません。お互いについて。そして、私はそのようなシステムを非常に簡単に推論することができました。何十ものシステムがあるとしても、それぞれが非常に隔離されているので、それらは私の脳を爆発させたくさせません。あなたは ズームインして特定の1つを操作するときは、全体としてすべての複雑さを考慮する必要があります。そのため、変更の結果を予測することも非常に簡単です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.