ar
Feedback
ToCode

ToCode

الذهاب إلى القناة على Telegram

טיפים קצרים למתכנתים מאת ינון פרק

إظهار المزيد
1 419
المشتركون
لا توجد بيانات24 ساعات
لا توجد بيانات7 أيام
لا توجد بيانات30 أيام
أرشيف المشاركات
ToCode
1 419
http://localhost:3000/ http://localhost:3000/about http://localhost:3000/contact תוכלו לשים לב שכל כניסה לדף מחליף את קומפוננטת Routes בקומפוננטה שמתאימה לכתובת, ולכן אני מקבל תמיד את שורת הכותרת ומתחתיה פעמיים את הקומפוננטה הראשית של הדף. ## מעבר בין דפים כשאנחנו עוברים בין דפים באמצעות שינוי שורת הכתובת בדפדפן או באמצעות אלמנט a, הדפדפן פונה מחדש לשרת וטוען את העמוד שמתאים לכתובת החדשה. בגלל שהשרת הוא שרת פיתוח של create-react-app, השרת הזה יודע להגיש תמיד את אותו קובץ HTML, ולכן אנחנו מקבלים את אותה אפליקציה בעמוד שמתאים לכתובת החדשה. אבל זה מנגנון בזבזני - הרי יש לי כבר בדפדפן את כל הקומפוננטות וכל המידע שצריך בשביל להציג את העמוד הבא, בשביל מה לי ללכת שוב לשרת רק כדי לקבל את אותם קבצי js, css ו html שכבר יש לי? קומפוננטה מיוחדת של react-router בשם Link מציעה בדיוק את האופטימיזציה הזו. היא מקבלת מאפיין to שאומר לאיזה כתובת צריך לעבור, ומשנה רק את הכתובת בדפדפן ואת הקומפוננטה שמוצגת, בלי לעבור דרך השרת. נעדכן את העמוד שלנו עם שתי שורות ניווט כדי להמחיש את ההתנהגות של Link:
function App() {
  return (
    <BrowserRouter>
      <h1>Welcome To React Router</h1>
      <nav>
        <h2>Navigation with &lt;Link/&gt; Component</h2>
        <Link to="/">Home Page</Link>
        <Link to="/about">About Page</Link>
        <Link to="/contact">Contact Page</Link>
      </nav>
      <nav>
        <h2>Navigation with &lt;a/&gt; Tag</h2>
        <a href="/">Home Page</a>
        <a href="/about">About Page</a>
        <a href="/contact">Contact Page</a>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  );
}
שימו לב איך כשאני עובר בין הדפים עם ה Link המעבר הוא חלק, אבל כשאני עובר באמצעות לחיצה על ה a אני רואה בדפדפן טעינה מחדש של העמוד. ## איך ריאקט ראוטר פותרת חלק מהבעיות שלנו ריאקט ראוטר לא פותר את כל האתגרים שדיברנו עליהם בתחילת השיעור, אבל הוא כן עוזר עם חלקם: 1. ריאקט ראוטר מטפל בשבילנו בכל נושא ההיסטוריה ואפשרות החזרה אחורה מהדפדפן מאחר וכל דף פנימי מקבל URL משלו. 2. באמצעות מנגנון Lazy של ריאקט, אנחנו יכולים לטעון רק את הקומפוננטות שאנחנו צריכים בשביל להציג את הנתיב הנוכחי ולא חייבים לשלוח את כל האפליקציה מראש. אם אתם רוצים לשמוע עוד על מנגנון Lazy של ריאקט תוכלו לצפות בשיעור ההרחבה על הנושא. 3. ריאקט ראוטר תומך ב Server Side Rendering. פיצ'ר זה הוא מעבר להיקף של קורס זה מאחר והוא מערב שינוי קוד בצד השרת. בגדול מה ש SSR אומר זה שאנחנו מייצרים את כל ה HTML בשרת לפני ששולחים אותו ללקוח (בעצם מריצים ריאקט בצד השרת ולא בדפדפן). שילוב SSR יאפשר לייצר HTML פשוט ללקוחות שצריכים אותו כדוגמת קוראי מסך ומנועי חיפוש. ## בונוס: איך לחבר אפליקציית React Router לשרת Node.JS אמיתי בעבודה עם שרת אמיתי בדרך כלל נצטרך להגדיר לכל נתיב איזה דף HTML להגיש באותו נתיב. אם השרת שלכם כתוב ב Node.JS אולי אתם משתמשים במידלוור כדי להגיש קבצים סטטיים מתיקיית public לפי שם הקובץ שמתאים לנתיב אליו ניסו לגלוש. הבעיה שכשמחברים את זה ל react-router זה לא עובד כל כך טוב - ריאקט ראוטר דורש שבכניסה לכל נתיב באפליקציה נקבל תמיד את אותו קובץ html ואת אותם קבצי js ו css, והוא כבר בצד לקוח ידאג להראות את הקומפוננטה המתאימה. בשרת Node.JS אנחנו יכולים לקבל את ההתנהגות הזאת באמצעות הגדרת נתיב כזה:
app.get('/*', function(req, res) {
  res.sendFile(path.join(__dirname, 'path/to/your/index.html'), function(err) {
    if (err) {
      res.status(500).send(err)
    }
  })
})

