תוכנית וובינרים לחודשים הקרובים
קוראים וותיקים כבר יודעים שבכל יום חמישי הראשון של החודש אני מעביר בזום וובינר על נושא טכנולוגי מעניין. הבוקר סגרתי את התוכנית לשלושת החודשים הקרובים והנה מה שצפוי לנו:
טיפים קצרים וחדשות למתכנתים
קוראים וותיקים כבר יודעים שבכל יום חמישי הראשון של החודש אני מעביר בזום וובינר על נושא טכנולוגי מעניין. הבוקר סגרתי את התוכנית לשלושת החודשים הקרובים והנה מה שצפוי לנו:
בעידן ה npm והפיתרונות האינסופיים הנטיה של מתכנתים רבים היא להגיד "מישהו כבר בטח בנה את זה" פחות או יותר על כל בעיה שנתקלים בה, במטרה לחסוך זמן ולבנות על הצלחה של אחרים. אבל מה קורה כשהניסיון להתבסס על עבודה של אחרים דווקא מבזבז לנו זמן? ומתי אנחנו עוצרים להתלבט אם כדאי לבנות לבד?
יש שתי סיבות מרכזיות לשימוש בפיתרון קיים-
שימוש בקוד קיים הופך את המערכת שלי לסטנדרטית יותר וכך קל יותר לגייס מתכנתים נוספים ולהם קל יותר להיכנס מהר לעניינים. אחד מקלפי המכירה המרכזיים של ריילס כשהיא רק יצאה היה הבלאגן ששרר בעולם ה PHP באותה תקופה, והטענה שאצלנו בריילס כל הפרויקטים מגיעים באותו מבנה תיקיות ומתכנתים יוכלו בקלות לעבור בין פרויקטים. היום ברור לגמרי שיותר קל לי לגייס מתכנתת לפרויקט ריאקט מאשר מתכנתת לפרויקט שישתמש ב Framework שאני המצאתי. המגויסת הטריה יודעת שעם טכנולוגיה שהיא מכירה היא תוכל להתחיל להיות פרודוקטיבית הרבה יותר מהר, ושעבודה בריאקט תתן לה ניסיון בעל ערך לקראת העבודה הבאה.
שימוש בקוד קיים מאפשר לי לחסוך זמן פיתוח באמצעות בניה על עבודה של אחרים. דוגמה טובה היא מה שהיה עולם פיתוח הווב בימים של jQuery, כשהיינו צריכים להתאים אתרים למספר דפדפנים והיו אינסוף טריקים שהיית צריך לכתוב כדי שדברים יעבדו טוב בכל מקום. ספריה כמו jQuery נתנה ממשק אחיד שעובד בכל מקום, וכך חסכה למכתנתים עבודה.
אבל גם לבניית פיתרון לבד יש יתרונות, לדוגמה:
מתכנתים שבונים פיתרון לבד מבינים טוב יותר את הבעיות, ובעתיד הידע הזה יעזור לנו לבחור פיתרון מוכן טוב יותר או להתאים פיתרון מדף לצרכים שלנו.
פיתרון שבנינו לבד יהיה מדויק וחסכוני יותר בהשוואה לפיתרון מדף. הפיתרון שלנו צריך לפתור רק את הבעיה שלנו, בעוד שפיתרון מדף פותר עוד המון בעיות שאולי לא רלוונטיות אלינו.
פיתרון שבנינו לבד מושפע רק מהפוליטיקה הפנימית שלנו, ולא מאינטרסים של אחרים.
בדרך כלל יהיה לי יותר קל לתקן באגים או להוסיף פיצ'רים לפיתרון שאני כתבתי, מאשר להתאים התנהגות של פיתרון חיצוני שאין לי שליטה על הקוד שלו.
בסוף השיקול האם לבנות לבד או לקחת פיתרון מוכן צריך להיות שיקול מקצועי ותלוי מצב. אם אתם מוצאים את עצמכם תמיד בוחרים בגישה אחת או בשניה, שווה לנסות לגוון כדי לראות איך זה מרגיש, ולאט לאט למצוא את נקודת האיזון שתתאים למוצר שלכם.
סוליד היא פריימוורק ריאקטיבי לפיתוח צד לקוח שלקח השראה מאוד גדולה מריאקט מבחינת התחביר, אבל הטמיע את הריאקטיביות בצורה טובה יותר מאשר ריאקט. אפשר לחשוב עליו כמו הכלאה בין ריאקט למובאקס, רק עם הרבה פחות מושגים שצריך להכיר.
ומאחר ואני חושב שהדרך הכי טובה לדבר על פריימוורק היא לראות קוד שכתוב בו - הכנתי לכם 3 תוכניות דוגמה קטנות עם סוליד שיעזרו לנו להבין מה מיוחד בו.
את הפקודה המופלאה הבאה גיליתי רק לא מזמן, והיא כבר הצליחה לפנות לי 30 ג'יגה בדיסק בלי להתאמץ. היא נקראת:
$ docker volume prune
בעבודה עם דוקר ו docker compose נוצרים כל הזמן Volumes, אבל אותם Volumes לא נמחקים בעבודה השוטפת (אלא אם כן מחקתם אותם בצורה יזומה). במיוחד מעצבנים ה Anonymous Volumes, כלומר Volumes עם שם אקראי שנוצרים מהוראת Volume ב Dockerfile של אימג'ים מסוימים, ואתם אפילו לא יודעים שיצרתם אותם.
הפקודה prune מוחקת במכה אחת את כל ה Volumes שאינם בשימוש, כלומר Volumes שאף קונטיינר לא משתמש בהם. הנה הפלט שהופיע אצלי באחת ההפעלות:
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
af3aae34cdad2d27ce238052ad76066f5007a994afd8e43e630ed1e065e1adc2
1bd5b35c4b045efcedab000946b4b0cc47a31c6ab9797aa7c15abe68a187dd24
215e9ed82a1248713a37856d91bef06322a0aeba29983bb3bda73c6d3474975f
81800e9125a43c536733d540776576c459857fa161aad9a022089ce9f29f4b39
b86e5619f32e50c1750d06463f93f1222b0a1cb654961b03eb05f693602036d5
21053b79f98090cef2ed0d2e370aab1c87902712554fe106fcdf7776ad49e432
36da000251fd03a84d7d7aad5d1387b88067993d4db37f93be101ee6b39a8fa0
7f46b0540d958befaaa9eba7f546f58ac345d43aca76d78c82fd6fa0f2310a9d
0503e2f1595109e47fe34520f50d0cae62bf61829b110329b1736dead696a36a
38487902f058dd998a018d694c6a0415df30f1764caccd06aaefed7bcdc7f4f9
9262a4d3d727b2ed8ec6d1eb53f9d12666492a31b7b156bdbf25c2cc4f9e72ba
85acc9837d945b8da346e0a3005a41f25f11bbe6dba3254acf0b050ea760f99b
f84ef10a22b6c39e4b500fff3dd2ab41a47d9c3c16521c72f51accf3ee9ae733
25b3e355acae3df348d705e90b932b2152e42c74a2e522352244b7a461a7854b
c887268872c39fff371c05f59989d35e0ed2662c784fde35c65a31622ef3cb90
3a69d7d9f5e96c566f39aedc8b3fc6c07d8bea3beebd47c3c624cc5e4dae593b
491272fb1d91144b92ad990edb92cf8d15a7ebfbf26d7b8d4bbc2c3a077c2166
b75c33d193925f9cbd059e863eb9e44e0612fd5614716eac7de4cbc5847dfbd8
Total reclaimed space: 2.519GB
בגלל שאני משתדל להיות מסודר ולמחוק Volumes אחרי שאני יוצר אותם, אפשר לראות שרוב ה Volumes ש prune מחק היו בכלל אנונימיים. זה לא הפריע להם לתפוס 2.5 ג'יגה של מקום על הלפטופ שלי - מקום שעכשיו אני יכול לשמור בו דברים חשובים יותר כמו תמונות של חתולים.
בתכנות לירות לעצמך ברגל זה מה שקורה כשאתה משתמש כל כך לא נכון בשפה שאתה עושה לעצמך נזק. בריאקט זה קורה לי בכל מה שקשור לעדכון סטייט ובמיוחד בהעברת הערך הלא נכון לפונקציית ה setState.
ניקח קומפוננטה לדוגמה שמציגה תיבת טקסט וסופרת כמה פעמים הטקסט בתיבה מתאפס, כלומר כל פעם שמישהו מוחק את כל הטקסט מהתיבה ערך המונה עולה ב-1. יודעים מה, ובשביל שיהיה מעניין, ניתן גם כפתור שבלחיצה עליו הטקסט מתאפס:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function handleChange(e) {
setText(e.target.value);
if (e.target.value === "") {
setResetCount(resetCount + 1);
}
}
function reset() {
setText("");
setResetCount(resetCount + 1);
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
שימו לב לקוד הכפול בשתי הפונקציות handleChange
ו reset
. יש שתי דרכים קלות לכתוב אותו בצורה יותר גנרית:
אפשר לכתוב פונקציה אחת ששתי הפונקציות יקראו לה, והיא גם תפעיל את setText
וגם תבדוק אם הטקסט ריק ותעלה את המונה.
אפשר להשתמש ב useEffect
שיבדוק אם הטקסט התרוקן ויעדכן את המונה.
היתרון בגישה הראשונה הוא ביצועים. במקום לשנות את הטקסט, לקרוא ל render, לגלות שהטקסט התעדכן ואז לשנות את המונה ושוב לקרוא ל render, אנחנו עושים את הכל בפעם אחת. הקוד אחרי עידכון נראה כך:
export default function App() {
const [text, _setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function setText(newText) {
_setText(newText);
if (text !== "" && newText === "") {
setResetCount(resetCount + 1);
}
}
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
עובד? עובד. אבל במקום לקבל פיתרון גנרי, הטמנו בקוד מוקש.
כי עכשיו בואו נוסיף כפתור שמוחק את התו האחרון מהטקסט. ביום רגיל אני יודע שבקריאה ל setter שתלוי בערך הקודם כדאי לי להעביר ל setter פונקציה, אבל הפעם כבר יש לי פונקציית setText
שיודעת לטפל בקשר בין שני משתני הסטייט, ואני רוצה להמשיך להשתמש בה, אז אני עלול לקחת את הצעד הראשון לכיוון האבדון:
function deleteLastChar() {
setText(text.slice(0, -1));
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
</div>
</div>
);
ולמה צעד לכיוון האבדון? כי הקוד עדיין עובד אבל המוקשים רק מתרבים. אם עכשיו אני רוצה להוסיף כפתור למחיקת שני תווים - שכמובן ישתמש בפונקציה שכבר יש לי - אז הקוד יפסיק לעבוד:
export default function App() {
const [text, _setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function setText(newText) {
_setText(newText);
if (text !== "" && newText === "") {
setResetCount(resetCount + 1);
}
}
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText(text.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
הניסיון לחסוך את ה render של useEffect נראה כמו תבנית הגיונית, אבל מסתבר שהוא עובד נגד ההגיון של ריאקט. אותו קוד עם useEffect היה כבר כמעט עובד:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
React.useEffect(() => {
if (text === "") {
setResetCount((c) => c + 1);
}
}, [text]);
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText((t) => t.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
ולמה כמעט? כי עכשיו אנחנו מקבלים הפעלה אחת מיותרת של האפקט בפעם הראשונה שהקומפוננטה מופיעה על המסך, כלומר הערך של resetCount מתחיל מאחד. הדרך המקובלת להתעלם מהאפקט הראשון היא להוסיף משתנה ref:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
const isFirstEffect = React.useRef(true);
React.useEffect(() => {
// skip the first time
if (isFirstEffect.current) {
isFirstEffect.current = false;
return;
}
if (text === "") {
setResetCount((c) => c + 1);
}
}, [text]);
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText((t) => t.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
מוזמנים לראות את הגירסה האחרונה עובדת בקודסנדבוקס בקישור הבא: https://codesandbox.io/s/affectionate-lamport-ddrc3y?file=/src/App.js.
לפני כמה חודשים העברתי כאן וובינר על קוברנטיס ובו סיפרתי איך קוברנטיס עובד ואיך לשחק איתו מהמחשב המקומי. השבוע ביום חמישי אעביר סוג של וובינר המשך לאותו מפגש, ובו אקח משימה יותר ספציפית: אני אקח יישום Node.JS שמשתמש בבסיס נתונים PostgreSQL ואעלה אותו יחד עם בסיס הנתונים שלו לקלאסטר קוברנטיס אמיתי באינטרנט.
ברוב הפרויקטים שנעבוד עליהם נצטרך להתמודד עם יותר מטכנולוגיה אחת, לדוגמה:
קוד צד שרת מתשאל בסיס נתונים באמצעות שפת השאילתות של בסיס הנתונים.
קוד php יוצר קוד html.
קוד ריאקט מגדיר Virtual DOM שיוצר קוד html ו JavaScript.
במצב כזה אנחנו הרבה פעמים נפגוש ספריות שנועדו להסתיר מאיתנו את הטכנולוגיה ה"פחות חשובה" - וככה אנחנו מגיעים ל ORM שמסתיר את ה SQL, או ASP.NET Server Controls שמסתירים את ה HTML וה JavaScript.
אז ב TypeORM אנחנו נשלוף משתמש מסוים עם:
const firstUser = await connection
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.getOne();
וב Rails אני אשלוף את אותם פרטי משתמש עם:
first_user = User.find(1)
כשאני כותב קוד וצריך לבחור אם להשתמש בטכנולוגיה "מסתירה" מהסוג הזה, אני קודם כל שואל - "האם הייתי עדיין בוחר בה אם הייתי יודע את הטכנולוגיה שמתחת?". במקרה של ריילס למרות שאני יודע SQL אני שמח להשתמש ב ORM שלהם. הוא באמת טוב יותר, ובמקומות שהוא לא טוב יותר אני יכול לכתוב SQL רגיל בלי בעיה.
במקרה של מונגו אחרי שלמדתי לעבוד איתו לא חזרתי למונגוס, כי העבודה הישירה עם מונגו יצרה קוד שהיה הרבה יותר קל לתחזוקה.
ואם אני לא יודע עדיין את הטכנולוגיה שמתחת מספיק טוב כדי לענות על השאלה? נו, זאת הזדמנות מצוינת להתחיל ללמוד.
שני דברים שמשפיעים מאוד על הפרודוקטיביות שלנו כמפתחים הם:
שיטות העבודה, הנהלים, התבניות וכל ההרגלים "האוטומטיים" איתם אנחנו עובדים.
הכלים בהם אנחנו משתמשים, שעוזרים לנו ליישם את שיטות העבודה הטובות שלנו.
והסדר בין השניים חשוב.
אחת הסיבות ש git נתפס בתור משהו כל כך "מסובך" היא שאנשים מגיעים אליו לא מוכנים. אתה חושב שאתה לוקח כלי שיקל עליך את העבודה, אבל בעצם אתה מקבל כלי שלא מתאים לשיטת העבודה שלך ועכשיו הכל רק יותר מסובך.
או מישהו סיפר לך ש Redux זה הדבר הכי טוב מאז המצאת הלחם החתוך, אבל כבר אחרי כמה ימים של עבודה איתו הקוד מסתבך ובמקום לתכנת את רק כותבת Boilerplate בשביל לשמח את רידאקס.
כלים הם תוצאה והם באים אחרי שיטת העבודה. מי שכבר עובד בגישה פונקציונאלית שמתאימה ל Redux ישמח מאוד לגלות את הכלי; ומי שמוכן ללמוד את שיטת העבודה האידאלית עם גיט יגלה שהמעבר לגיט משפר משמעותית את הפרודוקטיביות. אבל הכלי הוא לא הדבר החשוב כאן.
כדי לשפר פרודוקטיביות מתחילים עם שיפור שיטות העבודה. אחרי זה הכלים כבר ימצאו אתכם.
התער של אוקאם הוא עיקרון פילוסופי שטוען שכאשר קיימים הסברים שונים לאותה תופעה יש להעדיף את הפשוט מביניהם, שבו המספר המועט ביותר של מושגים וחוקים.
גם אם בעולם המדעי או האמיתי לא תמיד ברור איך ליישם עיקרון זה (או אם בכלל כדאי), בעבודה על קוד פיתרונות פשוטים הם יותר קלים לתחזוקה ויותר עמידים לטווח הרחוק. אם לדוגמה יש לי בעיה עם קומפוננטת UI מסוימת אני אנסה לפתור אותה בשלבים הבאים:
אולי בתוך אותה הספריה אני יכול להעביר פרמטר שיגרום לקומפוננטה להתנהג כמו שאני רוצה.
אולי אני יכול לדרוס חלק מהמראה של הקומפוננטה באמצעות שינוי CSS.
אולי אני יכול בקוד שלי לבנות קומפוננטה חדשה מתוך הקומפוננטה הקיימת, שמרחיבה אותה בצורה שאני רוצה.
אולי אני יכול ליצור Fork לספריה ולהוסיף לקומפוננטה מבפנים את המנגנון שאני צריך (בתקווה בלי לשבור שום דבר אחר).
אולי אני יכול להשתמש בקומפוננטה מקבילה מספריית UI אחרת.
אולי אני יכול לשנות את כל היישום שלי כך שישתמש בספריית UI אחרת שבה הקומפוננטה מתנהגת כמו שאני רוצה.
הרבה פעמים הפיתרון הפשוט - אלה שכאן בראש הרשימה - דורש יותר מחשבה ודיוק מאשר פיתרונות מורכבים יותר, ולפעמים כתיבה של כמה שורות CSS שיפתרו בעיה יכולה לקחת משמעותית יותר זמן מאשר לעשות פשוט fork לספריה ולתקן את הבעיה שם. הייתרון של הפיתרון הפשוט הוא לא זמן העבודה הקצר יותר ל Delivery, אלא היותו יציב יותר וקל יותר לתחזוקה בטווח הארוך.
אם אני עובר בבית ליד חתיכת לגו קטנה שזרוקה על הריצפה אני מיד ארים אותה. במקרה הטוב אחזיר אותה לקופסה, אבל בכל מקרה גם אם אני לא בטוח איפה הקופסה אני אעדיף לשים אותה על שולחן או משהו - כי אני יודע שאחרת אדרוך עליה בטעות כשאקום לילדים בלילה.
את אותו הגיון הרבה יותר קשה ליישם בקוד.
בכל מערכת אנחנו נתקלים באינסוף קטעי קוד עם מוקשים: משהו שנראה כמו באג (אבל בכל זאת עובד ולא ברור איך), פונקציה לא מתועדת, מנגנון שאין לו בדיקות, קוד מסורבל, ארוך ולא מספיק ברור, או אפילו תקלת אבטחה שאי אפשר לנצל אותה.
וכל פעם שרואים כזה מוקש האינסטינקט הראשון הוא להתרחק. אתה מספר לעצמך סיפורים כמו "זה לא הזמן עכשיו לתקן את זה", "אולי מישהו כתב את זה ככה בכוונה", "אם זה עובד לא נוגעים", "אני לא מבין מספיק טוב מה הקוד צריך לעשות ופוחד לשבור". בגלל שאנחנו לא יודעים איפה המקום הנכון של חתיכת הלגו, אנחנו משאירים אותה על הריצפה.
קוד טוב הוא קוד חי - ובשביל שקוד יהיה חי צריך להרגיש נוח לשנות אותו. גם אם בהתחלה תגלו שה"תיקונים" שלכם רק שוברים יותר דברים, לאט לאט וככל שתתרגלו לפרק מוקשים גם התיקונים שלכם ישתפרו וגם תרגישו הרבה יותר בנוח לעבוד על כל המערכת.