
Ahoj, Habr!
Naše společnost se specializuje na vývoj ERP softwarových řešení, ve kterých převládají transakční systémy s obrovským objemem business logiky a dokumentového oběhu podobně jako SED. Moderní verze našich produktů jsou založeny na technologiích JavaEE, ale také aktivně experimentujeme s mikroservisy. Jedním z nejproblematičtějších míst těchto řešení je integrace různých subsystémů, které patří do příbuzných domén. Úkoly integrace nám vždy způsobovaly velké bolesti hlavy, bez ohledu na architektonické styly, technologické stacky a frameworky, které jsme používali, nicméně v poslední době se objevily pokroky v řešení těchto úkolů.
V tomto článku vám představím poznatky a architektonické úvahy NPO „Krysta“ v uvedené oblasti. Také se podíváme na příklad jednoduchého řešení integračního úkolu z pohledu aplikovaného vývojáře a zjistíme, co se skrývá za touto jednoduchostí.
Vyloučení odpovědnosti
Popisovaná architektonická a technická řešení nabízím na základě osobní zkušenosti v kontextu konkrétních úkolů. Tato řešení si nečiní nárok na univerzálnost a nemusí být optimální za jiných podmínek použití.
Jaký je vztah k BPM?
Abychom na tuto otázku odpověděli, musíme se trochu ponořit do specifik aplikovaných úkolů našich řešení. Hlavní část business logiky v našem typickém transakčním systému zahrnuje vstup dat do databáze prostřednictvím uživatelských rozhraní, ruční a automatizovanou kontrolu těchto dat, jejich průchod přes nějaký workflow, publikaci do jiného systému / analytické databáze / archivu, generování reportů. Klíčovou funkcí systému pro zákazníky je tedy automatizace jejich vnitřních obchodních procesů.
Pro usnadnění používáme v komunikaci termín „dokument“ jako nějakou abstrakci souboru dat spojených společným klíčem, k němuž lze „připojit“ určitý workflow.
Ale co dělat s integrační logikou? Vždyť úkol integrace je generován architekturou systému, která je „rozdělena“ na části NE na přání zákazníka, ale pod vlivem zcela jiných faktorů:
- pod vlivem Conwayova zákona;
- v důsledku opětovného využití subsystémů, které byly dříve vyvinuty pro jiné produkty;
- na základě rozhodnutí architekta, vycházejícího z nefunkčních požadavků.
Existuje silné pokušení oddělit integrační logiku od obchodní logiky hlavního workflow, aby se obchodní logika nezanášela integračními artefakty a aby se aplikačnímu vývojáři ušetřila potřeba zabývat se specifiky architektonické struktury systému. Tento přístup má několik výhod, nicméně praxe ukazuje, že je neefektivní:
- řešení integračních úloh obvykle sklouzává k nejjednodušším variantám ve formě synchronních volání kvůli omezenosti rozšiřitelnosti v realizaci hlavního workflow (o nedostatcích synchronní integrace – níže);
- integrační artefakty přesto pronikají do hlavní obchodní logiky, když je potřeba zpětná vazba z jiného subsystému;
- aplikační vývojář ignoruje integraci a může ji snadno rozbít, pokud změní workflow;
- systém přestává být jednotným celkem z pohledu uživatele, stávají se patrnými "švy" mezi subsystémy, objevují se nadbytečné uživatelské operace, které iniciují přenos dat z jednoho subsystému do druhého.
Jiný přístup spočívá v pohlížení na integrační interakce jako na nedílnou součást hlavní obchodní logiky a workflow. Aby kvalifikační požadavky na aplikační vývojáře nezletěly do nebe, mělo by být vytváření nových integračních interakcí prováděno snadno a bez námahy, s minimálními možnostmi výběru způsobu řešení. Je to složitější, než se zdá: nástroj musí být dostatečně silný, aby uživateli poskytl potřebné množství možností jeho využití a zároveň nedovolil „střelit si do nohy“. Existuje mnoho otázek, na které by měl inženýr odpovědět v kontextu integračních úloh, ale na které by aplikační vývojář neměl přemýšlet ve své každodenní práci: hranice transakcí, konzistence, atomicita, bezpečnost, škálovatelnost, rozdělování zátěže a zdrojů, routování, marshalling, šíření a přepínání kontextů atd. Je třeba nabídnout aplikačním vývojářům dostatečně jednoduché šablony řešení, ve kterých jsou již skryty odpovědi na všechny podobné otázky. Tyto šablony by měly být dostatečně bezpečné: obchodní logika se často mění, což zvyšuje riziko vzniku chyb, cena chyb by měla zůstat na dostatečně nízké úrovni.
Ale co má společného s BPM? Přeci existuje mnoho variant realizace workflow…
Opravdu, v našich řešeních je velmi populární jiná realizace obchodních procesů - prostřednictvím deklarativního zadání diagramu přechodů stavů a připojení zpracovatelů s obchodní logikou na přechody. Přičemž stav, určený pro aktuální pozici "dokumentu" v obchodním procesu, je atributem samotného "dokumentu".

Takto vypadá proces na začátku projektu
Popularita tohoto přístupu je dána relativní jednoduchostí a rychlostí vytváření lineárních obchodních procesů. S narůstající složitostí programových systémů však automatizovaná část obchodního procesu roste a komplikuje se. Objevuje se potřeba dekompozice, opětovného využívání částí procesů a také větvení procesů, aby každá větev mohla probíhat paralelně. V takových podmínkách se nástroj stává nepohodlným a diagram stavových přechodů ztrácí na informativnosti (integrační interakce nejsou na diagramu vůbec reflektovány).

