Ενσωμάτωση στυλ BPM

Ενσωμάτωση στυλ BPM

Γεια σου, Habr!

Η εταιρεία μας ειδικεύεται στην ανάπτυξη λύσεων λογισμικού κατηγορίας ERP, στις οποίες τη μερίδα του λέοντος καταλαμβάνουν συστήματα συναλλαγών με τεράστιο όγκο επιχειρηματικής λογικής και ροής εργασιών a la EDMS. Οι σύγχρονες εκδόσεις των προϊόντων μας βασίζονται σε τεχνολογίες JavaEE, αλλά πειραματιζόμαστε επίσης ενεργά με μικροϋπηρεσίες. Ένας από τους πιο προβληματικούς τομείς τέτοιων λύσεων είναι η ενοποίηση διαφόρων υποσυστημάτων που σχετίζονται με γειτονικούς τομείς. Οι εργασίες ενσωμάτωσης μας προκαλούσαν πάντα τεράστιο πονοκέφαλο, ανεξάρτητα από τα αρχιτεκτονικά στυλ, τις στοίβες τεχνολογίας και τα πλαίσια που χρησιμοποιούμε, αλλά πρόσφατα σημειώθηκε πρόοδος στην επίλυση τέτοιων προβλημάτων.

Στο άρθρο που έφερε στην προσοχή σας, θα μιλήσω για την εμπειρία και την αρχιτεκτονική έρευνα του NPO Krista στον καθορισμένο χώρο. Θα εξετάσουμε επίσης ένα παράδειγμα απλής λύσης σε ένα πρόβλημα ενσωμάτωσης από τη σκοπιά ενός προγραμματιστή εφαρμογών και θα ανακαλύψουμε τι κρύβεται πίσω από αυτή την απλότητα.

Αποποίηση ευθυνών

Οι αρχιτεκτονικές και τεχνικές λύσεις που περιγράφονται στο άρθρο προσφέρονται από εμένα με βάση την προσωπική εμπειρία στο πλαίσιο συγκεκριμένων εργασιών. Αυτές οι λύσεις δεν ισχυρίζονται ότι είναι καθολικές και ενδέχεται να μην είναι βέλτιστες υπό άλλες συνθήκες χρήσης.

Τι σχέση έχει το BPM;

Για να απαντήσουμε σε αυτό το ερώτημα, πρέπει να εμβαθύνουμε λίγο στις ιδιαιτερότητες των εφαρμοζόμενων προβλημάτων των λύσεών μας. Το κύριο μέρος της επιχειρηματικής λογικής στο τυπικό σύστημα συναλλαγών μας είναι η εισαγωγή δεδομένων στη βάση δεδομένων μέσω διεπαφών χρήστη, ο χειροκίνητος και αυτόματα έλεγχος αυτών των δεδομένων, η μετάβασή τους σε κάποια ροή εργασιών, η δημοσίευσή τους σε άλλο σύστημα / αναλυτική βάση δεδομένων / αρχείο, η δημιουργία αναφορών. Έτσι, η βασική λειτουργία του συστήματος για τους πελάτες είναι η αυτοματοποίηση των εσωτερικών επιχειρηματικών διαδικασιών τους.

Για λόγους ευκολίας, χρησιμοποιούμε τον όρο "έγγραφο" στην επικοινωνία ως κάποια αφαίρεση ενός συνόλου δεδομένων, που ενώνεται με ένα κοινό κλειδί, στο οποίο μπορεί να "συνδεθεί" μια συγκεκριμένη ροή εργασίας.
Τι γίνεται όμως με τη λογική της ολοκλήρωσης; Εξάλλου, το έργο της ολοκλήρωσης δημιουργείται από την αρχιτεκτονική του συστήματος, το οποίο «πριονίζεται» σε μέρη ΟΧΙ κατόπιν αιτήματος του πελάτη, αλλά υπό την επίδραση εντελώς διαφορετικών παραγόντων:

  • υπό την επιρροή του νόμου του Conway?
  • ως αποτέλεσμα της επαναχρησιμοποίησης υποσυστημάτων που είχαν αναπτυχθεί προηγουμένως για άλλα προϊόντα·
  • όπως αποφάσισε ο αρχιτέκτονας, βάσει μη λειτουργικών απαιτήσεων.

Υπάρχει μεγάλος πειρασμός να διαχωρίσετε τη λογική ενσωμάτωσης από την επιχειρηματική λογική της κύριας ροής εργασιών, ώστε να μην ρυπαίνεται η επιχειρηματική λογική με τεχνουργήματα ενοποίησης και να σωθεί ο προγραμματιστής της εφαρμογής από την ανάγκη να εμβαθύνει στις ιδιαιτερότητες του αρχιτεκτονικού τοπίου του συστήματος. Αυτή η προσέγγιση έχει πολλά πλεονεκτήματα, αλλά η πρακτική δείχνει την αναποτελεσματικότητά της:

  • Η επίλυση προβλημάτων ολοκλήρωσης συνήθως ολισθαίνει στις απλούστερες επιλογές με τη μορφή σύγχρονων κλήσεων λόγω των περιορισμένων σημείων επέκτασης στην υλοποίηση της κύριας ροής εργασιών (περισσότερα για τις αδυναμίες της σύγχρονης ενοποίησης παρακάτω).
  • τα τεχνουργήματα ενοποίησης εξακολουθούν να διεισδύουν στην κύρια επιχειρηματική λογική όταν απαιτείται ανάδραση από άλλο υποσύστημα.
  • ο προγραμματιστής της εφαρμογής αγνοεί την ενσωμάτωση και μπορεί εύκολα να την διακόψει αλλάζοντας τη ροή εργασίας.
  • το σύστημα παύει να είναι ένα ενιαίο σύνολο από τη σκοπιά του χρήστη, οι "ραφές" μεταξύ των υποσυστημάτων γίνονται αισθητές, εμφανίζονται περιττές λειτουργίες χρήστη που ξεκινούν τη μεταφορά δεδομένων από το ένα υποσύστημα στο άλλο.

Μια άλλη προσέγγιση είναι να θεωρηθούν οι αλληλεπιδράσεις ολοκλήρωσης ως αναπόσπαστο μέρος της βασικής επιχειρηματικής λογικής και ροής εργασίας. Για να μην εκτοξεύονται οι απαιτήσεις δεξιοτήτων των προγραμματιστών εφαρμογών, η δημιουργία νέων αλληλεπιδράσεων ενοποίησης θα πρέπει να γίνεται εύκολα και φυσικά, με ελάχιστες επιλογές για την επιλογή λύσης. Αυτό είναι πιο δύσκολο από ό,τι φαίνεται: το εργαλείο πρέπει να είναι αρκετά ισχυρό ώστε να παρέχει στον χρήστη την απαραίτητη ποικιλία επιλογών για τη χρήση του και ταυτόχρονα να μην του επιτρέπει να πυροβοληθεί στο πόδι. Υπάρχουν πολλές ερωτήσεις που πρέπει να απαντήσει ένας μηχανικός στο πλαίσιο των εργασιών ενοποίησης, αλλά τις οποίες ένας προγραμματιστής εφαρμογών δεν πρέπει να σκέφτεται στην καθημερινή του εργασία: όρια συναλλαγών, συνέπεια, ατομικότητα, ασφάλεια, κλιμάκωση, κατανομή φορτίου και πόρων, δρομολόγηση, ομαδοποίηση, πλαισίων διάδοσης και εναλλαγής κ.λπ. Είναι απαραίτητο να προσφέρουμε στους προγραμματιστές εφαρμογών αρκετά απλά πρότυπα αποφάσεων, στα οποία οι απαντήσεις σε όλες αυτές τις ερωτήσεις είναι ήδη κρυμμένες. Αυτά τα μοτίβα θα πρέπει να είναι αρκετά ασφαλή: η επιχειρηματική λογική αλλάζει πολύ συχνά, γεγονός που αυξάνει τον κίνδυνο εισαγωγής σφαλμάτων, το κόστος των σφαλμάτων θα πρέπει να παραμείνει σε αρκετά χαμηλό επίπεδο.

