การรวมสไตล์ BPM

การรวมสไตล์ BPM

สวัสดี Habr!

บริษัทของเราเชี่ยวชาญในการพัฒนาโซลูชันซอฟต์แวร์ระดับ ERP ซึ่งส่วนใหญ่ถูกครอบครองโดยระบบธุรกรรมที่มีตรรกะทางธุรกิจจำนวนมากและการไหลของเอกสารและ EDMS ผลิตภัณฑ์ของเราในเวอร์ชันปัจจุบันใช้เทคโนโลยี JavaEE แต่เรากำลังทดลองไมโครเซอร์วิสอย่างจริงจังเช่นกัน หนึ่งในพื้นที่ที่มีปัญหามากที่สุดของการแก้ปัญหาดังกล่าวคือการบูรณาการระบบย่อยต่างๆ ที่เป็นของโดเมนที่อยู่ติดกัน ปัญหาการรวมระบบทำให้เราปวดหัวอย่างมากเสมอ ไม่ว่ารูปแบบสถาปัตยกรรม กลุ่มเทคโนโลยี และเฟรมเวิร์กที่เราใช้จะเป็นอย่างไรก็ตาม แต่เมื่อเร็วๆ นี้ มีความคืบหน้าในการแก้ไขปัญหาดังกล่าว

ในบทความที่ฉันแจ้งให้คุณทราบ ฉันจะพูดถึงประสบการณ์และการวิจัยทางสถาปัตยกรรมที่ NPO Krista มีในพื้นที่ที่กำหนด นอกจากนี้เรายังจะดูตัวอย่างวิธีแก้ปัญหาง่ายๆ สำหรับปัญหาการรวมจากมุมมองของนักพัฒนาแอปพลิเคชัน และค้นหาสิ่งที่ซ่อนอยู่เบื้องหลังความเรียบง่ายนี้

ข้อจำกัดความรับผิดชอบ

ฉันเสนอวิธีแก้ปัญหาทางสถาปัตยกรรมและทางเทคนิคที่อธิบายไว้ในบทความนี้โดยอาศัยประสบการณ์ส่วนตัวในบริบทของงานเฉพาะ โซลูชันเหล่านี้ไม่ได้อ้างว่าเป็นแบบสากลและอาจไม่เหมาะสมภายใต้เงื่อนไขการใช้งานอื่นๆ

BPM เกี่ยวอะไรกับเรื่องนี้?

เพื่อตอบคำถามนี้ เราต้องเจาะลึกลงไปอีกเล็กน้อยถึงปัญหาเฉพาะที่ประยุกต์ในแนวทางแก้ไขของเรา ส่วนหลักของตรรกะทางธุรกิจในระบบธุรกรรมทั่วไปของเราคือการป้อนข้อมูลลงในฐานข้อมูลผ่านอินเทอร์เฟซผู้ใช้ การตรวจสอบข้อมูลนี้ด้วยตนเองและแบบอัตโนมัติ ดำเนินการผ่านขั้นตอนการทำงานบางอย่าง การเผยแพร่ไปยังระบบอื่น / ฐานข้อมูลเชิงวิเคราะห์ / เก็บถาวร การสร้างรายงาน . ดังนั้นหน้าที่หลักของระบบสำหรับลูกค้าคือการทำให้กระบวนการทางธุรกิจภายในเป็นอัตโนมัติ

เพื่อความสะดวก เราใช้คำว่า "เอกสาร" ในการสื่อสารในฐานะที่เป็นนามธรรมของชุดข้อมูลที่รวมกันด้วยคีย์ทั่วไปซึ่งสามารถ "เชื่อมโยง" เวิร์กโฟลว์บางอย่างได้
แต่ตรรกะในการบูรณาการล่ะ? ท้ายที่สุดแล้ว งานบูรณาการถูกสร้างขึ้นโดยสถาปัตยกรรมของระบบซึ่ง "ตัด" ออกเป็นส่วน ๆ ที่ไม่ได้ตามคำขอของลูกค้า แต่อยู่ภายใต้อิทธิพลของปัจจัยที่แตกต่างกันโดยสิ้นเชิง:

  • ขึ้นอยู่กับกฎหมายของคอนเวย์
  • อันเป็นผลมาจากการนำระบบย่อยที่พัฒนาก่อนหน้านี้กลับมาใช้ซ้ำกับผลิตภัณฑ์อื่น
  • ขึ้นอยู่กับดุลยพินิจของสถาปนิก ตามความต้องการที่ไม่เกี่ยวกับการใช้งาน

มีสิ่งล่อใจอย่างมากที่จะแยกตรรกะการรวมออกจากตรรกะทางธุรกิจของเวิร์กโฟลว์หลัก เพื่อที่จะไม่ทำให้ตรรกะทางธุรกิจเสียหายด้วยสิ่งประดิษฐ์การรวม และช่วยให้นักพัฒนาแอปพลิเคชันไม่ต้องเจาะลึกคุณลักษณะของภูมิทัศน์ทางสถาปัตยกรรมของระบบ วิธีนี้มีข้อดีหลายประการ แต่การฝึกฝนแสดงให้เห็นว่าไม่ได้ผล:

  • การแก้ปัญหาการรวมมักจะกลับไปใช้ตัวเลือกที่ง่ายที่สุดในรูปแบบของการโทรแบบซิงโครนัสเนื่องจากจุดขยายที่จำกัดในการใช้งานเวิร์กโฟลว์หลัก (ข้อเสียของการรวมแบบซิงโครนัสจะกล่าวถึงด้านล่าง)
  • สิ่งประดิษฐ์บูรณาการยังคงเจาะลึกตรรกะทางธุรกิจหลักเมื่อจำเป็นต้องมีข้อเสนอแนะจากระบบย่อยอื่น
  • ผู้พัฒนาแอปพลิเคชันเพิกเฉยต่อการรวมระบบและสามารถทำลายมันได้อย่างง่ายดายโดยการเปลี่ยนเวิร์กโฟลว์
  • จากมุมมองของผู้ใช้ ระบบจะยุติการเป็นหนึ่งเดียว "รอยต่อ" ระหว่างระบบย่อยจะสังเกตเห็นได้ชัดเจน และการดำเนินการของผู้ใช้ที่ซ้ำซ้อนปรากฏขึ้น เริ่มต้นการถ่ายโอนข้อมูลจากระบบย่อยหนึ่งไปยังอีกระบบหนึ่ง

