Integracija sloga BPM

Integracija sloga BPM

Živjo, Habr!

Naše podjetje je specializirano za razvoj programskih rešitev razreda ERP, kjer levji delež zavzemajo transakcijski sistemi z ogromno poslovne logike in poteka dela a la EDMS. Sodobne različice naših izdelkov temeljijo na tehnologijah JavaEE, aktivno pa eksperimentiramo tudi z mikrostoritvami. Eno najbolj problematičnih področij tovrstnih rešitev je integracija različnih podsistemov, povezanih s sosednjimi domenami. Integracijske naloge so nam vedno povzročale velik glavobol, ne glede na arhitekturne sloge, tehnološke nize in okvire, ki jih uporabljamo, vendar je v zadnjem času prišlo do napredka pri reševanju tovrstnih problemov.

V članku, ki je na vašo pozornost, bom govoril o izkušnjah in arhitekturnih raziskavah NPO Krista na označenem območju. Preučili bomo tudi primer enostavne rešitve integracijskega problema z vidika razvijalca aplikacij in ugotovili, kaj se skriva za to preprostostjo.

Izjava o omejitvi odgovornosti

Arhitekturne in tehnične rešitve, opisane v članku, ponujam na podlagi osebnih izkušenj v okviru konkretnih nalog. Te rešitve ne trdijo, da so univerzalne in morda niso optimalne v drugih pogojih uporabe.

Kaj ima BPM s tem?

Da bi odgovorili na to vprašanje, se moramo nekoliko poglobiti v specifike uporabnih problemov naših rešitev. Glavni del poslovne logike v našem tipičnem transakcijskem sistemu je vnos podatkov v bazo podatkov prek uporabniških vmesnikov, ročno in samodejno preverjanje teh podatkov, posredovanje skozi nek delovni tok, objavljanje v drugem sistemu / analitični bazi podatkov / arhivu, generiranje poročil. Tako je ključna funkcija sistema za stranke avtomatizacija njihovih notranjih poslovnih procesov.

Zaradi udobja uporabljamo izraz "dokument" v komunikaciji kot neko abstrakcijo podatkovnega niza, združenega s skupnim ključem, na katerega je mogoče "pripeti" določen potek dela.
Kaj pa integracijska logika? Navsezadnje nalogo integracije generira arhitektura sistema, ki je "razrezana" na dele NE na zahtevo stranke, ampak pod vplivom popolnoma različnih dejavnikov:

  • pod vplivom Conwayevega zakona;
  • kot rezultat ponovne uporabe podsistemov, predhodno razvitih za druge izdelke;
  • po odločitvi arhitekta, na podlagi nefunkcionalnih zahtev.

Obstaja velika skušnjava, da bi integracijsko logiko ločili od poslovne logike glavnega delovnega toka, da ne bi onesnažili poslovne logike z integracijskimi artefakti in razvijalcu aplikacije prihranili poglabljanje v posebnosti arhitekturne krajine sistema. Ta pristop ima številne prednosti, vendar praksa kaže njegovo neučinkovitost:

  • reševanje integracijskih problemov običajno zdrsne na najpreprostejše možnosti v obliki sinhronih klicev zaradi omejenih razširitvenih točk pri implementaciji glavnega poteka dela (več o pomanjkljivostih sinhrone integracije spodaj);
  • artefakti integracije še vedno prodrejo v glavno poslovno logiko, ko so potrebne povratne informacije iz drugega podsistema;
  • razvijalec aplikacije ignorira integracijo in jo zlahka prekine s spremembo poteka dela;
  • sistem z vidika uporabnika preneha biti enotna celota, "šivi" med podsistemi postanejo opazni, pojavijo se odvečne uporabniške operacije, ki sprožijo prenos podatkov iz enega podsistema v drugega.

Drugi pristop je obravnavati integracijske interakcije kot sestavni del osnovne poslovne logike in poteka dela. Da zahteve po spretnostih razvijalcev aplikacij ne bodo skokovito narasle, je treba ustvarjanje novih integracijskih interakcij izvajati enostavno in naravno, z minimalnimi možnostmi izbire rešitve. To je težje, kot je videti: orodje mora biti dovolj zmogljivo, da uporabniku nudi potrebno pestrost možnosti za njegovo uporabo in se hkrati ne pusti ustreliti v nogo. Obstaja veliko vprašanj, na katera mora inženir odgovoriti v kontekstu integracijskih nalog, o katerih pa razvijalec aplikacij ne bi smel razmišljati pri svojem vsakdanjem delu: meje transakcij, doslednost, atomičnost, varnost, skaliranje, obremenitev in distribucija virov, usmerjanje, marshaling, propagacija in preklapljanje kontekstov itd. Razvijalcem aplikacij je treba ponuditi dokaj enostavne odločitvene predloge, v katerih se že skrivajo odgovori na vsa tovrstna vprašanja. Ti vzorci morajo biti dovolj varni: poslovna logika se zelo pogosto spreminja, kar povečuje tveganje za vnos napak, stroški napak morajo ostati na dokaj nizki ravni.

