BPM stila integrācija

BPM stila integrācija

Sveiki, Habr!

MÅ«su uzņēmums specializējas ERP klases programmatÅ«ras risinājumu izstrādē, kuru lauvas tiesu aizņem transakciju sistēmas ar milzÄ«gu biznesa loÄ£ikas un dokumentu plÅ«smas apjomu a la EDMS. MÅ«su produktu paÅ”reizējās versijas ir balstÄ«tas uz JavaEE tehnoloÄ£ijām, taču mēs arÄ« aktÄ«vi eksperimentējam ar mikropakalpojumiem. Viena no problemātiskākajām Ŕādu risinājumu jomām ir dažādu blakus domēnu apakÅ”sistēmu integrācija. Integrācijas problēmas mums vienmēr ir sagādājuÅ”as milzÄ«gas galvassāpes neatkarÄ«gi no izmantotajiem arhitektÅ«ras stiliem, tehnoloÄ£iju skursteņiem un ietvariem, taču pēdējā laikā ir vērojams progress Ŕādu problēmu risināŔanā.

Rakstā, kuru piedāvāju jÅ«su uzmanÄ«bai, pastāstÄ«Å”u par NPO ā€œKristaā€ pieredzi un arhitektÅ«ras izpēti norādÄ«tajā teritorijā. ApskatÄ«sim arÄ« vienkārÅ”a integrācijas problēmas risinājuma piemēru no lietojumprogrammu izstrādātāja viedokļa un uzzināsim, kas slēpjas aiz Ŕīs vienkārŔības.

Atruna

Rakstā aprakstÄ«tos arhitektoniskos un tehniskos risinājumus esmu piedāvājis, balstoties uz personÄ«go pieredzi konkrētu uzdevumu kontekstā. Å ie risinājumi nepretendē uz universāliem un var nebÅ«t optimāli citos lietoÅ”anas apstākļos.

Kāds BPM sakars ar to?

Lai atbildētu uz Å”o jautājumu, mums ir nedaudz dziļāk jāiedziļinās mÅ«su risinājumu pielietoto problēmu specifikā. Galvenā biznesa loÄ£ikas daļa mÅ«su tipiskajā darÄ«jumu sistēmā ir datu ievadÄ«Å”ana datu bāzē, izmantojot lietotāja saskarnes, Å”o datu manuāla un automatizēta pārbaude, to veikÅ”ana, izmantojot kādu darbplÅ«smu, publicÄ“Å”ana citā sistēmā / analÄ«tiskā datu bāzē / arhÄ«vā, atskaiÅ”u Ä£enerÄ“Å”ana. . Tādējādi sistēmas galvenā funkcija klientiem ir viņu iekŔējo biznesa procesu automatizācija.

ĒrtÄ«bas labad mēs saziņā lietojam terminu ā€œdokumentsā€ kā abstrakciju no datu kopas, ko vieno kopēja atslēga, ar kuru var ā€œsaistÄ«tā€ noteiktu darbplÅ«smu.
Bet kā ar integrācijas loÄ£iku? Galu galā integrācijas uzdevumu Ä£enerē sistēmas arhitektÅ«ra, kas tiek ā€œsagrieztaā€ daļās NEVIS pēc klienta pieprasÄ«juma, bet gan pilnÄ«gi atŔķirÄ«gu faktoru ietekmē:

  • pakļauts Konveja likumam;
  • iepriekÅ” citiem produktiem izstrādātu apakÅ”sistēmu atkārtotas izmantoÅ”anas rezultātā;
  • pēc arhitekta ieskatiem, pamatojoties uz nefunkcionālām prasÄ«bām.

Pastāv liels kārdinājums atdalÄ«t integrācijas loÄ£iku no galvenās darbplÅ«smas biznesa loÄ£ikas, lai nepiesārņotu biznesa loÄ£iku ar integrācijas artefaktiem un glābtu lietojumprogrammu izstrādātāju no nepiecieÅ”amÄ«bas iedziļināties sistēmas arhitektÅ«ras ainavas iezÄ«mēs. Å ai pieejai ir vairākas priekÅ”rocÄ«bas, taču prakse parāda tās neefektivitāti:

  • integrācijas problēmu risināŔana parasti atgriežas pie vienkārŔākajām iespējām sinhrono zvanu veidā, jo galvenās darbplÅ«smas Ä«stenoÅ”anā ir ierobežoti paplaÅ”inājuma punkti (sinhronās integrācijas trÅ«kumi ir apskatÄ«ti tālāk);
  • integrācijas artefakti joprojām iekļūst biznesa pamatloÄ£ikā, ja ir nepiecieÅ”ama atgriezeniskā saite no citas apakÅ”sistēmas;
  • lietojumprogrammas izstrādātājs ignorē integrāciju un var viegli to pārtraukt, mainot darbplÅ«smu;
  • sistēma pārstāj bÅ«t vienots veselums no lietotāja viedokļa, kļūst pamanāmas ā€œÅ”uvesā€ starp apakÅ”sistēmām un parādās liekas lietotāja darbÄ«bas, kas iniciē datu pārsÅ«tÄ«Å”anu no vienas apakÅ”sistēmas uz otru.

Vēl viena pieeja ir uzskatÄ«t integrācijas mijiedarbÄ«bu par galvenās biznesa loÄ£ikas un darbplÅ«smas neatņemamu sastāvdaļu. Lai nepieļautu lietojumprogrammu izstrādātāju kvalifikācijas strauju pieaugumu, jaunu integrācijas mijiedarbÄ«bu izveidei jābÅ«t vienkārÅ”ai un bez piepÅ«les, ar minimālām risinājuma izvēles iespējām. To izdarÄ«t ir grÅ«tāk, nekā Ŕķiet: instrumentam ir jābÅ«t pietiekami jaudÄ«gam, lai nodroÅ”inātu lietotājam nepiecieÅ”amās dažādas tā izmantoÅ”anas iespējas, neļaujot viņam ā€œieÅ”aut sev kājāā€. Ir daudzi jautājumi, uz kuriem inženierim ir jāatbild integrācijas uzdevumu kontekstā, bet par kuriem lietojumprogrammu izstrādātājam nevajadzētu domāt savā ikdienas darbā: darÄ«jumu robežas, konsekvence, atomitāte, droŔība, mērogoÅ”ana, slodzes un resursu sadale, marÅ”rutÄ“Å”ana, ŔķiroÅ”ana, izplatÄ«Å”anas un komutācijas konteksti utt. NepiecieÅ”ams piedāvāt lietojumprogrammu izstrādātājiem diezgan vienkārÅ”as risinājumu veidnes, kurās atbildes uz visiem Ŕādiem jautājumiem jau ir paslēptas. Å Ä«m veidnēm jābÅ«t diezgan droŔām: biznesa loÄ£ika mainās ļoti bieži, kas palielina kļūdu raÅ”anās risku, kļūdu izmaksām jāpaliek diezgan zemā lÄ«menÄ«.