Αλλά ακόμα, τι σχέση έχει το BPM; Υπάρχουν πολλές επιλογές για την υλοποίηση της ροής εργασιών...
Πράγματι, μια άλλη εφαρμογή επιχειρηματικών διαδικασιών είναι πολύ δημοφιλής στις λύσεις μας - μέσω της δηλωτικής ρύθμισης του διαγράμματος μετάβασης κατάστασης και της σύνδεσης χειριστών με επιχειρηματική λογική στις μεταβάσεις. Ταυτόχρονα, η κατάσταση που καθορίζει την τρέχουσα θέση του «εγγράφου» στην επιχειρηματική διαδικασία είναι χαρακτηριστικό του ίδιου του «εγγράφου».

Ενσωμάτωση στυλ BPM
Έτσι φαίνεται η διαδικασία στην αρχή του έργου

Η δημοτικότητα μιας τέτοιας υλοποίησης οφείλεται στη σχετική απλότητα και ταχύτητα δημιουργίας γραμμικών επιχειρηματικών διαδικασιών. Ωστόσο, καθώς τα συστήματα λογισμικού γίνονται πιο πολύπλοκα, το αυτοματοποιημένο μέρος της επιχειρηματικής διαδικασίας μεγαλώνει και γίνεται πιο περίπλοκο. Υπάρχει ανάγκη για αποσύνθεση, επαναχρησιμοποίηση τμημάτων διεργασιών, καθώς και διεργασίες διακλάδωσης ώστε κάθε κλάδος να εκτελείται παράλληλα. Κάτω από τέτοιες συνθήκες, το εργαλείο γίνεται άβολο και το διάγραμμα μετάβασης κατάστασης χάνει το περιεχόμενο πληροφοριών του (οι αλληλεπιδράσεις ενσωμάτωσης δεν αντικατοπτρίζονται καθόλου στο διάγραμμα).

Ενσωμάτωση στυλ BPM
Έτσι φαίνεται η διαδικασία μετά από πολλές επαναλήψεις αποσαφήνισης των απαιτήσεων

Η διέξοδος από αυτή την κατάσταση ήταν η ενσωμάτωση του κινητήρα jBPM σε ορισμένα προϊόντα με τις πιο σύνθετες επιχειρηματικές διαδικασίες. Βραχυπρόθεσμα, αυτή η λύση είχε κάποια επιτυχία: κατέστη δυνατή η εφαρμογή πολύπλοκων επιχειρηματικών διαδικασιών διατηρώντας παράλληλα ένα αρκετά ενημερωτικό και ενημερωμένο διάγραμμα στη σημείωση BPMN2.

Ενσωμάτωση στυλ BPM
Ένα μικρό μέρος μιας πολύπλοκης επιχειρηματικής διαδικασίας

Μακροπρόθεσμα, η λύση δεν ανταποκρίθηκε στις προσδοκίες: η υψηλή ένταση εργασίας για τη δημιουργία επιχειρηματικών διαδικασιών μέσω οπτικών εργαλείων δεν επέτρεψε την επίτευξη αποδεκτών δεικτών παραγωγικότητας και το ίδιο το εργαλείο έγινε ένα από τα πιο αντιπαθητικά μεταξύ των προγραμματιστών. Υπήρχαν επίσης παράπονα για την εσωτερική δομή του κινητήρα, η οποία οδήγησε στην εμφάνιση πολλών "μπαλωμάτων" και "πατερίτσες".

Η κύρια θετική πτυχή της χρήσης του jBPM ήταν η συνειδητοποίηση των πλεονεκτημάτων και των βλαβών από την ύπαρξη της δικής του επίμονης κατάστασης για ένα παράδειγμα επιχειρηματικής διαδικασίας. Είδαμε επίσης τη δυνατότητα χρήσης μιας προσέγγισης διαδικασίας για την υλοποίηση σύνθετων πρωτοκόλλων ενοποίησης μεταξύ διαφορετικών εφαρμογών χρησιμοποιώντας ασύγχρονες αλληλεπιδράσεις μέσω σημάτων και μηνυμάτων. Η παρουσία ενός επίμονου κράτους παίζει καθοριστικό ρόλο σε αυτό.

Με βάση τα παραπάνω μπορούμε να συμπεράνουμε: Η προσέγγιση διαδικασίας στο στυλ BPM μας επιτρέπει να επιλύουμε ένα ευρύ φάσμα εργασιών για την αυτοματοποίηση όλο και πιο περίπλοκων επιχειρηματικών διαδικασιών, να ενσωματώνουμε αρμονικά τις δραστηριότητες ενσωμάτωσης σε αυτές τις διαδικασίες και να διατηρούμε τη δυνατότητα οπτικής εμφάνισης της εφαρμοσμένης διαδικασίας με κατάλληλη σημείωση.

Μειονεκτήματα των σύγχρονων κλήσεων ως μοτίβο ενσωμάτωσης

Η σύγχρονη ενοποίηση αναφέρεται στην απλούστερη κλήση αποκλεισμού. Ένα υποσύστημα λειτουργεί ως η πλευρά του διακομιστή και εκθέτει το API με την επιθυμητή μέθοδο. Ένα άλλο υποσύστημα λειτουργεί ως πελάτης και, την κατάλληλη στιγμή, πραγματοποιεί μια κλήση με την προσδοκία ενός αποτελέσματος. Ανάλογα με την αρχιτεκτονική του συστήματος, οι πλευρές πελάτη και διακομιστή μπορούν να φιλοξενηθούν είτε στην ίδια εφαρμογή και διαδικασία, είτε σε διαφορετικές. Στη δεύτερη περίπτωση, πρέπει να εφαρμόσετε κάποια εφαρμογή του RPC και να παράσχετε ομαδοποίηση των παραμέτρων και του αποτελέσματος της κλήσης.

Ενσωμάτωση στυλ BPM

Ένα τέτοιο μοτίβο ολοκλήρωσης έχει ένα αρκετά μεγάλο σύνολο μειονεκτημάτων, αλλά χρησιμοποιείται πολύ ευρέως στην πράξη λόγω της απλότητάς του. Η ταχύτητα υλοποίησης αιχμαλωτίζει και σε κάνει να την εφαρμόζεις ξανά και ξανά σε συνθήκες «φλεγόμενων» προθεσμιών, γράφοντας τη λύση σε τεχνικό χρέος. Αλλά συμβαίνει επίσης ότι οι άπειροι προγραμματιστές το χρησιμοποιούν ασυνείδητα, απλώς μη συνειδητοποιώντας τις αρνητικές συνέπειες.