ToCode
1 419
# מבוא ל React Router גירסה 6 בעולם הישן של פיתוח ווב, לכל דף באתר היתה כתובת (URL) משלו, ודף HTML שהשרת שולח כשגולש נכנס לאותה כתובת. בריאקט, ובמיוחד אם יצרתם את האפליקציה עם create-react-app, זה קצת יותר קשה ליישום. אם היינו רוצים ללכת בדרך זו, זה היה אומר שצריך אפליקציית ריאקט חדשה עבור כל דף בגלל ש create-react-app מייצר רק קובץ HTML אחד. אבל אפילו אם נצליח לייצר כמה קבצי HTML, הניהול של כל העסק הזה לא שווה את המאמץ. במקום זה הדרך המקובלת לעבור בין דפים נקראת Single Page Application. הרעיון שיש לנו רק קובץ HTML אחד עם סט אחד של קומפוננטות ריאקט, וקוד ריאקט יודע להציג את הקומפוננטה שמתאימה ל URL הנוכחי. ספריית react-router, עליה נלמד בפרק זה, מספקת דרך קלה לבניית יישומים כאלה בריאקט. ## האתגרים ביישומי עמוד יחיד לפני שנראה איך כותבים יישום Single Page, אולי אתם כבר שואלים את עצמכם בשביל מה צריך ספריה מיוחדת? למה זה כל כך קשה? אז הנה רשימת אתגרים שבלי React Router הייתם צריכים לפתור לבד: 1. טעינת הדף הראשון ביישום עשויה להיות איטית, כי כל תוכן האתר (כל הקומפוננטות) מגיע במכה אחת. 2. דפדפנים יודעים לטפל אוטומטית בניווט אחורה ושמירת היסטוריה בין דפים, אבל ביישומי עמוד יחיד עלינו לבנות מנגנונים כאלה לבד (או להשתמש בספריות קיימות) כדי לתת למשתמשים את אותה חוויה. בנוסף ביישומי עמוד יחיד יש עוד מספר אתגרים שספריית react-router לא פותרת בשבילכם וכדאי להיות מודעים אליהם: 1. התאמה למנועי חיפוש עשויה להיות מורכבת, כיוון שרק אחרי הפעלת קוד JavaScript אפשר לדעת מה להציג. לא כל מנועי החיפוש יודעים JavaScript ולכן לחלקם יהיה קשה לנווט באתר. 2. התאמה לקוראי מסך או התקני נגישות אחרים יכולה להיות מורכבת, כיוון שחייבים להריץ קוד JavaScript כדי לנווט באתר והתקנים אלה לא תמיד יודעים להריץ JavaScript כמו שצריך. 3. זליגות זיכרון ב JavaScript הופכות לבעיה אמיתית: ביישום רגיל כל מעבר עמוד מאפס את הזיכרון ולכן גם אם חלק מהקוד שלכם לא מהודק עד הסוף הזליגה לא תעשה נזק. ביישום עמוד יחיד אוביקט בזיכרון יכול לגדול ולגדול לאורך כל חיי היישום, שיכולים להיות מאוד ארוכים. בואו נתקדם ל react-router ונראה איך היא עוזרת לנו לכתוב יישומי Single Page בקלות. ## איך עובד React Router ב React Router אנחנו מגדירים טבלת ניתוב, שזו טבלה שאומרת ל react-router עבור כל נתיב איזה קומפוננטה צריך להציג. את הטבלה אנחנו כותבים בתור קומפוננטות ריאקט, ו react-router "מחליף" את כל בלוק הקומפוננטות הזה בקומפוננטה האמיתית שצריך להראות עבור הנתיב הנוכחי. אפשר לדמיין את ההתנהגות שלו בתור switch/case מתוחכם, שמסתכל על שורת הכתובת ועל טבלת הניתוב, ולפי מה שכתוב שם מחליט איזה קומפוננטה להראות. כך נראית טבלת ניתוב פשוטה:
<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</BrowserRouter>
ננסה לקרוא את זה יחד- 1. אם הכתובת בדפדפן היא / אז תחליף את כל הבלוק שבתוך Routes בקומפוננטה Home. 2. אם הכתובת בדפדפן היא /about אז תחליף את כל הבלוק שבתוך Routes בקומפוננטה About. 3. אם הכתובת בדפדפן היא /contact אז תחליף את כל הבלוק שבתוך Routes בקומפוננטה Contact. את קומפוננטת BrowserRouter אני צריך לשים פעם אחת ביישום מסביב לכל הקומפוננטות שלי, אבל את Routes אני יכול לשים בכמה מקומות, וכל מקום בו אני שם Routes יוחלף בקומפוננטה שמתאימה לנתיב. הנה קובץ App.js שממחיש את המשחק:
import './App.css';
import {
  BrowserRouter,
  Routes,
  Route,
} from "react-router-dom";

