14

大きな「状態のみ」のオブジェクトをリファクタリングするための一般的な戦略は何ですか?

私は、国の空域のオンラインモデリング/シミュレーションを行う特定のソフトリアルタイム意思決定支援システムに取り組んでいます。このソフトウェアは、多数のライブデータフィードを消費し、空域内の多数のエンティティの「状態」の1分に1回の推定値を生成します。問題は、現在最も低いレベルのエンティティに到達するまで、きちんと分解されます。

私たちの数学的モデルは、これらの各エンティティの過去と未来の数時間のタイムラインで、およそ1分に1回、50以上のパラメーターを推定/予測します。現在、これらのレコードは、多くのフィールドを持つ単一のJavaクラスとしてエンコードされています(一部はに折りたたまれますArrayList)。私たちのモデルは進化しており、フィールド間の依存関係はまだ確定されていません。そのため、各インスタンスは複雑なモデルをさまよって、設定を積み上げていきます。

現在、次のようなものがあります。これは、ビルダーパターンアプローチを使用してレコードのコンテンツを構築し、既知の依存関係を強制します(モードを進化させる際のプログラマーエラーに対するチェックとして)。見積もりが完了したら、.build()typeメソッドを使用して、以下を不変の形式に変換します。

final class OneMinuteEstimate {

  enum EstimateState { INFANT, HEADER, INDEPENDENT, ... };
  EstimateState state = EstimateState.INFANT; 

  // "header" stuff
  DateTime estimatedAtTime = null;
  DateTime stamp = null;
  EntityId id = null;

  // independent fields
  int status1 = -1;
  ...

  // dependent/complex fields...
  ... goes on for 40+ more fields... 

  void setHeaderFields(...)
  {
     if (!EstimateState.INFANT.equals(state)) {
        throw new IllegalStateException("Must be in INFANT state to set header");
     }

     ... 
  }

}

非常に多くのこれらの見積もりが完了すると、それらはタイムラインにまとめられ、そこで集計パターン/傾向が分析されます。組み込みデータベースの使用を検討しましたが、パフォーマンスの問題に苦労しました。むしろ、これをデータモデリングの観点から整理してから、ソフトリアルタイムコードの一部を埋め込みデータストアに段階的に移動したいと考えています。

これの「時間に敏感な」部分が完了すると、製品はフラットファイルとデータベースにフラッシュされます。

問題:

  • それは巨大なクラスであり、フィールドが多すぎます。
  • クラスにエンコードされた動作はほとんどありません。ほとんどの場合、データフィールドのホルダーです。
  • メソッドの保守build()は非常に面倒です。
  • 多数の依存するモデリングコンポーネントがデータオブジェクトに適切に入力されるようにするためだけに「ステートマシン」の抽象化を手動で維持するのは不器用ですが、モデルが進化するにつれて多くのフラストレーションを軽減できます。
  • 特に、上記のレコードが非常に類似した「ロールアップ」に集約された場合、多くの重複があります。これは、時系列での上記の構造のローリング合計/平均またはその他の統計積に相当します。
  • 一部のフィールドはまとめることができますが、それらはすべて論理的に互いに「ピア」であり、私たちが試したブレークダウンでは、動作/ロジックが人為的に分割され、間接的に2つのレベルに到達する必要があります。

すぐに使えるアイデアは楽しませてくれましたが、これは段階的に進化させる必要があるものです。他の誰かが言う前に、そのモデルのデータ表現を把握するのがこれほど難しい場合、私たちの数学モデルは十分に鮮明ではないことを示唆する可能性があることに注意します。フェアポイントであり、私たちはそれに取り組んでいますが、それは多くの貢献者と多くの同時仮説が作用している研究開発環境の副作用だと思います。

(重要ではありませんが、これはJavaで実装されています。出力製品にはHSQLDBまたはPostgresを使用します。データベースだけで十分なパフォーマンスの問題があるため、一部は慣れていないため、永続性フレームワークは使用しません。単独で手作業でコーディングされたストレージルーチン...私たちは追加の抽象化に移行することに懐疑的です。)

4

5 に答える 5

5

私はあなたがしたのと同じ問題の多くを抱えていました。

少なくとも私はやったと思います、私がやったように聞こえます。表現は異なりますが、10,000フィートでは、ほとんど同じように聞こえます。離散的な「任意の」変数のクラップロードと、それらの間のアドホックな関係(基本的にはビジネス主導)。現時点で変更される可能性があります。

