הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

בוט טלגרם חדש

04/08/2022

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

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

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

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

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

import requests
import telepot
import re

def daily_post_url():
    response = requests.get(
        'https://www.tocode.co.il/blog',
        headers={'Accept': 'application/json'},
    )
    json_response = response.json()

    return json_response['blog']['posts'][0]['href']

def post_content(url):
    response = requests.get("https://www.tocode.co.il" + url + '/md')
    return response.text

def split_body_to_messages(body):
    output = re.findall(r"(.{1,4096}\b)", body, re.S)
    return output

url = daily_post_url()
body = post_content(url)

chat_id = "@tocodeil"
bot = telepot.Bot('my-secret-token')
bot.sendMessage(chat_id, "https://www.tocode.co.il" + url, disable_web_page_preview=None)

for msg in split_body_to_messages(body):
    bot.sendMessage(chat_id, body)

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

  1. קוד פשוט וקצר בהרבה (31 שורות לעומת מעל 700 בבוט הישן).

  2. רץ בתור סקריפט מ cron ולא צריך להישאר כל הזמן באוויר.

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

נ.ב. אם לא הכרתם לבלוג יש גם JSON API. עכשיו שיש לכם את הקוד תרגישו חופשי למשוך פוסטים גם לתוכניות שלכם.

נ.ב.ב. היום בעשר בבוקר וובינר על WSL. שווה לקפוץ להגיד שלום.

כלל אצבע לזיהוי הודעת קומיט גרועה

03/08/2022

"תיקון לצבעים"

"תיקון שגיאת כתיב"

"תיקוני UI"

"התחלתי לכתוב CSS"

"הוספתי בדיקות"

"שיפורים"

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

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

"תיקנתי את צבע הכותרת הראשית לפי ההערות מהמעצב. כרטיס ג'ירה עם העיצוב #121"

"תיקון מספר שגיאות כתיב בעמוד אודות"

"עדכנתי את קרוסלת התמונות בעמוד הבית כך שתאפשר ניווט מקלדת והקפאת האנימציה".

"בעקבות דיווח של משתמשים שלא הצליחו להירשם הוספתי בדיקה שנכשלת שמתאימה לדיווח".

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

טיפ פייתון: גנרטורים ו StopIteration

02/08/2022

במודול itertools של פייתון יש פונקציה בשם pairwise שמקבלת איטרטור ומחלקת אותו לזוגות. זה נראה ככה:

>>> list(itertools.pairwise(range(100)))
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 19), (19, 20), (20, 21), (21, 22), (22, 23), (23, 24), (24, 25), (25, 26), (26, 27), (27, 28), (28, 29), (29, 30), (30, 31), (31, 32), (32, 33), (33, 34), (34, 35), (35, 36), (36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 42), (42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 48), (48, 49), (49, 50), (50, 51), (51, 52), (52, 53), (53, 54), (54, 55), (55, 56), (56, 57), (57, 58), (58, 59), (59, 60), (60, 61), (61, 62), (62, 63), (63, 64), (64, 65), (65, 66), (66, 67), (67, 68), (68, 69), (69, 70), (70, 71), (71, 72), (72, 73), (73, 74), (74, 75), (75, 76), (76, 77), (77, 78), (78, 79), (79, 80), (80, 81), (81, 82), (82, 83), (83, 84), (84, 85), (85, 86), (86, 87), (87, 88), (88, 89), (89, 90), (90, 91), (91, 92), (92, 93), (93, 94), (94, 95), (95, 96), (96, 97), (97, 98), (98, 99)]

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

def nwise(iterable, n):
    while True:
        yield [next(iterable) for i in range(n)]

אבל כשאני מנסה להפעיל את הפונקציה באותה צורה כמו pairwise אני נכשל:

>>> list(nwise(range(100), 2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in nwise
  File "<stdin>", line 3, in <listcomp>
TypeError: 'range' object is not an iterator

וזה ברור - range אינו איטרטור. הפונקציות המובנות של itertools יודעות לעבוד עם iterable, כלומר משהו שאפשר לבנות ממנו איטרטור. הטעות הראשונה היא שלא יצרתי איטרטור מה iterable שהעבירו לי. תיקון ראשון ועוד ניסיון הרצה:

def nwise(iterable, n):
    it = iter(iterable)
    while True:
        yield [next(it) for i in range(n)]


>>> list(nwise(range(100), 2))
Traceback (most recent call last):
  File "<stdin>", line 4, in nwise
  File "<stdin>", line 4, in <listcomp>
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: generator raised StopIteration

הפעם הודעת שגיאה חדשה - בתוך לולאת ה while של nwise קראתי ל next עם פרמטר אחד. בהנחה שאין כלום באיטרטור, הקריאה תזרוק Exception וזה מה שאנחנו רואים בפלט.

תיקון אחד קל כאן הוא לתפוס את ה Exception בתוך הגנרטור ופשוט לסיים את הלולאה:

def nwise(iterable, n):
    it = iter(iterable)
    while True:
        try:
            yield [next(it) for i in range(n)]
        except StopIteration:
            break

תיקון שני קצת יותר מתוחכם אבל שיכול להפוך את הקוד הזה להרבה יותר פשוט הוא לוותר על הקריאה ל next ולהשתמש ב itertools כל הדרך:

def nwise(iterable, n):
    return zip(*[itertools.islice(iterable, i, None, n) for i in range(n)])

הרעיון הוא לקחת את ה iterable שלנו, לייצר ממנו n רצפים כשכל אחד מהם מתחיל ערך-אחד-קדימה ומתקדם בכפולות של n, ואז zip מחבר את כולם יחד.

ענפים מתפצלים

01/08/2022

ההתנהגות של git pull השתנתה בגירסה 2.27 של גיט, ואז שוב בגירסה 2.33, במטרה להפוך את הפקודה לקצת פחות הרפתקנית.

הפקודה git pull היא למעשה שילוב של שתי פקודות: היא קודם כל מושכת את כל הקומיטים מהענף המרוחק (fetch), ואחר כך היא משלבת אותם בענף שלנו - ברירת המחדל באמצעות merge.

הבעיה בשימוש ב merge כשמושכים קומיטים מענף מרוחק היא שלא כולם רוצים דווקא את ההתנהגות הזאת:

  1. אם היה ריבייס בענף המרוחק, merge עלול לפספס את זה ולהכניס קומיטים כפולים.

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

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

ברירת המחדל הישנה, merge, אומרת ל git להפעיל merge כשהוא מזהה קומיטים חדשים בענף המרוחק. בשביל לבחור אותה בכל הפעלה של pull נפעיל פעם אחת:

$ git config pull.rebase false

ברירת מחדל שניה שאנחנו יכולים לבחור היא rebase, והיא אומרת שאם יש קומיטים חדשים בענף המרוחק וקומיטים חדשים אצלנו, אז אחרי fetch נעשה rebase לקוד שלנו על הענף המרוחק העדכני. גישה כזאת תעבוד טוב אם מתכנתים אחרים עשו push אחרי rebase אבל היא עלולה להיות מבלבלת כי זה אומר שאחרי pull מזהי הקומיטים שלכם ישתנו. בשביל לבחור בה נפעיל פעם אחת:

$ git config pull.rebase true

וברירת המחדל השלישית והמועדפת עליי נקראת Fast Forward Only והיא אומרת ש pull יסרב לרוץ אם יש קומיטים גם בענף המרוחק וגם בענף שלי, ואז במצב כזה אני צריך לבחור לבד מה לעשות. בשביל לבחור בהתנהגות זו אני כותב פעם אחת:

$ git config --global pull.ff only

בואו נראה איך זה עובד. יצרתי מאגר והוצאתי אותו מסינכרון עם הענף המרוחק. הענף main נראה אצלי ככה:

$ git log --oneline --graph
* 036277f (HEAD -> main) d4
* a82b7c8 d3
* b69d6c1 d2
* bb4f039 d1
* 244c3c2 c1
* 8a9cf3b first commit

והענף המרוחק נראה ככה:

$ git log --oneline --graph origin/main
* 6bf42ea (origin/main) c4
* 589e8f4 c3
* 955b0f3 c2
* 244c3c2 c1
* 8a9cf3b first commit

ההסתעפות היא בקומיט c1, כאשר הענף המקומי המשיך ממנו ל d1 והענף המרוחק המשיך ממנו ל c2.

ניסיון עכשיו להפעיל pull נותן את השגיאה:

$ git pull

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

נבחר את ברירת המחדל האהובה עליי:

$ git config pull.ff only

ועכשיו אני מנסה שוב להפעיל pull:

$ git pull

fatal: Not possible to fast-forward, aborting.

עדיין כישלון אבל לפחות הפעם פחות חופר. עכשיו אני יודע שאי אפשר לעשות fast-forward ונדרשת החלטה - האם אני רוצה לעשות merge רגיל או rebase. בשביל להחליט לבצע merge רגיל אני מפעיל:

$ git pull --ff

או בשביל להחליט שאני מעדיף rebase אני יכול להפעיל:

$ git pull --rebase

הבחירה בין שתי האופציות כבר תלויה במקרה הספציפי ובאיך אני רוצה שהשינויים שלי ישתלבו עם השינוי המרוחק.

הנה במאגר שלי התוצאה אחרי הפעלת merge:

$ git pull --ff
$ git log --oneline --graph

*   78831a1 (HEAD -> main) Merge branch 'main' of https://github.com/ynonp/divergent-branches
|\
| * 6bf42ea (origin/main) c4
| * 589e8f4 c3
| * 955b0f3 c2
* | 036277f d4
* | a82b7c8 d3
* | b69d6c1 d2
* | bb4f039 d1
|/
* 244c3c2 c1
* 8a9cf3b first commit

או אחרי הפעלת rebase:

$ git pull --rebase
$ git log --oneline --graph

* f0a0b30 (HEAD -> main) d4
* 07cd430 d3
* e95dcad d2
* 3f03f93 d1
* 6bf42ea (origin/main) c4
* 589e8f4 c3
* 955b0f3 c2
* 244c3c2 c1
* 8a9cf3b first commit

הזמנה לוובינר: WSL

31/07/2022

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

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

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

  1. פקודות WSL בסיסיות - התקנת הפצות, שידרוגים, גיבויים.

  2. הקמת סביבת פיתוח ב WSL.

  3. שילוב תוכניות גרפיות ב WSL.

  4. הגדרות המכונה wslconfig.

  5. הגדרות רשת.

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

כל הסיפור קורה ביום חמישי הקרוב בשעה עשר בבוקר. הרשמה בחינם בקישור: https://www.tocode.co.il/workshops/119

למה קשה ללמוד ריאקט נייטיב

30/07/2022

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

// from: https://raw.githubusercontent.com/flatlogic/react-native-starter/master/src/modules/pages/PagesView.js

import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity, Image } from 'react-native';

import { colors, fonts } from '../../styles';

const chartIcon = require('../../../assets/images/pages/chart.png');
const calendarIcon = require('../../../assets/images/pages/calendar.png');
const chatIcon = require('../../../assets/images/pages/chat.png');
const galleryIcon = require('../../../assets/images/pages/gallery.png');
const profileIcon = require('../../../assets/images/pages/profile.png');
const loginIcon = require('../../../assets/images/pages/login.png');
const blogIcon = require('../../../assets/images/pages/blog.png');

export default function PagesScreen(props) {
  return (
    <View style={styles.container}>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Charts')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={chartIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Charts</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Gallery')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={galleryIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Gallery</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Profile')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={profileIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Profile</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Chat')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={chatIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Chats</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Calendar')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={calendarIcon}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Calendar</Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Auth')}
          style={styles.item}
        >
          <Image
            resizeMode="contain"
            source={loginIcon}
            tintColor={colors.primary}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Login</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.row}>
        <TouchableOpacity
          onPress={() => props.navigation.navigate('Blog')}
          style={styles.blogItem}
        >
          <Image
            resizeMode="contain"
            source={blogIcon}
            tintColor={colors.primary}
            style={styles.itemImage}
          />
          <Text style={styles.itemText}>Blog</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: colors.white,
    paddingTop: 10,
  },
  row: {
    flexDirection: 'row',
    paddingHorizontal: 10,
    marginTop: 10,
  },
  item: {
    flex: 1,
    height: 120,
    paddingVertical: 20,
    borderColor: colors.primaryLight,
    borderWidth: 1,
    borderRadius: 5,
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: 5,
  },
  blogItem: {
    width: '31%',
    height: 120,
    paddingVertical: 20,
    borderColor: colors.primaryLight,
    borderWidth: 1,
    borderRadius: 5,
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: 5,
  },
  itemText: {
    color: colors.primary,
    fontFamily: fonts.primary,
  },
  itemImage: {
    height: 35,
  },
});

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

  1. בגלל שמכונות היעד הן טלפונים, כדאי שיהיו לנו כמה טלפונים לבדיקה והיכרות לפחות בסיסית עם המושגים בפיתוח מובייל ובנוסף עם סביבות הפיתוח XCode ו Android Studio. זה לבד הרבה חומר ללמוד.

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

  3. ריאקט נייטיב עצמו הוא די רזה וכמעט בכל אפליקציה נרצה להוסיף ספריות הרחבה. קוד הדוגמה שהודבק למעלה למשל משתמש בספריית React Navigation כדי לנווט בין דפים באפליקציה. יש ספריה חיצונית שמטפלת בשמירת מידע על הדיסק ועוד ספריות רבות לנושאים לכאורה בסיסיים בפיתוח.

  4. עיצוב ריאקט נייטיב, למרות שנראה במבט ראשון כמו CSS (או יותר נכון כמו CSS In JS), עדיין דורש למידה ובדיקה גם לאנשים שמכירים טוב CSS כי לא כל המאפיינים נתמכים או נתמכים בכל הקומפוננטות.

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