อีกวิธีหนึ่งคือการพิจารณาการโต้ตอบแบบรวมเป็นส่วนหนึ่งของตรรกะทางธุรกิจหลักและขั้นตอนการทำงาน เพื่อป้องกันไม่ให้คุณสมบัติของนักพัฒนาแอปพลิเคชันพุ่งสูงขึ้น การสร้างการโต้ตอบการรวมใหม่ควรเป็นเรื่องง่ายและไม่ต้องใช้ความพยายาม โดยมีโอกาสเลือกโซลูชันน้อยที่สุด สิ่งนี้ทำได้ยากกว่าที่คิด: เครื่องมือจะต้องทรงพลังพอที่จะให้ทางเลือกที่หลากหลายแก่ผู้ใช้ในการใช้งาน โดยไม่ยอมให้เขา "ยิงเท้าตัวเอง" มีคำถามมากมายที่วิศวกรต้องตอบในบริบทของงานบูรณาการ แต่นักพัฒนาแอปพลิเคชันไม่ควรคำนึงถึงในงานประจำวันของเขา: ขอบเขตของธุรกรรม ความสม่ำเสมอ อะตอมมิกซิตี ความปลอดภัย การปรับขนาด โหลดและการกระจายทรัพยากร การกำหนดเส้นทาง การจัดกลุ่ม การกระจายและการสลับบริบท ฯลฯ จำเป็นต้องเสนอเทมเพลตโซลูชันที่ค่อนข้างง่ายให้กับนักพัฒนาแอปพลิเคชันซึ่งคำตอบสำหรับคำถามดังกล่าวทั้งหมดถูกซ่อนไว้แล้ว เทมเพลตเหล่านี้จะต้องค่อนข้างปลอดภัย: ตรรกะทางธุรกิจเปลี่ยนแปลงบ่อยมาก ซึ่งเพิ่มความเสี่ยงที่จะเกิดข้อผิดพลาด ต้นทุนของข้อผิดพลาดจะต้องอยู่ในระดับที่ค่อนข้างต่ำ

แต่ BPM เกี่ยวอะไรกับมัน? มีตัวเลือกมากมายสำหรับการนำเวิร์กโฟลว์ไปใช้...
อันที่จริงการนำกระบวนการทางธุรกิจไปใช้อีกอย่างหนึ่งนั้นได้รับความนิยมอย่างมากในโซลูชันของเรา - ผ่านคำจำกัดความที่ประกาศของแผนภาพการเปลี่ยนสถานะและการเชื่อมต่อของผู้จัดการกับตรรกะทางธุรกิจสำหรับการเปลี่ยน ในกรณีนี้ สถานะที่กำหนดตำแหน่งปัจจุบันของ "เอกสาร" ในกระบวนการทางธุรกิจคือคุณลักษณะของ "เอกสาร" เอง

การรวมสไตล์ BPM
นี่คือลักษณะของกระบวนการเมื่อเริ่มต้นโครงการ

ความนิยมของการนำไปใช้นี้เกิดจากความเรียบง่ายและความเร็วของการสร้างกระบวนการทางธุรกิจเชิงเส้น อย่างไรก็ตาม เนื่องจากระบบซอฟต์แวร์มีความซับซ้อนมากขึ้นอย่างต่อเนื่อง ส่วนอัตโนมัติของกระบวนการทางธุรกิจจึงเติบโตและซับซ้อนมากขึ้น มีความจำเป็นในการสลายตัว การนำส่วนต่างๆ ของกระบวนการกลับมาใช้ใหม่ รวมถึงกระบวนการแยกสาขาเพื่อให้แต่ละสาขาดำเนินการแบบขนาน ภายใต้เงื่อนไขดังกล่าว เครื่องมือจะไม่สะดวก และไดอะแกรมการเปลี่ยนสถานะจะสูญเสียเนื้อหาข้อมูล (การโต้ตอบการรวมระบบจะไม่สะท้อนให้เห็นในไดอะแกรมเลย)

การรวมสไตล์ BPM
นี่คือลักษณะของกระบวนการหลังจากการชี้แจงข้อกำหนดหลายครั้ง

ทางออกของสถานการณ์นี้คือการรวมเครื่องยนต์เข้าด้วยกัน เจบีพีเอ็ม เข้าสู่ผลิตภัณฑ์บางประเภทที่มีกระบวนการทางธุรกิจที่ซับซ้อนที่สุด ในระยะสั้นโซลูชันนี้ประสบความสำเร็จ: มีความเป็นไปได้ที่จะใช้กระบวนการทางธุรกิจที่ซับซ้อนในขณะที่ยังคงรักษาแผนภาพที่ให้ข้อมูลและเกี่ยวข้องอย่างเป็นธรรมในสัญกรณ์ บีพีเอ็มเอ็น2.

การรวมสไตล์ BPM
ส่วนเล็กๆ ของกระบวนการทางธุรกิจที่ซับซ้อน