Bet kāds BPM sakars ar to? Ir daudz iespēju darbplÅ«smas ievieÅ”anai...
PatieŔām, mÅ«su risinājumos ļoti populāra ir cita biznesa procesu realizācija - ar deklaratÄ«vu stāvokļa pārejas diagrammas definÄ«ciju un apstrādātāju savienoÅ”anu ar biznesa loÄ£iku pārejām. Å ajā gadÄ«jumā stāvoklis, kas nosaka ā€œdokumentaā€ paÅ”reizējo pozÄ«ciju biznesa procesā, ir paÅ”a ā€œdokumentaā€ atribÅ«ts.

BPM stila integrācija
Šādi izskatās process projekta sākumā

Å Ä«s ievieÅ”anas popularitāte ir saistÄ«ta ar lineāro biznesa procesu izveides relatÄ«vo vienkārŔību un ātrumu. Tomēr, tā kā programmatÅ«ras sistēmas nemitÄ«gi kļūst sarežģītākas, biznesa procesa automatizētā daļa aug un kļūst sarežģītāka. Ir nepiecieÅ”ama dekompozÄ«cija, procesu daļu atkārtota izmantoÅ”ana, kā arÄ« procesu sazaroÅ”ana, lai katrs zars tiktu izpildÄ«ts paralēli. Šādos apstākļos rÄ«ks kļūst neērts, un stāvokļa pārejas diagramma zaudē savu informācijas saturu (integrācijas mijiedarbÄ«ba diagrammā vispār netiek atspoguļota).

BPM stila integrācija
Šādi process izskatās pēc vairākām prasÄ«bu precizÄ“Å”anas iterācijām.

Izeja no Ŕīs situācijas bija dzinēja integrācija jBPM dažos produktos ar vissarežģītākajiem biznesa procesiem. ÄŖstermiņā Å”im risinājumam bija zināmi panākumi: kļuva iespējams Ä«stenot sarežģītus biznesa procesus, saglabājot diezgan informatÄ«vu un atbilstoÅ”u diagrammu apzÄ«mējumā. BPMN2.

BPM stila integrācija
Neliela daļa no sarežģīta biznesa procesa

Ilgtermiņā risinājums neattaisnoja cerÄ«bas: augstā darba intensitāte biznesa procesu veidoÅ”anā, izmantojot vizuālos rÄ«kus, neļāva sasniegt pieņemamus produktivitātes rādÄ«tājus, un pats rÄ«ks kļuva par vienu no visnepatÄ«kamākajiem izstrādātāju vidÅ«. Bija arÄ« sÅ«dzÄ«bas par dzinēja iekŔējo struktÅ«ru, kas izraisÄ«ja daudzu ā€œplāksteruā€ un ā€œkruÄ·uā€ parādÄ«Å”anos.

Galvenais pozitÄ«vais jBPM izmantoÅ”anas aspekts bija izpratne par ieguvumiem un kaitējumu, ko rada biznesa procesa instances pastāvÄ«gais stāvoklis. Mēs arÄ« redzējām iespēju izmantot procesa pieeju, lai ieviestu sarežģītus integrācijas protokolus starp dažādām lietojumprogrammām, izmantojot asinhronu mijiedarbÄ«bu, izmantojot signālus un ziņojumus. PastāvÄ«ga stāvokļa klātbÅ«tnei tajā ir izŔķiroÅ”a nozÄ«me.

Pamatojoties uz iepriekÅ” minēto, mēs varam secināt: Procesu pieeja BPM stilā ļauj risināt visdažādākos uzdevumus, lai automatizētu arvien sarežģītākus biznesa procesus, harmoniski iekļautu integrācijas darbÄ«bas Å”ajos procesos un saglabātu iespēju vizuāli attēlot realizēto procesu piemērotā notācijā.

Sinhrono zvanu kā integrācijas modeļa trūkumi

Sinhronā integrācija attiecas uz vienkārŔāko bloÄ·Ä“Å”anas zvanu. Viena apakÅ”sistēma darbojas kā servera puse un pakļauj API ar nepiecieÅ”amo metodi. Cita apakÅ”sistēma darbojas kā klienta puse un Ä«stajā laikā veic zvanu un gaida rezultātu. AtkarÄ«bā no sistēmas arhitektÅ«ras klienta un servera puses var atrasties vai nu vienā lietojumprogrammā un procesā, vai arÄ« dažādās. Otrajā gadÄ«jumā jums ir jāpiemēro RPC ievieÅ”ana un jānodroÅ”ina parametru un izsaukuma rezultātu ŔķiroÅ”ana.

BPM stila integrācija

Å im integrācijas modelim ir diezgan liels trÅ«kumu kopums, taču tā vienkārŔības dēļ tas tiek ļoti plaÅ”i izmantots praksē. ÄŖstenoÅ”anas ātrums valdzina un liek to izmantot atkal un atkal, saskaroties ar steidzamiem termiņiem, ierakstot risinājumu kā tehnisko parādu. Bet gadās arÄ«, ka nepieredzējuÅ”i izstrādātāji to izmanto neapzināti, vienkārÅ”i neapzinoties negatÄ«vās sekas.