function App() {
  return (
    <>
      <h1>Welcome To React Router</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

function Home() {
  return (<p>Home Page</p>);
}

function About() {
  return (<p>About Page</p>);
}

function Contact() {
  return (<p>Contact Page</p>);
}

export default App;
נסו להפעיל את היישום ולהיכנס לכתובות:

ToCode
1 419
# הזמנה לוובינר - חידושים בריאקט 18 ביום חמישי הקרוב בעשר בבוקר אעביר פה וובינר של שעה על ריאקט 18 והחידושים שהוא מביא. ריאקט 18 יצא בסוף מרץ והגירסה העדכנית של ריאקט היא כבר 18.2. הייתרון בלחכות קצת לפני שעושים וובינר על גירסה חדשה הוא שעכשיו יחסית ברור מה עובד, מה לא עובד ומה הכיוונים לעתיד, וזה מה שנראה גם בוובינר. בתוכנית: 1. נראה מהו useDeferredValue, איזה בעיה הוא בא לפתור ואיזה בעיות הוא יוצר. 2. ממנו נגיע לדבר על Concurrent Rendering, שזה הפיצ'ר שמאפשר את useDeferredValue ונעבור על הדוגמה של useTransition כדי לראות עוד ייתרון של הרינדור המקבילי. 3. נדבר על הקושי בעבודה עם רינדור מקבילי, ונראה את הדוגמה של Render Tearing. 4. נדבר על ההתנהגות החדשה של Strict Mode שעלולה לבלבל במצב פיתוח, ועל השינוי בהתנהגות של useEffect שוב בעקבות ה Concurrent Rendering. הוובינר יעבור בזום ביום חמישי בעשר בבוקר. הקישור לפרטים והרשמה הוא: https://www.tocode.co.il/workshops/117 שווה לבוא כדי שתוכלו לשאול שאלות ולהשתתף בשיחה, אבל אם לא תוכלו להגיע זה לא סוף העולם והקלטה מסודרת תעלה לאתר וליוטיוב כמה ימים אחרי המפגש. נתראה בחמישי, ינון

ToCode
1 419
# ריאקט 18 סוף סוף מציע פיתרון לבעיית ה label-ים עד שהגיע ריאקט או בכלל הרעיון של קומפוננטות, מתכנתים ומתכנתות כתבו קבצי HTML גדולים ובתוכם היו label-ים ו input-ים המתואמים ביניהם באמצעות id:
<label for="name">User Name</label>
<input type="text" id="name" />
ואז בא ריאקט ולימד אותנו שלא כדאי להשתמש ב id על אלמנטים כי אולי מישהו הולך להשתמש בקומפוננטה שלך בעוד כמה מקומות ואז יהיה במסמך id כפול. מפה מתכנתים ומתכנתות התחילו לחפש פיתרונות יצירתיים, החל מ:
<label>
User Name
<input type="text" />
</label>
ועד דברים יותר מתוחכמים כמו המצאת מזהים:
function MyForm() {
    const id = Math.random().toString(16);
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}
הבעיה בפיתרון הראשון היא שהוא מכריח markup מסוים. כל עוד זה עובד לכם הכל טוב, אבל לפעמים באמת ה label וה input לא צמודים אחד לשני. הבעיה בפיתרון השני היא שכנראה לא נקבל את אותו id ב Server Side Rendering, מה שייצור אי תאימות כשריאקט יריץ את כל ה render-ים בדפדפן ויקבל HTML עם מזהים שונים. ריאקט 18 סוף סוף מציעים פיתרון קל ונוח לבעיה עם הוק בשם useId. פונקציה זו תחזיר מזהה ייחודי ותבטיח לכם שתקבלו את אותו מזהה ברינדור בצד שרת ובצד הלקוח. לכן הדרך הנכונה בריאקט 18 לכתוב את אותו קוד היא:
import { useId } from 'react';

function MyForm() {
    const id = useId();
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}
ה id שמקבלים לא יצירתי במיוחד ובדוגמה שניסיתי קיבלתי מחרוזת כמו :r1:, :r3 ו :r5:. אני לא יודע למה דווקא r ולמה הוא מדלג על הזוגיים. אם יש לכם רעיון או מידע פנימי אשמח לשמוע בתגובות.

ToCode
1 419
# מודל מנטאלי מודל מנטאלי זה מה שיש לך בראש לפני שהתחלת ללמוד משהו. וזה הדבר שקובע כמה אפקטיבי יהיה הלימוד. עם המודל המנטאלי הנכון, מספיקות עשר דקות בשביל לדעת לעבוד עם גיט. עם המודל המנטאלי הלא נכון, גם חודש לא יעזור. כשאני לומד נושא חדש, או בקורסים שאני מלמד, אני יודע לחפש ולזהות את רגע ה"אהה!", הרגע הזה שבו דברים פתאום מסתדרים אחרי שהרבה זמן היה קשה להבין אותם, הרגע הזה שבו המודל המנטאלי משתנה כדי להתאים לנתונים החדשים. זה רגע המפתח בדרך להבין כל נושא שלא יהיה. השאלה האמיתית (בכל לימוד), היא לא "מה עושה פקודה X או Y", אלא "מה המודל המנטאלי שאני צריך כדי להבין את הכלי הזה".

ToCode
1 419
# היום למדתי: הקשר בין globals לניקוי ה DOM ב vitest יש ימים בהם מנגנון שבכלל לא חשבת עליו פתאום מחליט להזכיר לך שהוא דווקא די חשוב. כך קרה לי היום עם מנגנון הזרקת הגלובאליים של vitest. המנגנון בגדול אומר שאם תדליקו מתג בשם globals בקובץ הקונפיגורציה בצורה כזאת:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
  },
})
אז בכל קבצי הבדיקות שלכם תוכלו להסתמך על זה שיש describe, it ו expect ואולי עוד כמה משתנים גלובאליים. ברירת המחדל היא false ואז צריך לייבא הכל לבד עם שורה כזאת בתחילת קובץ בדיקות:
import { describe, it, expect } from 'vitest';
עד לפה אין סיבה להתרגש, אבל מסתבר שלהזרקת ה globals יש עוד אפקט והוא הרבה יותר מורגש: בעת בדיקת קומפוננטות ריאקט עם react-testing-library, הספריה תנקה את ה DOM בין בדיקה לבדיקה אם ה globals מוזרקים, אבל לא תנקה בלעדיהם. במילים אחרות בקובץ בדיקות כזה:
import { describe, it, expect } from 'vitest';
import { screen, render } from '@testing-library/react';
import App from './App';

