Хорошие стратегии реализации для инкапсуляции общих данных в программный конвейер

13

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

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

Я пролистывал книгу по шаблонам дизайна Gang of Four, чтобы найти какое-то вдохновение, но я не чувствовал, что там есть какое-то решение (Memento был несколько в том же духе, но не совсем). Я также посмотрел онлайн, но в тот момент, когда вы выполняете поиск по «конвейеру» или «рабочему процессу», вы получаете информацию о каналах Unix или запатентованные механизмы и платформы рабочих процессов.

У меня вопрос - как бы вы подошли к вопросу записи состояния выполнения конвейера программной обработки, чтобы в более поздних задачах использовалась информация, вычисленная ранее? Я предполагаю, что основное отличие от конвейеров Unix состоит в том, что вы не просто заботитесь о результатах непосредственно предшествующей задачи.


В соответствии с просьбой, некоторые псевдокод для иллюстрации моего варианта использования:

Объект "конвейерного контекста" имеет несколько полей, которые могут заполнять / читать различные шаги конвейера:

public class PipelineCtx {
    ... // fields
    public Foo getFoo() { return this.foo; }
    public void setFoo(Foo aFoo) { this.foo = aFoo; }
    public Bar getBar() { return this.bar; }
    public void setBar(Bar aBar) { this.bar = aBar; }
    ... // more methods
}

Каждый из этапов конвейера также является объектом:

public abstract class PipelineStep {
    public abstract PipelineCtx doWork(PipelineCtx ctx);
}

public class BarStep extends PipelineStep {
    @Override
    public PipelineCtx doWork(PipelieCtx ctx) {
        // do work based on the stuff in ctx
        Bar theBar = ...; // compute it
        ctx.setBar(theBar);

        return ctx;
    }
}

Точно так же для гипотетического FooStep, которому может потребоваться Bar, вычисленный BarStep перед этим, наряду с другими данными. И тогда у нас есть настоящий вызов API:

public class BlahOperation extends ProprietaryWebServiceApiBase {
    public BlahResponse handle(BlahRequest request) {
        PipelineCtx ctx = PipelineCtx.from(request);

        // some steps happen here
        // ...

        BarStep barStep = new BarStep();
        barStep.doWork(crx);

        // some more steps maybe
        // ...

        FooStep fooStep = new FooStep();
        fooStep.doWork(ctx);

        // final steps ...

        return BlahResponse.from(ctx);
    }
}
Русланд
источник
6
не пересекать пост, но помечать мод для перемещения
трещотка урод
1
Будет ли идти вперед, я думаю, мне следует больше времени уделять ознакомлению с правилами. Благодарность!
RuslanD
1
Вы избегаете какого-либо постоянного хранения данных для своей реализации, или что-то может быть захвачено на этом этапе?
CokoBWare
1
Привет РусланД и добро пожаловать! Это действительно больше подходит для программистов, чем переполнение стека, поэтому мы удалили версию SO. Помните о том, что упомянул @ratchetfreak, вы можете отметить модерацию и задать вопрос, который нужно перенести на более подходящий сайт, не нужно переходить пост. Основное правило при выборе между двумя сайтами заключается в том, что программисты решают проблемы, с которыми вы сталкиваетесь, когда вы проектируете свои проекты перед доской, а переполнение стека - более техническими проблемами (например, проблемами с реализацией). Для более подробной информации смотрите наш FAQ .
Яннис
1
Если вы измените архитектуру на DAG (ориентированный ациклический граф) вместо конвейера, вы можете явно передать результаты предыдущих шагов.
Патрик

Ответы:

4

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

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

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

Тогда у вас будет большая гибкость в реализации ваших реальных объектов сообщений. Один из подходов заключается в использовании огромного объекта данных, который реализует все необходимые интерфейсы. Другое - создать классы-обертки вокруг простого Map. Еще один способ - создать класс-оболочку для базы данных.

Парсифаль
источник
1

Есть несколько мыслей, которые приходят на ум, во-первых, у меня недостаточно информации.

  • Каждый шаг производит данные, используемые вне конвейера, или мы заботимся только о результатах последнего этапа?
  • Много ли проблем с большими данными? то есть. проблемы с памятью, скорость и т. д.

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

