Подтвердить что ты не робот

Рефакторинг большого объекта данных

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

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

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

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

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

4b9b3361

Ответ 1

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

По крайней мере, я думаю, что так и было, звучит так, как я. Представление было другим, но на высоте 10 000 футов звучит почти так же. Крафт дискретных, "произвольных" переменных и куча ad hoc отношений между ними (в основном бизнес-ориентированный), подлежащие изменению в момент уведомления.

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

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

Вся точка 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) не изменяется вообще.

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

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

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

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

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

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

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

О, и для примечания к внедрению для тех, кто не считает, что вы можете поразить предел кода 64K в Java-методе, я могу заверить вас, что это можно сделать:).

Ответ 2

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

Ответ 3

Из опыта работы с R & D с мягкими ограничениями производительности в реальном времени (а иногда и с монстрами), я бы предложил НЕ использовать OR mappers. В таких ситуациях вам лучше будет "касаться металла" и работать непосредственно с наборами результатов JDBC. Это мое предложение для приложений с мягкими ограничениями в реальном времени и огромным количеством элементов данных для каждого пакета. Что еще более важно, если количество отдельных классов (а не экземпляров классов, но определений классов), которые необходимо сохранить, велико, , и у вас также есть ограничения памяти в ваших спецификациях, вы также захотите избежать ORM, например спящий режим.

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

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

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

Фабрики:

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

Validation:

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

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

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

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

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

Отсутствие ввода:

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

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

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

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

Ответ 4

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

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

Ответ 5

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