また、別の問題がありますが、それはパフォーマンス要件でした。速い方が良いように聞こえますが、遅い方がベースラインのパフォーマンス要件を満たせないという理由だけで、遅い方が良いと思われます。

簡単に言うと、私が行ったのは、システム用の単純なドメイン固有のルール言語を設計したことです。

DSLの全体的なポイントは、関係を暗黙的に表現し、それらをモジュールにパッケージ化することでした。

非常に粗雑で不自然な例:

D = 7
C = A + B
B = A / 5
A = 10
RULE 1: IF (C < 10) ALERT "C is less than 10"
RULE 2: IF (C > 5) ALERT "C is greater than 5"
RULE 3: IF (D > 10) ALERT "D is greater than 10"
MODULE 1: RULE 1
MODULE 2: RULE 3
MODULE 3: RULE 1, RULE 2

まず、これは私の構文を代表するものではありません。

しかし、モジュールからわかるように、それは3つの単純なルールです。

ただし、重要なのは、ルール1がAとBに依存するCに依存し、BがAに依存することは明らかです。これらの関係は暗示されています。

したがって、そのモジュールの場合、これらの依存関係はすべて「それに付随します」。モジュール1のコードを生成したかどうかは、次のようになります。

public void module_1() {
    int a = 10;
    int b = a / 5;
    int c = a + b;
    if (c < 10) {
        alert("C is less than 10");
    }
}

一方、モジュール2を作成した場合、取得できるのは次のとおりです。

public void module_2() {
    int d = 7;
    if (d > 10) {
        alert("D is greater than 10.");
    }
}

モジュール3では、「無料」の再利用が表示されます。

public void module_3() {
    int a = 10;
    int b = a / 5;
    int c = a + b;
    if (c < 10) {
        alert("C is less than 10");
    }
    if (c > 5) {
        alert("C is greater than 5");
    }
}

したがって、ルールの「スープ」が1つある場合でも、モジュールは依存関係のベースをルート化するため、関係のないものを除外します。モジュールをつかみ、木を振って、残っているものをぶら下げたままにします。

私のシステムはDSLを使用してソースコードを生成しましたが、ミニランタイムインタープリターを簡単に作成することもできます。

単純なトポロジカルソートで依存関係グラフを処理してくれました。

したがって、これの良い点は、少なくともモジュール間で、最終的に生成されたロジックに不可避の重複があった一方で、ルールベースに重複がなかったことです。開発者/知識労働者としてあなたが維持しているのはルールベースです。

また、方程式を変更でき、副作用についてあまり心配する必要がないのも良い点です。たとえば、do C = A / 2に変更すると、突然、Bが完全に脱落します。ただし、IF(C <10)のルールはまったく変わりません。

いくつかの簡単なツールを使用して、依存関係グラフ全体を表示したり、孤立した変数(Bなど)を見つけたりすることができます。

ソースコードを生成することで、必要なだけ高速に実行されます。

私の場合、ルールが1つの変数を削除し、結果のモジュールから500行のソースコードが消えるのを見るのは興味深いことでした。これは、メンテナンスや開発中に手でクロールして削除する必要がなかった500行です。私がしなければならなかったのは、ルールベースの単一のルールを変更して「魔法」を起こさせることだけでした。

私はいくつかの簡単なのぞき穴の最適化を行い、変数を排除することさえできました。

そんなに難しいことではありません。ルール言語は、XMLまたは単純な式パーサーにすることができます。あなたがしたくないのであれば、それにフルボートYaccまたはANTLRを行く理由はありません。S式用のプラグインを挿入します。文法は必要ありません。脳の死んだ構文解析です。

実際、スプレッドシートは優れた入力ツールにもなります。フォーマットは厳密に行ってください。SVNにマージするのはちょっと面倒ですが(つまり、Do n't Do That)、エンドユーザーはそれを気に入っています。

あなたは実際のルールベースのシステムで逃げることができるかもしれません。私のシステムは実行時に動的ではなく、高度なゴールシークや推論を実際に必要としなかったため、そのようなシステムのオーバーヘッドは必要ありませんでした。しかし、箱から出してすぐに使えるなら、幸せな一日です。

ああ、そして実装上の注意として、Javaメソッドで64Kコードの制限に達することができると信じていない人のために、私はそれができることを保証できます:)。

于 2011-03-30T23:24:41.097 に答える
3