Papildus visredzamākajam apakÅ”sistēmas savienojamÄ«bas pieaugumam ir arÄ« mazāk acÄ«mredzamas problēmas ar darÄ«jumiem, kas saistÄ«ti ar "pieaugÅ”anu" un "izstiepÅ”anu". PatieŔām, ja biznesa loÄ£ika veic dažas izmaiņas, tad no darÄ«jumiem nevar izvairÄ«ties, un darÄ«jumi savukārt bloķē noteiktus lietojumprogrammu resursus, kurus ietekmē Ŕīs izmaiņas. Tas ir, kamēr viena apakÅ”sistēma negaida atbildi no otras, tā nevarēs pabeigt darÄ«jumu un noņemt slēdzenes. Tas ievērojami palielina dažādu seku risku:

  • Sistēmas reaģētspēja zÅ«d, lietotāji ilgi gaida atbildes uz pieprasÄ«jumiem;
  • serveris parasti pārstāj atbildēt uz lietotāju pieprasÄ«jumiem pārpildÄ«ta pavedienu pÅ«la dēļ: lielākā daļa pavedienu ir bloķēti resursā, ko aizņem darÄ«jums;
  • Sāk parādÄ«ties strupceļi: to raÅ”anās iespējamÄ«ba ir ļoti atkarÄ«ga no darÄ«jumu ilguma, darÄ«jumu loÄ£ikas un darÄ«jumā iesaistÄ«to bloÄ·Ä“Å”anas apjoma;
  • parādās darÄ«juma taimauta kļūdas;
  • serveris ā€œneizdodasā€ ar OutOfMemory, ja uzdevums prasa apstrādāt un mainÄ«t lielu datu apjomu, un sinhronās integrācijas klātbÅ«tne apgrÅ«tina apstrādes sadalÄ«Å”anu ā€œvieglākosā€ darÄ«jumos.

No arhitektÅ«ras viedokļa bloÄ·Ä“Å”anas zvanu izmantoÅ”ana integrācijas laikā noved pie kontroles zaudÄ“Å”anas pār atseviŔķu apakÅ”sistēmu kvalitāti: nav iespējams nodroÅ”ināt vienas apakÅ”sistēmas mērÄ·a kvalitātes rādÄ«tājus atseviŔķi no citas apakÅ”sistēmas kvalitātes rādÄ«tājiem. Ja apakÅ”sistēmas izstrādā dažādas komandas, tā ir liela problēma.

Lietas kļūst vēl interesantākas, ja integrētās apakÅ”sistēmas atrodas dažādās lietojumprogrammās un jums ir jāveic sinhronas izmaiņas abās pusēs. Kā nodroÅ”ināt Å”o izmaiņu transakciju?

Ja izmaiņas tiek veiktas atseviŔķos darÄ«jumos, jums bÅ«s jānodroÅ”ina uzticama izņēmumu apstrāde un kompensācija, un tas pilnÄ«bā novērÅ” sinhronās integrācijas galveno ieguvumu - vienkārŔību.

Prātā nāk arÄ« izplatÄ«tie darÄ«jumi, taču mēs tos neizmantojam savos risinājumos: ir grÅ«ti nodroÅ”ināt uzticamÄ«bu.

"Sāga" kā darījuma problēmas risinājums

Pieaugot mikropakalpojumu popularitātei, pieprasījums pēc Sāgas raksts.

Å is modelis lieliski atrisina iepriekÅ” minētās ilgo darÄ«jumu problēmas, kā arÄ« paplaÅ”ina sistēmas stāvokļa pārvaldÄ«bas iespējas no biznesa loÄ£ikas puses: kompensācija pēc neveiksmÄ«ga darÄ«juma var nevis atgriezt sistēmu sākotnējā stāvoklÄ«, bet nodroÅ”ināt alternatÄ«vs datu apstrādes ceļŔ. Tas arÄ« ļauj izvairÄ«ties no veiksmÄ«gi pabeigtu datu apstrādes darbÄ«bu atkārtoÅ”anas, mēģinot novest procesu lÄ«dz ā€œlabāmā€ beigām.

Interesanti, ka monolÄ«tajās sistēmās Å”is modelis ir aktuāls arÄ« tad, kad runa ir par brÄ«vi savienotu apakÅ”sistēmu integrāciju, un tiek novērota negatÄ«va ietekme, ko izraisa ilgstoÅ”i veikti darÄ«jumi un attiecÄ«gie resursu bloķējumi.

SaistÄ«bā ar mÅ«su biznesa procesiem BPM stilā ā€žSāgasā€ ir ļoti viegli ieviest: atseviŔķus ā€žSāgasā€ soļus var norādÄ«t kā darbÄ«bas biznesa procesa ietvaros, kā arÄ« biznesa procesa noturÄ«go stāvokli. nosaka ā€œSāgasā€ iekŔējo stāvokli. Tas ir, mēs neprasām nekādu papildu koordinācijas mehānismu. Viss, kas Jums nepiecieÅ”ams, ir ziņojumu brokeris, kas atbalsta "vismaz vienreizējās" garantijas kā transportu.

Bet Å”im risinājumam ir arÄ« sava ā€œcenaā€:

  • biznesa loÄ£ika kļūst sarežģītāka: jāizstrādā kompensācija;
  • bÅ«s jāatsakās no pilnÄ«gas konsekvences, kas var bÅ«t Ä«paÅ”i jutÄ«ga monolÄ«tām sistēmām;
  • ArhitektÅ«ra kļūst nedaudz sarežģītāka, un parādās papildu nepiecieÅ”amÄ«ba pēc ziņojumu brokera;
  • bÅ«s nepiecieÅ”ami papildu uzraudzÄ«bas un administrÄ“Å”anas rÄ«ki (lai gan kopumā tas ir labi: sistēmas apkalpoÅ”anas kvalitāte paaugstināsies).

MonolÄ«tām sistēmām "Sag" izmantoÅ”anas pamatojums nav tik acÄ«mredzams. Mikropakalpojumiem un citiem SOA, kur, visticamāk, jau ir brokeris un projekta sākumā tiek upurēta pilnÄ«ga konsekvence, Ŕī modeļa izmantoÅ”anas priekÅ”rocÄ«bas var ievērojami atsvērt trÅ«kumus, Ä«paÅ”i, ja biznesa loÄ£ikā ir ērta API lÄ«menÄ«.