Εκτός από την πιο εμφανή αύξηση της συνδεσιμότητας των υποσυστημάτων, υπάρχουν λιγότερο εμφανή προβλήματα με τη «διάδοση» και την «έκταση» των συναλλαγών. Πράγματι, εάν η επιχειρηματική λογική κάνει αλλαγές, τότε οι συναλλαγές είναι απαραίτητες και οι συναλλαγές, με τη σειρά τους, κλειδώνουν ορισμένους πόρους εφαρμογής που επηρεάζονται από αυτές τις αλλαγές. Δηλαδή, έως ότου ένα υποσύστημα περιμένει απάντηση από ένα άλλο, δεν θα μπορεί να ολοκληρώσει τη συναλλαγή και να απελευθερώσει τα λουκέτα. Αυτό αυξάνει σημαντικά τον κίνδυνο μιας ποικιλίας επιπτώσεων:

  • η ανταπόκριση του συστήματος χάνεται, οι χρήστες περιμένουν πολύ καιρό για απαντήσεις στα αιτήματα.
  • ο διακομιστής γενικά σταματά να ανταποκρίνεται στα αιτήματα των χρηστών λόγω μιας υπερχείλισης ομάδας νημάτων: τα περισσότερα από τα νήματα «στέκονται» στο κλείδωμα του πόρου που καταλαμβάνει η συναλλαγή.
  • αρχίζουν να εμφανίζονται αδιέξοδα: η πιθανότητα εμφάνισής τους εξαρτάται σε μεγάλο βαθμό από τη διάρκεια των συναλλαγών, το μέγεθος της επιχειρηματικής λογικής και τα λουκέτα που εμπλέκονται στη συναλλαγή.
  • Εμφανίζονται σφάλματα λήξης χρονικού ορίου συναλλαγής.
  • ο διακομιστής "πέφτει" στο OutOfMemory εάν ​​η εργασία απαιτεί επεξεργασία και αλλαγή μεγάλων ποσοτήτων δεδομένων και η παρουσία σύγχρονων ενσωματώσεων καθιστά πολύ δύσκολο τον διαχωρισμό της επεξεργασίας σε "ελαφρύτερες" συναλλαγές.

Από αρχιτεκτονική άποψη, η χρήση κλήσεων αποκλεισμού κατά την ενοποίηση οδηγεί σε απώλεια ποιοτικού ελέγχου μεμονωμένων υποσυστημάτων: είναι αδύνατο να επιτευχθούν ποιοτικοί στόχοι για ένα υποσύστημα μεμονωμένα από τους δείκτες ποιότητας ενός άλλου υποσυστήματος. Εάν τα υποσυστήματα αναπτύσσονται από διαφορετικές ομάδες, αυτό είναι μεγάλο πρόβλημα.

Τα πράγματα γίνονται ακόμη πιο ενδιαφέροντα εάν τα υποσυστήματα που ενσωματώνονται βρίσκονται σε διαφορετικές εφαρμογές και πρέπει να γίνουν σύγχρονες αλλαγές και στις δύο πλευρές. Πώς να κάνετε αυτές τις αλλαγές συναλλακτικές;

Εάν πραγματοποιηθούν αλλαγές σε ξεχωριστές συναλλαγές, τότε θα πρέπει να παρέχεται ισχυρός χειρισμός εξαιρέσεων και αντιστάθμιση, και αυτό εξαλείφει εντελώς το κύριο πλεονέκτημα των σύγχρονων ενσωματώσεων - την απλότητα.

Οι κατανεμημένες συναλλαγές έρχονται επίσης στο μυαλό, αλλά δεν τις χρησιμοποιούμε στις λύσεις μας: είναι δύσκολο να διασφαλίσουμε την αξιοπιστία.

Το «Saga» ως λύση στο πρόβλημα των συναλλαγών

Με την αυξανόμενη δημοτικότητα των μικροϋπηρεσιών, υπάρχει μια αυξανόμενη ζήτηση για Μοτίβο Saga.

Αυτό το μοτίβο λύνει τέλεια τα παραπάνω προβλήματα μακροχρόνιων συναλλαγών και επεκτείνει επίσης τις δυνατότητες διαχείρισης της κατάστασης του συστήματος από την πλευρά της επιχειρηματικής λογικής: η αποζημίωση μετά από μια αποτυχημένη συναλλαγή μπορεί να μην επαναφέρει το σύστημα στην αρχική του κατάσταση, αλλά να παρέχει μια εναλλακτική διαδρομή επεξεργασίας δεδομένων. Σας επιτρέπει επίσης να μην επαναλαμβάνετε τα βήματα επεξεργασίας δεδομένων που έχουν ολοκληρωθεί με επιτυχία όταν προσπαθείτε να φέρετε τη διαδικασία σε ένα «καλό» τέλος.

Είναι ενδιαφέρον ότι στα μονολιθικά συστήματα, αυτό το μοτίβο είναι επίσης σχετικό όταν πρόκειται για την ενσωμάτωση χαλαρά συζευγμένων υποσυστημάτων και υπάρχουν αρνητικές επιπτώσεις που προκαλούνται από μεγάλες συναλλαγές και τα αντίστοιχα κλείδωμα πόρων.

Όσον αφορά τις επιχειρηματικές μας διαδικασίες στο στυλ BPM, αποδεικνύεται ότι είναι πολύ εύκολο να εφαρμόσουμε τα Sagas: τα μεμονωμένα βήματα του Sagas μπορούν να οριστούν ως δραστηριότητες εντός της επιχειρηματικής διαδικασίας και η επίμονη κατάσταση της επιχειρηματικής διαδικασίας καθορίζει, μεταξύ άλλα πράγματα, η εσωτερική κατάσταση των Sagas. Δηλαδή, δεν χρειαζόμαστε κανέναν πρόσθετο συντονιστικό μηχανισμό. Το μόνο που χρειάζεστε είναι ένας μεσίτης μηνυμάτων με υποστήριξη για "τουλάχιστον μία φορά" εγγυήσεις ως μεταφορά.

Αλλά μια τέτοια λύση έχει επίσης το δικό της «τίμημα»:

  • Η επιχειρηματική λογική γίνεται πιο περίπλοκη: πρέπει να επεξεργαστείτε την αποζημίωση.
  • θα είναι απαραίτητο να εγκαταλειφθεί η πλήρης συνέπεια, η οποία μπορεί να είναι ιδιαίτερα ευαίσθητη για μονολιθικά συστήματα.
  • η αρχιτεκτονική γίνεται λίγο πιο περίπλοκη, υπάρχει επιπλέον ανάγκη για έναν μεσίτη μηνυμάτων.
  • θα απαιτηθούν πρόσθετα εργαλεία παρακολούθησης και διαχείρισης (αν και γενικά αυτό είναι ακόμη καλό: η ποιότητα της υπηρεσίας του συστήματος θα αυξηθεί).

Για τα μονολιθικά συστήματα, η αιτιολόγηση για τη χρήση του "Sags" δεν είναι τόσο προφανής. Για τις μικροϋπηρεσίες και άλλες SOA, όπου, πιθανότατα, υπάρχει ήδη ένας μεσίτης, και η πλήρης συνέπεια θυσιάστηκε στην αρχή του έργου, τα οφέλη από τη χρήση αυτού του μοτίβου μπορεί να αντισταθμίσουν σημαντικά τα μειονεκτήματα, ειδικά εάν υπάρχει ένα βολικό API στο επίπεδο επιχειρηματικής λογικής.