A vseeno, kaj ima BPM s tem? Obstaja veliko možnosti za izvajanje poteka dela ...
V naših rešitvah je namreč zelo priljubljena še ena implementacija poslovnih procesov - skozi deklarativno nastavitev diagrama prehodov stanj in povezovanje upravljalcev s poslovno logiko na prehode. Hkrati je stanje, ki določa trenutni položaj »dokumenta« v poslovnem procesu, atribut samega »dokumenta«.

Integracija sloga BPM
Takole izgleda proces na začetku projekta

Priljubljenost takšne izvedbe je posledica relativne enostavnosti in hitrosti ustvarjanja linearnih poslovnih procesov. Ker pa programski sistemi postajajo bolj zapleteni, avtomatizirani del poslovnega procesa raste in postaja bolj zapleten. Obstaja potreba po dekompoziciji, ponovni uporabi delov procesov, pa tudi po razcepu procesov, tako da se vsaka veja izvaja vzporedno. V takšnih pogojih orodje postane neprijetno in diagram prehoda stanja izgubi svojo informacijsko vsebino (integracijske interakcije se v diagramu sploh ne odražajo).

Integracija sloga BPM
Tako izgleda postopek po več iteracijah pojasnjevanja zahtev

Izhod iz te situacije je bila integracija motorja jBPM v nekatere izdelke z najbolj zapletenimi poslovnimi procesi. Kratkoročno je ta rešitev imela nekaj uspeha: postalo je mogoče izvajati zapletene poslovne procese ob ohranjanju dokaj informativnega in posodobljenega diagrama v zapisu BPMN2.

Integracija sloga BPM
Majhen del kompleksnega poslovnega procesa

Dolgoročno rešitev ni izpolnila pričakovanj: visoka delovna intenzivnost ustvarjanja poslovnih procesov prek vizualnih orodij ni omogočala doseganja sprejemljivih kazalnikov produktivnosti, samo orodje pa je postalo eno najbolj nevšečnih med razvijalci. Pritožbe so bile tudi glede notranje zgradbe motorja, kar je povzročilo pojav številnih "obližev" in "bergel".

Glavni pozitivni vidik uporabe jBPM je bilo spoznanje koristi in škode lastnega obstojnega stanja za instanco poslovnega procesa. Videli smo tudi možnost uporabe procesnega pristopa za implementacijo kompleksnih integracijskih protokolov med različnimi aplikacijami z uporabo asinhronih interakcij prek signalov in sporočil. Prisotnost obstojnega stanja ima pri tem ključno vlogo.

Na podlagi zgoraj navedenega lahko sklepamo: Procesni pristop v slogu BPM nam omogoča reševanje širokega nabora nalog za avtomatizacijo vedno kompleksnejših poslovnih procesov, harmonično vključevanje integracijskih aktivnosti v te procese in ohranjanje zmožnosti vizualnega prikaza izvedenega procesa v ustreznem zapisu.

Slabosti sinhronih klicev kot integracijskega vzorca

Sinhronska integracija se nanaša na najpreprostejši blokirajoči klic. En podsistem deluje kot strežniška stran in izpostavlja API z želeno metodo. Drugi podsistem deluje kot stran odjemalca in ob pravem času opravi klic s pričakovanjem rezultata. Glede na arhitekturo sistema lahko stran odjemalca in strežnika gostujeta v isti aplikaciji in procesu ali v različnih. V drugem primeru morate uporabiti neko implementacijo RPC in zagotoviti ranžiranje parametrov in rezultatov klica.

Integracija sloga BPM

Tak integracijski vzorec ima precej velik nabor pomanjkljivosti, vendar je zaradi svoje enostavnosti v praksi zelo razširjen. Hitrost implementacije očara in vas prisili, da jo vedno znova uporabljate v pogojih "gorečih" rokov in zapisujete rešitev v tehnični dolg. Zgodi pa se tudi, da ga neizkušeni razvijalci uporabljajo nezavedno, preprosto ne zavedajoč se negativnih posledic.