Takto vypadá proces po několika iteracích upřesnění požadavků.
Východiskem z této situace byla integrace enginu do některých produktů s nejkomplexnějšími obchodními procesy. V krátkodobém horizontu mělo toto řešení určité úspěchy: objevila se možnost realizace složitých obchodních procesů se stále dostatečně informativním a aktuálním diagramem v notaci .

Malá část složitého obchodního procesu.
Z dlouhodobého hlediska však toto řešení nesplnilo očekávání: vysoká pracnost při vytváření obchodních procesů prostřednictvím vizuálních nástrojů neumožnila dosáhnout přijatelných ukazatelů produktivity a samotný nástroj se stal jedním z nejméně oblíbených mezi vývojáři. K vnitřnímu uspořádání enginu byly rovněž výhrady, které vedly k vzniku mnoha "záplat" a "klobouků".
Hlavním pozitivním okamžikem použití jBPM bylo uvědomění si užitečnosti a škodlivosti existence vlastního persistentního stavu instance obchodního procesu. Také jsme si uvědomili možnost aplikace procesního přístupu pro realizaci složitých integračních protokolů mezi různými aplikacemi s využitím asynchronních interakcí prostřednictvím signálů a zpráv. Existující persistentní stav hraje v této souvislosti klíčovou roli.
Na základě uvedeného lze učinit závěr: procesní přístup ve stylu BPM nám umožňuje řešit široké spektrum úkolů v oblasti automatizace neustále se komplikujících obchodních procesů, harmonicky zapojovat do těchto procesů integrační aktivity a udržovat možnost vizuálního zobrazení realizovaného procesu v vhodné notaci.
Nevýhody synchronních volání jako integračního vzoru
Pod synchronní integrací rozumíme nejsnazší blokující volání. Jedna podsystém vystupuje jako serverová strana a poskytuje API s potřebnou metodou. Druhá podsystém vystupuje jako klientská strana a v potřebný okamžik provádí výzvu se čekáním na výsledek. V závislosti na architektuře systému mohou být klientská a serverová strana umístěny buď v jedné aplikaci a procesu, nebo v různých. V druhém případě je třeba použít nějakou implementaci RPC a zajistit marshalling parametrů a výsledku výzvy.

Tento integrační vzor má dostatečně velkou sadu nevýhod, ale je velmi široce používán v praxi díky své jednoduchosti. Rychlost realizace je lákavá a nutí jej používat znovu a znovu za podmínek "hořících" termínů, zapisujíc řešení do technického dluhu. Ale také se stává, že nezkušení vývojáři jej používají neuvědoměle, prostě si neuvědomují negativní důsledky.
Kromě nejzřejmějšího zvýšení provázanosti podsystémů existují i méně zřejmé problémy s "roztahováním" a "protažením" transakcí. Opravdu, pokud obchodní logika provádí nějaké změny, pak se neobejdeme bez transakcí, a transakce na druhé straně blokují určité zdroje aplikace, které jsou těmito změnami ovlivněny. To znamená, že dokud jedna podsystém nečeká na odpověď od druhé, nebude moci dokončit transakci a uvolnit blokace. To podstatně zvyšuje riziko vzniku různých efektů:
- ztrácí se odezva systému, uživatelé dlouho čekají na odpovědi na dotazy;
- server přestává odpovídat na dotazy uživatelů kvůli přeplněnému poolu vláken: většina vláken "stojí" na blokaci zdroje, obsazeno transakcí;
- začínají se objevovat deadlocky: pravděpodobnost jejich výskytu silně závisí na délce transakcí, množství obchodní logiky zapojené do transakce a blokacích;
- objevují se chyby vypršení času transakce;
- server "padá" kvůli OutOfMemory, pokud úkol vyžaduje zpracování a změnu velkých objemů dat, a přítomnost synchronních integrací výrazně ztěžuje rozdělení zpracování na "lehčí" transakce.
Z architektonického hlediska používání blokujících volání při integraci vede ke ztrátě kontroly nad kvalitou jednotlivých subsystémů: není možné zajistit cílové ukazatele kvality jednoho subsystému odděleně od ukazatelů kvality jiného subsystému. Pokud jsou subsystémy vyvíjeny různými týmy, představuje to velký problém.
Všechno je ještě zajímavější, pokud jsou integrované subsystémy v různých aplikacích a je třeba provést synchronní změny z obou stran. Jak zajistit transakčnost těchto změn?
Pokud jsou změny prováděny oddělenými transakcemi, pak je třeba zajistit spolehlivé zpracování výjimek a kompenzací, což zcela vyrovnává hlavní výhodu synchronních integrací – jednoduchost.
Na mysl také přicházejí distribuované transakce, ale ty v našich řešeních nepoužíváme: je obtížné zajistit spolehlivost.
Saga jako řešení problematiky transakcí
S rostoucí popularitou mikroservisů se stále více prosazuje .
Tento vzor skvěle řeší výše uvedené problémy dlouhých transakcí a zároveň rozšiřuje možnosti správy stavu systému z pohledu podnikové logiky: kompenzace po neúspěšné transakci nemusí vracet systém do původního stavu, ale může zajistit alternativní trasu zpracování dat. To také umožňuje nepřepakovávat úspěšně dokončené kroky zpracování dat při opakovaných pokusech dosáhnout 'dobrého' konce.
Co je zajímavé, v monolitních systémech je tento vzor také relevantní, pokud jde o integraci slabě propojených subsystémů a pozorují se negativní efekty způsobené dlouhými transakcemi a odpovídajícími blokacemi zdrojů.
Pokud jde o naše obchodní procesy ve stylu BPM, implementace 'Sagy' se ukazuje jako velmi snadná: jednotlivé kroky 'Sagy' mohou být zadány jako aktivity v rámci obchodního procesu a persistentní stav obchodního procesu určuje také vnitřní stav 'Sagy'. To znamená, že nepotřebujeme žádný další koordinační mechanismus. Bude potřeba pouze zprostředkovatel zpráv s podporou záruk 'at least once' jako transport.
Ale i takové řešení má svou 'cenu':
- byznysová logika se stává složitější: je třeba zpracovávat kompenzace;
- bude nutné se vzdát full consistency, což může být zvlášť citlivé pro monolitické systémy;
- architektura se mírně komplikuje, objevuje se další potřeba zprávového brokera;
- budou potřeba další monitorovací a administrační prostředky (i když celkově to dokonce dobře: kvalita poskytování služby se zlepší).
Pro monolitické systémy je oprávněnost použití „Sagas“ méně zřejmá. Pro mikroslužby a další SOA, kde pravděpodobně již existuje broker, a full consistency byla obětována již na začátku projektu, může mít použití této šablony výrazné výhody, zejména pokud existuje pohodlné API na úrovni byznysové logiky.
Inkapsulace byznysové logiky v mikroslužbách
Když jsme začali experimentovat s mikroslužbami, vznikla rozumná otázka: kam umístit doménovou byznysovou logiku vzhledem k servisu, který zajišťuje perzistenci doménových dat?
Při pohledu na architekturu různých BPMS se může zdát rozumné oddělit byznysovou logiku od perzistence: vytvořit vrstvu platformních a doménově nezávislých mikroslužeb, které vytvářejí prostředí a kontejner pro vykonávání doménové byznysové logiky, a perzistenci doménových dat zformovat jako samostatnou vrstvu velmi jednoduchých a lehkých mikroslužeb. Byznysové procesy v takovém případě provádějí orchestraci služeb vrstvy perzistence.