ในระยะยาว โซลูชันไม่เป็นไปตามความคาดหวัง: ความเข้มข้นของแรงงานที่สูงในการสร้างกระบวนการทางธุรกิจผ่านเครื่องมือแบบภาพไม่อนุญาตให้บรรลุตัวบ่งชี้ประสิทธิภาพการผลิตที่ยอมรับได้ และเครื่องมือเองก็กลายเป็นหนึ่งในเครื่องมือที่นักพัฒนาไม่ชอบมากที่สุด นอกจากนี้ยังมีข้อร้องเรียนเกี่ยวกับโครงสร้างภายในของเครื่องยนต์ซึ่งนำไปสู่การปรากฏของ "แผ่นแปะ" และ "ไม้ค้ำยัน" จำนวนมาก

ด้านบวกที่สำคัญของการใช้ jBPM คือการตระหนักถึงประโยชน์และผลเสียของการมีสถานะคงอยู่ของอินสแตนซ์กระบวนการธุรกิจของตัวเอง นอกจากนี้เรายังเห็นความเป็นไปได้ของการใช้วิธีการแบบกระบวนการเพื่อใช้โปรโตคอลการรวมที่ซับซ้อนระหว่างแอปพลิเคชันต่างๆ โดยใช้การโต้ตอบแบบอะซิงโครนัสผ่านสัญญาณและข้อความ การมีอยู่ของสภาวะถาวรมีบทบาทสำคัญในเรื่องนี้

จากข้อมูลข้างต้น เราสามารถสรุปได้: แนวทางกระบวนการในรูปแบบ BPM ช่วยให้เราสามารถแก้ไขงานต่างๆ มากมายเพื่อทำให้กระบวนการทางธุรกิจที่ซับซ้อนมากขึ้นเป็นไปโดยอัตโนมัติ ผสมผสานกิจกรรมบูรณาการเข้ากับกระบวนการเหล่านี้ได้อย่างกลมกลืน และรักษาความสามารถในการแสดงกระบวนการที่นำไปใช้ด้วยสายตาในรูปแบบที่เหมาะสม

ข้อเสียของการโทรแบบซิงโครนัสเป็นรูปแบบการรวม

การรวมแบบซิงโครนัสหมายถึงการบล็อกการโทรที่ง่ายที่สุด ระบบย่อยหนึ่งทำหน้าที่เป็นฝั่งเซิร์ฟเวอร์และเปิดเผย API ด้วยวิธีที่จำเป็น ระบบย่อยอื่นทำหน้าที่เป็นฝั่งไคลเอ็นต์และทำการโทรและรอผลลัพธ์ในเวลาที่เหมาะสม ขึ้นอยู่กับสถาปัตยกรรมระบบ ฝั่งไคลเอ็นต์และเซิร์ฟเวอร์สามารถอยู่ในแอปพลิเคชันและกระบวนการเดียวกัน หรือในแอปพลิเคชันและกระบวนการที่ต่างกัน ในกรณีที่สอง คุณจะต้องใช้ RPC บางส่วนและจัดเตรียมพารามิเตอร์และผลลัพธ์ของการโทร

การรวมสไตล์ BPM

รูปแบบการรวมนี้มีข้อเสียค่อนข้างมาก แต่ในทางปฏิบัติก็ใช้กันอย่างแพร่หลายมากเนื่องจากความเรียบง่าย ความเร็วของการติดตั้งใช้งานดึงดูดใจและบังคับให้คุณใช้งานซ้ำแล้วซ้ำอีกเมื่อต้องเผชิญกับกำหนดเวลาที่เร่งด่วน โดยบันทึกโซลูชันว่าเป็นหนี้ทางเทคนิค แต่มันก็เกิดขึ้นเช่นกันที่นักพัฒนาที่ไม่มีประสบการณ์ใช้มันโดยไม่รู้ตัว เพียงแต่ไม่ตระหนักถึงผลเสียที่ตามมา

นอกเหนือจากการเพิ่มขึ้นที่ชัดเจนที่สุดในการเชื่อมต่อระบบย่อยแล้ว ยังมีปัญหาที่ชัดเจนน้อยลงเกี่ยวกับธุรกรรมที่ "เติบโต" และ "ขยายออกไป" แท้จริงแล้ว หากตรรกะทางธุรกิจทำการเปลี่ยนแปลง ธุรกรรมก็ไม่สามารถหลีกเลี่ยงได้ และในทางกลับกัน ธุรกรรมก็จะบล็อกทรัพยากรแอปพลิเคชันบางตัวที่ได้รับผลกระทบจากการเปลี่ยนแปลงเหล่านี้ นั่นคือจนกว่าระบบย่อยหนึ่งจะรอการตอบสนองจากอีกระบบหนึ่ง ระบบจะไม่สามารถทำธุรกรรมให้เสร็จสิ้นและถอดการล็อคออกได้ สิ่งนี้จะเพิ่มความเสี่ยงของผลกระทบหลายประการอย่างมาก:

  • การตอบสนองของระบบหายไป ผู้ใช้รอเป็นเวลานานในการตอบคำถาม
  • โดยทั่วไปเซิร์ฟเวอร์จะหยุดตอบสนองต่อคำขอของผู้ใช้เนื่องจากมีเธรดพูลหนาแน่นเกินไป: เธรดส่วนใหญ่ถูกล็อคบนทรัพยากรที่ถูกครอบครองโดยธุรกรรม
  • การหยุดชะงักเริ่มปรากฏขึ้น: โอกาสที่จะเกิดขึ้นนั้นขึ้นอยู่กับระยะเวลาของธุรกรรม จำนวนตรรกะทางธุรกิจ และการล็อกที่เกี่ยวข้องกับธุรกรรม
  • ข้อผิดพลาดการหมดเวลาของธุรกรรมปรากฏขึ้น
  • เซิร์ฟเวอร์จะ “ล้มเหลว” ด้วย OutOfMemory หากงานนั้นต้องการการประมวลผลและการเปลี่ยนแปลงข้อมูลจำนวนมาก และการมีอยู่ของการรวมแบบซิงโครนัสทำให้ยากต่อการแยกการประมวลผลเป็นธุรกรรมที่ “เบากว่า”