Poleg najbolj očitnega povečanja povezljivosti podsistemov so manj očitne težave s »širjenjem« in »raztezanjem« transakcij. Dejansko, če poslovna logika naredi kakršne koli spremembe, potem so transakcije nepogrešljive, transakcije pa zaklenejo določene vire aplikacije, na katere te spremembe vplivajo. To pomeni, da dokler en podsistem ne počaka na odgovor drugega, ne bo mogel dokončati transakcije in sprostiti zaklepanja. To znatno poveča tveganje za različne učinke:

  • odzivnost sistema je izgubljena, uporabniki dolgo čakajo na odgovore na zahteve;
  • strežnik se na splošno preneha odzivati ​​na zahteve uporabnikov zaradi prepolnega nabora niti: večina niti "stoji" na zaklepu vira, ki ga zaseda transakcija;
  • se začnejo pojavljati zastoji: verjetnost njihovega pojava je močno odvisna od trajanja transakcije, količine poslovne logike in zapor, ki so vključene v transakcijo;
  • pojavijo se napake pri izteku časovne omejitve transakcije;
  • strežnik "pade" na OutOfMemory, če naloga zahteva obdelavo in spreminjanje velikih količin podatkov, prisotnost sinhronih integracij pa zelo oteži razdelitev obdelave na "lažje" transakcije.

Z arhitekturnega vidika uporaba blokiranja klicev med integracijo vodi do izgube nadzora kakovosti posameznih podsistemov: nemogoče je zagotoviti cilje kakovosti enega podsistema ločeno od ciljev kakovosti drugega podsistema. Če podsisteme razvijajo različne ekipe, je to velik problem.

Stvari postanejo še bolj zanimive, če so podsistemi, ki jih integriramo, v različnih aplikacijah in so potrebne sinhrone spremembe na obeh straneh. Kako narediti te spremembe transakcijske?

Če se spremembe izvedejo v ločenih transakcijah, bo treba zagotoviti robustno obravnavo izjem in kompenzacijo, kar popolnoma odpravi glavno prednost sinhronih integracij – preprostost.

Na misel pridejo tudi porazdeljene transakcije, ki pa jih v naših rešitvah ne uporabljamo: težko je zagotoviti zanesljivost.

"Saga" kot rešitev problema transakcij

Z naraščajočo priljubljenostjo mikrostoritev se povečuje tudi povpraševanje po Saga vzorec.

Ta vzorec odlično rešuje zgornje težave dolgih transakcij in tudi širi možnosti upravljanja stanja sistema s strani poslovne logike: nadomestilo po neuspešni transakciji morda ne bo povrnilo sistema v prvotno stanje, ampak bo zagotovilo alternativo pot obdelave podatkov. Prav tako vam omogoča, da ne ponavljate uspešno opravljenih korakov obdelave podatkov, ko poskušate proces pripeljati do »dobrega« konca.

Zanimivo je, da je v monolitnih sistemih ta vzorec pomemben tudi, ko gre za integracijo ohlapno sklopljenih podsistemov in obstajajo negativni učinki, ki jih povzročajo dolge transakcije in ustrezne blokade virov.

V zvezi z našimi poslovnimi procesi v slogu BPM se je izkazalo, da je implementacija sag zelo enostavna: posamezne korake sag lahko nastavimo kot aktivnosti znotraj poslovnega procesa, vztrajno stanje poslovnega procesa pa določa med druge stvari, notranje stanje sag. To pomeni, da ne potrebujemo nobenega dodatnega mehanizma usklajevanja. Vse, kar potrebujete, je posrednik sporočil s podporo za garancije "vsaj enkrat" kot prevoz.

A takšna rešitev ima tudi svojo »ceno«:

  • poslovna logika postane bolj zapletena: določiti morate nadomestilo;
  • treba bo opustiti popolno konsistenco, ki je lahko še posebej občutljiva za monolitne sisteme;
  • arhitektura postane nekoliko bolj zapletena, obstaja dodatna potreba po posredniku sporočil;
  • potrebna bodo dodatna nadzorna in skrbniška orodja (čeprav je to na splošno celo dobro: kakovost sistemske storitve se bo povečala).

