Хороші стратегії реалізації для інкапсуляції спільних даних у програмному конвеєрі


13

Я працюю над переосмисленням певних аспектів існуючої веб-служби. Те, як реалізуються API сервісних послуг, має своєрідний "конвеєр обробки", де є завдання, які виконуються послідовно. Не дивно, що для пізніших завдань може знадобитися інформація, обчислена попередніми завданнями, і в даний час так це робиться шляхом додавання полів до класу "стан конвеєра".

Я думав (і сподіваюся?), Що існує кращий спосіб обміну інформацією між кроками конвеєра, ніж наявність об'єкта даних із мільйонами полів, деякі з яких мають сенс для деяких етапів обробки, а не для інших. Це було б великим болем зробити цей клас потоком безпечним (я не знаю, чи це було б можливо), немає ніяких причин міркувати про його інваріантів (і це, швидше за все, у нього немає).

Я прогулював книгу зразків дизайну «Банда чотирьох», щоб знайти деяке натхнення, але мені не здалося, що там є рішення (Мементо дещо в тому ж дусі, але не зовсім). Я також дивився в Інтернеті, але вдруге, коли ви шукаєте "трубопровід" або "робочий процес", ви потрапляєте або з інформацією про труби 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, який, можливо, потребуватиме Бар, обчислений 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
Привіт RuslanD і ласкаво просимо! Це дійсно більше підходить для програмістів, ніж переповнення стека, тому ми видалили версію SO. Майте на увазі те, що згадувалося в @ratchetfreak, ви можете позначити увагу на модерацію та задати питання, яке потрібно перенести на більш підходящий сайт, не потрібно перекладати повідомлення. Основне правило вибору між двома сайтами полягає в тому, що програмісти мають проблеми, з якими ви стикаєтесь, коли ви перебуваєте перед дошкою, що розробляє ваші проекти, а переповнення стека - для отримання більш технічних проблем (наприклад, проблем із впровадженням). Більш детально див. Наші поширені запитання .
янніс

1
Якщо ви зміните архітектуру на оброблювану DAG (спрямований ациклічний графік) замість трубопроводу, ви можете явно передати результати попередніх етапів.
Патрік

Відповіді:


4

Основна причина використання конструкції трубопроводу полягає в тому, що ви хочете роз’єднати етапи. Або тому, що один етап може використовуватися в декількох конвеєрах (наприклад, інструменти оболонки Unix), або через те, що ви отримуєте певну користь від масштабування (тобто ви можете легко перейти від архітектури одновузла до архітектури багатовузлів).

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

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

Потім у вас є велика гнучкість у здійсненні ваших фактичних об'єктів повідомлень. Один із підходів - використовувати величезний об’єкт даних, який реалізує всі необхідні інтерфейси. Інше - створити обгорткові класи навколо простого Map. Ще одне - створити клас обгортки навколо бази даних.


1

Є декілька думок, які вискакують на думку, першою з яких є те, що мені не вистачає інформації.

  • Чи кожен крок створює дані, що використовуються поза конвеєром, чи нас цікавлять лише результати останнього етапу?
  • Чи є багато проблем з великими даними? тобто. проблеми з пам’яттю, проблеми з швидкістю тощо

Відповіді, ймовірно, змусять мене більш ретельно замислитися над дизайном, однак виходячи з того, що ви сказали, є два підходи, які я, мабуть, спершу розглянути.

Структуруйте кожен етап як власний об'єкт. П'ятий етап мав би від 1 до n-1 етапів як список делегатів. Кожен етап охоплює дані та обробку даних; зменшення загальної складності та полів в межах кожного об’єкта. Ви також можете на наступних етапах отримати доступ до даних за потребою на набагато більш ранніх етапах шляхом обходу делегатів. У вас все ще є досить щільне з'єднання по всіх об'єктах, оскільки важливі результати етапів (тобто всіх attrs), але вони значно зменшуються, і кожен етап / об'єкт, ймовірно, є більш читабельним і зрозумілим. Ви можете зробити його потоком безпечним, зробивши список делегатів ледачим та використовуючи безпечну чергу для потоків, щоб заповнити список делегатов у кожному об'єкті за потребою.

Як варіант, я б, мабуть, зробив щось подібне до того, що ти робиш. Масивний об'єкт даних, який проходить через функції, що представляють кожен етап. Часто це набагато швидше і невелика вага, але більш складна і схильна до помилок через це просто велика купа атрибутів даних. Очевидно, що не є безпечними для потоків.

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


1

Це схоже на схему ланцюга в GoF.

Хорошим початковим моментом було б ознайомитись із тим, що робить загальнодоступний ланцюжок .

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

З цією метою Chain API моделює обчислення як серію "команд", які можна об'єднати у "ланцюжок". API для команди складається з єдиного методу ( execute()), якому передається параметр "контексту", що містить динамічний стан обчислення, і повернене значення якого є булевим, що визначає, чи завершена обробка поточного ланцюга чи ні ( true), чи обробку слід делегувати наступній команді ланцюга (false).

"Контекстна" абстракція призначена для ізоляції виконання команд від середовища, в якому вони запущені (наприклад, команда, яка може використовуватися або в сервлет, або в портлет, не прив'язуючись безпосередньо до контрактів API будь-якого з цих середовищ). Для команд, яким потрібно виділити ресурси перед делегуванням, а потім випустити їх після повернення (навіть якщо команда deleged-to видає виняток), розширення "filter" на "command" забезпечує postprocess()метод цієї очистки. Нарешті, команди можна зберігати та шукати у "каталозі", щоб дозволити відстрочку рішення щодо того, яка команда (або ланцюг) реально виконується.

Для досягнення максимальної корисності API шаблону відповідальності, основні договори інтерфейсу визначаються таким чином, що мають нульові залежності, відмінні від відповідного JDK. Надані зручності реалізації базових класів цих API, а також більш спеціалізовані (але необов'язкові) реалізації для веб-середовища (тобто сервлетів та портлетів).

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


0

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

Другим рішенням може бути думка "повідомлення" замість конвеєра, можливо, з виділеною рамкою. Потім у вас є "актори", які отримують повідомлення від інших акторів та надсилають інші повідомлення іншим акторам. Ви впорядковуєте своїх акторів у конвеєрі та надаєте свої основні дані першому актору, який ініціює ланцюг. Обміну даними немає, оскільки обмін замінюється надсиланням повідомлень. Я знаю, що модель актора Scala може використовуватися на Java, оскільки тут немає нічого конкретного для Scala, але я ніколи не використовував її в програмі Java.

Рішення подібні, і ви можете реалізувати другий з першим. В основному, основні концепції - це поводження з незмінними даними, щоб уникнути традиційних проблем через обмін даними та створити явні та незалежні сутності, що представляють процеси у вашому трубопроводі. Якщо ви задовольняєте цим умовам, ви можете легко створити чіткі, прості трубопроводи та використовувати їх у паралельній програмі.


Гей, я оновив своє запитання деяким псевдокодом - насправді кроки явні.
Руслан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.