describe('App Changes Colors', () => {
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
});
אם יש הזרקת גלובאליים הכל עובד, אבל בלעדיה הבדיקה השניה נכשלת כי getByRole תקוע עם שני כפתורים על המסך, אחד מהבדיקה שהוא יצר והשני מהבדיקה הראשונה שלא נמחק. הפונקציה הרלוונטית מ testing-library שמטפלת בסיפור המחיקה נקראת cleanup וזה מה שמוסבר גם בתיעוד שלה:
    Please note that this is done automatically if the testing framework you're using supports the afterEach global and it is injected to your testing environment (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.
לכן אם אתם עובדים ב vitest בלי הזרקת גלובאליים תצטרכו לזכור להפעיל את cleanup בעצמכם. אבל יותר קל פשוט להדליק את הזרקת הגלובאליים ולא לחשוב על זה (עד העקיצה הבאה).

ToCode
1 419
# טיפ ריאקט: איך לא להשתמש ב useDeferred מצב Concurrent Mode החדש של ריאקט פותח אופציה להרבה שיפורי ביצועים, אבל כמו כל פיצ'ר חדש גם קל להתבלבל ולהשתמש בו לא נכון. נתבונן בקוד הבא:
function BrokenHugeList() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }
  console.log(`1 seach = ${deferredSearch}`);
  const searchResults = items.filter(i => i.includes(deferredSearch));
  console.log('2');

  return (
    <div>
      <input type="text" value={search} onChange={handleChange} />
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
}
הקוד משתמש ב useDeferredValue במטרה לשפר ביצועים. המחשבה היא שמשתמשים יקלידו טקסט בתיבה, ואז יהיה render נוסף שבעקבותיו המערכת תנסה לצייר על המסך את הפריטים שמתאימים לחיפוש, ואז באמצע הציור משתמש יקליד ערך אחר בתיבה וריאקט יוכל להפסיק את כל העבודה שלו, לזרוק לפח את מה שהוא רצה לצייר על המסך ולהתחיל מחדש עם הטקסט המעודכן. וזה באמת מה שקורה רק שצריך לזכור שתי נקודות שבסוף יכולות לפעול לרעתנו: 1. כדי לדעת מה לצייר בהתחלה, ריאקט כן מפעיל render נוסף עם הערך הישן של טקסט החיפוש. זה אומר שכשנסתכל במסך כלי הפיתוח אנחנו נראה את ההדפסה של 1 ו-2 מופיעה פעמיים אחרי כל שינוי טקסט, פעם אחת עם הטקסט הישן ופעם שניה עם החדש. 2. ריאקט בכל מקרה לא קוסם ולא יצליח "לעצור באמצע" את לולאת ה filter שאני כתבתי. בגלל זה כשהרשימה מספיק גדולה או שרוב העבודה היא חישובית, אנחנו עדיין נראה lag-ים. שילוב שתי הבעיות אומר שאם יש לנו לוגיקה מסובכת, לא רק ש useDeferredValue לא יעזור הוא אפילו יזיק. בגלל שאותה לוגיקה מסובכת תופעל פעמיים: פעם אחת עם הסטייט הישן ופעם שניה עם החדש. פיתרון אחד להפעלה הכפולה הוא להשתמש ב Memo, לדוגמה הקוד הבא לא סובל מבעיית רנדר כפול ונותן תוצאה קצת יותר מהירה:
const HugeList = React.memo(function HugeList(props) {
  const { search } = props;
  console.log(`3, search = ${search}`);
  const searchResults = items.filter(i => i.includes(search));
  console.log('4');

  return (
    <div>
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
});

function App() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }

  return (
    <>
      <input type="text" value={search} onChange={handleChange} /> 
      <HugeList search={deferredSearch} />
      <hr />
    </>
  );
}
נ.ב. גם אחרי השינוי, העבודה עם 50 אלף פריטים אומרת שמהירות התגובה של הקומפוננטה לא תהיה מזהירה, אבל עם כמה אלפים או אפילו 20 אלף פריטים שימוש ב useDeferredValue כן יכול לתת שיפור מורגש בזמני התגובה.