Za monolitne sisteme utemeljitev uporabe "Sags" ni tako očitna. Pri mikrostoritvah in drugih SOA-jih, kjer najverjetneje že obstaja posrednik in je bila na začetku projekta žrtvovana popolna doslednost, lahko koristi uporabe tega vzorca znatno odtehtajo slabosti, še posebej, če je na voljo priročen API ravni poslovne logike.

Enkapsulacija poslovne logike v mikrostoritvah

Ko smo začeli eksperimentirati z mikrostoritvami, se je pojavilo razumno vprašanje: kam postaviti domensko poslovno logiko glede na storitev, ki zagotavlja obstojnost domenskih podatkov?

Ko pogledamo arhitekturo različnih BPMS, se morda zdi smiselno ločiti poslovno logiko od vztrajnosti: ustvarite plast mikrostoritev, neodvisnih od platforme in domene, ki tvorijo okolje in vsebnik za izvajanje poslovne logike domene, in uredite obstojnost podatkov domene kot ločeno plast zelo preprostih in lahkih mikrostoritev. Poslovni procesi v tem primeru orkestrirajo storitve sloja vztrajnosti.

Integracija sloga BPM

Ta pristop ima zelo velik plus: funkcionalnost platforme lahko povečate, kolikor želite, od tega pa se bo "zredil" le pripadajoči sloj mikrostoritev platforme. Poslovni procesi iz katere koli domene takoj ob posodobitvi dobijo možnost uporabe nove funkcionalnosti platforme.

Podrobnejša študija je pokazala pomembne pomanjkljivosti tega pristopa:

  • storitev platforme, ki izvaja poslovno logiko več domen hkrati, nosi veliko tveganje kot ena sama točka napake. Pogoste spremembe poslovne logike povečajo tveganje za hrošče, ki vodijo do napak v celotnem sistemu;
  • težave z zmogljivostjo: poslovna logika deluje s svojimi podatki prek ozkega in počasnega vmesnika:
    • podatki bodo ponovno razvrščeni in prečrpani skozi omrežni sklad;
    • domenska storitev bo pogosto vrnila več podatkov, kot jih poslovna logika zahteva za obdelavo, zaradi nezadostnih zmogljivosti parametrizacije poizvedb na ravni zunanjega API-ja storitve;
    • več neodvisnih delov poslovne logike lahko vedno znova zahteva iste podatke za obdelavo (to težavo lahko ublažite z dodajanjem sejnih gradnikov, ki predpomnijo podatke, vendar to še dodatno zaplete arhitekturo in ustvarja težave s svežostjo podatkov in neveljavnostjo predpomnilnika);
  • transakcijske težave:
    • poslovni procesi s trajnim stanjem, ki jih shranjuje storitev platforme, niso skladni s podatki domene in ni preprostih načinov za rešitev te težave;
    • premik ključavnice domenskih podatkov izven transakcije: če je potrebna sprememba domenske poslovne logike, je treba po predhodnem preverjanju pravilnosti dejanskih podatkov izključiti možnost konkurenčne spremembe obdelanih podatkov. Zunanje blokiranje podatkov lahko pomaga rešiti težavo, vendar takšna rešitev prinaša dodatna tveganja in zmanjšuje splošno zanesljivost sistema;
  • dodatni zapleti pri posodabljanju: v nekaterih primerih morate posodobiti storitev obstojnosti in poslovno logiko sinhrono ali v strogem zaporedju.

Na koncu sem se moral vrniti k osnovam: enkapsulirati domenske podatke in domensko poslovno logiko v eno mikrostoritev. Ta pristop poenostavlja dojemanje mikrostoritve kot sestavne komponente v sistemu in ne povzroča zgornjih težav. Tudi to ni brezplačno:

  • Standardizacija API-ja je potrebna za interakcijo s poslovno logiko (zlasti za zagotavljanje uporabniških dejavnosti kot del poslovnih procesov) in storitvami platforme API; potrebna je večja pozornost do sprememb API-ja, združljivost naprej in nazaj;
  • potrebno je dodati dodatne izvajalne knjižnice, da se zagotovi delovanje poslovne logike kot dela vsake take mikrostoritve, kar povzroča nove zahteve za tovrstne knjižnice: lahkotnost in najmanj prehodnih odvisnosti;
  • razvijalci poslovne logike morajo spremljati različice knjižnic: če mikrostoritev že dolgo ni bila dokončana, bo najverjetneje vsebovala zastarelo različico knjižnic. To je lahko nepričakovana ovira pri dodajanju nove funkcije in lahko zahteva, da se stara poslovna logika takšne storitve preseli v nove različice knjižnic, če je med različicami prišlo do nezdružljivih sprememb.