Ενθυλάκωση της επιχειρηματικής λογικής στις μικροϋπηρεσίες

Όταν ξεκινήσαμε να πειραματιζόμαστε με μικροϋπηρεσίες, προέκυψε ένα εύλογο ερώτημα: πού να τοποθετήσουμε την επιχειρηματική λογική του τομέα σε σχέση με την υπηρεσία που παρέχει διατήρηση δεδομένων τομέα;

Κατά την εξέταση της αρχιτεκτονικής διαφόρων BPMS, μπορεί να φαίνεται λογικό να διαχωρίζεται η επιχειρηματική λογική από την επιμονή: δημιουργήστε ένα επίπεδο μικροϋπηρεσιών ανεξάρτητων από πλατφόρμα και τομέα που σχηματίζουν το περιβάλλον και το κοντέινερ για την εκτέλεση της επιχειρηματικής λογικής τομέα και τακτοποιήστε τη διατήρηση δεδομένων τομέα ως ξεχωριστό στρώμα από πολύ απλές και ελαφριές μικροϋπηρεσίες. Οι επιχειρηματικές διαδικασίες σε αυτήν την περίπτωση ενορχηστρώνουν τις υπηρεσίες του επιπέδου επιμονής.

Ενσωμάτωση στυλ BPM

Αυτή η προσέγγιση έχει ένα πολύ μεγάλο πλεονέκτημα: μπορείτε να αυξήσετε τη λειτουργικότητα της πλατφόρμας όσο θέλετε και μόνο το αντίστοιχο στρώμα microservices της πλατφόρμας θα «παχύνει» από αυτό. Οι επιχειρηματικές διαδικασίες από οποιονδήποτε τομέα έχουν την ευκαιρία να χρησιμοποιήσουν τη νέα λειτουργικότητα της πλατφόρμας αμέσως μόλις ενημερωθεί.

Μια πιο λεπτομερής μελέτη αποκάλυψε σημαντικές ελλείψεις αυτής της προσέγγισης:

  • μια υπηρεσία πλατφόρμας που εκτελεί την επιχειρηματική λογική πολλών τομέων ταυτόχρονα εγκυμονεί μεγάλους κινδύνους ως ένα μόνο σημείο αποτυχίας. Οι συχνές αλλαγές στην επιχειρηματική λογική αυξάνουν τον κίνδυνο σφαλμάτων που οδηγούν σε αστοχίες σε όλο το σύστημα.
  • ζητήματα απόδοσης: η επιχειρηματική λογική λειτουργεί με τα δεδομένα της μέσω μιας στενής και αργής διεπαφής:
    • τα δεδομένα για άλλη μια φορά θα ομαδοποιηθούν και θα αντληθούν μέσω της στοίβας δικτύου.
    • η υπηρεσία τομέα θα επιστρέφει συχνά περισσότερα δεδομένα από αυτά που απαιτεί η επιχειρηματική λογική για την επεξεργασία, λόγω ανεπαρκών δυνατοτήτων παραμετροποίησης ερωτημάτων στο επίπεδο του εξωτερικού API της υπηρεσίας.
    • πολλά ανεξάρτητα κομμάτια επιχειρηματικής λογικής μπορούν επανειλημμένα να ζητήσουν τα ίδια δεδομένα για επεξεργασία (μπορείτε να μετριαστείτε αυτό το πρόβλημα προσθέτοντας φασόλια συνεδρίας που αποθηκεύουν προσωρινά δεδομένα, αλλά αυτό περιπλέκει περαιτέρω την αρχιτεκτονική και δημιουργεί προβλήματα ανανέωσης δεδομένων και ακύρωσης της προσωρινής μνήμης).
  • θέματα συναλλαγών:
    • Οι επιχειρηματικές διαδικασίες με μόνιμη κατάσταση που αποθηκεύονται από την υπηρεσία πλατφόρμας δεν συνάδουν με τα δεδομένα τομέα και δεν υπάρχουν εύκολοι τρόποι επίλυσης αυτού του προβλήματος.
    • μετακίνηση του κλειδώματος των δεδομένων τομέα από τη συναλλαγή: εάν η επιχειρηματική λογική του τομέα χρειάζεται να κάνει αλλαγές, αφού πρώτα ελέγξει την ορθότητα των πραγματικών δεδομένων, είναι απαραίτητο να αποκλειστεί η πιθανότητα ανταγωνιστικής αλλαγής στα επεξεργασμένα δεδομένα. Ο εξωτερικός αποκλεισμός δεδομένων μπορεί να βοηθήσει στην επίλυση του προβλήματος, αλλά μια τέτοια λύση εγκυμονεί πρόσθετους κινδύνους και μειώνει τη συνολική αξιοπιστία του συστήματος.
  • πρόσθετες επιπλοκές κατά την ενημέρωση: σε ορισμένες περιπτώσεις, πρέπει να ενημερώσετε την υπηρεσία επιμονής και τη λογική της επιχείρησης συγχρονισμένα ή με αυστηρή σειρά.

Στο τέλος, έπρεπε να επιστρέψω στα βασικά: να ενσωματώσω δεδομένα τομέα και επιχειρηματική λογική τομέα σε μία μικρουπηρεσία. Αυτή η προσέγγιση απλοποιεί την αντίληψη της microservice ως αναπόσπαστο στοιχείο του συστήματος και δεν δημιουργεί τα παραπάνω προβλήματα. Δεν είναι επίσης δωρεάν:

  • Απαιτείται τυποποίηση API για την αλληλεπίδραση με την επιχειρηματική λογική (ιδίως για την παροχή δραστηριοτήτων χρήστη ως μέρος των επιχειρηματικών διαδικασιών) και τις υπηρεσίες πλατφόρμας API. πιο προσεκτική προσοχή στις αλλαγές API, απαιτείται συμβατότητα προς τα εμπρός και προς τα πίσω.
  • Απαιτείται η προσθήκη πρόσθετων βιβλιοθηκών χρόνου εκτέλεσης για να διασφαλιστεί η λειτουργία της επιχειρηματικής λογικής ως μέρος κάθε τέτοιας μικρουπηρεσίας, και αυτό δημιουργεί νέες απαιτήσεις για τέτοιες βιβλιοθήκες: ελαφρότητα και ελάχιστες μεταβατικές εξαρτήσεις.
  • Οι προγραμματιστές επιχειρηματικής λογικής πρέπει να παρακολουθούν τις εκδόσεις της βιβλιοθήκης: εάν μια μικρουπηρεσία δεν έχει οριστικοποιηθεί για μεγάλο χρονικό διάστημα, τότε πιθανότατα θα περιέχει μια ξεπερασμένη έκδοση των βιβλιοθηκών. Αυτό μπορεί να είναι ένα απροσδόκητο εμπόδιο για την προσθήκη μιας νέας δυνατότητας και μπορεί να απαιτεί τη μετεγκατάσταση της παλιάς επιχειρηματικής λογικής μιας τέτοιας υπηρεσίας σε νέες εκδόσεις των βιβλιοθηκών, εάν υπήρχαν μη συμβατές αλλαγές μεταξύ των εκδόσεων.

Ενσωμάτωση στυλ BPM

