DanLevy.net

מומחה צינורות: העברת מצב

שלום, קלוז'ר, ידידי הוותיק.

Hero image for מומחה צינורות: העברת מצב

מאסטר הצינורות: העברת מצב

נתקלתם באתגרים בהעברת מצב באמצעות צינורות פונקציונליים?

הארגון (או היעדרו) של הקוד שלכם משפיע ישירות על הקלות שבה מצב מועבר מצד לצד.

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

הקטע ה”אמיתי” הבא יהיה המוקד של מאמר זה: פונקציית צ’ק-אאוט, המקבלת userId ומערך של products. היא מחזירה שרשרת Promise המבצעת 4 פונקציות ברצף.

const checkout = (userId: number, products: number[]) => {
return getProductsSubtotal(userId, products)
.then(subTotal => applyTaxes(userId, subTotal))
.then(total => purchaseProducts(userId, total))
.then(result => sendReceipt(userId, result));
};

רגע אחד, הקוד הזה די סביר, לפחות מבחינת צינורות ב-JS!

עם זאת, יש בו כמה בעיות עדינות שעלולות להצטבר לבעיות משמעותיות יותר.

בעיה אחת היא שאנחנו מעבירים שוב ושוב את userId לכל פונקציה (הקשורה לוגית). כעת שלבו את זה עם בעיה נוספת שקל לפספס גם למפתחים וגם ל-TypeScript: החלפת הארגומנטים המספריים יוצרת בקלות באג שקט. (ראו applyTaxes ו-purchaseProducts. האם userId או amount בא קודם?)

לפני שנחליט איך לשפר את הקוד הזה, בואו נזהה כמה יתרונות/חסרונות.

יתרונות וחסרונות

יתרונות

חסרונות

פתרון, חלק 1: צרו מודול!

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

CartHelpers Factory

דוגמה למודול CartHelpers, שבו userId מועבר פעם אחת, וכל המתודות הן בעלות ארגומנט יחיד.

const CartHelpers = (userId: number) => {
return {
getProductsSubtotal: products => getProductsSubtotal(userId, products),
applyTaxes: subTotal => applyTaxes(userId, subTotal),
purchaseProducts: total => purchaseProducts(userId, total),
sendReceipt: invoice => sendReceipt(userId, invoice)
};
};

מחלקת CartHelpers

אם מחלקות הן הקטע שלכם, קל להתאים:

class CartHelpers {
constructor(userId) {
this.userId = userId;
}
getProductsSubtotal = products => getProductsSubtotal(this.userId, products);
applyTaxes = subTotal => applyTaxes(this.userId, subTotal);
purchaseProducts = total => purchaseProducts(this.userId, total);
sendReceipt = invoice => sendReceipt(this.userId, invoice);
}

כמה יתרונות מיידיים:

על ידי קיבוץ פונקציות קשורות, אנו יוצרים הזדמנות להקטין את שטח הפנים החשוף (למשל, checkout(), המתודות ה’ציבוריות’ של CartHelpers).

פחות שטח פנים === פחות עומס קוגניטיבי, בדיקות ותחזוקה טובות יותר. תכנן מערכות בכוונה ובמיקוד. ✨

שימוש ב-Checkout ו-CartHelpers

בואו נראה איך הפונקציה checkout() נראית עכשיו:

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
return Promise.resolve(products)
.then(products => cart.getProductsSubtotal(products))
.then(subTotal => cart.applyTaxes(subTotal))
.then(total => cart.purchaseProducts(total))
.then(result => cart.sendReceipt(result));
};
Checkout עם שיפורים נוספים

אפשר לשפר עוד? כן! אין צורך לחזור על ארגומנטים בכלל!

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

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 פונקציות נערמות כמו לגו וקוראות כמו "מילים אנושיות" רגילות! 💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

אם מרגיש לא טבעי לשלב פרמטרים לארגומנטים בודדים (אובייקט), שקול לפרק את הפונקציות שלך או לשלב אותן למודולים עם תחום מתאים יותר.

מאיפה להתחיל?

מצא פונקציות קשורות, וקבץ אותן יחד. (למשל CartHelpers.)

חלק מהאתגר במציאת מודולים לוגיים אפשריים הוא זיהוי קוד קשור מלכתחילה.

מה הופך פונקציות לקשורות?

טריק נחמד: חפש חזרות בפרמטרים של פונקציות. שאל האם יש קשר בפעולה? או אחריות בסיסית?

אמנם אין ‘תשובה נכונה’ יחידה לעיצוב מודולים, אבל זה עוזר לזהות 2-3 אפשרויות לארגון – צייר מתווה, כתוב קוד ‘דמיוני’, שאל ‘האם זה מעורר שמחה?’

אולי תרגיש ש-cart.sendReceipt() לא שייך למתודות הקשורות לתשלום. אולי customerNotifications.sendReceipt() הוא בית טוב יותר להודעות ללקוח. אם CartHelper הוא בעל חשיבות מספקת, הוא עשוי לפעול כ-controller הקורא באופן פנימי לכל ה-services הנחוצות, כמו customerNotifications.

איך יודעים אם אתה עוזר?

אם הקריאות לא נפגעת בזמן שאתה מבטל ארגומנטים אד-הוק, מזל טוב!!! כנראה שבנית מודול עם תחום ברור ועמיד!

אז, זה מעלה את השאלה, היכן נוסיף פונקציונליות?

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

  1. להרחיב/לשפץ מתודה קיימת. (כשקוד חדש קרוב מספיק לקוד קיים.)
  2. ליצור פונקציה חדשה (חמישית) במקום הרצוי בשרשרת. (בהנחה שקוד חדש אינו קשור לפונקציות קיימות.)

בסופו של דבר זה מקל על ההחלטה היכן שייכת פונקציונליות חדשה. (למשל cart.applyDiscounts(), cart.applyTaxes(), rewards.getBalance().)

סיכום

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

שאלות? תגובות? חששות? אל תהסס לפנות @justsml או דוא”ל.

הישארו מעודכנים לחלק הבא בסדרה

נחקור כיצד להחיל מצב חיצוני, ולהרחיב פונקציונליות במודול שלנו!

קריאה נוספת