จากมุมมองทางสถาปัตยกรรม การใช้การบล็อกการโทรระหว่างการรวมทำให้สูญเสียการควบคุมคุณภาพของระบบย่อยแต่ละระบบ: เป็นไปไม่ได้ที่จะรับประกันตัวบ่งชี้คุณภาพเป้าหมายของระบบย่อยหนึ่งโดยแยกจากตัวบ่งชี้คุณภาพของระบบย่อยอื่น หากระบบย่อยได้รับการพัฒนาโดยทีมที่แตกต่างกัน นี่ถือเป็นปัญหาใหญ่

สิ่งต่างๆ จะน่าสนใจยิ่งขึ้นหากระบบย่อยที่ถูกรวมอยู่ในแอปพลิเคชันที่แตกต่างกัน และคุณจำเป็นต้องทำการเปลี่ยนแปลงแบบซิงโครนัสทั้งสองด้าน จะมั่นใจในการทำธุรกรรมของการเปลี่ยนแปลงเหล่านี้ได้อย่างไร?

หากมีการเปลี่ยนแปลงในธุรกรรมที่แยกจากกัน คุณจะต้องจัดเตรียมการจัดการข้อยกเว้นและการชดเชยที่เชื่อถือได้ และสิ่งนี้จะขจัดประโยชน์หลักของการรวมระบบแบบซิงโครนัสโดยสิ้นเชิง นั่นก็คือความเรียบง่าย

ธุรกรรมแบบกระจายก็คำนึงถึงเช่นกัน แต่เราไม่ได้ใช้ธุรกรรมเหล่านี้ในโซลูชันของเรา: เป็นการยากที่จะรับรองความน่าเชื่อถือ

“Saga” ทางออกของปัญหาการทำธุรกรรม

ด้วยความนิยมที่เพิ่มขึ้นของไมโครเซอร์วิส จึงมีความต้องการ ลายซากะ.

รูปแบบนี้แก้ปัญหาที่กล่าวมาข้างต้นของการทำธุรกรรมที่ยาวนานได้อย่างสมบูรณ์แบบ และยังขยายความสามารถในการจัดการสถานะของระบบจากด้านข้างของตรรกะทางธุรกิจ: การชดเชยหลังจากธุรกรรมที่ล้มเหลวอาจไม่ทำให้ระบบกลับสู่สถานะเดิม แต่ให้ เส้นทางการประมวลผลข้อมูลทางเลือก นอกจากนี้ยังช่วยให้คุณหลีกเลี่ยงการทำซ้ำขั้นตอนการประมวลผลข้อมูลที่เสร็จสมบูรณ์เมื่อพยายามทำให้กระบวนการจบลงที่ "ดี"

สิ่งที่น่าสนใจคือ ในระบบเสาหิน รูปแบบนี้ยังมีความเกี่ยวข้องเมื่อกล่าวถึงการรวมระบบย่อยที่เชื่อมต่อกันอย่างหลวมๆ และผลกระทบด้านลบที่เกิดจากธุรกรรมที่ใช้เวลานานและการล็อคทรัพยากรที่เกี่ยวข้อง

ในส่วนที่เกี่ยวข้องกับกระบวนการทางธุรกิจของเราในรูปแบบ BPM ปรากฏว่าการนำ "Saga" ไปใช้นั้นง่ายมาก: แต่ละขั้นตอนของ "Saga" สามารถระบุเป็นกิจกรรมภายในกระบวนการทางธุรกิจได้ และสถานะที่คงอยู่ของกระบวนการทางธุรกิจด้วย กำหนดสถานะภายในของ "Saga" นั่นคือเราไม่จำเป็นต้องมีกลไกการประสานงานเพิ่มเติม สิ่งที่คุณต้องมีคือนายหน้าข้อความที่รองรับการรับประกัน "อย่างน้อยหนึ่งครั้ง" เพื่อเป็นพาหนะ

แต่โซลูชันนี้มี "ราคา" ของตัวเองด้วย:

  • ตรรกะทางธุรกิจมีความซับซ้อนมากขึ้น: จำเป็นต้องมีการชดเชย
  • จำเป็นต้องละทิ้งความสอดคล้องอย่างสมบูรณ์ซึ่งอาจมีความอ่อนไหวเป็นพิเศษสำหรับระบบเสาหิน
  • สถาปัตยกรรมจะซับซ้อนขึ้นเล็กน้อย และความต้องการเพิ่มเติมสำหรับนายหน้าข้อความก็ปรากฏขึ้น
  • จำเป็นต้องมีเครื่องมือตรวจสอบและดูแลระบบเพิ่มเติม (แม้ว่าโดยทั่วไปแล้วจะดี: คุณภาพของบริการระบบจะเพิ่มขึ้น)

สำหรับระบบเสาหิน เหตุผลในการใช้ "Sag" ยังไม่ชัดเจนนัก สำหรับไมโครเซอร์วิสและ SOA อื่นๆ ซึ่งส่วนใหญ่แล้วจะมีนายหน้าอยู่แล้ว และสูญเสียความสม่ำเสมออย่างสมบูรณ์เมื่อเริ่มโครงการ ประโยชน์ของการใช้รูปแบบนี้อาจมีมากกว่าข้อเสียอย่างมาก โดยเฉพาะอย่างยิ่งหากมี API ที่สะดวกสบายในตรรกะทางธุรกิจ ระดับ.

การห่อหุ้มตรรกะทางธุรกิจในไมโครเซอร์วิส

เมื่อเราเริ่มทดลองใช้ไมโครเซอร์วิส มีคำถามที่สมเหตุสมผลเกิดขึ้น: จะวางตรรกะทางธุรกิจโดเมนไว้ที่ใดที่เกี่ยวข้องกับบริการที่รับประกันความคงอยู่ของข้อมูลโดเมน