Ένα επίπεδο υπηρεσιών πλατφόρμας υπάρχει επίσης σε μια τέτοια αρχιτεκτονική, αλλά αυτό το επίπεδο δεν αποτελεί πλέον ένα κοντέινερ για την εκτέλεση της επιχειρηματικής λογικής τομέα, αλλά μόνο το περιβάλλον του, παρέχοντας βοηθητικές λειτουργίες "πλατφόρμας". Ένα τέτοιο επίπεδο χρειάζεται όχι μόνο για τη διατήρηση της ελαφρότητας των μικροϋπηρεσιών τομέα, αλλά και για την κεντρική διαχείριση.

Για παράδειγμα, οι δραστηριότητες των χρηστών στις επιχειρηματικές διαδικασίες δημιουργούν εργασίες. Ωστόσο, όταν εργάζεστε με εργασίες, ο χρήστης πρέπει να βλέπει εργασίες από όλους τους τομείς στη γενική λίστα, πράγμα που σημαίνει ότι πρέπει να υπάρχει μια κατάλληλη υπηρεσία πλατφόρμας εγγραφής εργασιών, απαλλαγμένη από επιχειρηματική λογική τομέα. Η διατήρηση της ενθυλάκωσης της επιχειρηματικής λογικής σε αυτό το πλαίσιο είναι αρκετά προβληματική, και αυτός είναι ένας άλλος συμβιβασμός αυτής της αρχιτεκτονικής.

Ενσωμάτωση επιχειρηματικών διαδικασιών μέσα από τα μάτια ενός προγραμματιστή εφαρμογών

Όπως ήδη αναφέρθηκε παραπάνω, ο προγραμματιστής εφαρμογών πρέπει να αφαιρεθεί από τα τεχνικά και μηχανολογικά χαρακτηριστικά της υλοποίησης της αλληλεπίδρασης πολλών εφαρμογών, προκειμένου να μπορεί να υπολογίζει σε καλή παραγωγικότητα ανάπτυξης.

Ας προσπαθήσουμε να λύσουμε ένα μάλλον δύσκολο πρόβλημα ολοκλήρωσης, που επινοήθηκε ειδικά για το άρθρο. Αυτή θα είναι μια εργασία "παιχνιδιού" που περιλαμβάνει τρεις εφαρμογές, όπου καθεμία από αυτές ορίζει κάποιο όνομα τομέα: "app1", "app2", "app3".

Μέσα σε κάθε εφαρμογή ξεκινούν επιχειρηματικές διαδικασίες που αρχίζουν να «παίζουν μπάλα» μέσω του διαύλου ενσωμάτωσης. Τα μηνύματα με το όνομα "Ball" θα λειτουργούν ως μπάλα.

Κανόνες παιχνιδιού:

  • ο πρώτος παίκτης είναι ο εμπνευστής. Προσκαλεί άλλους παίκτες στο παιχνίδι, ξεκινά το παιχνίδι και μπορεί να το τερματίσει ανά πάσα στιγμή.
  • άλλοι παίκτες δηλώνουν τη συμμετοχή τους στο παιχνίδι, "γνωρίζονται" μεταξύ τους και τον πρώτο παίκτη.
  • μετά την παραλαβή της μπάλας, ο παίκτης επιλέγει έναν άλλο παίκτη που συμμετέχει και του δίνει την μπάλα. Ο συνολικός αριθμός των περασμάτων υπολογίζεται.
  • κάθε παίκτης έχει «ενέργεια», η οποία μειώνεται με κάθε πάσα της μπάλας από αυτόν τον παίκτη. Όταν τελειώσει η ενέργεια, ο παίκτης αποβάλλεται από το παιχνίδι, ανακοινώνοντας την απόσυρσή του.
  • αν ο παίκτης μείνει μόνος, δηλώνει αμέσως την αποχώρησή του.
  • όταν αποκλείονται όλοι οι παίκτες, ο πρώτος παίκτης δηλώνει το τέλος του παιχνιδιού. Αν έφυγε νωρίτερα από το παιχνίδι, τότε μένει να ακολουθήσει το παιχνίδι για να το ολοκληρώσει.

Για να λύσω αυτό το πρόβλημα, θα χρησιμοποιήσω το DSL μας για επιχειρηματικές διαδικασίες, το οποίο σας επιτρέπει να περιγράψετε τη λογική στο Kotlin συμπαγή, με ένα ελάχιστο μπόιλερ.

Στην εφαρμογή app1, η επιχειρηματική διαδικασία του πρώτου παίκτη (είναι επίσης ο εμπνευστής του παιχνιδιού) θα λειτουργήσει:

κλάση 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}")
}

Εκτός από την εκτέλεση της επιχειρηματικής λογικής, ο παραπάνω κώδικας μπορεί να παράγει ένα μοντέλο αντικειμένου μιας επιχειρηματικής διαδικασίας που μπορεί να απεικονιστεί ως διάγραμμα. Δεν έχουμε εφαρμόσει ακόμα το οπτικοποιητή, επομένως χρειάστηκε να αφιερώσουμε λίγο χρόνο σχεδιάζοντας (εδώ απλοποίησα ελαφρώς τη σημείωση BPMN σχετικά με τη χρήση πυλών για τη βελτίωση της συνέπειας του διαγράμματος με τον παραπάνω κώδικα):

Ενσωμάτωση στυλ BPM

Η εφαρμογή 2 θα περιλαμβάνει την επιχειρηματική διαδικασία άλλου παίκτη:

κλάση 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}")
}

Διάγραμμα:

Ενσωμάτωση στυλ BPM

Στην εφαρμογή app3, θα κάνουμε τον παίκτη με μια ελαφρώς διαφορετική συμπεριφορά: αντί να επιλέξει τυχαία τον επόμενο παίκτη, θα ενεργήσει σύμφωνα με τον αλγόριθμο round-robin:

κλάση 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}")
}

Διαφορετικά, η συμπεριφορά του παίκτη δεν διαφέρει από την προηγούμενη, οπότε το διάγραμμα δεν αλλάζει.

Τώρα χρειαζόμαστε μια δοκιμή για να τα τρέξουμε όλα. Θα δώσω μόνο τον κωδικό της ίδιας της δοκιμής, για να μην γεμίσει το άρθρο με λέβητα (στην πραγματικότητα, χρησιμοποίησα το περιβάλλον δοκιμής που δημιουργήθηκε νωρίτερα για να δοκιμάσω την ενοποίηση άλλων επιχειρηματικών διαδικασιών):

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

Εκτελέστε τη δοκιμή, δείτε το αρχείο καταγραφής:

έξοδος κονσόλας

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

Από όλα αυτά μπορούν να εξαχθούν αρκετά σημαντικά συμπεράσματα:

  • Εάν υπάρχουν διαθέσιμα τα απαραίτητα εργαλεία, οι προγραμματιστές εφαρμογών μπορούν να δημιουργήσουν αλληλεπιδράσεις ενοποίησης μεταξύ των εφαρμογών χωρίς να ξεφεύγουν από την επιχειρηματική λογική.
  • η πολυπλοκότητα (πολυπλοκότητα) μιας εργασίας ολοκλήρωσης που απαιτεί μηχανικές ικανότητες μπορεί να κρυφτεί μέσα στο πλαίσιο, εάν αρχικά οριστεί στην αρχιτεκτονική του πλαισίου. Η δυσκολία της εργασίας (δυσκολία) δεν μπορεί να κρυφτεί, οπότε η λύση σε μια δύσκολη εργασία στον κώδικα θα φαίνεται ανάλογα.
  • κατά την ανάπτυξη της λογικής ολοκλήρωσης, είναι απαραίτητο να ληφθεί υπόψη τελικά η συνέπεια και η έλλειψη γραμμικοποίησης της αλλαγής κατάστασης όλων των συμμετεχόντων στην ένταξη. Αυτό μας αναγκάζει να περιπλέκουμε τη λογική για να την καταστήσουμε αναίσθητη στη σειρά με την οποία συμβαίνουν τα εξωτερικά γεγονότα. Στο παράδειγμά μας, ο παίκτης αναγκάζεται να λάβει μέρος στο παιχνίδι αφού ανακοινώσει την έξοδό του από το παιχνίδι: άλλοι παίκτες θα συνεχίσουν να του δίνουν την μπάλα μέχρι να φτάσουν οι πληροφορίες για την έξοδό του και να υποβληθούν σε επεξεργασία από όλους τους συμμετέχοντες. Αυτή η λογική δεν απορρέει από τους κανόνες του παιχνιδιού και είναι μια συμβιβαστική λύση στο πλαίσιο της επιλεγμένης αρχιτεκτονικής.