Integracija sloga BPM

V takšni arhitekturi je prisotna tudi plast platformskih storitev, vendar ta plast ne tvori več vsebnika za izvajanje domenske poslovne logike, temveč le njeno okolje, ki zagotavlja pomožne »platformske« funkcije. Taka plast je potrebna ne le za ohranjanje lahkotnosti domenskih mikrostoritev, ampak tudi za centralizacijo upravljanja.

Uporabniške dejavnosti v poslovnih procesih na primer ustvarjajo naloge. Pri delu z nalogami pa mora uporabnik na splošnem seznamu videti naloge iz vseh domen, kar pomeni, da mora obstajati ustrezna storitev platforme za registracijo nalog, očiščena domenske poslovne logike. Ohranjanje inkapsulacije poslovne logike v tem kontekstu je precej problematično in to je še en kompromis te arhitekture.

Integracija poslovnih procesov skozi oči razvijalca aplikacij

Kot že omenjeno, se mora razvijalec aplikacije abstrahirati od tehničnih in inženirskih značilnosti izvedbe interakcije več aplikacij, da lahko računa na dobro razvojno produktivnost.

Poskusimo rešiti precej težko težavo integracije, ki je bila posebej izumljena za članek. To bo "igrica", ki bo vključevala tri aplikacije, kjer vsaka od njih definira neko ime domene: "app1", "app2", "app3".

Znotraj vsake aplikacije se zaženejo poslovni procesi, ki začnejo »igrati« preko integracijskega vodila. Sporočila z imenom "Žoga" bodo delovala kot žoga.

Pravila igre:

  • prvi igralec je pobudnik. K igri povabi druge igralce, igro začne in jo lahko kadarkoli konča;
  • drugi igralci izjavijo svojo udeležbo v igri, se "seznanijo" drug z drugim in s prvim igralcem;
  • po prejemu žoge igralec izbere drugega sodelujočega igralca in mu poda žogo. Šteje se skupno število prehodov;
  • vsak igralec ima "energijo", ki se zmanjšuje z vsakim podajanjem žoge tega igralca. Ko zmanjka energije, je igralec izločen iz igre in naznani svojo upokojitev;
  • če igralec ostane sam, takoj izjavi, da odhaja;
  • ko so vsi igralci izločeni, prvi igralec razglasi konec igre. Če je igro zapustil prej, potem ostane igro slediti, da jo dokončate.

Za rešitev te težave bom uporabil naš DSL za poslovne procese, ki vam omogoča, da logiko v Kotlinu opišete kompaktno, z minimalno šablono.

V aplikaciji app1 bo deloval poslovni proces prvega igralca (je tudi pobudnik igre):

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

Poleg izvajanja poslovne logike lahko zgornja koda izdela objektni model poslovnega procesa, ki ga je mogoče vizualizirati kot diagram. Vizualizatorja še nismo implementirali, zato smo morali porabiti nekaj časa za risanje (tukaj sem nekoliko poenostavil zapis BPMN glede uporabe vrat, da bi izboljšal skladnost diagrama z zgornjo kodo):

Integracija sloga BPM

app2 bo vključeval poslovni proces drugega igralca:

razred 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:

Integracija sloga BPM

V aplikaciji app3 bomo igralca naredili z nekoliko drugačnim vedenjem: namesto da bi naključno izbiral naslednjega igralca, bo deloval po algoritmu krožnega dela:

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

V nasprotnem primeru se vedenje igralca ne razlikuje od prejšnjega, zato se diagram ne spremeni.

Zdaj potrebujemo test, da izvedemo vse. Navedel bom samo kodo samega testa, da ne bom zatrpal članka s predlogo (pravzaprav sem uporabil prej ustvarjeno testno okolje za testiranje integracije drugih poslovnih procesov):

testna igra()

@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;
}

Zaženite test, poglejte dnevnik:

izhod konzole

Взята блокировка ключа 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!