Tento přístup má velkou výhodu: lze bez obav zvyšovat funkčnost platformy, a „zhubnout“ z toho bude pouze odpovídající vrstva platformních mikroslužeb. Byznysové procesy z jakéhokoliv domény okamžitě získávají možnost využívat nové funkčnosti platformy, jakmile je aktualizována.
Podrobnější zpracování ukázalo podstatné nedostatky tohoto přístupu:
- platformní služba, vykonávající byznysovou logiku současně mnoha domén, nese velká rizika jako jediný bod selhání. Časté změny byznysové logiky zvyšují riziko vzniku chyb, které vedou k selháním, jež se šíří na celý systém;
- problémy s výkonem: byznysová logika pracuje se svými daty přes úzké a pomalé rozhraní:
- data se budou zbytečně marshallovat a zpracovávat přes síťový stack;
- doménová služba často poskytuje více dat, než je potřeba pro zpracování obchodní logiky, kvůli nedostatečným možnostem parametrizace požadavků na úrovni externí API služby;
- některé nezávislé části obchodní logiky mohou opakovaně vyžadovat stejná data pro zpracování (tento problém lze zmírnit přidáním sezeních komponent, které cachují data, ale to dále komplikuje architekturu a vytváří problémy s aktuálností dat a invalidací cache);
- problémy s transakcionalitou:
- obchodní procesy s perzistentním stavem, jehož uchovávání řídí platformní služba, se rozcházejí s doménovými daty, a jednoduché cesty k vyřešení tohoto problému se nepředpokládají;
- vynesení zámku doménových dat mimo transakci: pokud doménová obchodní logika vyžaduje provést změny po kontrole aktuálnosti dat, je třeba vyloučit možnost konkurentního změny zpracovávaných dat. Externí zámek dat může pomoci vyřešit problém, ale takové řešení nese další rizika a snižuje celkovou spolehlivost systému;
- další složitosti při aktualizaci: v některých případech je třeba aktualizovat služby perzistence a obchodní logiku synchronně nebo ve striktní posloupnosti.
Nakonec bylo nutné vrátit se k základům: zabalit doménová data a doménovou obchodní logiku do jednoho mikroslužby. Tento přístup usnadňuje vnímání mikroslužby jako celistvého komponentu v rámci systému a nevytváří výše uvedené problémy. To také není bez nákladů:
- je vyžadována standardizace API pro interakci s obchodní logikou (zejména pro zajištění uživatelských aktivit v rámci obchodních procesů) a API platformních služeb; je třeba věnovat větší pozornost změnám API, přímé a zpětné kompatibility;
- je třeba přidat další runtime knihovny pro zajištění fungování obchodní logiky v rámci každé z těchto mikroslužeb, což vyvolává nové požadavky na takové knihovny: lehkost a minimální tranzitivní závislosti;
- Vývojáři business logiky musí sledovat verze knihoven: pokud nějaký mikroslužba nebyla dlouho aktualizována, pravděpodobně bude obsahovat zastaralou verzi knihoven. To může představovat neočekávanou překážku při přidávání nové funkce a může vyžadovat migraci staré business logiky takové služby na nové verze knihoven, pokud došlo k nekompatibilním změnám mezi verzemi.