เมื่อดูสถาปัตยกรรมของ BPMS ต่างๆ อาจดูสมเหตุสมผลที่จะแยกตรรกะทางธุรกิจออกจากความคงอยู่: สร้างเลเยอร์ของแพลตฟอร์มและไมโครเซอร์วิสที่ไม่ขึ้นกับโดเมนซึ่งสร้างสภาพแวดล้อมและคอนเทนเนอร์สำหรับการดำเนินการตรรกะทางธุรกิจของโดเมน และออกแบบการคงอยู่ของข้อมูลโดเมนเป็น เลเยอร์ไมโครเซอร์วิสที่เรียบง่ายและมีน้ำหนักเบาที่แยกจากกัน กระบวนการทางธุรกิจในกรณีนี้จะทำหน้าที่ประสานการบริการของเลเยอร์การคงอยู่

การรวมสไตล์ BPM

วิธีการนี้มีข้อได้เปรียบที่ยิ่งใหญ่มาก: คุณสามารถเพิ่มฟังก์ชันการทำงานของแพลตฟอร์มได้มากเท่าที่คุณต้องการ และมีเพียงไมโครเซอร์วิสแพลตฟอร์มที่เกี่ยวข้องเท่านั้นที่จะ "อ้วน" จากสิ่งนี้ กระบวนการทางธุรกิจจากโดเมนใดๆ จะสามารถใช้ฟังก์ชันการทำงานใหม่ของแพลตฟอร์มได้ทันทีที่มีการอัปเดต

การศึกษาโดยละเอียดเพิ่มเติมเผยให้เห็นข้อเสียที่สำคัญของแนวทางนี้:

  • บริการแพลตฟอร์มที่ดำเนินการตรรกะทางธุรกิจของหลายโดเมนพร้อมกันนั้นมีความเสี่ยงสูงเนื่องจากจุดล้มเหลวเพียงจุดเดียว การเปลี่ยนแปลงตรรกะทางธุรกิจบ่อยครั้งจะเพิ่มความเสี่ยงของข้อผิดพลาดที่นำไปสู่ความล้มเหลวทั่วทั้งระบบ
  • ปัญหาด้านประสิทธิภาพ: ตรรกะทางธุรกิจทำงานกับข้อมูลผ่านอินเทอร์เฟซที่แคบและช้า:
    • ข้อมูลจะถูกจัดเรียงและสูบผ่านสแต็กเครือข่ายอีกครั้ง
    • บริการโดเมนมักจะให้ข้อมูลมากกว่าที่จำเป็นสำหรับตรรกะทางธุรกิจในการประมวลผลเนื่องจากความสามารถไม่เพียงพอสำหรับการกำหนดพารามิเตอร์คำขอที่ระดับ API ภายนอกของบริการ
    • ตรรกะทางธุรกิจหลายชิ้นที่เป็นอิสระสามารถขอข้อมูลเดียวกันเพื่อประมวลผลซ้ำได้ (ปัญหานี้สามารถบรรเทาได้ด้วยการเพิ่มส่วนประกอบเซสชันที่แคชข้อมูล แต่สิ่งนี้จะทำให้สถาปัตยกรรมซับซ้อนยิ่งขึ้นและสร้างปัญหาเกี่ยวกับความเกี่ยวข้องของข้อมูลและแคชเป็นโมฆะ)
  • ปัญหาการทำธุรกรรม:
    • กระบวนการทางธุรกิจที่มีสถานะคงอยู่ซึ่งจัดเก็บโดยบริการแพลตฟอร์มไม่สอดคล้องกับข้อมูลโดเมน และไม่มีวิธีง่ายๆ ในการแก้ปัญหานี้
    • การบล็อกข้อมูลโดเมนนอกธุรกรรม: หากตรรกะทางธุรกิจของโดเมนจำเป็นต้องทำการเปลี่ยนแปลงหลังจากตรวจสอบความถูกต้องของข้อมูลปัจจุบันในครั้งแรก จำเป็นต้องยกเว้นความเป็นไปได้ของการเปลี่ยนแปลงทางการแข่งขันในข้อมูลที่ประมวลผล การบล็อกข้อมูลภายนอกสามารถช่วยแก้ปัญหาได้ แต่วิธีแก้ปัญหาดังกล่าวมีความเสี่ยงเพิ่มเติมและลดความน่าเชื่อถือโดยรวมของระบบ
  • ปัญหาเพิ่มเติมเมื่ออัปเดต: ในบางกรณี บริการคงอยู่และตรรกะทางธุรกิจจำเป็นต้องได้รับการอัปเดตพร้อมกันหรือตามลำดับที่เข้มงวด

ท้ายที่สุดแล้ว เราต้องกลับไปสู่พื้นฐาน: รวมข้อมูลโดเมนและตรรกะทางธุรกิจโดเมนไว้ในไมโครเซอร์วิสเดียว แนวทางนี้ช่วยลดความยุ่งยากในการรับรู้ไมโครเซอร์วิสซึ่งเป็นองค์ประกอบสำคัญของระบบ และไม่ก่อให้เกิดปัญหาข้างต้น สิ่งนี้ไม่ได้ให้ฟรีเช่นกัน:

  • จำเป็นต้องมีการกำหนดมาตรฐาน API สำหรับการโต้ตอบกับตรรกะทางธุรกิจ (โดยเฉพาะเพื่อให้กิจกรรมผู้ใช้เป็นส่วนหนึ่งของกระบวนการทางธุรกิจ) และบริการแพลตฟอร์ม API ต้องการความเอาใจใส่อย่างระมัดระวังมากขึ้นต่อการเปลี่ยนแปลง API ความเข้ากันได้แบบไปข้างหน้าและย้อนกลับ
  • จำเป็นต้องเพิ่มรันไทม์ไลบรารีเพิ่มเติมเพื่อให้แน่ใจว่าการทำงานของตรรกะทางธุรกิจเป็นส่วนหนึ่งของไมโครเซอร์วิสดังกล่าว และทำให้เกิดข้อกำหนดใหม่สำหรับไลบรารีดังกล่าว: ความเบาและการพึ่งพาสกรรมกริยาขั้นต่ำ
  • นักพัฒนาตรรกะทางธุรกิจจำเป็นต้องตรวจสอบเวอร์ชันของไลบรารี: หากไมโครเซอร์วิสไม่ได้รับการสรุปผลเป็นเวลานาน ก็มีแนวโน้มว่าจะมีไลบรารีเวอร์ชันที่ล้าสมัยอยู่ นี่อาจเป็นอุปสรรคที่ไม่คาดคิดในการเพิ่มคุณสมบัติใหม่ และอาจจำเป็นต้องย้ายตรรกะทางธุรกิจเก่าของบริการดังกล่าวไปยังไลบรารีเวอร์ชันใหม่ หากมีการเปลี่ยนแปลงที่เข้ากันไม่ได้ระหว่างเวอร์ชัน