Biznesa loģikas iekapsulēŔana mikropakalpojumos

Kad sākām eksperimentēt ar mikropakalpojumiem, radās pamatots jautājums: kur likt domēna biznesa loÄ£iku attiecÄ«bā pret servisu, kas nodroÅ”ina domēna datu noturÄ«bu?

AplÅ«kojot dažādu BPMS arhitektÅ«ru, var Ŕķist saprātÄ«gi noŔķirt biznesa loÄ£iku no noturÄ«bas: izveidot platformu un domēna neatkarÄ«gu mikropakalpojumu slāni, kas veido vidi un konteineru domēna biznesa loÄ£ikas izpildei, un noformēt domēna datu noturÄ«bu kā atseviŔķs ļoti vienkārÅ”u un vieglu mikropakalpojumu slānis. Biznesa procesi Å”ajā gadÄ«jumā veic noturÄ«bas slāņa pakalpojumu orÄ·estrÄ“Å”anu.

BPM stila integrācija

Å ai pieejai ir ļoti liela priekÅ”rocÄ«ba: jÅ«s varat palielināt platformas funkcionalitāti, cik vien vēlaties, un tikai atbilstoÅ”ais platformas mikropakalpojumu slānis no tā kļūs ā€œresnsā€. Biznesa procesi no jebkura domēna var nekavējoties izmantot jauno platformas funkcionalitāti, tiklÄ«dz tā tiek atjaunināta.

Detalizētāks pētÄ«jums atklāja bÅ«tiskus Ŕīs pieejas trÅ«kumus:

  • platformas pakalpojums, kas vienlaikus izpilda daudzu domēnu biznesa loÄ£iku, rada lielus riskus kā vienu neveiksmes punktu. Biežas biznesa loÄ£ikas izmaiņas palielina kļūdu risku, kas izraisa visas sistēmas kļūmes;
  • veiktspējas problēmas: biznesa loÄ£ika strādā ar saviem datiem, izmantojot Å”auru un lēnu saskarni:
    • dati atkal tiks sakārtoti un pārsÅ«knēti caur tÄ«kla steku;
    • domēna pakalpojums bieži nodroÅ”ina vairāk datu, nekā nepiecieÅ”ams biznesa loÄ£ikas apstrādei, jo nav pietiekamu iespēju parametrizēt pieprasÄ«jumus pakalpojuma ārējā API lÄ«menÄ«;
    • vairākas neatkarÄ«gas biznesa loÄ£ikas daļas var atkārtoti pieprasÄ«t tos paÅ”us datus apstrādei (Å”o problēmu var mazināt, pievienojot sesijas komponentus, kas saglabā datus keÅ”atmiņā, taču tas vēl vairāk sarežģī arhitektÅ«ru un rada datu atbilstÄ«bas un keÅ”atmiņas nederÄ«guma problēmas);
  • darÄ«juma problēmas:
    • biznesa procesi ar pastāvÄ«gu stāvokli, ko glabā platformas pakalpojums, neatbilst domēna datiem, un nav vienkārÅ”u veidu, kā Å”o problēmu atrisināt;
    • domēna datu bloÄ·Ä“Å”anas izvietoÅ”ana ārpus darÄ«juma: ja domēna biznesa loÄ£ikā ir jāveic izmaiņas pēc kārtējo datu pareizÄ«bas pirmās pārbaudes, ir jāizslēdz iespēja veikt konkurētspējÄ«gas izmaiņas apstrādātajos datos. Ārējo datu bloÄ·Ä“Å”ana var palÄ«dzēt atrisināt problēmu, taču Ŕāds risinājums rada papildu riskus un samazina sistēmas kopējo uzticamÄ«bu;
  • papildu grÅ«tÄ«bas atjaunināŔanas laikā: dažos gadÄ«jumos pastāvÄ«bas pakalpojums un biznesa loÄ£ika ir jāatjaunina sinhroni vai stingrā secÄ«bā.

Galu galā mums bija jāatgriežas pie pamatiem: jāiekapsulē domēna dati un domēna biznesa loÄ£ika vienā mikropakalpojumā. Å Ä« pieeja vienkārÅ”o mikropakalpojuma uztveri kā sistēmas neatņemamu sastāvdaļu un nerada iepriekÅ” minētās problēmas. Tas arÄ« netiek sniegts bez maksas:

  • API standartizācija ir nepiecieÅ”ama mijiedarbÄ«bai ar biznesa loÄ£iku (jo Ä«paÅ”i, lai nodroÅ”inātu lietotāja darbÄ«bas kā daļu no biznesa procesiem) un API platformas pakalpojumiem; prasa rÅ«pÄ«gāku uzmanÄ«bu API izmaiņām, uz priekÅ”u un atpakaļ saderÄ«bu;
  • ir nepiecieÅ”ams pievienot papildu izpildlaika bibliotēkas, lai nodroÅ”inātu biznesa loÄ£ikas funkcionÄ“Å”anu kā daļu no katra Ŕāda mikropakalpojuma, un tas rada jaunas prasÄ«bas Ŕādām bibliotēkām: vieglums un pārejas atkarÄ«bu minimums;
  • biznesa loÄ£ikas izstrādātājiem ir jāuzrauga bibliotēku versijas: ja mikropakalpojums ilgu laiku nav pabeigts, visticamāk, tas saturēs novecojuÅ”u bibliotēku versiju. Tas var bÅ«t negaidÄ«ts Ŕķērslis jauna lÄ«dzekļa pievienoÅ”anai un var bÅ«t nepiecieÅ”ams migrēt Ŕāda pakalpojuma veco biznesa loÄ£iku uz jaunām bibliotēku versijām, ja starp versijām ir notikuÅ”as nesaderÄ«gas izmaiņas.

BPM stila integrācija

