Integrimi i stilit BPM

Integrimi i stilit BPM

Përshëndetje, Habr!

Kompania jonë është e specializuar në zhvillimin e zgjidhjeve softuerike të klasit ERP, në të cilat pjesën e luanit e zënë sistemet transaksionale me një sasi të madhe të logjikës së biznesit dhe rrjedhës së punës a la EDMS. Versionet moderne të produkteve tona bazohen në teknologjitë JavaEE, por ne po eksperimentojmë në mënyrë aktive edhe me mikroshërbimet. Një nga fushat më problematike të zgjidhjeve të tilla është integrimi i nënsistemeve të ndryshme që lidhen me domenet ngjitur. Detyrat e integrimit na kanë shkaktuar gjithmonë një dhimbje koke të madhe, pavarësisht nga stilet arkitekturore, raftet e teknologjisë dhe kornizat që përdorim, por kohët e fundit ka pasur përparim në zgjidhjen e problemeve të tilla.

Në artikullin e sjellë në vëmendjen tuaj, do të flas për përvojën dhe kërkimin arkitektonik të OJF-së Krista në zonën e caktuar. Ne gjithashtu do të shqyrtojmë një shembull të një zgjidhjeje të thjeshtë për një problem integrimi nga këndvështrimi i një zhvilluesi të aplikacionit dhe do të zbulojmë se çfarë fshihet pas kësaj thjeshtësie.

Përgjegjësia

Zgjidhjet arkitektonike dhe teknike të përshkruara në artikull ofrohen nga unë bazuar në përvojën personale në kontekstin e detyrave specifike. Këto zgjidhje nuk pretendojnë të jenë universale dhe mund të mos jenë optimale në kushte të tjera përdorimi.

Çfarë lidhje ka BPM me të?

Për t'iu përgjigjur kësaj pyetjeje, duhet të thellohemi pak në specifikat e problemeve të aplikuara të zgjidhjeve tona. Pjesa kryesore e logjikës së biznesit në sistemin tonë tipik të transaksioneve është futja e të dhënave në bazën e të dhënave përmes ndërfaqeve të përdoruesit, verifikimi manual dhe i automatizuar i këtyre të dhënave, kalimi i tyre përmes disa flukseve të punës, publikimi i tyre në një sistem tjetër/bazë të dhënash analitike/arkiv, gjenerimi i raporteve. Kështu, funksioni kryesor i sistemit për klientët është automatizimi i proceseve të tyre të brendshme të biznesit.

Për lehtësi, ne përdorim termin "dokument" në komunikim si një abstraksion i një grupi të dhënash, të bashkuar nga një çelës i përbashkët, të cilit mund t'i "bashkëngjitet" një rrjedhë specifike e punës.
Por ç'të themi për logjikën e integrimit? Në fund të fundit, detyra e integrimit gjenerohet nga arkitektura e sistemit, i cili "shërohet" në pjesë JO me kërkesë të klientit, por nën ndikimin e faktorëve krejtësisht të ndryshëm:

  • nën ndikimin e ligjit të Conway;
  • si rezultat i ripërdorimit të nënsistemeve të zhvilluara më parë për produkte të tjera;
  • siç ka vendosur arkitekti, bazuar në kërkesat jofunksionale.

Ekziston një tundim i madh për të ndarë logjikën e integrimit nga logjika e biznesit të fluksit kryesor të punës, në mënyrë që të mos ndotet logjika e biznesit me artefakte integrimi dhe të shpëtojë zhvilluesi i aplikacionit nga nevoja për të thelluar në veçoritë e peizazhit arkitektonik të sistemit. Kjo qasje ka një numër avantazhesh, por praktika tregon joefikasitetin e saj:

  • zgjidhja e problemeve të integrimit zakonisht rrëshqet në opsionet më të thjeshta në formën e thirrjeve sinkrone për shkak të pikave të kufizuara të zgjerimit në zbatimin e rrjedhës kryesore të punës (më shumë për mangësitë e integrimit sinkron më poshtë);
  • artefaktet e integrimit ende depërtojnë në logjikën kryesore të biznesit kur kërkohet reagim nga një nënsistem tjetër;
  • zhvilluesi i aplikacionit injoron integrimin dhe mund ta prishë lehtësisht atë duke ndryshuar rrjedhën e punës;
  • sistemi pushon së qeni një tërësi e vetme nga këndvështrimi i përdoruesit, "qepjet" midis nënsistemeve bëhen të dukshme, shfaqen operacione të tepërta të përdoruesit që nisin transferimin e të dhënave nga një nënsistem në tjetrin.

Një qasje tjetër është të konsiderohen ndërveprimet e integrimit si një pjesë integrale e logjikës kryesore të biznesit dhe rrjedhës së punës. Që kërkesat e aftësive të zhvilluesve të aplikacioneve të mos ngrihen në qiell, krijimi i ndërveprimeve të reja të integrimit duhet të bëhet lehtësisht dhe natyrshëm, me opsione minimale për zgjedhjen e një zgjidhjeje. Kjo është më e vështirë nga sa duket: mjeti duhet të jetë mjaft i fuqishëm për t'i siguruar përdoruesit shumëllojshmërinë e nevojshme të opsioneve për përdorimin e tij dhe në të njëjtën kohë të mos lejojë që të goditet në këmbë. Ka shumë pyetje që një inxhinier duhet t'i përgjigjet në kontekstin e detyrave të integrimit, por për të cilat një zhvillues aplikacioni nuk duhet të mendojë në punën e tij të përditshme: kufijtë e transaksionit, qëndrueshmëria, atomiciteti, siguria, shkallëzimi, ngarkesa dhe shpërndarja e burimeve, rrugëtimi, marshimi, Përhapja dhe ndërrimi i konteksteve, etj. Është e nevojshme t'u ofrohen zhvilluesve të aplikacioneve modele vendimesh mjaft të thjeshta, në të cilat përgjigjet për të gjitha pyetjet e tilla janë tashmë të fshehura. Këto modele duhet të jenë mjaft të sigurta: logjika e biznesit ndryshon shumë shpesh, gjë që rrit rrezikun e paraqitjes së gabimeve, kostoja e gabimeve duhet të mbetet në një nivel mjaft të ulët.