ToCode
1 419
מאמר מאוד מעניין עם נתונים רלוונטים שבדיוק מתחבר לפוסט מהבוקר https://alexkrupp.typepad.com/sensemaking/2022/06/angular-without-ssr-is-faster-than-nextjs-with-ssr-i-have-the-data.html

ToCode
1 419
# התמונה הגדולה ברור שיותר קל לדבר על הפרטים - לאיזה coverage להגיע, כמה זמן הבדיקות צריכות לרוץ, מי מתחזק את שרת ה CI שמריץ אותן או באיזה כלי צריך לכתוב את הבדיקות. אבל הערך האמיתי הוא בתמונה הגדולה - למה בכלל אנחנו בודקים? איזה ערך מקבלים מכל סוג בדיקות? כמה זה עולה לנו? זה לא עוזר לנסוע ב 100 קמ"ש בכיוון הלא נכון. גם אם קשה וארוכה, השיחה החשובה היא על התמונה הגדולה. כשזה יהיה ברור השאר ידאג לעצמו.

ToCode
1 419
# המחיר של יצירתיות השקענו כבר כל כך הרבה... כל הלקוחות שלנו מחכים לפיצ'ר הזה... זה ממש כמעט עובד... --- אם המשפטים האלה נשמעים לכם מוכרים זה לא במקרה. רוב האנשים, הצוותים והארגונים לא אוהבים להיכשל, וכשמשהו לא הולך בכיוון הרצוי הנטיה הטבעית היא להתעקש ולהמשיך. וזה נכון במיוחד בצד הטכני. הרי אותו פיצ'ר שאנחנו מנסים לבנות כבר קיים איפשהו באיזושהי מערכת בעולם, אז מה פתאום רק אני לא מצליח לבנות אותו? מה פתאום רק הצוות שלי לא מצליח לבנות אותו? ומה זה אומר עלינו? וזו לא שאלה של מחירים. המשמעות של כישלון היא לא שאני מסתכל על העלות ובוחר לא להתקדם עם הפיצ'ר. המשמעות של כישלון היא שאני עובד ועובד ולא רואה תוצאה. המשמעות היא חור שחור של באגים, בו כל צעד קדימה לוקח אותי שני צעדים אחורה. כישלון טכני זה להבין אחרי חצי שנה של עבודה שבעצם אין לך מושג מה אתה עושה. זה להבין אחרי חצי שנה של עבודה שבעצם למתכנתים שלך אין מושג מה הם עושים. שברגע הזה, זה לא משנה אם נקבור את כל מה שעשינו בחצי שנה האחרונה ונתחיל מחדש או נמשיך מאיפה שאנחנו נמצאים - כמות העבודה עד לסיום תהיה זהה. בתפיסת העולם של רובנו זאת מסקנה יותר מדי קשה לעיכול. --- לבעיה הזאת יכולים להיות רק שני פיתרונות: אנחנו יכולים להחליט שאצלנו לא יהיו כישלונות טכניים. שכשמתכנתים מתחילים לעבוד על פיצ'ר מסוים הם חייבים להגיע לתוצאה עובדת; או שאנחנו יכולים להחליט לאמץ את הכישלונות כחלק מתהליך הפיתוח, ולבנות מנגנונים שיזהו אותם ויהפכו אותם לנסבלים. בפיתרון הראשון, דרך טובה לא להיכשל היא לעודד אנשים לעשות פחות: לגייס מתכנתים שמעדיפים לקחת פחות סיכונים, לעבוד רק על פיצ'רים שאפשר לסיים מהר, ליצור תהליך פיתוח שבו כל פיצ'ר עובר 20 שלבים של ביקורת ואיפיון לפני שמגיע לצוות הטכני, וככה כשזה כבר מגיע הכל לעוס ומוכן לפיתוח. לא יכול להיכשל. ארגונים שרוצים ללכת יותר רחוק בדרך הזאת יכולים לייצר מדדי איכות כמו "כמה באגים תיקנת היום" או "כמה פיצ'רים מימשת השבוע" ואפילו להעניש על תקופות ארוכות בלי פיצ'רים. בפיתרון השני , בשביל להתמודד טוב יותר עם כשלונות אנחנו יכולים להפוך אותם למשהו יותר יום יומי: אנחנו יכולים להציע תקשורת טובה יותר בין צוותי הפיתוח לצוות הפרודאקט; אנחנו יכולים להצמיד לכל פיצ'ר, יחד עם האיפיון, גם מגבלת זמן שאחריה לא נמשיך לעבוד עליו; אנחנו יכולים לתת למתכנתים לעבוד על כמה פיצ'רים במקביל ולראות איזה כיוון מושך אותם יותר ועל איזה פיצ'רים אף אחד לא רוצה לעבוד, ואולי אותם לקבור. בקצרה ליצור תרבות בה מתכנתים ירגישו נוח להגיד "כרגע אין לי מושג איך להתקדם עם הפיצ'ר הזה. בואו נשים אותו בצד ואולי בעתיד נחזור אליו". תרבות ארגונית נטולת כשלונות נותנת למנהלים שקט נפשי. תרבות ארגונית שמחבקת כשלונות נותנת למתכנתים חופש יצירתי ומאפשרת פיתוח מערכות טובות יותר, בגלל שיהיה לנו יותר קל לקחת סיכונים שמיטיבים עם המוצר. היא גם מורידה לחץ ומאפשרת למתכנתים להתרכז בעבודה ולא לחפש קיצורי דרך רק בשביל לסמן "וי" על עוד פיצ'ר. כמתכנת, זאת התרבות הארגונית שאני מעדיף לעבוד בה.