Στη συνέχεια, ας μιλήσουμε για τις διάφορες λεπτότητες της λύσης μας, τους συμβιβασμούς και άλλα σημεία.

Όλα τα μηνύματα σε μια ουρά

Όλες οι ενσωματωμένες εφαρμογές λειτουργούν με έναν δίαυλο ενσωμάτωσης, ο οποίος αναπαρίσταται ως εξωτερικός μεσίτης, ένα BPMQueue για μηνύματα και ένα θέμα BPMTopic για σήματα (γεγονότα). Η διέλευση όλων των μηνυμάτων από μια ουρά είναι από μόνη της ένας συμβιβασμός. Σε επίπεδο επιχειρηματικής λογικής, μπορείτε πλέον να εισάγετε όσους νέους τύπους μηνυμάτων θέλετε χωρίς να κάνετε αλλαγές στη δομή του συστήματος. Πρόκειται για μια σημαντική απλοποίηση, αλλά εγκυμονεί ορισμένους κινδύνους, οι οποίοι, στο πλαίσιο των τυπικών καθηκόντων μας, μας φάνηκαν όχι τόσο σημαντικοί.

Ενσωμάτωση στυλ BPM

Ωστόσο, υπάρχει μια λεπτότητα εδώ: κάθε εφαρμογή φιλτράρει τα μηνύματα "της" από την ουρά στην είσοδο, με το όνομα του τομέα της. Επίσης, ο τομέας μπορεί να καθοριστεί στα σήματα, εάν πρέπει να περιορίσετε το "εύρος" του σήματος σε μία μόνο εφαρμογή. Αυτό θα αυξήσει το εύρος ζώνης του διαύλου, αλλά η επιχειρηματική λογική πρέπει τώρα να λειτουργεί με ονόματα τομέα: υποχρεωτικό για τη διευθυνσιοδότηση μηνυμάτων, επιθυμητό για σήματα.

Διασφάλιση της αξιοπιστίας του διαύλου ολοκλήρωσης

Η αξιοπιστία αποτελείται από πολλά πράγματα:

  • Ο επιλεγμένος μεσολαβητής μηνυμάτων είναι ένα κρίσιμο στοιχείο της αρχιτεκτονικής και ένα μόνο σημείο αποτυχίας: πρέπει να είναι επαρκώς ανεκτικό σε σφάλματα. Θα πρέπει να χρησιμοποιείτε μόνο εφαρμογές δοκιμασμένες στο χρόνο με καλή υποστήριξη και μεγάλη κοινότητα.
  • είναι απαραίτητο να διασφαλιστεί η υψηλή διαθεσιμότητα του μεσίτη μηνυμάτων, για τον οποίο πρέπει να διαχωριστεί φυσικά από τις ενσωματωμένες εφαρμογές (η υψηλή διαθεσιμότητα εφαρμογών με εφαρμοσμένη επιχειρηματική λογική είναι πολύ πιο δύσκολη και ακριβή στην παροχή).
  • ο μεσίτης υποχρεούται να παρέχει «τουλάχιστον μία φορά» εγγυήσεις παράδοσης. Αυτή είναι μια υποχρεωτική απαίτηση για αξιόπιστη λειτουργία του διαύλου ολοκλήρωσης. Δεν χρειάζονται εγγυήσεις επιπέδου "ακριβώς μία φορά": οι επιχειρηματικές διαδικασίες συνήθως δεν είναι ευαίσθητες στην επαναλαμβανόμενη άφιξη μηνυμάτων ή συμβάντων και σε ειδικές εργασίες όπου αυτό είναι σημαντικό, είναι ευκολότερο να προστεθούν πρόσθετοι έλεγχοι στην επιχειρηματική λογική παρά να χρησιμοποιούνται συνεχώς μαλλον "ακριβες" " εγγυησεις?
  • Η αποστολή μηνυμάτων και σημάτων πρέπει να εμπλέκεται σε μια κοινή συναλλαγή με αλλαγή στην κατάσταση των επιχειρηματικών διαδικασιών και των δεδομένων τομέα. Η προτιμώμενη επιλογή θα ήταν να χρησιμοποιήσετε το μοτίβο Εξερχόμενα συναλλαγών, αλλά θα απαιτήσει έναν επιπλέον πίνακα στη βάση δεδομένων και ένα ρελέ. Σε εφαρμογές JEE, αυτό μπορεί να απλοποιηθεί χρησιμοποιώντας έναν τοπικό διαχειριστή JTA, αλλά η σύνδεση με τον επιλεγμένο μεσίτη πρέπει να μπορεί να λειτουργεί σε λειτουργία XA;
  • Οι χειριστές εισερχόμενων μηνυμάτων και συμβάντων πρέπει επίσης να συνεργάζονται με τη συναλλαγή αλλαγής της κατάστασης της επιχειρηματικής διαδικασίας: εάν μια τέτοια συναλλαγή αποσυρθεί, τότε η λήψη του μηνύματος πρέπει επίσης να ακυρωθεί.
  • Τα μηνύματα που δεν μπόρεσαν να παραδοθούν λόγω σφαλμάτων θα πρέπει να αποθηκεύονται σε ξεχωριστό κατάστημα D.L.Q. (Ουρά νεκρών επιστολών). Για να γίνει αυτό, δημιουργήσαμε μια ξεχωριστή microservice πλατφόρμας που αποθηκεύει τέτοια μηνύματα στον αποθηκευτικό χώρο της, τα ευρετηριάζει με χαρακτηριστικά (για γρήγορη ομαδοποίηση και αναζήτηση) και εκθέτει το API για προβολή, αποστολή εκ νέου στη διεύθυνση προορισμού και διαγραφή μηνυμάτων. Οι διαχειριστές συστήματος μπορούν να εργαστούν με αυτήν την υπηρεσία μέσω της διεπαφής ιστού τους.
  • στις ρυθμίσεις του μεσίτη, πρέπει να προσαρμόσετε τον αριθμό των επαναλήψεων παράδοσης και τις καθυστερήσεις μεταξύ των παραδόσεων, προκειμένου να μειώσετε την πιθανότητα εισόδου μηνυμάτων στο DLQ (είναι σχεδόν αδύνατο να υπολογίσετε τις βέλτιστες παραμέτρους, αλλά μπορείτε να ενεργήσετε εμπειρικά και να τις προσαρμόσετε κατά τη διάρκεια λειτουργία);
  • ο χώρος αποθήκευσης DLQ θα πρέπει να παρακολουθείται συνεχώς και το σύστημα παρακολούθησης θα πρέπει να ειδοποιεί τους διαχειριστές του συστήματος, ώστε να μπορούν να ανταποκρίνονται όσο το δυνατόν γρηγορότερα όταν εμφανίζονται μηνύματα που δεν έχουν παραδοθεί. Αυτό θα μειώσει τη "ζώνη ζημιάς" μιας αποτυχίας ή σφάλματος επιχειρησιακής λογικής.
  • ο δίαυλος ενοποίησης δεν πρέπει να είναι ευαίσθητος στην προσωρινή απουσία εφαρμογών: οι συνδρομές θεμάτων πρέπει να είναι ανθεκτικές και το όνομα τομέα της εφαρμογής πρέπει να είναι μοναδικό, ώστε κάποιος άλλος να μην προσπαθεί να επεξεργαστεί το μήνυμά του από την ουρά κατά τη διάρκεια της απουσίας της εφαρμογής.

