هر سه تکه اجرا میشوند، هر سه کار را انجام میدهند، و دو تا از آنها نباید وارد پروژه شوند. کار سادهای بود: «شمارهی کاربر را ذخیره کن و پیامک خوشامد بفرست.» از دستیار کدنویسی سه بار خواسته شد و سه جواب آمد.
کدنویسی با هوش مصنوعی سرعت نوشتن را بالا برده، ولی سرعت بازبینی را نه. همین فاصله مهارت تازهای میخواهد که کمتر جایی آموزش داده میشود: قضاوت دربارهی کدی که خودت ننوشتهای. این سه تکه، که مثالی فرضی برای نشان دادن همین قضاوتاند، نشان میدهند این قضاوت از کجا شروع میشود.
سه جواب برای یک درخواست
تکهی اول: ساده و بیمحافظ
کاربر را میسازد، پیامک را میفرستد و کاربر را برمیگرداند. سه ایراد دارد. شماره را اعتبارسنجی نمیکند، پس شمارهی خراب هم ذخیره میشود و پیامک هم برایش میرود و پول خرج میشود. اگر پیامک شکست بخورد، کاربر ساخته شده و استثنا بالا میرود؛ صدا زدن دوباره یک کاربر تکراری میسازد. و برای ارسال پیامک هیچ مهلت زمانی ندارد.
تکهی دوم: «امنترش کردیم»
همان کد اول است که کلش داخل یک try و catch پیچیده شده و در catch مقدار null برمیگرداند. ظاهرش محتاطتر است و از اولی بدتر است. حالا هر خطایی بیصدا خورده میشود. کسی که این تابع را صدا زده نمیداند چه شد: کاربر ساخته شد؟ پیامک رفت؟ هیچکدام؟ null به هیچکدام از این پرسشها جواب نمیدهد.
تکهی سوم: بلندتر، ولی نه به این دلیل بهتر
اول شماره را به شکل استاندارد موبایل ایرانی درمیآورد و اگر نامعتبر بود با دلیل روشن برمیگردد. کاربر را با «پیدا کن یا بساز» در سطح پایگاه داده یکتا میکند، نه با «اول بخوان، بعد بنویس». اگر کاربر از قبل بوده، پیامک دوباره نمیفرستد. ارسال پیامک مهلت دهثانیهای دارد. و اگر پیامک شکست بخورد، هر دو واقعیت را برمیگرداند: کاربر ساخته شد، خوشامد نرفت. خطا را هم با لاگر خود پروژه ثبت میکند.
طول کد معیار نیست. تکهی سوم بهتر است چون به هر پرسشی که دربارهی مسیرهای ناخوش میشود پرسید جواب روشن دارد: شمارهی خراب، کاربر تکراری، سرویس پیامکی که جواب نمیدهد، و صدا زده شدن دوبارهی همین تابع. کسی که این تابع را صدا میزند هم از نتیجه میفهمد دقیقا چه شده و میتواند تصمیم بگیرد؛ مثلا خوشامد را بعدا دوباره بفرستد.
مقایسه با شش پرسش
| پرسش | تکهی اول | تکهی دوم | تکهی سوم |
|---|---|---|---|
| ورودی نامعتبر را میگیرد؟ | نه | نه | بله |
| خطا کجا میرود؟ | بالا پرتاب میشود | خورده میشود | لاگ میشود و در نتیجه میآید |
| اجرای تکراری امن است؟ | نه | نه | بله |
| عوارض جانبی محافظت شده؟ | نه | نه | مهلت دارد |
| وابستگی تازه آورده؟ | نه | نه | نه |
| با بقیهی پروژه میخواند؟ | معلوم نیست | معلوم نیست | از لاگر پروژه استفاده میکند |
یک جزئیات هم فقط با خواندن دقیق پیدا میشود: در تکهی سوم، شماره پیش از ثبت در لاگ پوشانده شده است. لاگ کردن شمارهی کامل نشت داده است، و هیچکدام از دو تکهی دیگر به آن فکر نکرده بودند.
هر سه تکه در مسیر خوش کار میکردند. تفاوت در چیزی بود که در مسیر خوش دیده نمیشود. و نکتهی تکهی دوم از همه مهمتر است: کدی که شبیه کد محتاط است میتواند از کد ساده بدتر باشد. try و catch بدون تصمیم دربارهی اینکه با خطا چه کنیم، فقط خطا را نامرئی میکند.
شش پرسش پیش از پذیرش هر تکه
- چه چیزی را فرض گرفته که به او نگفتهام؟ شکل داده، وجود یک فیلد، ترتیب اجرا.
- خطاها کجا میروند؟ اگر catch دارد، چیزی میگوید یا فقط قورت میدهد؟
- با ورودی خالی، صفر یا تکراری چه میکند؟
- عوارض جانبی دارد؟ اگر چیزی مینویسد، میفرستد یا پول خرج میکند، سختگیرتر بخوانش.
- وابستگی تازه آورده؟ اگر بله: لازم است؟ پروانهاش چیست؟ کسی نگهش میدارد؟
- با بقیهی کد پروژه میخواند؟ همان الگوی مدیریت خطا، همان روش لاگ، همان سبک نامگذاری.
همهی کدها یکاندازه وقت نمیخواهند. کدی را که عوارض جانبی ندارد سریع بخوان؛ کدی را که مینویسد یا میفرستد خط به خط. نسبت خطر به حجم در این دو کاملا فرق دارد. و بهجای پرسیدن «درست است؟» از خودت بپرس «در چه شرایطی غلط میشود؟». پرسش دوم تقریبا همیشه چیزی پیدا میکند.
مشکل اصلی: کدی که مال این پروژه نیست
رایجترین ایراد کد تولیدشده غلط بودنش نیست؛ این است که به این پروژه تعلق ندارد. کتابخانهای را صدا میزند که پروژه ندارد، الگوی مدیریت خطایش با بقیه فرق دارد، یا طوری لاگ میکند که با سیستم هشدار پروژه جور نیست. همهی اینها «کار میکنند»، تا روزی که کسی بخواهد نگهشان دارد.
برای دیدن اثر این موضوع، یک آزمایش ساده روی کد خودت انجام بده. یک کار کوچک واقعی انتخاب کن. یک بار فقط توصیف کار را به دستیار بده و خروجی را نگه دار. بار دوم همراهش سه چیز بفرست: یک تابع مشابه از همان پروژه، قیدهای صریح (بدون وابستگی تازه، این لاگر، این سبک)، و اینکه خروجی کجا استفاده میشود. هر دو خروجی را با شش پرسش بسنج و بشمار هر کدام چند جا رد شد. وقتی را که برای نوشتن این زمینه گذاشتی هم یادداشت کن.
نتیجه معمولا این است: خروجی بیزمینه از نظر منطق درست است و از نظر تعلق به پروژه غلط. و مقایسهی زمان غافلگیرکننده است؛ نوشتن زمینه چند دقیقه میگیرد و اصلاح خروجی بیزمینه اغلب بیشتر.
خطاهایی که در کد تولیدشده تکرار میشوند
- متدی که وجود ندارد. کد کاملا معقول به نظر میرسد و تابعی را صدا میزند که در آن نسخه از کتابخانه نیست. سریعترین تشخیص: مستندات همان نسخه را نگاه کن، نه حافظهات را.
- الگوی درست در جای غلط. مثلا تلاش دوباره روی کاری که عوارض جانبی دارد. جای دیگری کاملا درست است؛ اینجا پیامک را دو بار میفرستد.
- اعتبارسنجی ظاهری. بررسی میکند مقدار وجود دارد، نه اینکه معتبر است. شرطی که فقط خالی نبودن شماره را بسنجد، کلمهی «سلام» را هم قبول میکند.
- ارقام فارسی. اعتبارسنجی عدد با الگویی که فقط ارقام لاتین را میشناسد. کاربر ایرانی عدد را فارسی مینویسد و کد بیصدا ردش میکند.
وقتی نوشتن دو برابر سریع شد
اگر دستیار سرعت نوشتنت را دو برابر کرده و عادت بازبینیات همان مانده، تنها نتیجه این است که کد بازبینینشده را سریعتر تولید میکنی. گلوگاه جابهجا شده است: از نوشتن به خواندن. پس سهم وقتی که برای بازبینی میگذاری باید بیشتر شود، نه کمتر، و تستها هم کم نمیشوند. شبکهی ایمنی وقتی سریعتر کد میسازی لازمتر است.
وابستگیای که روز فروش محصول پیدا میشود
دستیار کدنویسی راحت کتابخانه پیشنهاد میدهد و نصبش چند ثانیه طول میکشد. ولی اگر محصولی که میسازی قرار است فروخته شود، پروانهی هر وابستگی اهمیت پیدا میکند. MIT، Apache و BSD معمولا دردسری نمیسازند. AGPL میتواند تو را ملزم کند کد منبع کل محصول را منتشر کنی، حتی اگر فقط از راه شبکه ارائه شود.
این را معمولا وقتی میفهمی که وکیل خریدار سر میز است. بیرون کشیدن یک وابستگی از محصول جاافتاده میتواند ماهها کار باشد. ممیزی پروانه را پیش از وابسته شدن انجام بده، نه پیش از فروش.
کاری که از تو برمیآید این است که بدانی چه داری. فهرست پروانهی همهی وابستگیها را دربیاور، هر چه GPL یا AGPL است علامت بزن و بنویس کجا استفاده میشود، و برای هر کدام یک تصمیم مکتوب بگیر: نگه میداریم، جایگزین میکنیم، یا خودمان مینویسیم. ابزاری که فقط هنگام ساخت به کار میرود معمولا وضعیت متفاوتی دارد با کتابخانهای که در سرویس زنده اجرا میشود، ولی این تفکیک را با اطمینان انجام نده. برای محصولی که فروخته میشود، این پرسش حقوقی است و جواب دقیقش را باید از کسی بگیری که کارش همین است. این نوشته جای آن مشورت را نمیگیرد.
تمرین روی سه تکه از کار خودت
سه تکه کد تولیدشده از کار اخیرت را بردار. هر سه را با شش پرسش بسنج و جدولی شبیه جدول بالا بساز. برای هر «نه» یک سناریوی مشخص بنویس که در آن کد غلط میشود؛ نه یک نگرانی کلی، یک ورودی مشخص یا یک ترتیب مشخص از اتفاقها. اگر برای هیچ خانهای سناریو پیدا نکردی، احتمالا هنوز با چشم مسیر خوش میخوانی.
اگر میخواهی این قضاوت را کنار بقیهی مهارتهای کد حرفهای تمرین کنی (خواندن کدبیس ناآشنا، پیدا کردن شکست خاموش، تستی که واقعا محافظت میکند و بازبینی کد) جزئیات این دوره را ببین.