コミュニケーターの例に関するプログラムのアーキテクチャー

プログラムのアーキテクチャを設計した経験を共有したいと思います。 アーキテクチャは、複雑な内部構造と多数の内部接続を持つプロジェクトにとって非常に重要です。 解決方法の選択に誤りがあると、プロジェクトのさらなる開発が強く必要になり、雪崩のような困難とエラーの成長につながります。 関係のもつれを解くよりも、最初からすべてを書く方が簡単な瞬間さえあるかもしれません。

画像

例として、シングルユーザーアプリケーションのかなり単純なアーキテクチャを取り上げます。 たとえば、コミュニケーターはネットワーク通信用のプログラムであり、多くの異なるプロトコルをサポートし、その外観を変更でき、新しい機能の追加やさらなる開発にオープンである必要があります。





どこから始めるか



手始めに、コード設計のルールに従うことをお勧めします。 これも非常に重要です。正確なコードは読みやすく、多くの設計エラーは簡単に見つけて修正できるためです。 良い例がここにあります 。 主なことを思い出させてください:



1.変数、関数、クラス、およびそれらのメソッドの「話す」名前。 ある名前には、それが何であるかが明確でした。 純粋にローカルな変数(サイクルカウンター、中間値)に対して例外を作成できます。



2.インデントされたブロックの内容(条件、サイクル)、ブロックの開始と終了は同じレベルである必要があります。



3.コメントはコードを説明していますが、エンコーダーの雰囲気を反映していません。



私の例では、実際の構文を参照せずにJavaScriptに似た疑似言語を使用します。 その目的は、既成のコードを提供するのではなく、アイデアを示すためです。 ソースコードハイライターで強調表示されたコード



最初の試み



シンプルにしよう。 たとえば、プログラムには次のものがあります。



*メインウィンドウ

*設定ウィンドウ



画像

すべてがシンプルに思えます。 メインウィンドウのモジュールでは、サーバーに接続するためのコード、サーバーにメッセージを送信し、サーバーからのメッセージを解析する機能を記述します。 設定ウィンドウから設定を取得します。 そして、設定ウィンドウで、ファイルから設定を読み書きします。 さらに一歩進んでみましょう。



*メッセージングウィンドウ

*連絡先リストウィンドウ



画像

同時に、連絡先リストウィンドウは、メインウィンドウと互いの両方にネストできます。 これも解決可能です。すべて一緒にリンクできます。 しかし、1つの要素にわずかな変更があったとしても、すべての関係を追跡する必要があります。 そして、プログラム要素が多いほど、関係はますます混乱します。 プラグイン、異なるプロトコル、言語のサポートを追加すると、プロジェクト全体ががんになります。



になる方法



関係の数を減らして、整理してみましょう。



直接ではなく、ツリーの形で関係を構築します。 ツリーのルートにあるのはプログラムの中核であり、他のすべての関係はこのツリーを通過します。 ツリーの異なるブランチ間の直接的な関係は避けてください。 新しい機能が必要な場合は、その定義をカーネルに追加します。 実装は別のブランチになります。



画像



個別のユーザーインターフェイス(UI)、ロジック、およびデータ。 UIはデータマッピングであり、リポジトリではありません。 ロジックはUIとデータを接続します。 たとえば、設定ウィンドウにはデータがなく、すべての設定は構成に保存され、設定ウィンドウにはそれらのみが表示されます。



直接的なコード呼び出し(モジュール関数を呼び出すカーネル関数を呼び出す)を避け、メッセージ(受信者が取得するメッセージを作成する)またはコマンドキュー(コマンドをキューに入れ、カーネルが処理する)を使用することをお勧めします。 これにより、プログラムがマルチタスクになり、内部エラーとクラッシュが発生しやすくなります。



画像



インターフェイスのオープン性と下位互換性を可能な限り維持します。 パラメータの固定セットを持つ特定のグローバル関数がある場合、その定義を変更すると、それを使用するすべてのモジュールの再作業が必要になります。 これはプログラム内では重要ではありません;最新のIDEにはこのための優れたツールがあります。 そして、プラグインインターフェイスを変更すると、すべての古い開発が機能しなくなります。



機能の互換性を維持する方法は多数あります。 古い関数に似た新しい関数を作成し、古い関数をそのまま残すことができます。 たとえば、ExecCommand(CmdText)およびExecCommand2(Cmd、Params)。 渡すことができるパラメーターは2つだけです。パラメーターを持つ構造体へのポインターと、構造体のバージョンです。 オープン構造を使用できます。 私は、コマンドラインの形式でパラメータを渡すオプションを個人的に決めました。



方法



公開データとオブジェクトはカーネルで定義されます。 たとえば、プログラム設定が保存され、どこからでもアクセスできるようにする必要がある構成。 この場合、新しいオプションを自由に追加できるように、構成を開いておく必要があります。 構成を連想配列(名前と値)の形式で使用します。名前と値は文字列の形式です。 これにより、互換性とセキュリティが100%保証されますが、パフォーマンスがいくらか低下します(名前で値を見つけ、値の文字列を必要な型に変換するため)。 また、コンポーネントがSQLサーバーの特定の実装へのバインドを持たず、システム全体をやり直すことなくSQLサーバーを変更できるように、カーネルを介してSQLを使用する必要があります。 または、カーネルを介してオブジェクトを取得し、任意のテーブルとクエリを処理できます。



//

function SomeIPClient.Connect();