Διασφάλιση της ασφάλειας νημάτων της επιχειρηματικής λογικής

Η ίδια περίπτωση μιας επιχειρηματικής διαδικασίας μπορεί να λάβει πολλά μηνύματα και συμβάντα ταυτόχρονα, η επεξεργασία των οποίων θα ξεκινήσει παράλληλα. Ταυτόχρονα, για έναν προγραμματιστή εφαρμογών, όλα θα πρέπει να είναι απλά και ασφαλή για το νήμα.

Η επιχειρησιακή λογική της διαδικασίας επεξεργάζεται κάθε εξωτερικό γεγονός που επηρεάζει αυτήν την επιχειρηματική διαδικασία ξεχωριστά. Αυτά τα γεγονότα μπορεί να είναι:

  • εκκίνηση μιας παρουσίας επιχειρηματικής διαδικασίας·
  • μια ενέργεια χρήστη που σχετίζεται με μια δραστηριότητα σε μια επιχειρηματική διαδικασία·
  • λήψη ενός μηνύματος ή σήματος στο οποίο έχει εγγραφεί μια παρουσία επιχειρηματικής διαδικασίας·
  • λήξη του χρονοδιακόπτη που έχει οριστεί από την περίπτωση της επιχειρηματικής διαδικασίας·
  • ενέργεια ελέγχου μέσω API (π.χ. ακύρωση διαδικασίας).

Κάθε τέτοιο συμβάν μπορεί να αλλάξει την κατάσταση μιας παρουσίας επιχειρηματικής διαδικασίας: ορισμένες δραστηριότητες μπορούν να τελειώσουν και άλλες να ξεκινήσουν, οι αξίες των μόνιμων ιδιοτήτων μπορούν να αλλάξουν. Το κλείσιμο οποιασδήποτε δραστηριότητας μπορεί να έχει ως αποτέλεσμα την ενεργοποίηση μιας ή περισσότερων από τις ακόλουθες δραστηριότητες. Αυτοί, με τη σειρά τους, μπορούν να σταματήσουν να περιμένουν άλλα συμβάντα ή, εάν δεν χρειάζονται πρόσθετα δεδομένα, μπορούν να ολοκληρώσουν την ίδια συναλλαγή. Πριν από το κλείσιμο της συναλλαγής, η νέα κατάσταση της επιχειρηματικής διαδικασίας αποθηκεύεται στη βάση δεδομένων, όπου θα περιμένει το επόμενο εξωτερικό συμβάν.

Τα μόνιμα δεδομένα επιχειρηματικής διαδικασίας που είναι αποθηκευμένα σε μια σχεσιακή βάση δεδομένων είναι ένα πολύ βολικό σημείο συγχρονισμού επεξεργασίας κατά τη χρήση του SELECT FOR UPDATE. Εάν μια συναλλαγή κατάφερε να πάρει την κατάσταση της επιχειρηματικής διαδικασίας από τη βάση δεδομένων για να την αλλάξει, τότε καμία άλλη παράλληλη συναλλαγή δεν θα μπορεί να πάρει την ίδια κατάσταση για άλλη αλλαγή και μετά την ολοκλήρωση της πρώτης συναλλαγής, η δεύτερη είναι εγγυημένα θα λάβει την ήδη αλλαγμένη κατάσταση.

Χρησιμοποιώντας απαισιόδοξες κλειδαριές στην πλευρά του DBMS, εκπληρώνουμε όλες τις απαραίτητες απαιτήσεις ΟΞΥκαι διατηρούν επίσης τη δυνατότητα κλιμάκωσης της εφαρμογής με επιχειρηματική λογική, αυξάνοντας τον αριθμό των εμφανίσεων που εκτελούνται.

Ωστόσο, τα απαισιόδοξα λουκέτα μας απειλούν με αδιέξοδα, πράγμα που σημαίνει ότι η ΕΠΙΛΟΓΗ ΓΙΑ ΕΝΗΜΕΡΩΣΗ θα πρέπει ακόμα να περιορίζεται σε κάποιο εύλογο χρονικό όριο σε περίπτωση αδιεξόδου σε ορισμένες κραυγαλέες υποθέσεις της επιχειρηματικής λογικής.

Ένα άλλο πρόβλημα είναι ο συγχρονισμός της έναρξης της επιχειρηματικής διαδικασίας. Αν και δεν υπάρχει περίπτωση επιχειρηματικής διαδικασίας, δεν υπάρχει κατάσταση στη βάση δεδομένων, επομένως η περιγραφόμενη μέθοδος δεν θα λειτουργήσει. Εάν θέλετε να διασφαλίσετε τη μοναδικότητα μιας παρουσίας επιχειρηματικής διαδικασίας σε ένα συγκεκριμένο εύρος, τότε χρειάζεστε κάποιο είδος αντικειμένου συγχρονισμού που σχετίζεται με την κλάση διεργασίας και το αντίστοιχο εύρος. Για να λύσουμε αυτό το πρόβλημα, χρησιμοποιούμε έναν διαφορετικό μηχανισμό κλειδώματος που μας επιτρέπει να κλειδώνουμε έναν αυθαίρετο πόρο που καθορίζεται από ένα κλειδί σε μορφή URI μέσω μιας εξωτερικής υπηρεσίας.

Στα παραδείγματά μας, η επιχειρηματική διαδικασία InitialPlayer περιέχει μια δήλωση

uniqueConstraint = UniqueConstraints.singleton

Επομένως, το αρχείο καταγραφής περιέχει μηνύματα σχετικά με τη λήψη και την απελευθέρωση της κλειδαριάς του αντίστοιχου κλειδιού. Δεν υπάρχουν τέτοια μηνύματα για άλλες επιχειρηματικές διαδικασίες: το uniqueConstraint δεν έχει οριστεί.

Προβλήματα επιχειρηματικής διαδικασίας με επίμονη κατάσταση

Μερικές φορές το να έχεις μια επίμονη κατάσταση όχι μόνο βοηθά, αλλά και εμποδίζει πραγματικά την ανάπτυξη.
Τα προβλήματα ξεκινούν όταν πρέπει να κάνετε αλλαγές στην επιχειρηματική λογική ή/και στο μοντέλο επιχειρηματικής διαδικασίας. Καμία τέτοια αλλαγή δεν βρέθηκε να είναι συμβατή με την παλιά κατάσταση των επιχειρηματικών διαδικασιών. Εάν υπάρχουν πολλές "ζωντανές" περιπτώσεις στη βάση δεδομένων, τότε η πραγματοποίηση μη συμβατών αλλαγών μπορεί να προκαλέσει πολλά προβλήματα, τα οποία συναντούσαμε συχνά όταν χρησιμοποιούμε το jBPM.