Por megjithatë, çfarë lidhje ka BPM me të? Ka shumë opsione për zbatimin e rrjedhës së punës ...
Në të vërtetë, një zbatim tjetër i proceseve të biznesit është shumë i popullarizuar në zgjidhjet tona - përmes vendosjes deklarative të diagramit të tranzicionit të gjendjes dhe lidhjes së mbajtësve me logjikën e biznesit me tranzicionet. Në të njëjtën kohë, gjendja që përcakton pozicionin aktual të "dokumentit" në procesin e biznesit është një atribut i vetë "dokumentit".

Integrimi i stilit BPM
Kështu duket procesi në fillim të projektit

Popullariteti i një zbatimi të tillë është për shkak të thjeshtësisë dhe shpejtësisë relative të krijimit të proceseve lineare të biznesit. Megjithatë, ndërsa sistemet softuerike bëhen më komplekse, pjesa e automatizuar e procesit të biznesit rritet dhe bëhet më komplekse. Ekziston nevoja për dekompozim, ripërdorim të pjesëve të proceseve, si dhe procese të forkimit në mënyrë që çdo degë të ekzekutohet paralelisht. Në kushte të tilla, mjeti bëhet i papërshtatshëm dhe diagrami i tranzicionit të gjendjes humbet përmbajtjen e tij të informacionit (ndërveprimet e integrimit nuk pasqyrohen fare në diagram).

Integrimi i stilit BPM
Kështu duket procesi pas disa përsëritjeve të sqarimit të kërkesave

Rruga për të dalë nga kjo situatë ishte integrimi i motorit jBPM në disa produkte me proceset më komplekse të biznesit. Në afat të shkurtër, kjo zgjidhje pati njëfarë suksesi: u bë e mundur zbatimi i proceseve komplekse të biznesit duke ruajtur një diagram mjaft informues dhe të përditësuar në shënim. BPMN2.

Integrimi i stilit BPM
Një pjesë e vogël e një procesi kompleks biznesi

Në terma afatgjatë, zgjidhja nuk i përmbushi pritshmëritë: intensiteti i lartë i punës së krijimit të proceseve të biznesit përmes mjeteve vizuale nuk lejoi arritjen e treguesve të pranueshëm të produktivitetit dhe vetë mjeti u bë një nga më të papëlqyerit midis zhvilluesve. Kishte gjithashtu ankesa për strukturën e brendshme të motorit, gjë që çoi në shfaqjen e shumë "arna" dhe "paterica".

Aspekti kryesor pozitiv i përdorimit të jBPM ishte realizimi i përfitimeve dhe dëmeve të të paturit të gjendjes së tij të vazhdueshme për një shembull procesi biznesi. Ne pamë gjithashtu mundësinë e përdorimit të një qasjeje procesi për zbatimin e protokolleve komplekse të integrimit midis aplikacioneve të ndryshme duke përdorur ndërveprime asinkrone përmes sinjaleve dhe mesazheve. Prania e një shteti të qëndrueshëm luan një rol vendimtar në këtë.

Bazuar në sa më sipër, mund të konkludojmë: Qasja e procesit në stilin BPM na lejon të zgjidhim një gamë të gjerë detyrash për automatizimin e proceseve gjithnjë e më komplekse të biznesit, të përshtatim në mënyrë harmonike aktivitetet e integrimit në këto procese dhe të ruajmë aftësinë për të shfaqur vizualisht procesin e zbatuar në një shënim të përshtatshëm.

Disavantazhet e thirrjeve sinkrone si model integrimi

Integrimi sinkron i referohet thirrjes më të thjeshtë të bllokimit. Një nënsistem vepron si ana e serverit dhe ekspozon API-në me metodën e dëshiruar. Një nënsistem tjetër vepron si palë klienti dhe, në kohën e duhur, bën një telefonatë me pritjen e një rezultati. Në varësi të arkitekturës së sistemit, anët e klientit dhe serverit mund të strehohen ose në të njëjtin aplikacion dhe proces, ose në të ndryshëm. Në rastin e dytë, ju duhet të aplikoni njëfarë zbatimi të RPC dhe të siguroni ndarjen e parametrave dhe rezultatin e thirrjes.

Integrimi i stilit BPM

Një model i tillë integrimi ka një grup mjaft të madh të metash, por përdoret shumë gjerësisht në praktikë për shkak të thjeshtësisë së tij. Shpejtësia e zbatimit të magjeps dhe të bën ta aplikosh herë pas here në kushtet e afateve “djegëse”, duke e shkruar zgjidhjen në borxh teknik. Por ndodh gjithashtu që zhvilluesit e papërvojë e përdorin atë në mënyrë të pandërgjegjshme, thjesht duke mos kuptuar pasojat negative.