ToCode
1 419
# ריפקטורינג ותכנות מונחה עצמים (או: הסיבה שתכנות מונחה עצמים לא עובד) ביום 7 של Advent Of Code האחרון קיבלו את פנינו חבורה של סרטנים שניסו להגן עלינו מלוויתן תוקפני. בשביל לעשות את זה הם היו צריכים למצוא מקום טוב לעמוד בו. ## חלק ראשון הקלט של התרגיל היה רשימת המיקומים של הסרטנים על ציר אופקי בלבד, למשל:
16,1,2,0,4,2,7,1,2,14
ואנחנו צריכים למצוא להם מקום טוב לעמוד בו. המקום הכי טוב בשבילם יהיה זה שהכי קל לכולם להגיע אליו, כלומר שסכום הצעדים שכל הסרטנים צריכים לעשות כדי להגיע ליעד יהיה הכי קטן. אינטואיציה אחת לפתור את התרגיל הזה למי שרוצה ללכת בגישה מונחית עצמים היא לכתוב מחלקה בשביל כל הסרטנים האלה. ברובי כתבתי משהו כזה:
class CrabsSwarm
  def initialize(filename)
    @crabs = File.read(filename).split(',').map(&:to_i)
  end

  def fuel_needed_to_reach(target)
    @crabs.map {|e| (e - target).abs }.sum
  end

  def solve
    target = (@crabs.min..@crabs.max).min_by {|target| fuel_needed_to_reach(target) }
    fuel_needed_to_reach(target)
  end