Iz vsega tega je mogoče potegniti več pomembnih zaključkov:

  • če so na voljo potrebna orodja, lahko razvijalci aplikacij ustvarijo integracijske interakcije med aplikacijami, ne da bi se oddaljili od poslovne logike;
  • kompleksnost (kompleksnost) integracijske naloge, ki zahteva inženirske kompetence, se lahko skrije znotraj ogrodja, če je na začetku določena v arhitekturi ogrodja. Težavnosti naloge (težavnosti) ni mogoče skriti, zato bo temu primerno videti tudi rešitev zahtevne naloge v kodi;
  • pri razvoju logike integracije je treba upoštevati končno konsistentnost in pomanjkanje linearizabilnosti spremembe stanja vseh udeležencev integracije. To nas sili v zapletanje logike, da bi bila neobčutljiva na vrstni red, v katerem se dogajajo zunanji dogodki. V našem primeru je igralec prisiljen sodelovati v igri, potem ko napove svoj izstop iz igre: drugi igralci mu bodo še naprej podajali žogo, dokler informacija o njegovem izstopu ne doseže in je obdelana s strani vseh udeležencev. Ta logika ne izhaja iz pravil igre in je kompromisna rešitev v okviru izbrane arhitekture.

Nato se pogovorimo o različnih podrobnostih naše rešitve, kompromisih in drugih točkah.

Vsa sporočila v eni čakalni vrsti

Vse integrirane aplikacije delujejo z enim integracijskim vodilom, ki je predstavljeno kot zunanji posrednik, enim BPMQueue za sporočila in eno temo BPMTopic za signale (dogodke). Posredovanje vseh sporočil skozi eno čakalno vrsto je samo po sebi kompromis. Na ravni poslovne logike lahko zdaj uvedete poljubno število novih vrst sporočil, ne da bi spreminjali strukturo sistema. Gre za precejšnjo poenostavitev, ki pa prinaša določena tveganja, ki pa se nam v kontekstu naših tipičnih nalog niso zdela tako pomembna.

Integracija sloga BPM

Vendar pa je tukaj ena subtilnost: vsaka aplikacija filtrira "svoja" sporočila iz čakalne vrste na vhodu, po imenu svoje domene. Prav tako lahko domeno določite v signalih, če morate omejiti »obseg« signala na eno samo aplikacijo. To bi moralo povečati pasovno širino vodila, vendar mora poslovna logika zdaj delovati z imeni domen: obvezno za naslavljanje sporočil, zaželeno za signale.

Zagotavljanje zanesljivosti integracijskega vodila

Zanesljivost je sestavljena iz več stvari:

  • Izbrani posrednik sporočil je kritična komponenta arhitekture in ena sama točka napake: biti mora dovolj toleranten na napake. Uporabljajte samo časovno preizkušene izvedbe z dobro podporo in veliko skupnostjo;
  • zagotoviti je treba visoko razpoložljivost posrednika sporočil, ki mora biti fizično ločen od integriranih aplikacij (visoko razpoložljivost aplikacij z uporabno poslovno logiko je veliko težje in dražje zagotoviti);
  • posrednik je dolžan zagotoviti "vsaj enkrat" dobavne garancije. To je obvezna zahteva za zanesljivo delovanje integracijskega vodila. Ni potrebe po jamstvih na ravni "točno enkrat": poslovni procesi običajno niso občutljivi na ponavljajoče se prispele sporočila ali dogodke, pri posebnih nalogah, kjer je to pomembno, pa je lažje dodati dodatna preverjanja poslovni logiki kot nenehno uporabljati precej "drage" garancije;
  • pošiljanje sporočil in signalov mora biti vključeno v skupno transakcijo s spremembo stanja poslovnih procesov in domenskih podatkov. Najboljša možnost bi bila uporaba vzorca Transakcijsko pošiljanje, vendar bo zahteval dodatno tabelo v bazi podatkov in rele. V aplikacijah JEE je to mogoče poenostaviti z uporabo lokalnega upravitelja JTA, vendar mora povezava z izbranim posrednikom delovati v načinu XA;
  • upravljavci dohodnih sporočil in dogodkov morajo delati tudi s transakcijo spreminjanja stanja poslovnega procesa: če se taka transakcija vrne nazaj, je treba preklicati tudi prejem sporočila;
  • sporočila, ki jih ni bilo mogoče dostaviti zaradi napak, je treba shraniti v ločeno shrambo DLQ (Čakalna vrsta mrtvih pisem). Da bi to naredili, smo ustvarili ločeno mikrostoritev platforme, ki shranjuje taka sporočila v svojo shrambo, jih indeksira po atributih (za hitro združevanje in iskanje) in izpostavi API za ogled, ponovno pošiljanje na ciljni naslov in brisanje sporočil. Sistemski skrbniki lahko delajo s to storitvijo prek svojega spletnega vmesnika;
  • v nastavitvah posrednika morate prilagoditi število ponovnih poskusov dostave in zamike med dostavo, da zmanjšate verjetnost, da bi sporočila prišla v DLQ (skoraj nemogoče je izračunati optimalne parametre, lahko pa ukrepate empirično in jih prilagodite med delovanje);
  • skladišče DLQ je treba nenehno nadzorovati, sistem za spremljanje pa mora obvestiti sistemske skrbnike, da se lahko čim hitreje odzovejo, ko se pojavijo nedostavljena sporočila. To bo zmanjšalo "območje škode" napake ali napake poslovne logike;
  • integracijsko vodilo mora biti neobčutljivo na začasno odsotnost aplikacij: naročnine na teme morajo biti trajne, ime domene aplikacije pa mora biti unikatno, da nekdo drug ne poskuša obdelati njenega sporočila iz čakalne vrste med odsotnostjo aplikacije.