Përveç rritjes më të dukshme në lidhjen e nënsistemeve, ka probleme më pak të dukshme me "përhapjen" dhe "shtrirjen" e transaksioneve. Në të vërtetë, nëse logjika e biznesit bën ndonjë ndryshim, atëherë transaksionet janë të domosdoshme, dhe transaksionet, nga ana tjetër, bllokojnë disa burime aplikacioni të prekura nga këto ndryshime. Kjo do të thotë, derisa një nënsistem të presë një përgjigje nga një tjetër, ai nuk do të jetë në gjendje të përfundojë transaksionin dhe të lëshojë bllokimet. Kjo rrit ndjeshëm rrezikun e një sërë efektesh:

  • reagimi i sistemit humbet, përdoruesit presin një kohë të gjatë për përgjigjet e kërkesave;
  • serveri në përgjithësi ndalon t'u përgjigjet kërkesave të përdoruesve për shkak të një grupi thread të tejmbushur: shumica e thread-ve "qëndrojnë" në bllokimin e burimit të zënë nga transaksioni;
  • ngërçet fillojnë të shfaqen: probabiliteti i shfaqjes së tyre varet fuqimisht nga kohëzgjatja e transaksioneve, sasia e logjikës së biznesit dhe bllokimet e përfshira në transaksion;
  • shfaqen gabime të skadimit të afatit të transaksionit;
  • serveri "bie" në OutOfMemory nëse detyra kërkon përpunim dhe ndryshim të sasive të mëdha të të dhënave, dhe prania e integrimeve sinkrone e bën shumë të vështirë ndarjen e përpunimit në transaksione "më të lehta".

Nga pikëpamja arkitekturore, përdorimi i thirrjeve bllokuese gjatë integrimit çon në një humbje të kontrollit të cilësisë së nënsistemeve individuale: është e pamundur të sigurohen objektivat e cilësisë së një nënsistem të veçuar nga objektivat e cilësisë së një nënsistemi tjetër. Nëse nënsistemet zhvillohen nga ekipe të ndryshme, ky është një problem i madh.

Gjërat bëhen edhe më interesante nëse nënsistemet që integrohen janë në aplikacione të ndryshme dhe duhet të bëhen ndryshime sinkrone në të dyja anët. Si t'i bëni këto ndryshime transaksionale?

Nëse ndryshimet bëhen në transaksione të veçanta, atëherë do të duhet të sigurohet trajtim dhe kompensim i fuqishëm i përjashtimeve, dhe kjo eliminon plotësisht avantazhin kryesor të integrimeve sinkron - thjeshtësinë.

Transaksionet e shpërndara gjithashtu vijnë në mendje, por ne nuk i përdorim ato në zgjidhjet tona: është e vështirë të sigurohet besueshmëria.

"Saga" si një zgjidhje për problemin e transaksioneve

Me popullaritetin në rritje të mikroshërbimeve, ka një kërkesë në rritje për të Modeli i sagës.

Ky model zgjidh në mënyrë të përsosur problemet e mësipërme të transaksioneve të gjata, dhe gjithashtu zgjeron mundësitë e menaxhimit të gjendjes së sistemit nga ana e logjikës së biznesit: kompensimi pas një transaksioni të pasuksesshëm mund të mos e kthejë sistemin në gjendjen e tij origjinale, por të sigurojë një alternativë. rruga e përpunimit të të dhënave. Gjithashtu ju lejon të mos përsërisni hapat e përfunduar me sukses të përpunimit të të dhënave kur përpiqeni ta çoni procesin në një fund "të mirë".

Interesante, në sistemet monolitike, ky model është gjithashtu i rëndësishëm kur bëhet fjalë për integrimin e nënsistemeve të lidhura lirshëm dhe ka efekte negative të shkaktuara nga transaksionet e gjata dhe bllokimet përkatëse të burimeve.

Në lidhje me proceset tona të biznesit në stilin BPM, rezulton të jetë shumë e lehtë për të zbatuar Sagas: hapat individualë të Sagas mund të vendosen si aktivitete brenda procesit të biznesit, dhe gjendja e vazhdueshme e procesit të biznesit përcakton, ndër të tjera. , gjendja e brendshme e Sagave. Kjo do të thotë, ne nuk kemi nevojë për ndonjë mekanizëm koordinues shtesë. Gjithçka që ju nevojitet është një ndërmjetës mesazhesh me mbështetje për "të paktën një herë" garanci si transport.

Por një zgjidhje e tillë ka edhe "çmimin" e vet:

  • logjika e biznesit bëhet më komplekse: duhet të përpunoni kompensimin;
  • do të jetë e nevojshme të braktiset konsistenca e plotë, e cila mund të jetë veçanërisht e ndjeshme për sistemet monolite;
  • arkitektura bëhet pak më e ndërlikuar, ekziston një nevojë shtesë për një ndërmjetës mesazhesh;
  • do të kërkohen mjete shtesë monitorimi dhe administrimi (edhe pse në përgjithësi kjo është edhe e mirë: cilësia e shërbimit të sistemit do të rritet).

Për sistemet monolitike, justifikimi për përdorimin e "Sags" nuk është aq i qartë. Për mikroshërbimet dhe SOA-të e tjera, ku, ka shumë të ngjarë, ekziston tashmë një ndërmjetës dhe qëndrueshmëria e plotë u sakrifikua në fillim të projektit, përfitimet e përdorimit të këtij modeli mund të tejkalojnë ndjeshëm disavantazhet, veçanërisht nëse ka një API të përshtatshme në niveli i logjikës së biznesit.

Enkapsulimi i logjikës së biznesit në mikroshërbime