העלות של קוד חיצוני

29/07/2022

גירסה 14 של user-event יצאה עם עדכון לממשק של הספריה. בגירסאות קודמות היה אפשר לכתוב:

userEvent.click(screen.getByText('Check'))

ואחרי שידרוג לגירסה 14 אנחנו צריכים להוסיף await לפקודה:

await user.click(screen.getByRole('button', {name: /click me!/i}))

זה השינוי הכי קל בעולם - רק צריך למצוא כל מקום שהשתמשת בספריה ולהוסיף await לפניו. ובכל זאת קשה לי לדמיין מתכנתת שקמה בבוקר ואומרת "היום אני הולכת לשדרג את user-event ולעדכן את כל הטסטים שלי, פשוט כי בא לי לשדרג". בכל עבודה על מערכת, או שיש עכשיו באגים קריטיים שצריך לפתור, או שיש פיצ'רים דחופים שצריכים להגיע ללקוחות, או שאין לקוחות ואז זה עוד יותר מלחיץ וצריך דחוף להתקדם בפיתוח כדי שמישהו ישלם על העבודה. אף פעם אין זמן טוב לעדכן את כל הבדיקות רק בשביל לשדרג ספריה.

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

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

ללמוד לקבל מגבלות

28/07/2022

נניח שהגעתם לפרויקט ריאקט ויש בו עץ קומפוננטות מורכב שנגמר באיזה input, משהו כזה רק דמיינו שכל קומפוננטה יושבת בקובץ אחר והרבה יותר מורכבת:

function A(props) {
    return <B />
}

function B(props) {
    return <C />
}

function C(props) {
    return <D />
}

function D(props) {
    return <input type="text" />
}

ועכשיו אני רוצה להוסיף checkbox בקומפוננטה A שיגרום לתיבת הטקסט ב D להיות disabled. בואו נעשה את זה עוד יותר מעניין ונדמיין ש C ו D בכלל נכתבו בפרויקט אחר ויש עוד כמה עשרות פרויקטים שמשתמשים בהם.

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

בריאקט בשביל שקומפוננטה A תשפיע על קומפוננטה D אני צריך להעביר סטייט ביניהן - כלומר אני צריך להגדיר משתנה סטייט ב A, לשלוח אותו ל B, שתשלח אותו ל C, שתשלח אותו ל D שבתורה צריכה להעביר אותו ל input. ותקוו שיש לכם מספיק קפה בשביל כל הריפקטורינג הזה.

אבל בעצם אני יודע ריאקט. ואני יכול לרמות. אני יכול לכתוב קוד ב A שיצלול פנימה ל D וישנה את מצב ה disabled של התיבה. בשבילכם אפילו כתבתי את זה:

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function App() {
  const [disabled, setDisabled] = useState(false);
  const el = useRef(null);

  function handleChange(e) {
    if (e.target.checked) {
      setDisabled(true);
    } else {
      setDisabled(false);
    }
  }

  useEffect(() => {
    const checkbox = el.current.querySelector('input[type="text"]');
    checkbox.disabled = disabled;
  }, [disabled]);

  return (
    <div className="App" ref={el}>
      <h1>Hello CodeSandbox</h1>
      <label>
        <input type="checkbox" checked={disabled} onChange={handleChange} />
        Disabled
      </label>
      <A />
    </div>
  );
}

function A(props) {
  return <B />;
}

function B(props) {
  return <C />;
}

function C(props) {
  return <D />;
}

function D(props) {
  return <input type="text" />;
}

והנה אפילו סנדבוקס עם הקוד עובד: https://codesandbox.io/s/throbbing-snowflake-u6gv87?file=/src/App.js

מה שמחזיר אותנו לכותרת.

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

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

מדריך sed

משמעות השם sed היא Stream Editor, ותפקידו של sed בהתאם: הוא עורך טקסט שמקבל את הקלט שלו בתור זרם (מקובץ או מ Pipeline) ועורך את הטקסט שמגיע לפי הוראות שהעברנו מראש. במדריך זה אספר על סד, קודם התיאוריה ואז דרך דוגמאות.

המשך קריאה

מבחן התוצאה

26/07/2022

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

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

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

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

האמת כמובן יותר מורכבת.

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

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