תכנות ריאקטיבי – Reactive Programming

תכנות ריאקטיבי – reactive programming – היא פרדיגמה של תכנות אסינכרוני המתמקדת בזרמי מידע (data streams) והתפשטות של שינויים (propagation of changes).
בתכלס זה הרבה מילים גדולות, אז בואו ננסה לפרק אותם למשהו יותר מובן

בסופו של דבר, מדובר על אפיון של מודל, שנועד לטפל בתכנות אסינכרוני (להבדיל ממקבילי) על מנת שהקוד שלנו יהיה יותר מובנה וקריא.

הערה חשובה לפני שנתחיל.
אני מתייחס פה הרבה לJava, כי זה פוסט הכנה לספריית Reactor.
אבל יש עוד הרבה מימושים של תכנות ריאקטיבי.
למשל JavaScript עושה את זה באופן אחר.

היסטוריה

אבן הדרך הראשונה לכיוון של reactive programming הייתה הספריה Reactive Extensions של Microsoft ולאקו סיסטם של Net.
לאחר מכן הגיעה הספריה RxJava שהייתה המימוש של RT, רק עבור ה-JVM.
עם הזמן נכנסו מימושים סטנדרטיים עבור תכנות ריאקטיבי ל-Java, כמו ה-Flow בגרסה Java 9.

השוואה לתבניות מוכרות

באופן כללי, אפשר לחשוב על reactive programming כעל מימוש של Observer Design Pattern.
אפשר גם לחשוב על זה כעל מימוש של Iterator Design Pattern.
אך ההבדל העיקרי בין תכנות ריאקטיבי לבין Iterator הוא ש-Iterator מבוסס על משיכה,
ואילו Reactive, בדומה ל-Observer מתבסס על דחיפה.

זה כואב לחכות

אפליקציות מודרניות יכולות להגיע למספרים עצומים של משתמשים שרצים במקביל.
ולמרות שהחומרה שלנו משתדרגת כל הזמן, היא עדיין לא עומדת בקצב.
יש לנו שתי דרכים עיקריות לשפר ביצועים:

  1. מקביליות – להשתמש בעוד threads, או בעוד חומרה (עוד VM, עוד זיכרון, עוד דיסקים וכו)
  2. אסינכרוניות – לעבוד עם המשאבים שיש לנו בצורה יעילה, ולצמצם את זמן ההמתנה.


כבני אדם, הכי קל לנו לכתוב קוד סיכנרוני.
כלומר שכל שורה רצה רק אחרי שהשורה הקודמת סיימת את כל הפעולות שלה.
ככה הכי קל לנו לעקוב אחרי הקוד ולהבין מה קורה.
זה עובד סה"כ די טוב, עד שמגיעים לצוואר בקבוק, ואז אנחנו מכניסים עוד threads כדי שפעולות שונות ירוצו במקביל.

אבל כאן אנחנו נכנסים לשדה חדש של בעיות, בעיות של מקביליות וניהול threads.
בינינו, מעטים האנשים שבאמת שולטים ברזי המקצוע.
אבל עוד יותר גרוע, אם נביט מקרוב, ברגע שאנחנו מכניסים מקביליות לקוד שלנו, פעולות כמו IO (קריאה לdata base או קריאות API), חלק מהמשאבים שלנו מתבזבזים בזמן שה- thread שקרא לפעולה מחכה לתשובה.
הוא לא משחרר את המשאבים שהוא החזיק, הוא רק נכנס למצב idle.

אסיכוניות להצלה

הגישה השניה שהזכרתי, אסינכרוניות, מחפש דרך יותר יעילה.
על ידי כתיבת קוד אסינכרוני, כאשר אנחנו מגיעים למצב של IO, הקוד האסינכרוני שם את המשימה (task) בצד, ומשתמש במשאבים שלה כדי להריץ task אחרת.
וכאשר הוא מקבל תשובה עבור ה-task הראשונה, הוא מחזיר אותה למצב ריצה.
ככה בעצם ניצלנו את המשאבים בצורה הרבה יותר יעילה.

ההבדל בין קוד אסיכרוני לבין קוד מקבילי
ההבדל בין קוד אסינכרוני לבין קוד מקבילי