Kur filluam të eksperimentonim me mikroshërbimet, lindi një pyetje e arsyeshme: ku të vendoset logjika e biznesit të domenit në lidhje me shërbimin që ofron qëndrueshmëri të të dhënave të domenit?

Kur shikojmë arkitekturën e BPMS-ve të ndryshme, mund të duket e arsyeshme të ndash logjikën e biznesit nga qëndrueshmëria: krijoni një shtresë mikroshërbimesh të pavarura nga platforma dhe domeni që formojnë mjedisin dhe kontejnerin për ekzekutimin e logjikës së biznesit të domenit dhe rregulloni qëndrueshmërinë e të dhënave të domenit si një të veçantë. shtresë e mikroshërbimeve shumë të thjeshta dhe të lehta. Proceset e biznesit në këtë rast orkestrojnë shërbimet e shtresës së qëndrueshmërisë.

Integrimi i stilit BPM

Kjo qasje ka një plus shumë të madh: ju mund të rrisni funksionalitetin e platformës aq sa dëshironi, dhe vetëm shtresa përkatëse e mikroshërbimeve të platformës do të "shëndoshet" nga kjo. Proceset e biznesit nga çdo domen marrin menjëherë mundësinë për të përdorur funksionalitetin e ri të platformës sapo ajo të përditësohet.

Një studim më i detajuar zbuloi mangësi të rëndësishme të kësaj qasjeje:

  • një shërbim platformë që ekzekuton logjikën e biznesit të shumë domeneve në të njëjtën kohë mbart rreziqe të mëdha si një pikë e vetme dështimi. Ndryshimet e shpeshta në logjikën e biznesit rrisin rrezikun e gabimeve që çojnë në dështime në të gjithë sistemin;
  • çështjet e performancës: logjika e biznesit punon me të dhënat e saj përmes një ndërfaqe të ngushtë dhe të ngadaltë:
    • të dhënat do të grumbullohen përsëri dhe do të pompohen përmes grupit të rrjetit;
    • shërbimi i domenit shpesh do të kthejë më shumë të dhëna sesa kërkon logjika e biznesit për përpunim, për shkak të aftësive të pamjaftueshme të parametrizimit të pyetjeve në nivelin e API-së së jashtme të shërbimit;
    • disa pjesë të pavarura të logjikës së biznesit mund të rikërkojnë në mënyrë të përsëritur të njëjtat të dhëna për përpunim (ju mund ta zbutni këtë problem duke shtuar sesion beans që ruajnë të dhënat e memories, por kjo e ndërlikon më tej arkitekturën dhe krijon probleme të freskisë së të dhënave dhe zhvlerësimit të cache-it);
  • çështjet e transaksionit:
    • proceset e biznesit me gjendje të vazhdueshme të ruajtura nga shërbimi i platformës nuk janë në përputhje me të dhënat e domenit dhe nuk ka mënyra të lehta për të zgjidhur këtë problem;
    • zhvendosja e bllokimit të të dhënave të domenit jashtë transaksionit: nëse logjika e biznesit të domenit duhet të bëjë ndryshime, pasi të keni kontrolluar së pari korrektësinë e të dhënave aktuale, është e nevojshme të përjashtohet mundësia e një ndryshimi konkurrues në të dhënat e përpunuara. Bllokimi i jashtëm i të dhënave mund të ndihmojë në zgjidhjen e problemit, por një zgjidhje e tillë mbart rreziqe shtesë dhe zvogëlon besueshmërinë e përgjithshme të sistemit;
  • komplikime shtesë gjatë përditësimit: në disa raste, ju duhet të përditësoni shërbimin e qëndrueshmërisë dhe logjikën e biznesit në mënyrë sinkrone ose në sekuencë strikte.

Në fund, më duhej të kthehesha te bazat: të përmbledh të dhënat e domenit dhe logjikën e biznesit të domenit në një mikroshërbim. Kjo qasje thjeshton perceptimin e mikroshërbimit si një komponent integral në sistem dhe nuk shkakton problemet e mësipërme. Kjo gjithashtu nuk është falas:

  • Standardizimi i API kërkohet për ndërveprim me logjikën e biznesit (në veçanti, për të ofruar aktivitete të përdoruesve si pjesë e proceseve të biznesit) dhe shërbimet e platformës API; vëmendje më e kujdesshme ndaj ndryshimeve të API, kërkohet përputhshmëri përpara dhe prapa;
  • kërkohet të shtohen biblioteka shtesë të kohës së funksionimit për të siguruar funksionimin e logjikës së biznesit si pjesë e çdo mikroshërbimi të tillë, dhe kjo krijon kërkesa të reja për biblioteka të tilla: lehtësi dhe një minimum varësish kalimtare;
  • Zhvilluesit e logjikës së biznesit duhet të mbajnë gjurmët e versioneve të bibliotekës: nëse një mikroshërbim nuk është finalizuar për një kohë të gjatë, atëherë ka shumë të ngjarë që ai të përmbajë një version të vjetëruar të bibliotekave. Kjo mund të jetë një pengesë e papritur për shtimin e një veçorie të re dhe mund të kërkojë që logjika e vjetër e biznesit të një shërbimi të tillë të migrohet në versionet e reja të bibliotekave nëse ka ndryshime të papajtueshme midis versioneve.

Integrimi i stilit BPM

Një shtresë e shërbimeve të platformës është gjithashtu e pranishme në një arkitekturë të tillë, por kjo shtresë nuk formon më një kontejner për ekzekutimin e logjikës së biznesit të domenit, por vetëm mjedisin e tij, duke ofruar funksione "platformë" ndihmëse. Një shtresë e tillë nevojitet jo vetëm për të ruajtur lehtësinë e mikroshërbimeve të domenit, por edhe për të centralizuar menaxhimin.