{

// ,

// ,

//

UserName = Core.Config[ 'UserName' ];

Password = Core.Config[ 'Password' ];

IPConnection.Open(Host, Port, UserName, Password);

}









MVCパターン(model-view-behavior)を実装します。 たとえば、テキストメッセージの入力フィールドと「送信」ボタンがあります。 ボタンを押してもすぐに送信コードが開始されることはありません。送信にはかなりの時間がかかる場合があり、プログラムは常に「ハング」します。 また、送信コードが入力フィールドからデータを取得する場合、ユーザーがこのデータを変更したり、ウィンドウを閉じたりすることができた可能性があります。 したがって、ユーザーが実行するすべてのアクションは、メッセージまたはコマンドに減らす必要があります。 これを行うために、コマンド、送信者、受信者の名前とパラメーターを受け取る関数をカーネルに提供できます。 または、さまざまな基本アクションのための一連の機能-設定ウィンドウを開く、プログラムを最小化または閉じる、サウンドを再生するなど...単純なプログラムでは、メッセージなしで機能のみを実行できます。 ただし、まだ公開されているすべての関数はカーネル内に存在する必要があるため、異なるプログラムコンポーネント間に直接的な関係はありません。



//

function OnCloseWindowClick();

{

Core.CloseWindow(CurrentWindowID);

}



function ExecCmd(CommandText);

{

if (CommandText = 'WINDOW_CLOSE' ) CloseWindow();

}



//

function Core.CloseWindow(WindowID);

{

Core.CmdQueue.Add( 'WINDOW_CLOSE ' + WindowID);

}








この例では、閉じるボタンを押しても、ウィンドウはすぐには閉じませんが、ウィンドウ識別子とともにコマンドがカーネルに送信されます。 これにより、ところで、コードチェーンの実行が終了し、プログラム内の別のコマンドがコマンド自体を処理できます。 この場合、カーネルは、ウィンドウを閉じているすべての(または関心のある)モジュールにメッセージを送信して、ウィンドウを閉じることに関連する一般的なアクションを実行できます。 そして、その後ウィンドウを閉じるように指示します。 コマンドを処理する際にオーバーヘッドが発生しますが、完全な柔軟性と制御。



サーバー通信モジュールや通信プロトコルパーサーなどの自律プログラムコンポーネントは、カーネルを介して直接ではなく、直接相互にやり取りできます。 ただし、コアに順番にドッキングする必要があります。 つまり、モジュールの特定の平均インターフェイスがカーネルにドッキングされます。これは、内部実装の機能に依存しません。 これは、たとえば、カーネルメッセージを受信して​​コマンドを実行するグ​​ローバルクラスの相続人です。 そして、このクラス全体でさえ、複雑さの度合いが異なる個別のプログラムでさえ隠すことができます。 これにより、システム全体の高いスケーラビリティと柔軟性が実現します。新しい機能を追加でき、プログラムの半分を書き換える必要はありません。新しい機能とカーネルの互換性を確保するのに十分です。



//

IPClient = class () // "" ,

{

integer ID;

string Host;

string Port;

virtual function Connect();

virtual function Disconnect();

virtual function SendData(Data);

}



//

SomeIPClient = class (Core.CIPClient)

{

function Connect();

function Disconnect();

function SendData(Data);

function ParseServerData(Data);

}









この例では、コンポーネントは、サーバーとの通信が実装されるグローバルクラスCore.IPClientの継承者を定義します。 そして、そのようなコンポーネントは多数存在する可能性があり、カーネルはそれらをIDで区別します。 コンポーネントオブジェクトは、ディスパッチャ(マネージャ)に格納されます-格納されたコンポーネントにアクセスして管理するためのメソッドを持つオブジェクトのリスト。 したがって、頭痛のないより多くの新しいコンポーネントを追加できます-ディスパッチャがそれらの操作を処理します。 コンポーネント構造は、別の大きなトピックです。



//

//

//

// ,

// . .

function StartComponent()

{

NewComponent = New CSomeComponent;

Core.ComponentManager.Add(NewComponent); //



NewWindow = New CSomeComponentWindow;

Core.WindowManager.Add(NewWindow); //

}



//

//

// .

// , 'WINDOW_CLOSE 15';

function Core.WindowManager.ProccesCmd(CmdText)

{

Cmd = GetParam(CmdText, 0); // 'WINDOW_CLOSE'

WindowID = GetParam(CmdText, 1); // (ID ) '15'

Window = Self.GetWindowByID(WindowID); // ID

Window.ExecCmd(Cmd); //

}









その結果、コンポーネントオブジェクトを収集し、それらの間でメッセージとコマンドを送信するカーネルを取得します。 さらに、各コンポーネントは個別のスレッドで動作することができ、コンポーネントでエラーが発生した場合、プログラム全体がこれから外れることはありません-コンポーネントを閉じて再起動できます。 外出先でもコンポーネントのプラグを切断してデバッグできます。 もちろん、このようなアーキテクチャには欠点があります。 基本的に、これらは内部コマンドを処理するためのコンピューター時間の非生産的な損失です。 したがって、一部の操作を高速化するために、コマンドの個別の優先度キューまたは関数の直接呼び出しを使用できます。 または、モジュールを直接操作できるように、モジュールに別のモジュールのオブジェクトへの直接リンクを渡します。 これにより、関係スキームが複雑になり、信頼性が低下しますが、パフォーマンスは向上します。



All Articles