การรวมสไตล์ BPM

เลเยอร์ของบริการแพลตฟอร์มยังปรากฏอยู่ในสถาปัตยกรรมดังกล่าว แต่เลเยอร์นี้ไม่ได้สร้างคอนเทนเนอร์สำหรับการดำเนินการตรรกะทางธุรกิจโดเมนอีกต่อไป แต่มีเพียงสภาพแวดล้อมเท่านั้นที่ให้ฟังก์ชัน "แพลตฟอร์ม" เสริม เลเยอร์ดังกล่าวมีความจำเป็นไม่เพียงแต่เพื่อรักษาลักษณะของไมโครเซอร์วิสโดเมนแบบเบาเท่านั้น แต่ยังรวมถึงการจัดการแบบรวมศูนย์ด้วย

ตัวอย่างเช่น กิจกรรมผู้ใช้ในกระบวนการทางธุรกิจจะสร้างงาน อย่างไรก็ตาม เมื่อทำงานกับงาน ผู้ใช้จะต้องดูงานจากโดเมนทั้งหมดในรายการทั่วไป ซึ่งหมายความว่าจะต้องมีบริการลงทะเบียนงานแพลตฟอร์มที่เกี่ยวข้อง โดยไม่มีตรรกะทางธุรกิจของโดเมน การรักษาการห่อหุ้มตรรกะทางธุรกิจในบริบทดังกล่าวค่อนข้างเป็นปัญหา และนี่คือการประนีประนอมอีกประการหนึ่งของสถาปัตยกรรมนี้

บูรณาการกระบวนการทางธุรกิจผ่านสายตาของนักพัฒนาแอปพลิเคชัน

ดังที่ได้กล่าวไว้ข้างต้น นักพัฒนาแอปพลิเคชันจะต้องถูกแยกออกจากคุณลักษณะด้านเทคนิคและวิศวกรรมของการนำการโต้ตอบของแอปพลิเคชันต่างๆ ไปใช้ เพื่อให้สามารถวางใจในประสิทธิภาพการพัฒนาที่ดีได้

เรามาลองแก้ปัญหาการรวมระบบที่ค่อนข้างยากซึ่งคิดค้นขึ้นเป็นพิเศษสำหรับบทความ นี่จะเป็นงาน "เกม" ที่เกี่ยวข้องกับสามแอปพลิเคชัน โดยแต่ละแอปพลิเคชันจะกำหนดชื่อโดเมนที่แน่นอน: "app1", "app2", "app3"

ภายในแต่ละแอปพลิเคชัน กระบวนการทางธุรกิจจะเปิดตัวโดยเริ่ม "เล่นบอล" ผ่านบัสบูรณาการ ข้อความชื่อ “บอล” จะทำหน้าที่เป็นลูกบอล

กฎของเกม:

  • ผู้เล่นคนแรกคือผู้ริเริ่ม เขาเชิญผู้เล่นคนอื่นเข้าร่วมเกม เริ่มเกม และสามารถจบเกมได้ตลอดเวลา
  • ผู้เล่นคนอื่นประกาศการมีส่วนร่วมในเกม "ทำความรู้จัก" ซึ่งกันและกันและเป็นผู้เล่นคนแรก
  • หลังจากได้รับลูกบอลแล้ว ผู้เล่นจะเลือกผู้เล่นอีกคนที่เข้าร่วมและส่งบอลให้เขา นับจำนวนการส่งทั้งหมด
  • ผู้เล่นแต่ละคนมี "พลังงาน" ซึ่งจะลดลงตามการส่งบอลแต่ละครั้งโดยผู้เล่นคนนั้น เมื่อพลังงานหมด ผู้เล่นจะออกจากเกม และประกาศลาออก
  • หากผู้เล่นถูกทิ้งให้อยู่ตามลำพังเขาจะประกาศการจากไปทันที
  • เมื่อผู้เล่นทั้งหมดถูกกำจัด ผู้เล่นคนแรกจะประกาศว่าเกมจบลง หากเขาออกจากเกมเร็ว เขายังคงติดตามเกมจนจบ

เพื่อแก้ไขปัญหานี้ ฉันจะใช้ 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

app2 จะรวมกระบวนการทางธุรกิจของผู้เล่นอื่น:

คลาส 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 เราจะสร้างผู้เล่นที่มีพฤติกรรมแตกต่างออกไปเล็กน้อย: แทนที่จะสุ่มเลือกผู้เล่นคนต่อไป เขาจะทำตามอัลกอริทึมแบบพบกันหมด:

คลาส 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}")
}

ไม่เช่นนั้นพฤติกรรมของผู้เล่นก็ไม่แตกต่างไปจากครั้งก่อน ดังนั้นแผนภาพจึงไม่เปลี่ยนแปลง