Për shembull, aktivitetet e përdoruesve në proceset e biznesit gjenerojnë detyra. Megjithatë, kur punon me detyra, përdoruesi duhet të shohë detyrat nga të gjitha domenet në listën e përgjithshme, që do të thotë se duhet të ketë një shërbim të përshtatshëm të platformës së regjistrimit të detyrave, të pastruar nga logjika e biznesit të domenit. Mbajtja e kapsulimit të logjikës së biznesit në këtë kontekst është mjaft problematike dhe ky është një tjetër kompromis i kësaj arkitekture.

Integrimi i proceseve të biznesit përmes syve të një zhvilluesi të aplikacionit

Siç u përmend më lart, zhvilluesi i aplikacionit duhet të abstragohet nga veçoritë teknike dhe inxhinierike të zbatimit të ndërveprimit të disa aplikacioneve në mënyrë që të jetë në gjendje të llogarisë në produktivitetin e mirë të zhvillimit.

Le të përpiqemi të zgjidhim një problem mjaft të vështirë integrimi, të shpikur posaçërisht për artikullin. Kjo do të jetë një detyrë "lojë" që përfshin tre aplikacione, ku secili prej tyre përcakton një emër domain: "app1", "app2", "app3".

Brenda çdo aplikacioni nisin proceset e biznesit që fillojnë të "luajnë top" përmes autobusit të integrimit. Mesazhet me emrin "Ball" do të veprojnë si top.

Rregullat e lojës:

  • lojtari i parë është iniciator. Ai fton lojtarët e tjerë në lojë, e fillon lojën dhe mund ta përfundojë atë në çdo kohë;
  • lojtarët e tjerë deklarojnë pjesëmarrjen e tyre në lojë, "njihen" me njëri-tjetrin dhe lojtarin e parë;
  • pasi merr topin, lojtari zgjedh një lojtar tjetër pjesëmarrës dhe ia kalon topin atij. Numri i përgjithshëm i kalimeve llogaritet;
  • çdo lojtar ka "energji", e cila zvogëlohet me çdo pasim të topit nga ai lojtar. Kur energjia mbaron, lojtari eliminohet nga loja, duke njoftuar daljen në pension;
  • nëse lojtari mbetet vetëm, ai menjëherë deklaron largimin e tij;
  • kur të gjithë lojtarët eliminohen, lojtari i parë shpall fundin e lojës. Nëse ai u largua nga loja më herët, atëherë mbetet të ndjekim lojën për ta përfunduar atë.

Për të zgjidhur këtë problem, unë do të përdor DSL-në tonë për proceset e biznesit, e cila ju lejon të përshkruani logjikën në Kotlin në mënyrë kompakte, me një minimum prej një pllake boiler.

Në aplikacionin app1, procesi i biznesit të lojtarit të parë (ai është edhe iniciatori i lojës) do të funksionojë:

Klasa 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<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    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)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

private fun MessageSendInstance<InitialPlayer, Int>.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Përveç ekzekutimit të logjikës së biznesit, kodi i mësipërm mund të prodhojë një model objekti të një procesi biznesi që mund të vizualizohet si një diagram. Ne nuk e kemi zbatuar ende vizualizuesin, kështu që na duhej të kalonim pak kohë duke vizatuar (këtu thjeshtova pak shënimin BPMN në lidhje me përdorimin e portave për të përmirësuar qëndrueshmërinë e diagramit me kodin e mësipërm):

Integrimi i stilit BPM

app2 do të përfshijë procesin e biznesit të një lojtari tjetër:

Klasa 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<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(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<RandomPlayer>(name = "RandomPlayer", 
                                                   version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("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<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("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<RandomPlayer, Int>.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ë:

Integrimi i stilit BPM

Në aplikacionin app3, ne do ta bëjmë lojtarin me një sjellje paksa të ndryshme: në vend që të zgjedhë rastësisht lojtarin tjetër, ai do të veprojë sipas algoritmit të rrumbullakët:

Klasa RoundRobin Player

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<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(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<RoundRobinPlayer>(
        name = "RoundRobinPlayer", 
        version = 1) {

    val waitNewGameSignal = signalWait<String>("NewGame")
    val waitStopGameSignal = signalWait<String>("StopGame")
    val sendPlayerJoin = signal<String>("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom<String>("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<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!, 
                signal.sender.domain, 
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait<String>("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("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<RoundRobinPlayer, Int>.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}")
}

Përndryshe, sjellja e lojtarit nuk ndryshon nga ajo e mëparshme, kështu që diagrami nuk ndryshon.

Tani na duhet një provë për t'i ekzekutuar të gjitha. Unë do të jap vetëm kodin e vetë testit, në mënyrë që të mos rrëmbej artikullin me një pllakë bojler (në fakt, kam përdorur mjedisin e testimit të krijuar më herët për të testuar integrimin e proceseve të tjera të biznesit):

testLojë ()

@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");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    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<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Kryeni testin, shikoni regjistrin:

prodhimi i konsolës

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

Nga e gjithë kjo mund të nxirren disa përfundime të rëndësishme:

  • nëse mjetet e nevojshme janë të disponueshme, zhvilluesit e aplikacioneve mund të krijojnë ndërveprime integruese ndërmjet aplikacioneve pa u shkëputur nga logjika e biznesit;
  • kompleksiteti (kompleksiteti) i një detyre integruese që kërkon kompetenca inxhinierike mund të fshihet brenda kornizës nëse fillimisht është përcaktuar në arkitekturën e kornizës. Vështirësia e detyrës (vështirësia) nuk mund të fshihet, kështu që zgjidhja e një detyre të vështirë në kod do të duket në përputhje me rrethanat;
  • gjatë zhvillimit të logjikës së integrimit, është e nevojshme të merret parasysh përfundimisht konsistenca dhe mungesa e linearizimit të ndryshimit të gjendjes së të gjithë pjesëmarrësve në integrim. Kjo na detyron të ndërlikojmë logjikën në mënyrë që ta bëjmë atë të pandjeshëm ndaj rendit në të cilin ndodhin ngjarjet e jashtme. Në shembullin tonë, lojtari detyrohet të marrë pjesë në lojë pasi të njoftojë daljen e tij nga loja: lojtarët e tjerë do të vazhdojnë t'ia kalojnë topin derisa informacioni për daljen e tij të arrijë dhe të përpunohet nga të gjithë pjesëmarrësit. Kjo logjikë nuk rrjedh nga rregullat e lojës dhe është një zgjidhje kompromisi brenda kuadrit të arkitekturës së zgjedhur.

Më tej, le të flasim për hollësitë e ndryshme të zgjidhjes sonë, kompromiset dhe pikat e tjera.

Të gjitha mesazhet në një radhë

Të gjitha aplikacionet e integruara punojnë me një autobus integrimi, i cili përfaqësohet si një ndërmjetës i jashtëm, një BPMQueue për mesazhet dhe një temë BPMTopic për sinjalet (ngjarjet). Kalimi i të gjitha mesazheve përmes një radhe të vetme është në vetvete një kompromis. Në nivelin e logjikës së biznesit, tani mund të prezantoni sa më shumë lloje të reja mesazhesh që dëshironi pa bërë ndryshime në strukturën e sistemit. Ky është një thjeshtësim domethënës, por mbart disa rreziqe, të cilat, në kuadrin e detyrave tona tipike, na dukeshin jo aq të rëndësishme.

Integrimi i stilit BPM

Megjithatë, ka një hollësi këtu: çdo aplikacion filtron mesazhet "e tij" nga radha në hyrje, me emrin e domenit të tij. Gjithashtu, domeni mund të specifikohet në sinjale, nëse duhet të kufizoni "sferën" e sinjalit në një aplikacion të vetëm. Kjo duhet të rrisë gjerësinë e brezit të autobusit, por logjika e biznesit tani duhet të funksionojë me emra domain: të detyrueshëm për adresimin e mesazheve, të dëshirueshëm për sinjalet.

Sigurimi i besueshmërisë së autobusit të integrimit

Besueshmëria përbëhet nga disa gjëra:

  • Ndërmjetësi i zgjedhur i mesazheve është një komponent kritik i arkitekturës dhe një pikë e vetme dështimi: duhet të jetë mjaftueshëm tolerant ndaj gabimeve. Ju duhet të përdorni vetëm zbatime të testuara me kohë me mbështetje të mirë dhe një komunitet të madh;
  • është e nevojshme të sigurohet disponueshmëria e lartë e ndërmjetësit të mesazheve, për të cilën ai duhet të ndahet fizikisht nga aplikacionet e integruara (disponueshmëria e lartë e aplikacioneve me logjikë të aplikuar të biznesit është shumë më e vështirë dhe e kushtueshme për t'u siguruar);
  • ndërmjetësi është i detyruar të japë "të paktën një herë" garanci dorëzimi. Kjo është një kërkesë e detyrueshme për funksionimin e besueshëm të autobusit të integrimit. Nuk ka nevojë për garanci të nivelit "saktësisht një herë": proceset e biznesit zakonisht nuk janë të ndjeshme ndaj mbërritjes së përsëritur të mesazheve ose ngjarjeve, dhe në detyra të veçanta ku kjo është e rëndësishme, është më e lehtë të shtoni kontrolle shtesë në logjikën e biznesit sesa të përdorni vazhdimisht. garanci mjaft "të shtrenjta";
  • dërgimi i mesazheve dhe sinjaleve duhet të përfshihet në një transaksion të përbashkët me një ndryshim në gjendjen e proceseve të biznesit dhe të dhënave të domenit. Opsioni i preferuar do të ishte përdorimi i modelit Kutia dalëse e transaksioneve, por do të kërkojë një tabelë shtesë në bazën e të dhënave dhe një stafetë. Në aplikacionet JEE, kjo mund të thjeshtohet duke përdorur një menaxher lokal JTA, por lidhja me ndërmjetësin e zgjedhur duhet të jetë në gjendje të funksionojë në modalitet XA;
  • mbajtësit e mesazheve dhe ngjarjeve hyrëse duhet gjithashtu të punojnë me transaksionin e ndryshimit të gjendjes së procesit të biznesit: nëse një transaksion i tillë rikthehet, atëherë marrja e mesazhit gjithashtu duhet të anulohet;
  • mesazhet që nuk mund të dërgoheshin për shkak të gabimeve duhet të ruhen në një dyqan të veçantë D.L.Q. (Radha e shkronjave të vdekura). Për ta bërë këtë, ne krijuam një mikroshërbim të veçantë platformë që ruan mesazhe të tilla në ruajtjen e tij, i indekson ato sipas atributeve (për grupim dhe kërkim të shpejtë) dhe ekspozon API-në për shikim, ridërgim në adresën e destinacionit dhe fshirjen e mesazheve. Administratorët e sistemit mund të punojnë me këtë shërbim përmes ndërfaqes së tyre të internetit;
  • në cilësimet e ndërmjetësit, ju duhet të rregulloni numrin e riprovave të dorëzimit dhe vonesave midis dërgesave në mënyrë që të zvogëloni mundësinë e hyrjes së mesazheve në DLQ (është pothuajse e pamundur të llogaritni parametrat optimalë, por mund të veproni në mënyrë empirike dhe t'i rregulloni ato gjatë operacion);
  • dyqani DLQ duhet të monitorohet vazhdimisht dhe sistemi i monitorimit duhet të njoftojë administratorët e sistemit në mënyrë që ata të përgjigjen sa më shpejt që të jetë e mundur kur ndodhin mesazhe të padorëzuara. Kjo do të zvogëlojë "zonën e dëmtimit" të një dështimi ose gabimi logjik të biznesit;
  • autobusi i integrimit duhet të jetë i pandjeshëm ndaj mungesës së përkohshme të aplikacioneve: abonimet e temave duhet të jenë të qëndrueshme dhe emri i domenit të aplikacionit duhet të jetë unik në mënyrë që dikush tjetër të mos përpiqet të përpunojë mesazhin e tij nga radha gjatë mungesës së aplikacionit.

Sigurimi i sigurisë së fijeve të logjikës së biznesit

I njëjti shembull i një procesi biznesi mund të marrë disa mesazhe dhe ngjarje njëherësh, përpunimi i të cilave do të fillojë paralelisht. Në të njëjtën kohë, për një zhvillues aplikacioni, gjithçka duhet të jetë e thjeshtë dhe e sigurt për fijet.

Logjika e biznesit të procesit përpunon çdo ngjarje të jashtme që ndikon në këtë proces biznesi individualisht. Këto ngjarje mund të jenë:

  • nisja e një shembulli të procesit të biznesit;
  • një veprim përdoruesi që lidhet me një aktivitet brenda një procesi biznesi;
  • marrja e një mesazhi ose sinjali në të cilin është abonuar një shembull i procesit të biznesit;
  • skadimi i kohëmatësit të vendosur nga shembulli i procesit të biznesit;
  • veprimin e kontrollit nëpërmjet API (p.sh. ndërprerja e procesit).

Çdo ngjarje e tillë mund të ndryshojë gjendjen e një shembulli të procesit të biznesit: disa aktivitete mund të përfundojnë dhe të tjera të fillojnë, vlerat e pronave të qëndrueshme mund të ndryshojnë. Mbyllja e çdo aktiviteti mund të rezultojë në aktivizimin e një ose më shumë prej aktiviteteve të mëposhtme. Ata, nga ana tjetër, mund të ndalojnë së prituri për ngjarje të tjera, ose, nëse nuk kanë nevojë për të dhëna shtesë, mund të kryejnë të njëjtin transaksion. Para mbylljes së transaksionit, gjendja e re e procesit të biznesit ruhet në bazën e të dhënave, ku do të presë për ngjarjen e jashtme të radhës.

Të dhënat e vazhdueshme të procesit të biznesit të ruajtura në një bazë të dhënash relacionale janë një pikë shumë e përshtatshme sinkronizimi përpunimi kur përdorni SELECT FOR UPDATE. Nëse një transaksion arriti të marrë gjendjen e procesit të biznesit nga baza për ta ndryshuar atë, atëherë asnjë transaksion tjetër paralelisht nuk do të mund të marrë të njëjtën gjendje për një ndryshim tjetër, dhe pas përfundimit të transaksionit të parë, i dyti është të garantuara për të marrë gjendjen tashmë të ndryshuar.

Duke përdorur bravë pesimiste në anën DBMS, ne përmbushim të gjitha kërkesat e nevojshme ACID, dhe gjithashtu ruani aftësinë për të shkallëzuar aplikacionin me logjikën e biznesit duke rritur numrin e rasteve të ekzekutimit.

Megjithatë, bravat pesimiste na kërcënojnë me ngërçe, që do të thotë se ZGJIDH PËR PËRDITËSIM duhet të kufizohet ende në një kohë të arsyeshme në rast të bllokimeve në disa raste skandaloze në logjikën e biznesit.

Një problem tjetër është sinkronizimi i fillimit të procesit të biznesit. Ndërsa nuk ka asnjë shembull të procesit të biznesit, nuk ka gjendje as në bazën e të dhënave, kështu që metoda e përshkruar nuk do të funksionojë. Nëse dëshironi të siguroni veçantinë e një shembulli të procesit të biznesit në një fushë të caktuar, atëherë keni nevojë për një lloj objekti sinkronizimi të lidhur me klasën e procesit dhe shtrirjen përkatëse. Për të zgjidhur këtë problem, ne përdorim një mekanizëm tjetër mbylljeje që na lejon të bllokojmë një burim arbitrar të specifikuar nga një çelës në formatin URI përmes një shërbimi të jashtëm.

Në shembujt tanë, procesi i biznesit InitialPlayer përmban një deklaratë

uniqueConstraint = UniqueConstraints.singleton

Prandaj, regjistri përmban mesazhe për marrjen dhe lëshimin e kyçit të çelësit përkatës. Nuk ka mesazhe të tilla për procese të tjera biznesi: kufizimi unik nuk është vendosur.

Problemet e procesit të biznesit me gjendje të vazhdueshme

Ndonjëherë të kesh një gjendje të vazhdueshme jo vetëm që ndihmon, por edhe pengon vërtet zhvillimin.
Problemet fillojnë kur ju duhet të bëni ndryshime në logjikën e biznesit dhe/ose modelin e procesit të biznesit. Asnjë ndryshim i tillë nuk është gjetur të jetë në përputhje me gjendjen e vjetër të proceseve të biznesit. Nëse ka shumë raste "live" në bazën e të dhënave, atëherë bërja e ndryshimeve të papajtueshme mund të shkaktojë shumë telashe, të cilat shpesh i hasim kur përdorim jBPM.

Në varësi të thellësisë së ndryshimit, mund të veproni në dy mënyra:

  1. krijoni një lloj të ri procesi biznesi në mënyrë që të mos bëni ndryshime të papajtueshme me atë të vjetër dhe përdorni atë në vend të atij të vjetër kur filloni instanca të reja. Instancat e vjetra do të vazhdojnë të punojnë "sipas mënyrës së vjetër";
  2. migrojnë gjendjen e vazhdueshme të proceseve të biznesit kur përditësojnë logjikën e biznesit.

Mënyra e parë është më e thjeshtë, por ka kufizimet dhe disavantazhet e saj, për shembull:

  • dyfishimi i logjikës së biznesit në shumë modele të procesit të biznesit, një rritje në vëllimin e logjikës së biznesit;
  • shpesh kërkohet një kalim i menjëhershëm në një logjikë të re biznesi (pothuajse gjithmonë për sa i përket detyrave të integrimit);
  • zhvilluesi nuk e di se në cilën pikë është e mundur të fshihen modelet e vjetëruara.

Në praktikë, ne përdorim të dyja qasjet, por kemi marrë një sërë vendimesh për të thjeshtuar jetën tonë:

  • në bazën e të dhënave, gjendja e vazhdueshme e procesit të biznesit ruhet në një formë lehtësisht të lexueshme dhe lehtësisht të përpunueshme: në një varg të formatit JSON. Kjo ju lejon të kryeni migrime si brenda aplikacionit ashtu edhe jashtë. Në raste ekstreme, ju gjithashtu mund ta shkulni atë me doreza (veçanërisht të dobishme në zhvillim gjatë korrigjimit);
  • Logjika e biznesit të integrimit nuk përdor emrat e proceseve të biznesit, kështu që në çdo kohë është e mundur të zëvendësohet zbatimi i një prej proceseve pjesëmarrëse me një të ri, me një emër të ri (për shembull, "InitialPlayerV2"). Lidhja ndodh përmes emrave të mesazheve dhe sinjaleve;
  • modeli i procesit ka një numër versioni, të cilin e rritim nëse bëjmë ndryshime të papajtueshme në këtë model dhe ky numër ruhet së bashku me gjendjen e shembullit të procesit;
  • gjendja e vazhdueshme e procesit lexohet nga baza së pari në një model objekti të përshtatshëm me të cilin mund të funksionojë procedura e migrimit nëse numri i versionit të modelit ka ndryshuar;
  • procedura e migrimit vendoset pranë logjikës së biznesit dhe quhet "dembel" për çdo rast të procesit të biznesit në momentin e restaurimit të tij nga baza e të dhënave;
  • nëse duhet të migroni gjendjen e të gjitha rasteve të procesit shpejt dhe në mënyrë sinkrone, përdoren zgjidhje më klasike të migrimit të bazës së të dhënave, por duhet të punoni me JSON atje.

A kam nevojë për një kornizë tjetër për proceset e biznesit?

Zgjidhjet e përshkruara në artikull na lejuan të thjeshtojmë ndjeshëm jetën tonë, të zgjerojmë gamën e çështjeve të zgjidhura në nivelin e zhvillimit të aplikacionit dhe ta bëjmë më tërheqëse idenë e ndarjes së logjikës së biznesit në mikroshërbime. Për këtë është bërë shumë punë, është krijuar një kuadër shumë “i lehtë” për proceset e biznesit, si dhe komponentë shërbimi për zgjidhjen e problemeve të identifikuara në kuadër të një game të gjerë detyrash të aplikuara. Ne kemi një dëshirë për të ndarë këto rezultate, për të sjellë zhvillimin e komponentëve të përbashkët në akses të hapur nën një licencë falas. Kjo do të kërkojë disa përpjekje dhe kohë. Kuptimi i kërkesës për zgjidhje të tilla mund të jetë një nxitje shtesë për ne. Në artikullin e propozuar, shumë pak vëmendje i kushtohet aftësive të vetë kornizës, por disa prej tyre janë të dukshme nga shembujt e paraqitur. Nëse megjithatë botojmë kornizën tonë, do t'i kushtohet një artikull i veçantë. Ndërkohë, do t'ju jemi mirënjohës nëse lini një koment të vogël duke iu përgjigjur pyetjes:

Vetëm përdoruesit e regjistruar mund të marrin pjesë në anketë. Hyni, te lutem

A kam nevojë për një kornizë tjetër për proceset e biznesit?

  • 18,8%Po, kam kohë që kërkoj diçka të tillë.

  • 12,5%është interesante të mësoni më shumë rreth zbatimit tuaj, mund të jetë i dobishëm2

  • 6,2%ne përdorim një nga kornizat ekzistuese, por po mendojmë ta zëvendësojmë1

  • 18,8%ne përdorim një nga kornizat ekzistuese, gjithçka i përshtatet3

  • 18,8%përballimi pa kornizë3

  • 25,0%shkruani tuajën4

16 përdorues votuan. 7 përdorues abstenuan.

Burimi: www.habr.com

Shto një koment