Šādā arhitektÅ«rā ir arÄ« platformas pakalpojumu slānis, taču Å”is slānis vairs neveido konteineru domēna biznesa loÄ£ikas izpildei, bet tikai tā vidi, nodroÅ”inot papildu ā€œplatformasā€ funkcijas. Šāds slānis ir nepiecieÅ”ams ne tikai, lai saglabātu domēna mikropakalpojumu vieglo raksturu, bet arÄ« lai centralizētu pārvaldÄ«bu.

Piemēram, lietotāju darbÄ«bas biznesa procesos Ä£enerē uzdevumus. Tomēr, strādājot ar uzdevumiem, lietotājam uzdevumi no visiem domēniem ir jāredz vispārējā sarakstā, kas nozÄ«mē, ka ir jābÅ«t atbilstoÅ”am platformas uzdevumu reÄ£istrācijas pakalpojumam, kas atbrÄ«vots no domēna biznesa loÄ£ikas. UzņēmējdarbÄ«bas loÄ£ikas iekapsulÄ“Å”anas saglabāŔana Ŕādā kontekstā ir diezgan problemātiska, un tas ir vēl viens Ŕīs arhitektÅ«ras kompromiss.

Biznesa procesu integrācija ar lietojumprogrammu izstrādātāja acīm

Kā minēts iepriekÅ”, lietojumprogrammu izstrādātājam ir jābÅ«t abstrahētam no vairāku lietojumprogrammu mijiedarbÄ«bas ievieÅ”anas tehniskajām un inženiertehniskajām iezÄ«mēm, lai varētu paļauties uz labu izstrādes produktivitāti.

Mēģināsim atrisināt diezgan sarežģītu integrācijas problēmu, kas Ä«paÅ”i izdomāta rakstam. Å is bÅ«s ā€œspēļuā€ uzdevums, kas ietver trÄ«s lietojumprogrammas, kur katra no tām definē noteiktu domēna nosaukumu: ā€œapp1ā€, ā€œapp2ā€, ā€œapp3ā€.

Katrā lietojumprogrammā tiek palaisti biznesa procesi, kas sāk ā€œspēlēt bumbuā€, izmantojot integrācijas kopni. Ziņojumi ar nosaukumu ā€œBumbaā€ darbosies kā bumba.

Spēles noteikumi:

  • pirmais spēlētājs ir iniciators. ViņŔ uzaicina uz spēli citus spēlētājus, sāk spēli un var to beigt jebkurā laikā;
  • pārējie spēlētāji deklarē savu dalÄ«bu spēlē, ā€œiepazÄ«stā€ viens otru un pirmo spēlētāju;
  • pēc bumbas saņemÅ”anas spēlētājs izvēlas citu dalÄ«bnieku un piespēlē viņam bumbu. Tiek skaitÄ«ts kopējais sÅ«tÄ«jumu skaits;
  • Katram spēlētājam ir "enerÄ£ija", kas samazinās ar katru Ŕī spēlētāja bumbas piespēli. Kad enerÄ£ija beidzas, spēlētājs pamet spēli, paziņojot par izstāŔanos;
  • ja spēlētājs paliek viens, viņŔ nekavējoties paziņo par savu aizieÅ”anu;
  • Kad visi spēlētāji ir izslēgti, pirmais spēlētājs paziņo, ka spēle ir beigusies. Ja viņŔ pamet spēli agri, viņam ir jāseko spēlei, lai to pabeigtu.

Lai atrisinātu Å”o problēmu, es izmantoÅ”u mÅ«su DSL biznesa procesiem, kas ļauj mums kompakti aprakstÄ«t Kotlin loÄ£iku, izmantojot minimālu plāksni.

Pirmā spēlētāja (jeb spēles iniciatora) biznesa process darbosies lietotnē app1:

klases 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}")
}

Papildus biznesa loÄ£ikas izpildei iepriekÅ” minētais kods var izveidot biznesa procesa objekta modeli, ko var vizualizēt diagrammas veidā. Mēs vēl neesam ieviesuÅ”i vizualizētāju, tāpēc mums bija jāpavada nedaudz laika zÄ«mÄ“Å”anai (Å”eit es nedaudz vienkārÅ”oju BPMN apzÄ«mējumu attiecÄ«bā uz vārtu izmantoÅ”anu, lai uzlabotu diagrammas konsekvenci ar zemāk esoÅ”o kodu):

BPM stila integrācija

app2 ietvers otra spēlētāja biznesa procesu:

klases 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}")
}

Diagramma:

BPM stila integrācija

App3 lietojumprogrammā mēs padarÄ«sim spēlētāju ar nedaudz atŔķirÄ«gu uzvedÄ«bu: tā vietā, lai nejauÅ”i izvēlētos nākamo spēlētāju, viņŔ rÄ«kosies saskaņā ar apļa algoritmu:

klases 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<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}")
}

Pretējā gadÄ«jumā spēlētāja uzvedÄ«ba neatŔķiras no iepriekŔējās, tāpēc diagramma nemainās.

Tagad mums ir nepiecieÅ”ams tests, lai to visu izpildÄ«tu. Es doÅ”u tikai paÅ”a testa kodu, lai nepārblÄ«vētu rakstu (patiesÄ«bā es izmantoju iepriekÅ” izveidoto testa vidi, lai pārbaudÄ«tu citu biznesa procesu integrāciju):

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");
    // Š¢ŠµŠæŠµŃ€ŃŒ Š½ŃƒŠ¶Š½Š¾ Š½ŠµŠ¼Š½Š¾Š³Š¾ ŠæŠ¾Š“Š¾Š¶Š“Š°Ń‚ŃŒ, ŠæŠ¾ŠŗŠ° ŠøŠ³Ń€Š¾ŠŗŠø "ŠæŠ¾Š·Š½Š°ŠŗŠ¾Š¼ŃŃ‚ся" Š“руŠ³ с Š“руŠ³Š¾Š¼.
    // Š–Š“Š°Ń‚ŃŒ чŠµŃ€ŠµŠ· 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;
}

Izpildīsim testu un apskatīsim žurnālu:

konsoles izvade

Š’Š·ŃŃ‚Š° Š±Š»Š¾ŠŗŠøрŠ¾Š²ŠŗŠ° ŠŗŠ»ŃŽŃ‡Š° 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!