ตอนนี้เราจำเป็นต้องมีการทดสอบเพื่อดำเนินการทั้งหมดนี้ ฉันจะให้เฉพาะรหัสของการทดสอบเท่านั้นเพื่อไม่ให้บทความยุ่งเหยิงด้วยสำเร็จรูป (อันที่จริงฉันใช้สภาพแวดล้อมการทดสอบที่สร้างขึ้นก่อนหน้านี้เพื่อทดสอบการรวมกระบวนการทางธุรกิจอื่น ๆ ):

ทดสอบเกม()

@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

อย่างไรก็ตาม มีข้อละเอียดอ่อนอย่างหนึ่งที่นี่: แต่ละแอปพลิเคชันจะกรองข้อความ "ของมัน" จากคิวที่ทางเข้าตามชื่อโดเมน โดเมนยังสามารถระบุในสัญญาณได้ หากคุณต้องการจำกัด "ขอบเขตการมองเห็น" ของสัญญาณให้เหลือเพียงแอปพลิเคชันเดียว สิ่งนี้ควรเพิ่มปริมาณงานของบัส แต่ตรรกะทางธุรกิจต้องดำเนินการกับชื่อโดเมน: สำหรับการจัดการข้อความ - จำเป็น สำหรับสัญญาณ - เป็นที่ต้องการ

รับประกันความน่าเชื่อถือของ Integration Bus

ความน่าเชื่อถือประกอบด้วยหลายจุด:

  • นายหน้าข้อความที่เลือกเป็นองค์ประกอบที่สำคัญของสถาปัตยกรรมและเป็นจุดเดียวของความล้มเหลว: จะต้องทนทานต่อข้อผิดพลาดอย่างเพียงพอ คุณควรใช้เฉพาะการใช้งานที่ผ่านการทดสอบตามเวลา พร้อมการสนับสนุนที่ดีและชุมชนขนาดใหญ่
  • จำเป็นต้องตรวจสอบให้แน่ใจว่านายหน้าข้อความมีความพร้อมใช้งานสูง ซึ่งจะต้องแยกทางกายภาพออกจากแอปพลิเคชันแบบรวม (ความพร้อมใช้งานสูงของแอปพลิเคชันที่มีตรรกะทางธุรกิจที่ประยุกต์ใช้นั้นยากและมีราคาแพงกว่ามากในการรับรอง)
  • นายหน้ามีหน้าที่ต้องรับประกันการจัดส่ง "อย่างน้อยหนึ่งครั้ง" นี่เป็นข้อกำหนดบังคับสำหรับการทำงานที่เชื่อถือได้ของบัสรวม ไม่จำเป็นต้องมีการรับประกันระดับ "เพียงครั้งเดียว": ตามกฎแล้วกระบวนการทางธุรกิจจะไม่อ่อนไหวต่อการมาถึงของข้อความหรือเหตุการณ์ซ้ำๆ และในงานพิเศษซึ่งเป็นสิ่งสำคัญ จะเพิ่มการตรวจสอบเพิ่มเติมให้กับธุรกิจได้ง่ายกว่า ตรรกะมากกว่าการใช้การรับประกันที่ค่อนข้าง "แพง" อย่างต่อเนื่อง
  • การส่งข้อความและสัญญาณจะต้องเกี่ยวข้องกับธุรกรรมโดยรวมที่มีการเปลี่ยนแปลงสถานะของกระบวนการทางธุรกิจและข้อมูลโดเมน ตัวเลือกที่ต้องการคือการใช้รูปแบบ กล่องขาออกของธุรกรรมแต่จะต้องมีตารางเพิ่มเติมในฐานข้อมูลและตัวทวนสัญญาณ ในแอปพลิเคชัน JEE สิ่งนี้สามารถทำให้ง่ายขึ้นได้โดยใช้ผู้จัดการ JTA ในพื้นที่ แต่การเชื่อมต่อกับโบรกเกอร์ที่เลือกจะต้องสามารถทำงานได้ XA;
  • ตัวจัดการข้อความและเหตุการณ์ขาเข้าจะต้องทำงานกับธุรกรรมที่เปลี่ยนสถานะของกระบวนการทางธุรกิจ: หากธุรกรรมดังกล่าวถูกย้อนกลับ การรับข้อความจะต้องถูกยกเลิก
  • ข้อความที่ไม่สามารถส่งได้เนื่องจากข้อผิดพลาดจะต้องจัดเก็บไว้ในที่เก็บข้อมูลแยกต่างหาก ดีแอลคิว (คิวจดหมายตาย) เพื่อจุดประสงค์นี้ เราได้สร้างไมโครเซอร์วิสแพลตฟอร์มแยกต่างหากที่เก็บข้อความดังกล่าวในพื้นที่จัดเก็บข้อมูล จัดทำดัชนีข้อความตามคุณลักษณะ (สำหรับการจัดกลุ่มและการค้นหาอย่างรวดเร็ว) และเปิดเผย API สำหรับการดู ส่งอีกครั้งไปยังที่อยู่ปลายทาง และการลบข้อความ ผู้ดูแลระบบสามารถทำงานกับบริการนี้ผ่านทางเว็บอินเตอร์เฟส
  • ในการตั้งค่านายหน้า คุณต้องปรับจำนวนการลองส่งซ้ำและความล่าช้าระหว่างการส่งเพื่อลดโอกาสที่ข้อความจะเข้าสู่ DLQ (แทบจะเป็นไปไม่ได้เลยที่จะคำนวณพารามิเตอร์ที่เหมาะสมที่สุด แต่คุณสามารถดำเนินการเชิงประจักษ์และปรับเปลี่ยนระหว่างการดำเนินการได้ );
  • ที่เก็บ DLQ จะต้องได้รับการตรวจสอบอย่างต่อเนื่อง และระบบตรวจสอบจะต้องแจ้งเตือนผู้ดูแลระบบเพื่อว่าเมื่อข้อความที่ไม่ได้จัดส่งเกิดขึ้น พวกเขาสามารถตอบสนองได้อย่างรวดเร็วที่สุด วิธีนี้จะช่วยลด “พื้นที่ที่ได้รับผลกระทบ” ของความล้มเหลวหรือข้อผิดพลาดทางตรรกะทางธุรกิจ
  • บัสการรวมจะต้องไม่ไวต่อการไม่มีแอปพลิเคชันชั่วคราว: การสมัครสมาชิกหัวข้อจะต้องมีความคงทน และชื่อโดเมนของแอปพลิเคชันจะต้องไม่ซ้ำกัน เพื่อที่ในขณะที่แอปพลิเคชันหายไป บุคคลอื่นจะไม่พยายามประมวลผลข้อความจาก คิว.