Структурируйте каждую стадию как свой собственный объект. На n-м этапе список делегатов будет состоять из этапов от 1 до n-1. Каждый этап инкапсулирует данные и обработку данных; уменьшение общей сложности и полей внутри каждого объекта. Вы также можете получить более поздние этапы доступа к данным по мере необходимости с более ранних этапов, пройдя через делегатов. У вас все еще есть довольно тесная связь между всеми объектами, потому что важны результаты этапов (то есть всех атрибутов), но они значительно уменьшены, и каждый этап / объект, вероятно, более читабелен и понятен. Вы можете сделать его потокобезопасным, сделав список делегатов ленивым и используя потокобезопасную очередь, чтобы заполнить список делегатов в каждом объекте по мере необходимости.

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

Честно говоря, я делал это позже для ETL и некоторых других подобных проблем. Я был сосредоточен на производительности из-за объема данных, а не удобства обслуживания. Кроме того, они были единственными, которые больше не использовались бы.

dietbuddha
источник
1

Это похоже на Chain Pattern в GoF.

Хорошей отправной точкой было бы посмотреть, что делает обыкновенная цепочка .

Популярная методика организации выполнения сложных потоков обработки - это шаблон «Цепочка ответственности», описанный (среди многих других мест) в классической книге шаблонов проектирования «Бригада четырех». Хотя основные контракты API, необходимые для реализации этого шаблона проектирования, предельно просты, полезно иметь базовый API, который облегчает использование шаблона и (что более важно) поощряет составление реализаций команд из множества различных источников.

С этой целью Цепной API моделирует вычисления как серию «команд», которые могут быть объединены в «цепочку». API для команды состоит из единственного метода ( execute()), которому передается параметр «context», содержащий динамическое состояние вычисления, и возвращаемое значение которого является логическим значением, которое определяет, была ли завершена обработка для текущей цепочки ( true) или следует ли делегировать обработку следующей команде в цепочке (false).

Абстракция «контекст» предназначена для изоляции реализаций команд от среды, в которой они выполняются (например, команды, которые можно использовать в сервлете или портлете, без привязки непосредственно к контрактам API любой из этих сред). Для команд, которым необходимо распределить ресурсы перед делегированием, а затем освободить их по возвращении (даже если команда с делегированием выдает исключение), расширение «filter» для «command» предоставляет postprocess()метод для этой очистки. Наконец, команды могут быть сохранены и найдены в «каталоге», что позволяет отложить принятие решения о том, какая команда (или цепочка) фактически выполняется.

Чтобы максимизировать полезность API шаблонов цепочки ответственности, контракты фундаментальных интерфейсов определяются способом с нулевыми зависимостями, отличными от соответствующего JDK. Предоставляются удобные реализации базовых классов этих API, а также более специализированные (но необязательные) реализации для веб-среды (т. Е. Сервлетов и портлетов).

Учитывая, что реализации команд спроектированы в соответствии с этими рекомендациями, целесообразно использовать API цепочки ответственности в «фронт-контроллере» инфраструктуры веб-приложений (например, Struts), но также иметь возможность использовать ее в бизнесе. уровни логики и постоянства для моделирования сложных вычислительных требований с помощью композиции. Кроме того, разделение вычислений на дискретные команды, которые работают в контексте общего назначения, позволяет легче создавать команды, которые могут быть проверены модулем, поскольку влияние выполнения команды может быть непосредственно измерено путем наблюдения за соответствующими изменениями состояния в предоставленном контексте ...

Олдрин Лил
источник
0

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

Второе решение может заключаться в том, чтобы думать «сообщение» вместо конвейера, возможно, с помощью специализированной структуры. Затем у вас есть «актеры», получающие сообщения от других актеров и отправляющие другие сообщения другим акторам. Вы организуете своих актеров в конвейер и передаете свои первичные данные первому актеру, который инициирует цепочку. Обмен данными отсутствует, поскольку обмен заменяется отправкой сообщений. Я знаю, что актерскую модель Scala можно использовать в Java, поскольку здесь нет ничего специфичного для Scala, но я никогда не использовал ее в программах на Java.

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

mgoeminne
источник
Эй, я обновил свой вопрос с помощью некоторого псевдокода - у нас на самом деле есть явные шаги.
РусланД