Ανάλογα με το βάθος της αλλαγής, μπορείτε να ενεργήσετε με δύο τρόπους:

  1. δημιουργήστε έναν νέο τύπο επιχειρηματικής διαδικασίας για να μην κάνετε ασύμβατες αλλαγές στον παλιό και χρησιμοποιήστε τον αντί του παλιού κατά την έναρξη νέων παρουσιών. Οι παλιές περιπτώσεις θα συνεχίσουν να λειτουργούν "με τον παλιό τρόπο".
  2. μεταφέρουν τη μόνιμη κατάσταση των επιχειρηματικών διαδικασιών κατά την ενημέρωση της επιχειρηματικής λογικής.

Ο πρώτος τρόπος είναι απλούστερος, αλλά έχει τους περιορισμούς και τα μειονεκτήματά του, για παράδειγμα:

  • επικάλυψη της επιχειρηματικής λογικής σε πολλά μοντέλα επιχειρηματικών διαδικασιών, αύξηση του όγκου της επιχειρηματικής λογικής.
  • συχνά απαιτείται μια άμεση μετάβαση σε μια νέα επιχειρηματική λογική (σχεδόν πάντα όσον αφορά τα καθήκοντα ολοκλήρωσης).
  • ο προγραμματιστής δεν γνωρίζει σε ποιο σημείο είναι δυνατό να διαγράψει παρωχημένα μοντέλα.

Στην πράξη, χρησιμοποιούμε και τις δύο προσεγγίσεις, αλλά έχουμε λάβει ορισμένες αποφάσεις για να απλοποιήσουμε τη ζωή μας:

  • στη βάση δεδομένων, η μόνιμη κατάσταση της επιχειρηματικής διαδικασίας αποθηκεύεται σε μια εύκολα αναγνώσιμη και εύκολα επεξεργαζόμενη μορφή: σε μια συμβολοσειρά μορφής JSON. Αυτό σας επιτρέπει να πραγματοποιείτε μετεγκαταστάσεις τόσο εντός όσο και εκτός της εφαρμογής. Σε ακραίες περιπτώσεις, μπορείτε επίσης να το τροποποιήσετε με λαβές (ιδιαίτερα χρήσιμο στην ανάπτυξη κατά την αποσφαλμάτωση).
  • η επιχειρηματική λογική ενοποίησης δεν χρησιμοποιεί τα ονόματα των επιχειρηματικών διαδικασιών, έτσι ώστε ανά πάσα στιγμή να είναι δυνατή η αντικατάσταση της υλοποίησης μιας από τις συμμετέχουσες διεργασίες με μια νέα, με ένα νέο όνομα (για παράδειγμα, "InitialPlayerV2"). Η δέσμευση γίνεται μέσω των ονομάτων των μηνυμάτων και των σημάτων.
  • το μοντέλο διεργασίας έχει έναν αριθμό έκδοσης, τον οποίο αυξάνουμε εάν κάνουμε μη συμβατές αλλαγές σε αυτό το μοντέλο και αυτός ο αριθμός αποθηκεύεται μαζί με την κατάσταση της παρουσίας διεργασίας.
  • η μόνιμη κατάσταση της διαδικασίας διαβάζεται από τη βάση πρώτα σε ένα βολικό μοντέλο αντικειμένου με το οποίο μπορεί να λειτουργήσει η διαδικασία μετεγκατάστασης εάν έχει αλλάξει ο αριθμός έκδοσης του μοντέλου.
  • η διαδικασία μετεγκατάστασης τοποθετείται δίπλα στην επιχειρηματική λογική και ονομάζεται "τεμπέλης" για κάθε περίπτωση της επιχειρηματικής διαδικασίας τη στιγμή της επαναφοράς της από τη βάση δεδομένων.
  • Εάν πρέπει να μετεγκαταστήσετε την κατάσταση όλων των παρουσιών διεργασίας γρήγορα και συγχρονισμένα, χρησιμοποιούνται πιο κλασικές λύσεις μετεγκατάστασης βάσης δεδομένων, αλλά πρέπει να εργαστείτε με JSON εκεί.

Χρειάζομαι άλλο πλαίσιο για τις επιχειρηματικές διαδικασίες;

Οι λύσεις που περιγράφονται στο άρθρο μας επέτρεψαν να απλοποιήσουμε σημαντικά τη ζωή μας, να επεκτείνουμε το φάσμα των θεμάτων που επιλύονται σε επίπεδο ανάπτυξης εφαρμογών και να κάνουμε πιο ελκυστική την ιδέα του διαχωρισμού της επιχειρηματικής λογικής σε μικροϋπηρεσίες. Για αυτό, έχει γίνει πολλή δουλειά, έχει δημιουργηθεί ένα πολύ «ελαφρύ» πλαίσιο για επιχειρηματικές διαδικασίες, καθώς και στοιχεία υπηρεσιών για την επίλυση των προβλημάτων που έχουν εντοπιστεί στο πλαίσιο ενός ευρέος φάσματος εφαρμοζόμενων εργασιών. Έχουμε την επιθυμία να μοιραστούμε αυτά τα αποτελέσματα, να φέρουμε την ανάπτυξη κοινών στοιχείων σε ανοιχτή πρόσβαση με δωρεάν άδεια. Αυτό θα απαιτήσει λίγη προσπάθεια και χρόνο. Η κατανόηση της ζήτησης για τέτοιες λύσεις θα μπορούσε να είναι ένα επιπλέον κίνητρο για εμάς. Στο προτεινόμενο άρθρο, δίνεται πολύ λίγη προσοχή στις δυνατότητες του ίδιου του πλαισίου, αλλά μερικές από αυτές είναι ορατές από τα παραδείγματα που παρουσιάζονται. Εάν παρόλα αυτά δημοσιεύσουμε το πλαίσιο μας, θα αφιερωθεί ένα ξεχωριστό άρθρο σε αυτό. Εν τω μεταξύ, θα είμαστε ευγνώμονες εάν αφήσετε ένα μικρό σχόλιο απαντώντας στην ερώτηση:

Μόνο εγγεγραμμένοι χρήστες μπορούν να συμμετάσχουν στην έρευνα. Συνδεθείτε, Σας παρακαλούμε.

Χρειάζομαι άλλο πλαίσιο για τις επιχειρηματικές διαδικασίες;

  • 18,8%Ναι, έψαχνα πολύ καιρό για κάτι τέτοιο.

  • 12,5%είναι ενδιαφέρον να μάθετε περισσότερα για την εφαρμογή σας, μπορεί να είναι χρήσιμο2

  • 6,2%χρησιμοποιούμε ένα από τα υπάρχοντα πλαίσια, αλλά σκεφτόμαστε να το αντικαταστήσουμε1

  • 18,8%χρησιμοποιούμε ένα από τα υπάρχοντα πλαίσια, όλα ταιριάζουν3

  • 18,8%αντιμετώπιση χωρίς πλαίσιο3

  • 25,0%γράψτε το δικό σας4

Ψήφισαν 16 χρήστες. 7 χρήστες απείχαν.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο