Я работаю над перефакторингом определенных аспектов существующего веб-сервиса. Реализация сервисных 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);
}
}
источник
Ответы:
Основная причина использования конструкции конвейера заключается в том, что вы хотите разделить этапы. Либо потому, что один этап может использоваться в нескольких конвейерах (например, инструменты оболочки Unix), либо потому, что вы получаете некоторое преимущество в масштабировании (то есть вы можете легко перейти от одноузловой архитектуры к многоузловой архитектуре).
В любом случае каждому этапу конвейера нужно дать все, что ему нужно для выполнения своей работы. Нет причин, по которым вы не можете использовать внешнее хранилище (например, базу данных), но в большинстве случаев лучше передавать данные с одного этапа на другой.
Однако это не означает, что вы должны или должны передавать один большой объект сообщения с каждым возможным полем (хотя см. Ниже). Вместо этого каждый этап в конвейере должен определять интерфейсы для своих входных и выходных сообщений, которые идентифицируют только те данные, которые нужны этапу.
Тогда у вас будет большая гибкость в реализации ваших реальных объектов сообщений. Один из подходов заключается в использовании огромного объекта данных, который реализует все необходимые интерфейсы. Другое - создать классы-обертки вокруг простого
Map
. Еще один способ - создать класс-оболочку для базы данных.источник
Есть несколько мыслей, которые приходят на ум, во-первых, у меня недостаточно информации.
Ответы, вероятно, заставят меня подумать о дизайне более тщательно, однако, исходя из того, что вы сказали, есть 2 подхода, которые я, вероятно, рассмотрю в первую очередь.
Структурируйте каждую стадию как свой собственный объект. На n-м этапе список делегатов будет состоять из этапов от 1 до n-1. Каждый этап инкапсулирует данные и обработку данных; уменьшение общей сложности и полей внутри каждого объекта. Вы также можете получить более поздние этапы доступа к данным по мере необходимости с более ранних этапов, пройдя через делегатов. У вас все еще есть довольно тесная связь между всеми объектами, потому что важны результаты этапов (то есть всех атрибутов), но они значительно уменьшены, и каждый этап / объект, вероятно, более читабелен и понятен. Вы можете сделать его потокобезопасным, сделав список делегатов ленивым и используя потокобезопасную очередь, чтобы заполнить список делегатов в каждом объекте по мере необходимости.
В качестве альтернативы я бы сделал что-то похожее на то, что вы делаете. Массивный объект данных, который проходит через функции, представляющие каждый этап. Это часто намного быстрее и легче, но более сложно и подвержено ошибкам, потому что это просто большая куча атрибутов данных. Очевидно, не потокобезопасный.
Честно говоря, я делал это позже для ETL и некоторых других подобных проблем. Я был сосредоточен на производительности из-за объема данных, а не удобства обслуживания. Кроме того, они были единственными, которые больше не использовались бы.
источник
Это похоже на Chain Pattern в GoF.
Хорошей отправной точкой было бы посмотреть, что делает обыкновенная цепочка .
источник
Первое решение, которое я могу себе представить, - сделать шаги явными. Каждый из них становится объектом, способным обработать фрагмент данных и передать его следующему объекту процесса. Каждый процесс производит новый (в идеале неизменяемый) продукт, так что взаимодействие между процессами отсутствует, а затем нет риска из-за обмена данными. Если некоторые процессы занимают больше времени, чем другие, вы можете разместить буфер между двумя процессами. Если вы правильно используете планировщик для многопоточности, он выделит больше ресурсов для очистки буферов.
Второе решение может заключаться в том, чтобы думать «сообщение» вместо конвейера, возможно, с помощью специализированной структуры. Затем у вас есть «актеры», получающие сообщения от других актеров и отправляющие другие сообщения другим акторам. Вы организуете своих актеров в конвейер и передаете свои первичные данные первому актеру, который инициирует цепочку. Обмен данными отсутствует, поскольку обмен заменяется отправкой сообщений. Я знаю, что актерскую модель Scala можно использовать в Java, поскольку здесь нет ничего специфичного для Scala, но я никогда не использовал ее в программах на Java.
Решения похожи, и вы можете реализовать второй с первым. По сути, основные концепции состоят в том, чтобы иметь дело с неизменяемыми данными, чтобы избежать традиционных проблем, связанных с совместным использованием данных, и создать явные и независимые объекты, представляющие процессы в вашем конвейере. Если вы удовлетворяете этим условиям, вы можете легко создавать простые и понятные конвейеры и использовать их в параллельной программе.
источник