Рефакторинг большого объекта данных - PullRequest
13 голосов
/ 30 марта 2011

Каковы некоторые распространенные стратегии для рефакторинга больших объектов «только для состояния»?

Я работаю над специальной системой поддержки принятия решений в режиме реального времени, которая выполняет онлайн-моделирование / симуляциюнационального воздушного пространства.Эта часть программного обеспечения потребляет несколько потоков данных в реальном времени и выдает ежеминутную оценку "состояния" большого числа объектов в воздушном пространстве.Проблема аккуратно разбирается до тех пор, пока мы не достигнем того, что в настоящее время является сущностью самого низкого уровня.

Наша математическая модель оценивает / прогнозирует свыше 50 параметров на временной шкале в несколько часов в прошлом и будущем для каждого из этих объектов,примерно раз в минуту.В настоящее время эти записи кодируются как один Java-класс с большим количеством полей (некоторые сворачиваются в ArrayList).Наша модель развивается, и зависимости между полями еще не установлены в камне, поэтому каждый экземпляр блуждает по запутанной модели, накапливая настройки по мере продвижения.

В настоящее время у нас есть что-то вроде следующего, в котором используется шаблонный подход для построения содержимого записи и применения известных зависимостей (как проверка на ошибку программиста, так и на развитие режима).оценка сделана, мы преобразуем нижеследующее в неизменяемую форму, используя метод типа .build().

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() чрезвычайно громоздко.
  • Неловко поддерживать абстракцию «конечного автомата» вручную только для того, чтобыбольшое количество зависимых компонентов моделирования правильно заполняет объект данных, но это спасло нас от многих разочарований по мере развития модели.
  • Существует много дублирования, особенно когда записи, описанные выше, объединяются в оченьаналогичные «свертки», которые равны скользящим суммам / средним значениям или другим статистическим продуктам вышеуказанной структуры во временных рядах.
  • Хотя некоторые поля могут быть объединены вместе, все они логически «равны» друг другу,и любой пробой, который мы пробовали, приводил к искусственному разделению поведения / логики и необходимости достигать двух уровней глубоко в косвенном направлении.

Идеи «из коробки» развлекали, но это то, что нам нужно развиватьпостепенно.Прежде чем кто-то еще скажет это, я отмечу, что можно предположить, что наша математическая модель недостаточно четкая, если представление данных для этой модели так трудно получить.Справедливо, и мы работаем над этим, но я думаю, что это побочный эффект от среды НИОКР с большим количеством участников и множеством параллельных гипотез в игре.

(Не то чтобы это важно, ноэто реализовано в Java. Мы используем HSQLDB или Postgres для выходных продуктов. Мы не используем какую-либо постоянную инфраструктуру, частично из-за недостатка знакомства, частично из-за того, что у нас достаточно проблем с производительностью только с одной базой данных и запрограммированными вручную системами хранения... мы скептически относимся к переходу к дополнительной абстракции.)

Ответы [ 5 ]

5 голосов
/ 31 марта 2011

У меня была большая часть той же проблемы, что и вы.

По крайней мере, я так думаю, звучит так, как я.Представление было другим, но на высоте 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 зависит от C, который зависит от A и B, а 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");
    }
}

Итак, хотя у меня есть один «суп» правил, Модули основывают базу зависимостей и таким образом отфильтровывают вещи, которые им не нужныоколо.Возьмите модуль, встряхните дерево и сохраните то, что осталось.

Моя система использовала DSL для генерации исходного кода, но вы также можете легко создать мини-интерпретатор времени выполнения.

ПростойТопологическая сортировка обработала граф зависимостей для меня.

Итак, приятно то, что в то время как в окончательной сгенерированной логике было неизбежное дублирование, по крайней мере, между модулями, в правиле не было никакого дублированиябаза.То, что вы, как разработчик / специалист по знаниям, поддерживаете базу правил.

Что также приятно, так это то, что вы можете изменить уравнение и не беспокоиться о побочных эффектах.Например, если я меняю do C = A / 2, то внезапно B выпадает полностью.Но правило для IF (C <10) совершенно не меняется. </p>

С помощью нескольких простых инструментов вы можете показать весь график зависимостей, найти потерянные переменные (например, B) и т. Д.

Сгенерировав исходный код, он будет работать так быстро, как вы хотите.

В моем случае было интересно увидеть, как правило отбрасывает одну переменную и видит, как 500 строк исходного кода исчезают изПолученный модуль.Это 500 строк, которые мне не нужно было пролистывать вручную и удалять при обслуживании и разработке.Все, что мне нужно было сделать, это изменить одно правило в моей базе правил и позволить «магии» произойти.

Я даже смог провести некоторую простую оптимизацию глазка и устранить переменные.

Дело не в томтрудно сделать.Ваш язык правил может быть XML или анализатором простых выражений.Нет смысла ездить на полной лодке Yacc или ANTLR, если вы этого не хотите.Я вставлю плагин для S-выражений, грамматика не нужна, разбор мозгов.

Электронные таблицы также являются отличным инструментом ввода.Просто будьте строги в форматировании.Отчасти это отстой для слияния в SVN (так что не делайте этого), но конечным пользователям это нравится.

Возможно, вам удастся избежать реальной системы, основанной на правилах.Моя система не была динамичной во время выполнения, и на самом деле она не нуждалась в сложных поисках целей и логических выводах, поэтому мне не нужны были издержки такой системы.Но если кто-то работает на вас из коробки, то счастливого дня.

О, и для примечания по реализации, для тех, кто не верит, что вы можете достичь предела кода в 64 КБ в методе Java, хорошо, ямогу заверить вас, что это может быть сделано :).

3 голосов
/ 31 марта 2011

Из опыта работы также с научно-исследовательскими разработками с мягкими ограничениями производительности в реальном времени (и иногда с классами жира), я бы предложил НЕ использовать мапперы ИЛИ.В таких ситуациях вам лучше иметь дело с «прикосновением к металлу» и работать непосредственно с наборами результатов JDBC.Это мое предложение для приложений с мягкими ограничениями в реальном времени и огромным количеством элементов данных в пакете.Что еще более важно, если число отдельных классов (не экземпляров классов, а определений классов), которые необходимо сохранить, велико, , и у вас также есть ограничения памяти в ваших спецификациях , вы также захотите избегать ORM, таких как Hibernate..

Возвращаясь к исходному вопросу:

Похоже, у вас есть типичная проблема: 1) отображение нескольких элементов данных в модели ОО и 2) такие несколько элементов данных не отображаютсяхороший способ группировки или сегрегации (и любая попытка группировки имеет тенденцию просто не чувствовать себя правильным.) Иногда модель предметной области не подходит для такой агрегации и обычно предлагает искусственный способ сделать этозаканчивается компромиссами, которые не удовлетворяют всем требованиям и желаниям проектирования.

Что еще хуже, модель ОО обычно требует / ожидает, чтобы все элементы присутствовали в классе как поля класса.Такой класс обычно не имеет поведения, поэтому это просто 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 ограничено пакетом, в котором оно находится.Будет ли это проблемой, будет зависеть от конкретного домена и предполагаемого использования.

Фабрики:

Проблема в том, что фабрики могут (и, скорее всего, будут) очень сложными.Опять природа зверя.

Проверка:

Другая проблема, связанная с этим подходом, заключается в том, что теперь вам нужно беспокоиться о коде, который ожидает присутствия поля X.Наличие исходного класса конвертов монстров частично освобождает вас от этого беспокойства, потому что, по крайней мере, синтаксически, все поля есть ...

... независимо от того, установлены поля или нет, это был другой вопрос, который все еще остается сэту новую модель, которую я предлагаю.

Так что, если у вас есть клиентский код, который ожидает увидеть поле X, клиентский код должен выдать какой-то тип исключения, если поле отсутствует (или для компьютера или для чтениякак-то разумно по умолчанию.) В таких случаях вам придется

  1. Определить закономерности присутствия поля.Клиенты, которые ожидают, что поле X будет присутствовать, могут быть сгруппированы отдельно (разделены по уровням) от клиентов, которые ожидают присутствие какого-то другого поля.

  2. Связывание пользовательских валидаторов (прокси с интерфейсами конвертов только для чтения)) которые либо генерируют исключения, либо вычисляют значения по умолчанию для пропущенных полей в соответствии с некоторыми правилами (правила предоставляются программно, с интерпретатором или с механизмом правил.)

Отсутствие набора текста:

Это может быть спорным, но люди, привыкшие работать со статической типизацией, могут чувствовать себя неловко, теряя преимущества статической типизации, переходя на слабо типизированный подход на основе карт.Противоположным аргументом этого является то, что большинство веб-сайтов работают по принципу свободной печати, даже на стороне Java (JSTL, EL.)

Проблемы в стороне, чем больше максимальное число возможных полей и тем меньшесреднее количество полей, присутствующих в любой момент времени, наиболее эффективным будет такой подход.Это добавляет дополнительную сложность кода, но это природа зверя.

Эта сложность не исчезнет и будет присутствовать в вашей модели класса или в вашем коде проверки.Сериализация и передача по сети намного эффективнее, особенно, если вы ожидаете огромное количество отдельных передач данных.

Надеюсь, это поможет.

3 голосов
/ 31 марта 2011

Разделение большого объекта данных очень похоже на нормализацию большой реляционной таблицы (первая и вторая нормальная форма).Следуйте правилам, чтобы достичь хотя бы второй нормальной формы, и у вас может получиться хорошее разложение исходного класса.

1 голос
/ 31 марта 2011

На самом деле это похоже на частую проблему, с которой сталкиваются разработчики игр, раздутые классы с многочисленными переменными и методами из-за глубокого дерева наследования и т. Д.

В этом блоге 1004 * о том, как и почемувыбрать композицию вместо наследования, может быть, это поможет.

0 голосов
/ 05 апреля 2011

Один из способов интеллектуального разделения большого класса данных - это посмотреть на схемы доступа клиентских классов. Например, если набор классов обращается только к полям 1-20, а другой набор классов обращается только к полям 25-30, возможно, эти группы полей принадлежат отдельным классам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...