รับรองความปลอดภัยของเธรดของตรรกะทางธุรกิจ

อินสแตนซ์เดียวกันของกระบวนการทางธุรกิจสามารถรับข้อความและเหตุการณ์ต่างๆ ได้ในคราวเดียว ซึ่งการประมวลผลจะเริ่มพร้อมกัน ในเวลาเดียวกัน สำหรับนักพัฒนาแอปพลิเคชัน ทุกอย่างควรเรียบง่ายและปลอดภัยสำหรับเธรด

ตรรกะทางธุรกิจของกระบวนการจะประมวลผลเหตุการณ์ภายนอกแต่ละรายการที่ส่งผลต่อกระบวนการทางธุรกิจนั้นทีละรายการ เหตุการณ์ดังกล่าวอาจเป็น:

  • การเปิดตัวอินสแตนซ์กระบวนการทางธุรกิจ
  • การกระทำของผู้ใช้ที่เกี่ยวข้องกับกิจกรรมภายในกระบวนการทางธุรกิจ
  • การรับข้อความหรือสัญญาณที่สมัครรับอินสแตนซ์กระบวนการทางธุรกิจ
  • การทริกเกอร์ตัวจับเวลาที่กำหนดโดยอินสแตนซ์กระบวนการทางธุรกิจ
  • ควบคุมการดำเนินการผ่าน API (เช่น การหยุดชะงักของกระบวนการ)

แต่ละเหตุการณ์ดังกล่าวสามารถเปลี่ยนสถานะของอินสแตนซ์กระบวนการทางธุรกิจได้: กิจกรรมบางอย่างอาจสิ้นสุดและบางกิจกรรมอาจเริ่มต้นขึ้น และค่าของคุณสมบัติถาวรอาจเปลี่ยนแปลงได้ การปิดกิจกรรมใดๆ อาจส่งผลให้มีการเปิดใช้งานกิจกรรมต่อไปนี้อย่างน้อยหนึ่งกิจกรรม ในทางกลับกันสามารถหยุดรอเหตุการณ์อื่น ๆ หรือหากไม่ต้องการข้อมูลเพิ่มเติมก็สามารถดำเนินการในธุรกรรมเดียวกันได้ ก่อนที่จะปิดธุรกรรม สถานะใหม่ของกระบวนการทางธุรกิจจะถูกบันทึกไว้ในฐานข้อมูล ซึ่งจะรอให้เหตุการณ์ภายนอกถัดไปเกิดขึ้น

ข้อมูลกระบวนการทางธุรกิจแบบถาวรที่จัดเก็บไว้ในฐานข้อมูลเชิงสัมพันธ์เป็นจุดที่สะดวกมากสำหรับการซิงโครไนซ์การประมวลผล หากคุณใช้ SELECT FOR UPDATE หากธุรกรรมหนึ่งจัดการเพื่อรับสถานะของกระบวนการทางธุรกิจจากฐานสำหรับการเปลี่ยนแปลง จะไม่มีธุรกรรมอื่นใดแบบคู่ขนานที่จะได้รับสถานะเดียวกันสำหรับการเปลี่ยนแปลงอีกครั้ง และหลังจากเสร็จสิ้นธุรกรรมแรกแล้ว รายการที่สองคือ รับประกันว่าจะได้รับสถานะที่เปลี่ยนแปลงไปแล้ว

การใช้การล็อกในแง่ร้ายในด้าน DBMS ทำให้เราสามารถปฏิบัติตามข้อกำหนดที่จำเป็นทั้งหมดได้ กรดและยังรักษาความสามารถในการปรับขนาดแอปพลิเคชันด้วยตรรกะทางธุรกิจโดยการเพิ่มจำนวนอินสแตนซ์ที่ทำงานอยู่

อย่างไรก็ตาม การล็อกในแง่ร้ายคุกคามเราด้วยการชะงักงัน ซึ่งหมายความว่า SELECT FOR UPDATE ยังคงถูกจำกัดอยู่เพียงการหมดเวลาที่เหมาะสมในกรณีที่เกิดการชะงักงันในบางกรณีร้ายแรงในตรรกะทางธุรกิจ

ปัญหาอีกประการหนึ่งคือการซิงโครไนซ์การเริ่มต้นกระบวนการทางธุรกิจ แม้ว่าจะไม่มีอินสแตนซ์ของกระบวนการทางธุรกิจ แต่ไม่มีสถานะในฐานข้อมูล ดังนั้นวิธีที่อธิบายไว้จะไม่ทำงาน หากคุณต้องการตรวจสอบความเป็นเอกลักษณ์ของอินสแตนซ์กระบวนการทางธุรกิจในขอบเขตเฉพาะ คุณจะต้องมีออบเจ็กต์การซิงโครไนซ์บางประเภทที่เชื่อมโยงกับคลาสกระบวนการและขอบเขตที่เกี่ยวข้อง เพื่อแก้ไขปัญหานี้ เราใช้กลไกการล็อกอื่นที่ช่วยให้เราสามารถล็อกทรัพยากรที่กำหนดโดยคีย์ในรูปแบบ 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 รายงดออกเสียง

ที่มา: will.com

เพิ่มความคิดเห็น