Zagotavljanje varnosti niti poslovne logike

Ista instanca poslovnega procesa lahko prejme več sporočil in dogodkov hkrati, katerih obdelava se bo začela vzporedno. Hkrati mora biti za razvijalca aplikacij vse preprosto in varno za niti.

Procesna poslovna logika obdela vsak zunanji dogodek, ki vpliva na ta poslovni proces posebej. Ti dogodki so lahko:

  • zagon instance poslovnega procesa;
  • dejanje uporabnika, povezano z aktivnostjo znotraj poslovnega procesa;
  • prejem sporočila ali signala, na katerega je naročena instanca poslovnega procesa;
  • potek časovnika, ki ga je nastavila instanca poslovnega procesa;
  • nadzorno dejanje prek API-ja (npr. prekinitev procesa).

Vsak tak dogodek lahko spremeni stanje primerka poslovnega procesa: nekatere dejavnosti se lahko končajo in druge začnejo, lahko se spremenijo vrednosti trajnih lastnosti. Zapiranje katere koli dejavnosti lahko povzroči aktiviranje ene ali več od naslednjih dejavnosti. Ti pa lahko prenehajo čakati na druge dogodke ali, če ne potrebujejo dodatnih podatkov, lahko dokončajo v isti transakciji. Pred zaključkom transakcije se novo stanje poslovnega procesa shrani v bazo podatkov, kjer bo počakalo na naslednji zunanji dogodek.

Vztrajni podatki poslovnega procesa, shranjeni v relacijski bazi podatkov, so zelo priročna točka za sinhronizacijo obdelave pri uporabi SELECT FOR UPDATE. Če je ena transakcija uspela pridobiti stanje poslovnega procesa iz baze podatkov, da bi ga spremenila, potem nobena druga vzporedna transakcija ne bo mogla pridobiti enakega stanja za drugo spremembo, po zaključku prve transakcije pa je druga zajamčeno prejeti že spremenjeno stanje.

Z uporabo pesimističnih ključavnic na strani DBMS izpolnjujemo vse potrebne zahteve KISLINA, prav tako pa ohranite možnost prilagajanja aplikacije s poslovno logiko s povečanjem števila delujočih primerkov.

Vendar nam pesimistična zaklepanja grozijo z zastoji, kar pomeni, da bi morala biti SELECT FOR UPDATE še vedno omejena na neko razumno časovno omejitev v primeru zastojev pri nekaterih hudih primerih v poslovni logiki.

Druga težava je sinhronizacija začetka poslovnega procesa. Čeprav ni instance poslovnega procesa, tudi v bazi podatkov ni stanja, zato opisana metoda ne bo delovala. Če želite zagotoviti edinstvenost primerka poslovnega procesa v določenem obsegu, potem potrebujete nekakšen sinhronizacijski objekt, povezan z razredom procesa in ustreznim obsegom. Za rešitev te težave uporabljamo drugačen mehanizem zaklepanja, ki nam omogoča, da prek zunanje storitve zaklenemo poljuben vir, določen s ključem v formatu URI.

V naših primerih poslovni proces InitialPlayer vsebuje deklaracijo

uniqueConstraint = UniqueConstraints.singleton

Zato dnevnik vsebuje sporočila o prevzemu in sprostitvi ključavnice ustreznega ključa. Za druge poslovne procese takih sporočil ni: uniqueConstraint ni nastavljen.

Težave s poslovnimi procesi s stalnim stanjem

Včasih vztrajno stanje ne le pomaga, ampak tudi resnično ovira razvoj.
Težave se začnejo, ko morate spremeniti poslovno logiko in/ali model poslovnih procesov. Nobena takšna sprememba ni združljiva s starim stanjem poslovnih procesov. Če je v bazi podatkov veliko "živih" primerkov, potem lahko nekompatibilne spremembe povzročijo veliko težav, na katere smo pogosto naleteli pri uporabi jBPM.

Glede na globino spremembe lahko ukrepate na dva načina:

  1. ustvarite nov tip poslovnega procesa, da ne boste naredili nezdružljivih sprememb starega, in ga uporabite namesto starega pri zagonu novih primerkov. Stare instance bodo še naprej delovale "po starem";
  2. migrirati obstojno stanje poslovnih procesov pri posodabljanju poslovne logike.

Prvi način je preprostejši, vendar ima svoje omejitve in slabosti, na primer:

  • podvajanje poslovne logike v številnih modelih poslovnih procesov, povečanje obsega poslovne logike;
  • pogosto je potreben takojšen prehod na novo poslovno logiko (skoraj vedno v smislu integracijskih nalog);
  • razvijalec ne ve, na kateri točki je mogoče izbrisati zastarele modele.

V praksi uporabljamo oba pristopa, vendar smo sprejeli številne odločitve, da bi si poenostavili življenje:

  • v bazi podatkov je trajno stanje poslovnega procesa shranjeno v lahko berljivi in ​​enostavno obdelani obliki: v nizu formata JSON. To vam omogoča izvajanje selitev znotraj aplikacije in zunaj nje. V skrajnih primerih ga lahko prilagodite tudi z ročaji (še posebej uporabno pri razvoju med odpravljanjem napak);
  • integracijska poslovna logika ne uporablja imen poslovnih procesov, tako da je kadarkoli mogoče zamenjati izvedbo enega od sodelujočih procesov z novim, z novim imenom (npr. "InitialPlayerV2"). Vezava poteka preko imen sporočil in signalov;
  • model procesa ima številko različice, ki jo povečamo, če naredimo nezdružljive spremembe v tem modelu, in ta številka je shranjena skupaj s stanjem instance procesa;
  • obstojno stanje procesa se najprej prebere iz baze v priročen objektni model, s katerim lahko deluje postopek selitve, če se je številka različice modela spremenila;
  • postopek migracije je postavljen poleg poslovne logike in se imenuje "leni" za vsako instanco poslovnega procesa v času njegove obnovitve iz baze podatkov;
  • če morate hitro in sinhrono preseliti stanje vseh instanc procesa, se uporabijo bolj klasične rešitve za selitev baze podatkov, vendar morate tam delati z JSON.

Ali potrebujem drugo ogrodje za poslovne procese?

Rešitve, opisane v članku, so nam omogočile, da smo si bistveno poenostavili življenje, razširili nabor vprašanj, ki jih rešujemo na ravni razvoja aplikacij, in naredili idejo o ločevanju poslovne logike na mikrostoritve privlačnejšo. Za to je bilo opravljenega veliko dela, ustvarjeno je bilo zelo "lahko" ogrodje za poslovne procese, pa tudi storitvene komponente za reševanje ugotovljenih problemov v okviru širokega nabora uporabnih nalog. Imamo željo deliti te rezultate, spraviti razvoj skupnih komponent v odprt dostop pod brezplačno licenco. To bo zahtevalo nekaj truda in časa. Razumevanje povpraševanja po tovrstnih rešitvah bi nam lahko bila dodatna spodbuda. V predlaganem prispevku je zmožnostim samega ogrodja namenjene zelo malo pozornosti, nekatere pa so razvidne iz predstavljenih primerov. Če kljub temu objavimo naš okvir, mu bo posvečen poseben članek. Medtem vam bomo hvaležni, če pustite nekaj povratnih informacij z odgovorom na vprašanje:

V anketi lahko sodelujejo samo registrirani uporabniki. Prijaviti se, prosim.

Ali potrebujem drugo ogrodje za poslovne procese?

  • 18,8%Ja, nekaj takega že dolgo iščem.

  • 12,5%zanimivo je izvedeti več o vaši implementaciji, morda bo koristno2

  • 6,2%uporabljamo enega od obstoječih okvirov, vendar razmišljamo o zamenjavi1

  • 18,8%uporabljamo enega od obstoječih okvirov, vse ustreza3

  • 18,8%spopadanje brez ogrodja3

  • 25,0%napiši svoje4

Glasovalo je 16 uporabnikov. 7 uporabnikov se je vzdržalo.

Vir: www.habr.com

Dodaj komentar