ソフトなリアルタイムのパフォーマンス制約(場合によってはモンスターの脂肪クラス)を伴うR&D関連の作業の経験から、ORマッパーを使用しないことをお勧めします。このような状況では、「金属に触れる」ことを扱い、JDBC結果セットを直接操作する方がよいでしょう。これは、ソフトなリアルタイムの制約があり、パッケージごとに大量のデータ項目があるアプリに対する私の提案です。さらに重要なことに、永続化する必要のある個別のクラス(クラスインスタンスではなくクラス定義)の数が多く、仕様にメモリの制約がある場合は、HibernateのようなORMも避けたいと思うでしょう。

元の質問に戻ります。

あなたが抱えていると思われるのは、1)複数のデータ項目をOOモデルにマッピングすること、および2)そのような複数のデータ項目がグループ化または分離の良い方法を示さないことの典型的な問題です(そしてグループ化の試みは単に正しく感じられない傾向があります。 )ドメインモデルがそのような集約に適さない場合があり、そうするための人工的な方法を考え出すことは、通常、すべての設計要件と要望を満たさない妥協に終わります。

さらに悪いことに、OOモデルでは通常、すべてのアイテムがクラスのフィールドとしてクラスに存在する必要があります。このようなクラスは通常、動作がないため、-のようstructな構造、別名data envelopeまたはdata shuttleです。

しかし、そのような状況では、次の質問があります。

アプリケーションは、常に40、50以上のデータ項目すべてを一度に読み取り/書き込みする必要がありますか? *すべてのデータ項目が常に存在する必要がありますか?*

問題のドメインの詳細はわかりませんが、一般的に、すべてのデータ項目を一度に処理する必要はほとんどないことがわかりました。これは、テーブルからすべての行を一度にクエリする必要がないため、リレーショナルモデルが優れているところです。問題のテーブル/ビューの投影として必要なものだけをプルします。

潜在的に多数のデータ項目がある、平均してネットワークに渡されるデータ項目の数が最大数より少ない状況では、プロパティパターンを使用する方がよいでしょう。

すべてのアイテムを保持するモンスターエンベロープクラスを定義する代わりに:

// java pseudocode
class envelope
{
   field1, field2, field3... field_n;
   ...
   setFields(m1,m2,m3,...m_n){field1=m1; .... };
   ...
}

辞書を定義します(たとえば、マップに基づいて):

// java pseudocode
public enum EnvelopeField {field1, field2, field3,... field_n);

interface Envelope //package visible
{
   // typical map-based read fields.
   Object get(EnvelopeField  field);
   boolean isEmpty();

   // new methods similar to existing ones in java.lang.Map, but
   // more semantically aligned with envelopes and fields.
   Iterator<EnvelopeField> fields();
   boolean hasField(EnvelopeField field); 
}

// a "marker" interface
// code that only needs to read envelopes must operate on
// these interfaces.
public interface ReadOnlyEnvelope extends Envelope {} 

// the read-write version of envelope, notice that
// it inherits from Envelope, but not from ReadOnlyEnvelope.
// this is done to make it difficult (but not impossible
// unfortunately) to "cast-up" a read only envelope into a
// mutable one.
public interface MutableEnvelope extends Envelope
{
   Object put(EnvelopeField field); 

   // to "cast-down" or "narrow" into a read only version type that
   // cannot directly be "cast-up" back into a mutable.
   ReadOnlyEnvelope readOnly();
}

// the standard interface for map-based envelopes.
public interface MapBasedEnvelope extends 
   Map<EnvelopeField,java.lang.Object>
   MutableEnvelope
{
}

// package visible, not public
class EnvelopeImpl extends HashMap<EnvelopeField,java.lang.Object> 
  implements MapBasedEnvelope, ReadOnlyEnvelope
{
   // get, put, isEmpty are automatically inherited from HashMap
   ... 
   public Iterator<EnvelopeField> fields(){ return this.keySet().iterator(); }
   public boolean hasField(EnvelopeField field){ return this.containsKey(field); }

   // the typecast is redundant, but it makes the intention obvious in code.
   public ReadOnlyEnvelope readOnly(){ return (ReadOnlyEnvelope)this; }
}

public class final EnvelopeFactory
{
    static public MapBasedEnvelope new(){ return new EnvelopeImpl(); }
}