אבל בואו נהיה הוגנים, הרי אם קוד אסינכרוני היה תמיד יעיל יותר, אז כולם היו משתמשים בו כל הזמן. אבל זה לא המצב.
אז במה טוב יותר קוד מקבילים מקוד אסינכרוני?
כאשר יש לנו הרבה משימות שלא צריכות IO, אלא רק לזמן מעבד.
במקרים כאלו אנחנו נעדיף לעבוד בקוד מקבילי

אסינכרוניות בג'אווה

באופן דיפולטיבי, ג'אווה מאפשרת לנו שתי אופציות עיקריות לכתוב קוד אסינכרוני:

  • Callback
  • FutureאסיכרוניותFuture


Callback

מתודות אסינכרוניות אשר אין להן ערך החזרה (void).
הן מקבלות כפרמטר פונקציית lambda או מחלקה אנונימית.
הפרמטר הזה נקרא כאשר התוצאה שלנו מגיעה.
דוגמה מוכרת היא ה-EventListener.

אז למה לא להשתמש ב-callback?
ובכן מאוד קשה לכתוב callback שקורא ל-callback שקורא ל-callback.
מאוד מהר מגיעים לקוד שהוא מאוד לא מובן.
בואו נסתכל על הדוגמה הבאה:
אנחנו רוצים להציג את הטופ 5פריטים מומלצים ליוזר שלנו.
מעורבים בו שלושה services:

  • אחד שנותן את הID של הטופ 5 עבור ה-user
  • אחד שמביא את המידע של כל פריט
  • ואחד שממליץ אם ליוזר אין 5 מועדפים
Callback from Hell
Callback from Hell

תרשו לי לדלג על ההסבר פה, אבל זה ממש ממש קשה להבין מה קורה פה נכון?
ואנחנו משתמשים פה רק בשלושה callbacks
כבר יצא לי לראות קוד עם הרבה יותר.
וזה עוד בלי שניסינו בכלל להוסיף timeout או cache.
ב-callback זה הופך להיות משימה מאוד מאוד קשה.
רק כדוגמה, תראו כמה קוד ריאקטיבי שעושה את אותו דבר (אפילו עם timeout ו-cache) הרבה יותר קריא:

אותו קוד כמו בcallback רק בreactive
אותו קוד כמו בcallback רק ב-reactor

Future

מתודות אסינכרוניות אשר באופן מיידי, אחרי שקוראין להן, הן מחזירות <Future<T
התהליך האסינכרוני מחשב את T, אבל future עוטף את הגישה אליו.
T לא זמין באופן מיידי, ואפשר לגשת אליו רק כאשר הוא מוכן.

אז לדעתי future הם קצת יותר טובים מאשר callbacks.
אבל גם הם, כאשר קוראים לכמה אחד בתוך השני, זה מאוד קשה להבנה.
לסנכרן בין כמה futures זה אפשרי, אבל לא קל בכלל.
ובנוסף ל-future יש בעיות מובנות:

  • זה קל מאוד להגיע ל-blocking code עם future.
  • הם לא תומכים ב- lazy computation.
  • חסרה תמיכה בערכי החזרה מירובים, ובטיפול בשגיאות


הפעם נסתכל על דוגמה אחרת:
אנחנו מקבלים רשימה של IDs, שמהן אנחנו רוצים להביא שם וסטטיסטיקה של האובייקט

Future example
Future example

גם כאן אפשר לראות כמה זה מורכב להבנה במבט ראשוני, ומאוד קשה לשייך מה שייך למה.
ושוב, הנה אותו קוד רק בצורה ריאקטיבית

אותו קוד כמו ב-future רק בצורה ריאקטיבית
אותו קוד כמו ב-future רק עם reactor

נחמד לא?

סיכום

בפוסט הזה עברנו על מה זה תכנות אסינכרוני, ומה ההבדל בינו לבין reactive programming
למדנו גם מתי כדאי להשתמש בכל אחד מהם
בנוסף ראינו את האופציות הדיפולטיבות שיש לג'אווה לתכנות ריאקטיבי
וראינו כמה הן פחות מובנות לעומת הקוד של reactor
בפעם הבאה ניכנס יותר לעומק לספרייה.

וכמו תמיד, אשמח לקבל כל הערה, הארה או שאלה שיש לכם

2 מחשבות על “תכנות ריאקטיבי – Reactive Programming”

השאר תגובה

Scroll to Top