end
יש אגב עוד המון חלוקות אפשריות למחלקות ואולי בכלל לא חייבים מחלקות. זה לא חשוב בשביל הסיפור שלנו. תראו מה קורה בחלק השני. ## חלק שני - משנים את החישוב למרות שמצאתי מקום ממש טוב בשביל הסרטנים (על קלט הדוגמה זו היתה משבצת 2), הם לא הסכימו ללכת לשם. ההסבר בתרגיל היה שהצוללת של כל סרטן לא צורכת דלק בצורה לינארית אלא בסידרה חשבונית. כלומר בשביל לעבור מרחק של 3 נקודות הסרטן צריך להשתמש ב 1+2+3 כלומר 6 יחידות דלק. עם הנתונים החדשים נשלחנו למצוא נקודה חדשה אליה יוכלו הסרטנים להתקבץ. הבעיה, שלמרות שבחלק הראשון חשבתי שחילקתי נכון את התרגיל למחלקה ופונקציות, כשאני מגיע להרחיב את אותה מחלקה בחלק השני אני נאלץ לכתוב:
class CrabsSwarmPart2 < CrabsSwarm
  def fuel_needed_to_reach(target)
    @crabs.map {|e| (0..(e - target).abs).sum }.sum
  end
end
בום. ה map מהחלק הראשון מופיע שוב גם בחלק השני. זה לא נראה חשוב ובגלל זה בהרבה פרויקטים עוצרים כאן. אבל בעולם האמיתי זאת יכולה להיות בעיה. ## מה אני לומד מהדרישה החדשה הבעיה פה היא שבמקום לחפש מה אני לומד מהדרישה החדשה, אני ממשיך להתעקש להתאים את הדרישה החדשה לסכימה שכבר כתבתי. לאורך זמן ככל שיותר דרישות יופיעו ולא יתאימו לסכימה שחשבתי עליה מראש, מימוש כל דרישה נוספת הופך ליותר ויותר קשה. האלטרנטיבה היא לשאול - מה אני לומד מהדרישה החדשה? במקרה של הסרטנים אני לומד שיש סיכוי טוב שהם ירצו להמשיך להחליף כלי תחבורה בעתיד, ושכלי תחבורה סרטניים שונים ביניהם בצריכת הדלק שלהם. זאת הנקודה ששווה לי להתאמץ ולשכתב חלק מהקוד כדי לתת יותר משקל לדרישה החדשה. הנה הניסיון השני שלי:
class CrabsSwarm
  def initialize(filename)
    @crabs = File.read(filename).split(',').map(&:to_i)
  end

  def energy(from, to)
    (from - to).abs
  end

  def fuel_needed_to_reach(target)
    @crabs.map {|e| energy(e, target) }.sum
  end

  def solve
    target = (@crabs.min..@crabs.max).min_by {|target| fuel_needed_to_reach(target) }
    fuel_needed_to_reach(target)
  end
end
נכון, קוד יותר ארוך, אבל גם יותר קל להרחבה בנקודה הנכונה. פיתרון החלק השני יהיה בסך הכל שינוי של הפונקציה energy:
class CrabsSwarmPart2 < CrabsSwarm
  def energy(from, to)
    (0..(from - to).abs).sum
  end
end
וזה בדיוק מה שהחלק השני מדבר עליו. ליישום אותו מהלך בעולם האמיתי יש השלכות: זה אומר שאנחנו מוכנים לעדכן גם דברים בסיסיים במערכת בשביל לממש פיצ'רים חדשים, ושאין קוד חשוב מכדי לשנות אותו. גישה כזאת משאירה את כל קוד המערכת "חי" ועוזרת לייצר מערכות שמותאמות לדרישות האמיתיות והעדכניות של החיים.