No tā visa mēs varam izdarīt vairākus svarīgus secinājumus:

  • ar nepiecieÅ”amajiem rÄ«kiem lietojumprogrammu izstrādātāji var izveidot integrācijas mijiedarbÄ«bu starp lietojumprogrammām, nepārtraucot biznesa loÄ£iku;
  • integrācijas uzdevuma sarežģītÄ«ba, kam nepiecieÅ”amas inženieru kompetences, var tikt paslēpts ietvarā, ja tas sākotnēji ir iekļauts ietvara arhitektÅ«rā. Problēmas sarežģītÄ«bu nevar noslēpt, tāpēc sarežģītas problēmas risinājums kodā izskatÄ«sies tā;
  • Izstrādājot integrācijas loÄ£iku, obligāti jāņem vērā iespējamā konsekvence un visu integrācijas dalÄ«bnieku stāvokļa izmaiņu linearizācijas trÅ«kums. Tas liek mums sarežģīt loÄ£iku, lai padarÄ«tu to nejutÄ«gu pret ārējo notikumu norises kārtÄ«bu. MÅ«su piemērā spēlētājs ir spiests piedalÄ«ties spēlē pēc tam, kad viņŔ ir paziņojis par izstāŔanos no spēles: citi spēlētāji turpinās viņam piespēlēt bumbu, lÄ«dz informācija par viņa izstāŔanos sasniegs un to apstrādās visi dalÄ«bnieki. Šāda loÄ£ika neizriet no spēles noteikumiem un ir kompromisa risinājums izvēlētās arhitektÅ«ras ietvaros.

Tālāk mēs runāsim par mūsu risinājuma dažādajām sarežģītībām, kompromisiem un citiem jautājumiem.

Visas ziņas ir vienā rindā

Visas integrētās lietojumprogrammas darbojas ar vienu integrācijas kopni, kas tiek parādÄ«ta ārējā brokera veidā, vienu BPMQueue ziņojumiem un vienu BPMTopic tēmu signāliem (notikumiem). Visu ziņojumu ievietoÅ”ana vienā rindā ir kompromiss. Biznesa loÄ£ikas lÄ«menÄ« tagad varat ieviest tik daudz jaunu ziņojumu veidu, cik vēlaties, neveicot izmaiņas sistēmas struktÅ«rā. Tas ir bÅ«tisks vienkārÅ”ojums, taču tas rada zināmus riskus, kas mÅ«su tipisko uzdevumu kontekstā mums neŔķita tik nozÄ«mÄ«gi.

BPM stila integrācija

Tomēr Å”eit ir viens smalkums: katra lietojumprogramma filtrē ā€œsavusā€ ziņojumus no rindas pie ieejas pēc sava domēna nosaukuma. Domēnu var norādÄ«t arÄ« signālos, ja ir jāierobežo signāla ā€œredzamÄ«bas jomaā€ tikai vienai lietojumprogrammai. Tam vajadzētu palielināt kopnes caurlaidspēju, bet biznesa loÄ£ikai tagad jādarbojas ar domēna nosaukumiem: ziņojumu adresÄ“Å”anai - obligāti, signāliem - vēlams.

Integrācijas kopnes uzticamības nodroŔināŔana

Uzticamība sastāv no vairākiem punktiem:

  • Izvēlētais ziņojumu brokeris ir svarÄ«ga arhitektÅ«ras sastāvdaļa un viens atteices punkts: tam ir jābÅ«t pietiekami izturÄ«gam pret kļūmēm. Jums vajadzētu izmantot tikai laika pārbaudÄ«tas implementācijas ar labu atbalstu un lielu kopienu;
  • nepiecieÅ”ams nodroÅ”ināt augstu ziņojumu brokera pieejamÄ«bu, kam tas ir fiziski jāatdala no integrētajām aplikācijām (augstu pieejamÄ«bu lietojumprogrammām ar pielietoto biznesa loÄ£iku ir daudz grÅ«tāk un dārgāk nodroÅ”ināt);
  • brokerim ir pienākums nodroÅ”ināt ā€œvismaz vienu reiziā€ piegādes garantijas. Tā ir obligāta prasÄ«ba uzticamai integrācijas kopnes darbÄ«bai. Nav nepiecieÅ”amas "tieÅ”i vienreiz" lÄ«meņa garantijas: biznesa procesi parasti nav jutÄ«gi pret atkārtotu ziņojumu vai notikumu ieraÅ”anos, un Ä«paÅ”os uzdevumos, kur tas ir svarÄ«gi, ir vieglāk pievienot biznesam papildu pārbaudes. loÄ£ika nekā pastāvÄ«gi izmantot diezgan ā€œdārgasā€ garantijas;
  • ziņojumu un signālu sÅ«tÄ«Å”anai jābÅ«t iesaistÄ«tai kopējā darÄ«jumā ar izmaiņām biznesa procesu un domēna datu stāvoklÄ«. Vēlamā iespēja bÅ«tu izmantot modeli DarÄ«jumu izsÅ«tne, bet tam bÅ«s nepiecieÅ”ama papildu tabula datu bāzē un atkārtotājs. JEE lietojumprogrammās to var vienkārÅ”ot, izmantojot vietējo JTA menedžeri, bet savienojumam ar izvēlēto brokeri ir jāspēj darboties XA;
  • ienākoÅ”o ziņojumu un notikumu apstrādātājiem ir jāstrādā arÄ« ar darÄ«jumu, kas maina biznesa procesa stāvokli: ja Ŕāds darÄ«jums tiek atvilkts, tad ziņojuma saņemÅ”ana ir jāatceļ;
  • ziņojumi, kurus nevarēja piegādāt kļūdu dēļ, ir jāuzglabā atseviŔķā krātuvē D.L.Q. (miruÅ”o vēstuļu rinda). Å im nolÅ«kam mēs izveidojām atseviŔķu platformas mikropakalpojumu, kas glabā Ŕādus ziņojumus savā krātuvē, indeksē tos pēc atribÅ«tiem (ātrai grupÄ“Å”anai un meklÄ“Å”anai) un pakļauj API apskatei, atkārtotai nosÅ«tÄ«Å”anai uz mērÄ·a adresi un ziņojumu dzÄ“Å”anai. Sistēmas administratori var strādāt ar Å”o pakalpojumu, izmantojot savu tÄ«mekļa saskarni;
  • brokera iestatÄ«jumos ir jāpielāgo piegādes atkārtojumu skaits un kavÄ“Å”anās starp piegādēm, lai samazinātu iespēju, ka ziņojumi nonāks DLQ (optimālos parametrus aprēķināt gandrÄ«z neiespējami, taču var rÄ«koties empÄ«riski un pielāgot tos darbÄ«bas laikā );
  • DLQ veikals ir nepārtraukti jāuzrauga, un pārraudzÄ«bas sistēmai ir jābrÄ«dina sistēmas administratori, lai, ja rodas nepiegādāti ziņojumi, viņi varētu reaģēt pēc iespējas ātrāk. Tas samazinās kļūmes vai biznesa loÄ£ikas kļūdas ā€œietekmēto zonuā€;
  • integrācijas kopnei ir jābÅ«t nejutÄ«gai pret Ä«slaicÄ«gu lietojumprogrammu neesamÄ«bu: tēmas abonementiem ir jābÅ«t noturÄ«giem, un lietojumprogrammas domēna nosaukumam jābÅ«t unikālam, lai, kamēr lietojumprogramma nav pieejama, kāds cits nemēģinātu apstrādāt tās ziņojumus no rindā.

Biznesa loģikas pavedienu droŔības nodroŔināŔana

Viena un tā pati biznesa procesa instance var saņemt vairākus ziņojumus un notikumus vienlaikus, kuru apstrāde sāksies paralēli. Tajā paŔā laikā lietojumprogrammu izstrādātājam visam jābÅ«t vienkārÅ”am un droÅ”am.

Procesa biznesa loÄ£ika apstrādā katru ārējo notikumu, kas ietekmē Å”o biznesa procesu atseviŔķi. Šādi notikumi varētu bÅ«t:

  • biznesa procesa instances palaiÅ”ana;
  • lietotāja darbÄ«ba, kas saistÄ«ta ar darbÄ«bu biznesa procesā;
  • ziņojuma vai signāla saņemÅ”ana, kurai ir abonēta biznesa procesa instance;
  • biznesa procesa instances iestatÄ«ta taimera iedarbināŔana;
  • kontrolēt darbÄ«bu, izmantojot API (piemēram, procesa pārtraukums).

Katrs Ŕāds notikums var mainÄ«t biznesa procesa gadÄ«juma stāvokli: dažas darbÄ«bas var beigties un citas var sākties, un pastāvÄ«go Ä«paŔību vērtÄ«bas var mainÄ«ties. Jebkuras darbÄ«bas aizvērÅ”ana var izraisÄ«t vienas vai vairāku tālāk norādÄ«to darbÄ«bu aktivizÄ“Å”anu. Tie, savukārt, var pārtraukt gaidÄ«t citus notikumus vai, ja viņiem nav nepiecieÅ”ami nekādi papildu dati, var pabeigt to paÅ”u darÄ«jumu. Pirms darÄ«juma slēgÅ”anas jaunais biznesa procesa stāvoklis tiek saglabāts datu bāzē, kur tas gaidÄ«s nākamo ārējo notikumu.

PastāvÄ«gi biznesa procesu dati, kas tiek glabāti relāciju datu bāzē, ir ļoti ērts punkts apstrādes sinhronizÄ“Å”anai, ja izmantojat SELECT FOR UPDATE. Ja vienam darÄ«jumam izdevās iegÅ«t biznesa procesa stāvokli no tā maiņas bāzes, tad neviens cits paralēli darÄ«jums nevarēs iegÅ«t tādu paÅ”u stāvokli citai izmaiņai, un pēc pirmā darÄ«juma pabeigÅ”anas tiek veikts otrais. garantēta saņemt jau mainÄ«to stāvokli.

Izmantojot pesimistiskās slēdzenes DBVS pusē, mēs izpildām visas nepiecieÅ”amās prasÄ«bas ACID, kā arÄ« saglabā iespēju mērogot lietojumprogrammu ar biznesa loÄ£iku, palielinot darbojoÅ”os gadÄ«jumu skaitu.

Tomēr pesimistiskas slēdzenes mÅ«s draud ar strupceļu, kas nozÄ«mē, ka SELECT FOR UPDATE joprojām ir jāierobežo lÄ«dz noteiktam saprātÄ«gam taimautam gadÄ«jumam, ja dažos ārkārtÄ«gos biznesa loÄ£ikas gadÄ«jumos rodas strupceļŔ.

Vēl viena problēma ir biznesa procesa sākuma sinhronizācija. Kamēr nav biznesa procesa gadÄ«juma, datu bāzē nav stāvokļa, tāpēc aprakstÄ«tā metode nedarbosies. Ja jums ir jānodroÅ”ina biznesa procesa instances unikalitāte noteiktā tvērumā, tad jums bÅ«s nepiecieÅ”ams sava veida sinhronizācijas objekts, kas saistÄ«ts ar procesa klasi un atbilstoÅ”o tvērumu. Lai atrisinātu Å”o problēmu, mēs izmantojam citu bloÄ·Ä“Å”anas mehānismu, kas ļauj mums bloķēt patvaļīgu resursu, kas norādÄ«ts ar atslēgu URI formātā, izmantojot ārēju pakalpojumu.

Mūsu piemēros InitialPlayer biznesa process satur deklarāciju

uniqueConstraint = UniqueConstraints.singleton

Tāpēc žurnālā ir ziņojumi par atbilstoŔās atslēgas slēdzenes paņemÅ”anu un atlaiÅ”anu. Citiem biznesa procesiem Ŕādu ziņojumu nav: unikāls ierobežojums nav iestatÄ«ts.

Biznesa procesu problēmas ar pastāvīgu stāvokli

Dažreiz pastāvÄ«gs stāvoklis ne tikai palÄ«dz, bet arÄ« patieŔām kavē attÄ«stÄ«bu.
Problēmas sākas tad, kad ir jāveic izmaiņas biznesa loÄ£ikā un/vai biznesa procesa modelÄ«. Ne visas Ŕādas izmaiņas ir savietojamas ar veco biznesa procesu stāvokli. Ja datu bāzē ir daudz reāllaika instanču, tad nesaderÄ«gu izmaiņu veikÅ”ana var radÄ«t daudz nepatikÅ”anas, ar kurām bieži saskārāmies, lietojot jBPM.

Atkarībā no izmaiņu dziļuma varat rīkoties divos veidos:

  1. izveidojiet jaunu biznesa procesa veidu, lai neveiktu nesaderÄ«gas izmaiņas vecajā, un izmantojiet to vecā vietā, palaižot jaunas instances. Vecās kopijas turpinās darboties ā€œkā lÄ«dz Å”imā€;
  2. migrējiet pastāvīgo biznesa procesu stāvokli, atjauninot biznesa loģiku.

Pirmais veids ir vienkārŔāks, taču tam ir savi ierobežojumi un trÅ«kumi, piemēram:

  • biznesa loÄ£ikas dublÄ“Å”anās daudzos biznesa procesu modeļos, palielinot biznesa loÄ£ikas apjomu;
  • Bieži vien ir nepiecieÅ”ama tÅ«lÄ«tēja pāreja uz jaunu biznesa loÄ£iku (integrācijas uzdevumu ziņā - gandrÄ«z vienmēr);
  • izstrādātājs nezina, kurā brÄ«dÄ« novecojuÅ”os modeļus var izdzēst.

Praksē mēs izmantojam abas pieejas, taču esam pieņēmuÅ”i vairākus lēmumus, lai atvieglotu mÅ«su dzÄ«vi:

  • Datu bāzē biznesa procesa pastāvÄ«gais stāvoklis tiek glabāts viegli lasāmā un viegli apstrādājamā formā: JSON formāta virknē. Tas ļauj veikt migrāciju gan lietojumprogrammā, gan ārēji. Kā pēdējo lÄ«dzekli varat to labot manuāli (Ä«paÅ”i noderÄ«gi izstrādē atkļūdoÅ”anas laikā);
  • integrācijas biznesa loÄ£ikā netiek izmantoti biznesa procesu nosaukumi, lai jebkurā brÄ«dÄ« bÅ«tu iespēja viena iesaistÄ«tā procesa ievieÅ”anu aizstāt ar jaunu ar jaunu nosaukumu (piemēram, ā€œInitialPlayerV2ā€). SaistÄ«Å”ana notiek ar ziņojumu un signālu nosaukumiem;
  • procesa modelim ir versijas numurs, kuru mēs palielinām, ja Å”ajā modelÄ« veicam nesaderÄ«gas izmaiņas, un Å”is numurs tiek saglabāts kopā ar procesa instances stāvokli;
  • procesa noturÄ«gais stāvoklis vispirms tiek nolasÄ«ts no datu bāzes ērtā objekta modelÄ«, ar kuru var strādāt migrācijas procedÅ«ra, ja ir mainÄ«jies modeļa versijas numurs;
  • migrācijas procedÅ«ra tiek novietota blakus biznesa loÄ£ikai un tiek saukta par "slinku" katram biznesa procesa gadÄ«jumam tā atjaunoÅ”anas laikā no datu bāzes;
  • ja nepiecieÅ”ams ātri un sinhroni migrēt visu procesu gadÄ«jumu stāvokli, tiek izmantoti klasiskāki datu bāzes migrācijas risinājumi, taču jāstrādā ar JSON.

Vai jums ir nepiecieŔams cits biznesa procesu ietvars?

Rakstā aprakstÄ«tie risinājumi ļāva bÅ«tiski vienkārÅ”ot mÅ«su dzÄ«vi, paplaÅ”ināt lietojumprogrammu izstrādes lÄ«menÄ« atrisināto problēmu loku un padarÄ«t pievilcÄ«gāku ideju par biznesa loÄ£ikas nodalÄ«Å”anu mikropakalpojumos. Lai to panāktu, tika ieguldÄ«ts liels darbs, izveidots ļoti ā€œvieglsā€ biznesa procesu ietvars, kā arÄ« servisa komponenti, lai atrisinātu konstatētās problēmas visdažādāko aplikāciju problēmu kontekstā. Mēs vēlamies dalÄ«ties ar Å”iem rezultātiem un padarÄ«t kopÄ«gu komponentu izstrādi atvērtu piekļuvi saskaņā ar bezmaksas licenci. Tas prasÄ«s zināmas pÅ«les un laiku. Izpratne par pieprasÄ«jumu pēc Ŕādiem risinājumiem mums varētu bÅ«t papildu stimuls. Piedāvātajā rakstā ļoti maz uzmanÄ«bas tiek pievērsts paÅ”a ietvara iespējām, taču dažas no tām ir redzamas no sniegtajiem piemēriem. Ja mēs publicēsim savu sistēmu, tam tiks veltÄ«ts atseviŔķs raksts. Tikmēr bÅ«sim pateicÄ«gi, ja atstāsiet nelielu atsauksmi, atbildot uz jautājumu:

Aptaujā var piedalīties tikai reģistrēti lietotāji. Ielogoties, lūdzu.

Vai jums ir nepiecieŔams cits biznesa procesu ietvars?

  • 18,8%Jā, es kaut ko tādu meklēju jau ilgu laiku

  • 12,5%Mani interesē uzzināt vairāk par jÅ«su ievieÅ”anu, tas varētu bÅ«t noderÄ«gi2

  • 6,2%Mēs izmantojam vienu no esoÅ”ajiem ietvariem, bet domājam par nomaiņu1

  • 18,8%Izmantojam vienu no esoÅ”ajiem karkasiem, viss kārtÄ«bā3

  • 18,8%mēs iztiekam bez ietvara3

  • 25,0%uzraksti savu 4

Nobalsoja 16 lietotāji. 7 lietotāji atturējās.

Avots: www.habr.com

Pievieno komentāru