Vrstva platformových služeb v této architektuře je také přítomna, ale tato vrstva už nevytváří kontejner pro provádění doménové business logiky, ale pouze její prostředí, poskytující doplňkové "platformní" funkce. Taková vrstva je potřebná nejen pro zachování lehkosti doménových mikroslužeb, ale také pro centralizaci správy.
Například uživatelské aktivity v business procesech generují úkoly. Při práci s úkoly však musí uživatel vidět úkoly ze všech domén v celkovém seznamu, a proto musí existovat odpovídající platformní služba pro registraci úkolů, zbavená doménové business logiky. Zachovat inkapsulaci business logiky v tomto kontextu je dost problematické, a to je další kompromis této architektury.
Integrace business procesů očima aplikačního vývojáře
Jak již bylo řečeno výše, aplikační vývojář by měl být odabstrahován od technických a inženýrských aspektů realizace interakce několika aplikací, aby bylo možné očekávat dobrou produktivitu vývoje.
Pokud se pokusíme vyřešit poměrně složitý integrační úkol, který byl speciálně vymyšlen pro tento článek. Bude to "hrací" úkol zahrnující tři aplikace, z nichž každá určuje určité doménové jméno: "app1", "app2", "app3".
Uvnitř každé aplikace se spouštějí business procesy, které začínají "hrát s míčem" prostřednictvím integrační sběrnice. Míčem budou zprávy s názvem "Ball".
Pravidla hry:
- první hráč – iniciátor. Ten zve ostatní hráče do hry, zahajuje hru a může ji kdykoli ukončit;
- ostatní hráči se hlásí k účasti ve hře, "poznávají" se navzájem a s prvním hráčem;
- přijmout míč, hráč si vybírá jiného účastnického hráče a předává mu míč. Počet přenosů se eviduje;
- každý hráč má "energii", která se snižuje s každým přihráním míče tímto hráčem. Jakmile energie vyprší, hráč odstoupí ze hry a oznámí svůj odchod;
- pokud zůstal hráč sám, okamžitě oznámí svůj odchod;
- když všichni hráči odstoupí, první hráč oznámí ukončení hry. Pokud sám odstoupil dříve, musí sledovat hru, aby ji dokončil.
Pro tuto úlohu využiji náš DSL pro obchodní procesy, který umožňuje popsat logiku v Kotlinu kompaktně s minimem boilerplate kódu.
V aplikaci app1 bude fungovat obchodní proces prvního hráče (též iniciátora hry):
class InitialPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.constraint.UniqueConstraints
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.dsl.taskOperation
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList : ArrayList()
// Tato třída je instancí procesu: inkapsuluje jeho vnitřní stav
class InitialPlayer : ProcessImpl(initialPlayerModel) {
var playerName: String by persistent("Player1")
var energy: Int by persistent(30)
var players: PlayersList by persistent(PlayersList())
var shotCounter: Int = 0
}
// Toto je deklarace modelu procesu: vytváří se jednou, používá ji všichni
// instance procesu patřičné třídy
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Podle pravidel je prvním hráčem iniciátor hry a měl by být jediným
uniqueConstraint = UniqueConstraints.singleton
// Vyhlašujeme aktivity, z nichž se skládá obchodní proces
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "Připojilo se ${players.size} hráčů. Začneme?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... připojil se hráč ${signal.data} ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... hráč ${signal.data} opustil hru ...")
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val throwStartBall = messageSend("Ball") {
messageData = { 1 }
activation = { selectNextPlayer() }
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
// Nyní konstruujeme graf procesu z vyhlášených aktivit
startFrom(sendNewGameSignal)
.fork("mainFork") {
next(startTask)
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut)
.branch("checkPlayers") {
ifTrue { players.isEmpty() }
.next(sendStopGameSignal)
.terminate()
ifElse().next(waitPlayerOut)
}
}
startTask.fork("afterStart") {
next(throwStartBall)
.branch("mainLoop") {
ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
ifElse().next(waitBall).next(throwBall).loop()
}
next(stopTask).next(sendStopGameSignal)
}
// Přidáme na aktivity další zpracovatele pro logging
sendNewGameSignal.onExit { println("Hrajeme!") }
sendStopGameSignal.onExit { println("Stop!") }
sendPlayerOut.onExit { println("$playerName: Jsem venku!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Krok ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Kromě provádění obchodní logiky umí uvedený kód vrátit objektový model obchodního procesu, který může být vizualizován jako diagram. Vizualizátor jsme zatím neimplementovali, takže jsem musel strávit trochu času kreslením (zde jsem mírně zjednodušil BPMN notaci v části používání bran, abych zlepšil konzistenci diagramu s uvedeným kódem):

Aplikace app2 bude zahrnovat obchodní proces jiného hráče:
class RandomPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RandomPlayer : ProcessImpl(randomPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RandomPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val randomPlayerModel = processModel(name = "RandomPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = if (process.players.isNotEmpty())
process.players.random()
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Diagram:

V aplikaci app3 změníme chování hráče: místo náhodného výběru dalšího hráče bude postupovat podle algoritmu round-robin:
class RoundRobinPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RoundRobinPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var nextPlayerIndex: Int by persistent(-1)
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val roundRobinPlayerModel = processModel(
name = "RoundRobinPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
var idx = process.nextPlayerIndex + 1
if (idx >= process.players.size) {
idx = 0
}
process.nextPlayerIndex = idx
val player = if (process.players.isNotEmpty())
process.players[idx]
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Jinak se chování hráče neliší od předchozího, takže diagram se nemění.
Nyní potřebujeme test, abychom to všechno spuštěli. Připojím pouze kód samotného testu, abych nezaplnil článek boilerplate (ve skutečnosti jsem použil testovací prostředí vytvořené dříve pro testování integrace jiných obchodních procesů):
testGame()
@Test
public void testGame() throws InterruptedException {
String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
String pl1 = startProcess(app1, "InitialPlayer");
// Nyní je potřeba chvíli počkat, aby se hráči "seznámili".
// Čekání pomocí sleep je špatné řešení, ale nejjednodušší.
// Nedělejte to v seriózních testech!
Thread.sleep(1000);
// Spouštíme hru, zatímco uzavíráme uživatelskou aktivitu
assertTrue(closeTask(app1, pl1, "Start"));
app1.getWaiting().waitProcessFinished(pl1);
app2.getWaiting().waitProcessFinished(pl2);
app2.getWaiting().waitProcessFinished(pl3);
app3.getWaiting().waitProcessFinished(pl4);
app3.getWaiting().waitProcessFinished(pl5);
}
private Map playerParams(String name, int energy) {
Map params = new HashMap();
params.put("playerName", name);
params.put("energy", energy);
return params;
}Spouštíme test, sledujeme log:
výstup konzole
Zablokován klíč lock://app1/process/InitialPlayer
Hrajme!
Odbokován klíč lock://app1/process/InitialPlayer
Hráč2: Jsem tady!
Hráč3: Jsem tady!
Hráč4: Jsem tady!
Hráč5: Jsem tady!
... připojit hráče Hráč2 ...
... připojit hráče Hráč4 ...
... připojit hráče Hráč3 ...
... připojit hráče Hráč5 ...
Krok 1: Hráč1 >>> Hráč3
Krok 2: Hráč3 >>> Hráč5
Krok 3: Hráč5 >>> Hráč3
Krok 4: Hráč3 >>> Hráč4
Krok 5: Hráč4 >>> Hráč3
Krok 6: Hráč3 >>> Hráč4
Krok 7: Hráč4 >>> Hráč5
Krok 8: Hráč5 >>> Hráč2
Krok 9: Hráč2 >>> Hráč5
Krok 10: Hráč5 >>> Hráč4
Krok 11: Hráč4 >>> Hráč2
Krok 12: Hráč2 >>> Hráč4
Krok 13: Hráč4 >>> Hráč1
Krok 14: Hráč1 >>> Hráč4
Krok 15: Hráč4 >>> Hráč3
Krok 16: Hráč3 >>> Hráč1
Krok 17: Hráč1 >>> Hráč2
Krok 18: Hráč2 >>> Hráč3
Krok 19: Hráč3 >>> Hráč1
Krok 20: Hráč1 >>> Hráč5
Krok 21: Hráč5 >>> Hráč1
Krok 22: Hráč1 >>> Hráč2
Krok 23: Hráč2 >>> Hráč4
Krok 24: Hráč4 >>> Hráč5
Krok 25: Hráč5 >>> Hráč3
Krok 26: Hráč3 >>> Hráč4
Krok 27: Hráč4 >>> Hráč2
Krok 28: Hráč2 >>> Hráč5
Krok 29: Hráč5 >>> Hráč2
Krok 30: Hráč2 >>> Hráč1
Krok 31: Hráč1 >>> Hráč3
Krok 32: Hráč3 >>> Hráč4
Krok 33: Hráč4 >>> Hráč1
Krok 34: Hráč1 >>> Hráč3
Krok 35: Hráč3 >>> Hráč4
Krok 36: Hráč4 >>> Hráč3
Krok 37: Hráč3 >>> Hráč2
Krok 38: Hráč2 >>> Hráč5
Krok 39: Hráč5 >>> Hráč4
Krok 40: Hráč4 >>> Hráč5
Krok 41: Hráč5 >>> Hráč1
Krok 42: Hráč1 >>> Hráč5
Krok 43: Hráč5 >>> Hráč3
Krok 44: Hráč3 >>> Hráč5
Krok 45: Hráč5 >>> Hráč2
Krok 46: Hráč2 >>> Hráč3
Krok 47: Hráč3 >>> Hráč2
Krok 48: Hráč2 >>> Hráč5
Krok 49: Hráč5 >>> Hráč4
Krok 50: Hráč4 >>> Hráč2
Krok 51: Hráč2 >>> Hráč5
Krok 52: Hráč5 >>> Hráč1
Krok 53: Hráč1 >>> Hráč5
Krok 54: Hráč5 >>> Hráč3
Krok 55: Hráč3 >>> Hráč5
Krok 56: Hráč5 >>> Hráč2
Krok 57: Hráč2 >>> Hráč1
Krok 58: Hráč1 >>> Hráč4
Krok 59: Hráč4 >>> Hráč1
Krok 60: Hráč1 >>> Hráč4
Krok 61: Hráč4 >>> Hráč3
Krok 62: Hráč3 >>> Hráč2
Krok 63: Hráč2 >>> Hráč5
Krok 64: Hráč5 >>> Hráč4
Krok 65: Hráč4 >>> Hráč5
Krok 66: Hráč5 >>> Hráč1
Krok 67: Hráč1 >>> Hráč5
Krok 68: Hráč5 >>> Hráč3
Krok 69: Hráč3 >>> Hráč4
Krok 70: Hráč4 >>> Hráč2
Krok 71: Hráč2 >>> Hráč5
Krok 72: Hráč5 >>> Hráč2
Krok 73: Hráč2 >>> Hráč1
Krok 74: Hráč1 >>> Hráč4
Krok 75: Hráč4 >>> Hráč1
Krok 76: Hráč1 >>> Hráč2
Krok 77: Hráč2 >>> Hráč5
Krok 78: Hráč5 >>> Hráč4
Krok 79: Hráč4 >>> Hráč3
Krok 80: Hráč3 >>> Hráč1
Krok 81: Hráč1 >>> Hráč5
Krok 82: Hráč5 >>> Hráč1
Krok 83: Hráč1 >>> Hráč4
Krok 84: Hráč4 >>> Hráč5
Krok 85: Hráč5 >>> Hráč3
Krok 86: Hráč3 >>> Hráč5
Krok 87: Hráč5 >>> Hráč2
Krok 88: Hráč2 >>> Hráč3
Hráč2: Jsem venku!
Krok 89: Hráč3 >>> Hráč4
... hráč Hráč2 je venku ...
Krok 90: Hráč4 >>> Hráč1
Krok 91: Hráč1 >>> Hráč3
Krok 92: Hráč3 >>> Hráč1
Krok 93: Hráč1 >>> Hráč4
Krok 94: Hráč4 >>> Hráč3
Krok 95: Hráč3 >>> Hráč5
Krok 96: Hráč5 >>> Hráč1
Krok 97: Hráč1 >>> Hráč5
Krok 98: Hráč5 >>> Hráč3
Krok 99: Hráč3 >>> Hráč5
Krok 100: Hráč5 >>> Hráč4
Krok 101: Hráč4 >>> Hráč5
Hráč4: Jsem venku!
... hráč Hráč4 je venku ...
Krok 102: Hráč5 >>> Hráč1
Krok 103: Hráč1 >>> Hráč3
Krok 104: Hráč3 >>> Hráč1
Krok 105: Hráč1 >>> Hráč3
Krok 106: Hráč3 >>> Hráč5
Krok 107: Hráč5 >>> Hráč3
Krok 108: Hráč3 >>> Hráč1
Krok 109: Hráč1 >>> Hráč3
Krok 110: Hráč3 >>> Hráč5
Krok 111: Hráč5 >>> Hráč1
Krok 112: Hráč1 >>> Hráč3
Krok 113: Hráč3 >>> Hráč5
Krok 114: Hráč5 >>> Hráč3
Krok 115: Hráč3 >>> Hráč1
Krok 116: Hráč1 >>> Hráč3
Krok 117: Hráč3 >>> Hráč5
Krok 118: Hráč5 >>> Hráč1
Krok 119: Hráč1 >>> Hráč3
Krok 120: Hráč3 >>> Hráč5
Krok 121: Hráč5 >>> Hráč3
Hráč5: Jsem venku!
... hráč Hráč5 je venku ...
Krok 122: Hráč3 >>> Hráč5
Krok 123: Hráč5 >>> Hráč1
Hráč5: Jsem venku!
Krok 124: Hráč1 >>> Hráč3
... hráč Hráč5 je venku ...
Krok 125: Hráč3 >>> Hráč1
Krok 126: Hráč1 >>> Hráč3
Hráč1: Jsem venku!
... hráč Hráč1 je venku ...
Krok 127: Hráč3 >>> Hráč3
Hráč3: Jsem venku!
Krok 128: Hráč3 >>> Hráč3
... hráč Hráč3 je venku ...
Hráč3: Jsem venku!
Stop!
Krok 129: Hráč3 >>> Hráč3
Hráč3: Jsem venku!Z toho všeho lze učinit několik důležitých závěrů:
- při dostupnosti nezbytných nástrojů mohou aplikační vývojáři vytvářet integrační interakce mezi aplikacemi, aniž by se odtrhli od business logiky;
- složitost integračního úkolu, která vyžaduje inženýrské kompetence, lze skrýt uvnitř rámce, pokud je to od začátku zakotveno v architektuře rámce. Obtížnost úkolu však skrýt nelze, a proto bude řešení obtížného úkolu v kódu odpovídat tomu;
- při vývoji integrační logiky je nezbytné brát v úvahu eventual consistency a absenci linearizovatelnosti změny stavu všech účastníků integrace. To nutí komplikovat logiku tak, aby byla necitlivá na pořadí, ve kterém nastávají externí události. V našem příkladu je hráč nucen se účastnit hry až poté, co oznámí svůj odchod: ostatní hráči mu budou i nadále přihrávat míč, dokud informace o jeho odchodu nedorazí a nebude zpracována všemi účastníky. Tato logika neplyne z pravidel hry a představuje kompromisní řešení v rámci zvolené architektury.
Dále si promluvíme o různých nuancích našeho řešení, kompromisech a dalších aspektech.
Všechny zprávy - v jedné frontě
Všechny integrované aplikace pracují s jednou integrační sběrnicí, která je představena jako externí broker, jednou frontou BPMQueue - pro zprávy a jedním tématem BPMTopic - pro signály (události). Procházet všechny zprávy skrze jednu frontu je samo o sobě kompromisem. Na úrovni business logiky lze nyní zavádět neomezené množství nových typů zpráv, aniž by se měnila struktura systému. To je značné zjednodušení, ale přináší to určitá rizika, která se v kontextu našich typových úloh zdála být nikoli tak významná.

Avšak zde je jedna nuance: každá aplikace filtruje „své“ zprávy z fronty již na vstupu podle názvu své domény. Doména může být také uvedena v signálech, pokud je třeba omezit „viditelnost“ signálu na jedinou aplikaci. To by mělo zvýšit propustnost sběrnice, ale obchodní logika nyní musí pracovat s názvy domén: pro adresaci zpráv – povinné, pro signály – žádoucí.
Zajištění spolehlivosti integrační sběrnice
Spolehlivost se skládá z několika bodů:
- vybraný broker zpráv – kriticky důležitá součást architektury a jediné místo selhání: musí být dostatečně odolný proti poruchám. Měly by být použity pouze časem prověřené implementace s dobrou podporou a velkým komunitním zázemím;
- je třeba zajistit vysokou dostupnost brokera zpráv, k čemuž by měl být fyzicky oddělen od integrovaných aplikací (zajištění vysoké dostupnosti aplikací s aplikační obchodní logikou je mnohem složitější a dražší);
- broker musí zajistit záruky „alespoň jednou“ pro dodání. To je povinný požadavek pro spolehlivý provoz integrační sběrnice. Na záruky úrovně „přesně jednou“ není třeba myslet: obchodní procesy obvykle nejsou citlivé na opakované doručení zpráv nebo událostí, a v případě speciálních úkolů, kde to je důležité, je jednodušší přidat další kontrolu do obchodní logiky, než stále využívat dostatečně „nákladné“ záruky;
- odesílání zpráv a signálů by mělo být součástí celkové transakce s měněním stavu obchodních procesů a doménových dat. Preferovanou variantou by bylo použití vzoru , ale to si vyžádá existenci další tabulky v databázi a retranslátora. V aplikacích JEE lze tento bod zjednodušit použitím lokálního JTA managera, ale připojení k vybranému brokeru by mělo umět fungovat v režimu ;
- zpracovatelé příchozích zpráv a událostí by měli také pracovat s transakcí změny stavu obchodního procesu: pokud je taková transakce vrácena, musí být i příjem zprávy zrušen;
- zprávy, které se nepodařilo doručit kvůli chybám, je nutné ukládat do samostatného úložiště (Dead Letter Queue). Pro tento účel jsme vytvořili samostatnou platformu mikroservisu, která ukládá takové zprávy do svého úložiště, indexuje je podle atributů (pro rychlou skupinovou a vyhledávací funkci) a poskytuje API pro prohlížení, opětovné odesílání na cílovou adresu a odstraňování zpráv. Administrátoři systému mohou s touto službou pracovat prostřednictvím svého webového rozhraní;
- v nastavení brokera je třeba upravit počet opakování pokusů o doručení a zpoždění mezi doručeními, aby se snížila pravděpodobnost, že zprávy skončí v DLQ (vypočítat optimální parametry je prakticky nemožné, ale můžeme postupovat empiricky a přizpůsobit je během používání);
- úložiště DLQ by mělo být neustále monitorováno a monitorovací systém by měl upozorňovat administrátory systému, aby na nedoručené zprávy reagovali co nejrychleji. To sníží "zónu poškození" vzniklého selhání nebo chyby obchodní logiky;
- integrační sběrnice by měla být necitlivá na dočasnou nepřítomnost aplikací: přihlášení k tématu by měla být trvalá a doménové jméno aplikace by mělo být unikátní, takže během nepřítomnosti aplikace se zprávy z fronty nepokouší zpracovat někdo jiný.
Zajištění vlákno-bezpečnosti obchodní logiky
Jednomu a tomu samému instanci obchodního procesu může být přijato několik zpráv a událostí, jejichž zpracování se spustí paralelně. Zároveň pro aplikačního vývojáře musí být vše jednoduché a vlákno-bezpečné.
Obchodní logika procesu zpracovává každou externí událost, která ovlivňuje tento obchodní proces, samostatně. Takovými událostmi mohou být:
- spuštění instance obchodního procesu;
- akce uživatele, která se vztahuje k aktivitě uvnitř obchodního procesu;
- příchod zprávy nebo signálu, na který je instance obchodního procesu přihlášena;
- vyvolání časovače, který je nastavenou instancí obchodního procesu;
- řízení prostřednictvím API (např. nouzové přerušení procesu).
Každá taková událost může změnit stav instance podnikového procesu: mohou skončit některé aktivity a začít jiné, mohou se změnit hodnoty persistovaných vlastností. Uzavření jakékoliv aktivity může vést k aktivaci jedné nebo více následujících aktivit. Ty mohou buď čekat na jiné události, nebo pokud nepotřebují žádná další data, mohou skončit ve stejné transakci. Před uzavřením transakce se nový stav podnikového procesu ukládá do databáze, kde bude čekat na výskyt další externí události.
Persistovaná data podnikového procesu uložená v relační databázi představují velmi pohodlný bod synchronizace zpracování, pokud se použije SELECT FOR UPDATE. Pokud jedné transakci podaří získat stav podnikového procesu z databáze pro jeho změnu, žádná jiná transakce jej paralelně nemůže získat pro jinou změnu, a po ukončení první transakce druhá zaručeně získá již změněný stav.
Použitím pesimistických zámků na straně DBMS splňujeme všechny potřebné požadavky , a zároveň zachováváme možnost škálování aplikace s podnikovou logikou zvýšením počtu spuštěných instancí.
Nicméně pesimistické zámky nám hrozí mrtvými zámky, a proto by SELECT FOR UPDATE mělo být přesto omezeno nějakým rozumným časovým limitem pro případ vzniku mrtvých zámků v nějakých extrémních případech v podnikové logice.
Další problém je synchronizace spuštění podnikového procesu. Dokud neexistuje instance podnikového procesu, neexistuje ani jeho stav v databázi, takže popsaná metoda není použitelná. Pokud je potřeba zajistit jedinečnost instance podnikového procesu v určitém rozsahu, pak bude zapotřebí nějaký synchronizační objekt, asociovaný s třídou procesu a odpovídajícím rozsahem. Pro vyřešení tohoto problému používáme jiný mechanismus zámků, který umožňuje vzít zámek na libovolný zdroj, zadaný klíčem ve formátu URI, přes externí službu.
V našich příkladech podnikový proces InitialPlayer obsahuje deklaraci
uniqueConstraint = UniqueConstraints.singletonV protokolu se proto objevují zprávy o zablokování a odblokování odpovídajícího klíče. U jiných obchodních procesů takové zprávy nejsou: uniqueConstraint není nastaven.
Problémy obchodních procesů s trvalým stavem
Někdy trvalý stav nejen pomáhá, ale také hodně brání ve vývoji.
Problémy začínají, když je potřeba provést změny v obchodní logice a/nebo modelu obchodního procesu. Ne každá taková změna se ukáže jako kompatibilní se starým stavem obchodních procesů. Pokud je v databázi mnoho „živých“ instancí, může provedení nekompatibilních změn způsobit mnoho problémů, se kterými jsme se často setkávali při používání jBPM.
V závislosti na hloubce změn lze postupovat dvěma způsoby:
- vytvořit nový typ obchodního procesu, aby se neprováděly nekompatibilní změny ve starém, a použít ho namísto starého při spouštění nových instancí. Staré instance budou pokračovat v běhu „po starém“;
- migrace trvalého stavu obchodních procesů při aktualizaci obchodní logiky.
První cesta je jednodušší, ale má svá omezení a nevýhody, například:
- duplicitní obchodní logiku v mnoha modelech obchodních procesů, zvýšení objemu obchodní logiky;
- často je vyžadován okamžitý přechod na novou obchodní logiku (u integračních úloh – skoro vždy);
- vývojář neví, kdy může odstranit zastaralé modely.
V praxi používáme oba přístupy, ale přijali jsme řadu rozhodnutí, abychom si proces zjednodušili:
- v databázi je trvalý stav obchodního procesu uložen v snadno čitelném a snadno zpracovatelném formátu: v řetězci formátu JSON. To umožňuje provádět migrace jak uvnitř aplikace, tak zvenčí. V krajním případě je možné to opravit i ručně (zejména užitečné při vývoji během ladění);
- integrační obchodní logika nepoužívá názvy obchodních procesů, aby bylo možné kdykoli nahradit implementaci jednoho z účastnících se procesů novou, s novým názvem (například „InitialPlayerV2“). Spojení probíhá prostřednictvím názvů zpráv a signálů;
- Model procesu má číslo verze, které zvyšujeme, pokud do této modelu provedeme nekompatibilní změny, a toto číslo je uchováváno spolu se stavem instance procesu.
- Persistující stav procesu se nejprve načte z databáze do pohodlného objektového modelu, se kterým může pracovat migrační procedura, pokud se číslo verze modelu změnilo.
- Migrační procedura je umístěna vedle obchodní logiky a je volána "lenivě" pro každou instanci obchodního procesu v okamžiku jejího obnovení z databáze.
- Pokud je třeba migrovat stav všech instancí procesu operativně a synchronně, používají se tradičnější řešení pro migraci databází, ale tam je nutné pracovat s JSON.
Potřebujeme ještě jeden rámec pro obchodní procesy?
Popsaná řešení v článku nám umožnila výrazně si usnadnit život, rozšířit okruh otázek, které se řeší na úrovni aplikačního vývoje, a učinit myšlenky na oddělení obchodní logiky do mikroslužeb přitažlivějšími. Pro toto bylo odvedeno mnoho práce, byl vytvořen velmi "lehkovážný" rámec pro obchodní procesy a také pomocné komponenty k řešení vymezených problémů v kontextu širokého spektra aplikačních úloh. Máme zájem sdílet tyto výsledky, přenést vývoj obecných komponent do otevřeného přístupu pod svobodnou licencí. To bude vyžadovat určité úsilí a čas. Pochopení poptávky po takových řešeních by pro nás mohlo být dodatečným stimulem. V navrhovaném článku je věnováno velmi málo pozornosti možnostem samotného rámce, ale některé z nich jsou viditelné z uvedených příkladů. Pokud nakonec publikujeme náš rámec, bude mu věnován samostatný článek. Zatím budeme vděční, pokud zanecháte krátkou odezvu, když odpovíte na otázku:
Pouze registrovaní uživatelé se mohou účastnit ankety. , prosím.
Potřebujeme ještě jeden rámec pro obchodní procesy?
18,8%Ano, dlouho hledáme něco podobného.
12,5%Zajímá mě, jakou implementaci máte, možná by se mi hodila.
6,2%Používáme jeden z existujících rámců, ale zvažujeme výměnu.
18,8%Používáme jeden z existujících rámců, všechno vyhovuje.
18,8%Zvládáme to bez rámce.
25,0%Píšeme si vlastní.
Hlasovalo 16 uživatelů. Zdrželo se 7 uživatelů.
Zdroj: habr.com