read-only内部フラグを設定する必要はありません。必要なのは、エンベロープインスタンスをインスタンスとしてダウンキャストするEnvelopeことだけです(ゲッターのみを提供します)。

読み取りを期待するコードは読み取り専用エンベロープで動作する必要があり、フィールドの変更を期待するコードは可変エンベロープで動作する必要があります。実際のインスタンスの作成は、工場で区分化されます。

つまり、コンパイラを使用して、コード規則、どのインターフェイスをどこでどのように使用するかを管理するルールを確立することにより、読み取り専用にする(または変更可能にする)ようにします。

読み取るだけでよいコードとは別に、書く必要があるセクションにコードを階層化できます。それが完了すると、単純なコードレビュー(またはgrep)で、間違ったインターフェイスを使用しているコードを特定できます。)

問題:

非公開の親インターフェース:

Envelope誤った/悪意のあるコードが読み取り専用エンベロープをベースエンベロープにキャストしてから可変エンベロープに戻すことを防ぐために、パブリックインターフェイスとして宣言されていません。意図されたフローは、可変から読み取り専用です-双方向であることを意図していません。

ここでの問題は、の拡張子がEnvelopeそれを含むパッケージに制限されていることです。それが問題であるかどうかは、特定のドメインと使用目的によって異なります。

工場:

問題は、工場が非常に複雑になる可能性がある(そしておそらくそうなる)ということです。繰り返しますが、獣の性質。

検証:

このアプローチで発生するもう1つの問題は、フィールドXが存在することを期待するコードについて心配する必要があることです。元のモンスターエンベロープクラスがあると、少なくとも構文的にはすべてのフィールドが存在するため、その心配から部分的に解放されます...

...フィールドが設定されているかどうかにかかわらず、それは私が提案しているこの新しいモデルにまだ残っている別の問題でした。

したがって、フィールドXが表示されることを期待するクライアントコードがある場合、フィールドが存在しない場合(または、コンピューターに、または何らかの方法で適切なデフォルトを読み取る場合)、クライアントコードは何らかの種類の例外をスローする必要があります。

  1. フィールドプレゼンスのパターンを特定します。フィールドXが存在することを期待するクライアントは、他のフィールドが存在することを期待するクライアントとは別にグループ化(階層化)される場合があります。

  2. 一部のルール(プログラムで、インタープリター、またはルールエンジンで提供されるルール)に従って、例外をスローするか、欠落しているフィールドのデフォルト値を計算するカスタムバリデーター(読み取り専用エンベロープインターフェイスへのプロキシ)を関連付けます。

タイピングの欠如:

これは議論の余地があるかもしれませんが、静的型付けで作業していた人々は、大まかに型付けされたマップベースのアプローチに行くことによって静的型付けの利点を失うことに不安を感じるかもしれません。これに対する反論は、ほとんどのWebは、Java側(JSTL、EL)でも、緩い型付けアプローチで機能するというものです。

問題はさておき、可能なフィールドの最大数が多く、常に存在するフィールドの平均数が少ないほど、このアプローチが最も効果的なパフォーマンスになります。コードがさらに複雑になりますが、それが獣の性質です。

その複雑さは解消されず、クラスモデルまたは検証コードのいずれかに存在します。ただし、特に大量の個別データ転送が予想される場合は、シリアル化とネットワークへの転送の方がはるかに効率的です。

それが役に立てば幸い。

于 2011-03-31T00:09:26.893 に答える
3

ラージデータオブジェクトの分割は、ラージリレーショナルテーブルの正規化(第1および第2正規形)と非常によく似ています。ルールに従って、少なくとも2番目の正規形に到達すると、元のクラスが適切に分解される可能性があります。

于 2011-03-30T22:36:43.700 に答える
1

実際、これはゲーム開発者が直面する頻繁な問題のように見えます。深い継承ツリーなどのために、多数の変数とメソッドを保持する肥大化したクラスです。

継承よりもコンポジションを選択する方法と理由についてのこのブログ投稿があります。おそらくそれが役立つでしょう。

于 2011-03-30T21:19:14.987 に答える
0

大規模なデータクラスをインテリジェントに分割できる1つの方法は、クライアントクラスによるアクセスのパターンを調べることです。たとえば、クラスのセットがフィールド1〜20にのみアクセスし、別のクラスのセットがフィールド25〜30にのみアクセスする場合、これらのフィールドのグループは別々のクラスに属している可能性があります。

于 2011-04-05T00:58:46.140 に答える