بازبینی کد (Code Review)
هدف بازبینی، پیدا کردن مشکلهای مهم قبل از رسیدن به کاربر و یاد گرفتن تیم از هم است. وقتت را روی درستی، طراحی، امنیت و تست بگذار و ظاهر کد را به ابزار بسپار. نظر را روشن، مهربان و با دلیل بنویس.
نویسنده: bezzad
مشکل: «به نظر خوب است»
در فروشگاه اینترنتی ما، یک برنامهنویس قابلیت «کد تخفیف» را اضافه کرده است. یک Pull Request با ۹۰۰ خط تغییر میفرستد.
دو بازبین آن را میبینند:
- بازبین اول پنج دقیقه نگاه میکند و مینویسد «به نظر خوب است». تأیید.
- بازبین دوم بیست نظر مینویسد: فاصله اضافه، ترتیب using ها، اسم یک متغیر.
یک هفته بعد، یک کد تخفیف ده درصدی هزار بار استفاده میشود. سقف استفاده هیچ وقت چک نمیشد. هیچ کدام از دو بازبین این را ندید.
هر دو بازبین وقت گذاشتند. ولی هیچ کدام به سؤال اصلی نگاه نکرد: آیا این کد درست کار میکند؟
هدف Code Review چیست؟
بازبینی کد چند هدف دارد. به ترتیب اهمیت:
- پیدا کردن مشکل قبل از کاربر. باگ منطقی، مشکل امنیتی، حالتهایی که فراموش شدهاند.
- سالم نگه داشتن طراحی. کد در جای درست است؟ یک سال بعد هم قابل تغییر است؟
- پخش دانش در تیم. حالا دستکم دو نفر این بخش را میشناسند.
- یک سبک مشترک. کد همه تیم شبیه هم باشد.
ظاهر کد را به ابزار بسپار
بحث درباره فاصله و ترتیب خطها، وقت آدمها را هدر میدهد و فقط حال همه را بد میکند. ماشین این کار را بهتر و بیطرف انجام میدهد:
- فایل editorconfig. قانونهای سبک کد را یک جا برای همه تیم مینویسد.
- دستور dotnet format. کد را بر اساس همان قانونها مرتب میکند. در CI میشود فقط چک کرد که کد مرتب است یا نه.
- تحلیلگرهای کد (Analyzers). هشدارهای کامپایلر و قانونهای کیفیت را در زمان ساخت نشان میدهند. میشود هشدارهای مهم را خطا کرد تا ساخت شکست بخورد.
- تستها در CI. اگر تستها شکست خوردهاند، آدم هنوز وقتش را نگذارد.
<!-- Directory.Build.props: same rules for every project -->
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
حالا بازبین آزاد است تا روی چیزهایی فکر کند که ماشین نمیفهمد.
یک بازبینی واقعی
این بخش اصلی همان Pull Request کد تخفیف است. قبل از خواندن ادامه، خودت نگاه کن. چند مشکل میبینی؟
public async Task<decimal> ApplyDiscount(Guid orderId, string code)
{
var order = await _db.Orders.FindAsync(orderId);
var discount = await _db.Discounts.FirstAsync(d => d.Code == code);
order.Total = order.Total - order.Total * discount.Percent / 100;
discount.UsedCount++;
await _db.SaveChangesAsync();
return order.Total;
}
یک بازبین خوب از پایه هرم شروع میکند:
- درستی. تاریخ انقضا و سقف استفاده چک نمیشود. همان باگ هزار بار استفاده.
- درستی. اگر کاربر دو بار دکمه را بزند، تخفیف دو بار کم میشود.
- درستی. اگر کد تخفیف وجود نداشته باشد، متد FirstAsync خطا میدهد و کاربر خطای 500 میبیند، نه پیام «کد نامعتبر است».
- امنیت. هیچ چکی نیست که این سفارش مال همین کاربر باشد. هر کسی با شناسه سفارش، روی سفارش دیگران تخفیف میگذارد.
- همزمانی. اگر دو مشتری همزمان آخرین ظرفیت کد را استفاده کنند، هر دو موفق میشوند. افزایش UsedCount بدون کنترل همزمانی است.
- طراحی. قانون تخفیف داخل سرویس است و جمع سفارش از بیرون تغییر میکند. قانون باید داخل خود سفارش باشد.
- تست. هیچ تستی برای انقضا، سقف و دو بار اعمال نیست.
- جزئی. اسم متد پسوند Async ندارد و CancellationToken نمیگیرد.
بعد از بازبینی، کد این شکل را پیدا میکند:
public async Task<IResult> ApplyDiscountAsync(
Guid orderId, string code, string customerId, CancellationToken ct)
{
var order = await db.Orders.SingleOrDefaultAsync(
o => o.Id == orderId && o.CustomerId == customerId, ct);
if (order is null)
return Results.NotFound();
var discount = await db.Discounts.SingleOrDefaultAsync(d => d.Code == code, ct);
if (discount is null || !discount.CanBeUsed(time.GetUtcNow()))
return Results.BadRequest("The discount code is not valid.");
order.ApplyDiscount(discount); // throws if the order already has a discount
discount.MarkUsed(); // UsedCount is a concurrency token
await db.SaveChangesAsync(ct); // a second, parallel use fails here
return Results.Ok(order.Total);
}
چطور نظر بنویسیم؟
نظر خوب سه چیز دارد: مشکل، دلیل و اهمیت. درباره کد حرف بزن، نه درباره آدم.
بد
- «این غلط است.»
- «چرا اینطوری نوشتی؟»
- «من بودم این کار را نمیکردم.»
- «سقف چک نشده.» (بدون اینکه معلوم باشد مانع ادغام است یا نه)
خوب
- «مانع ادغام: سقف استفاده چک نمیشود. یک کد ده درصدی میتواند بینهایت بار استفاده شود. میشود یک تست برایش اضافه کنی؟»
- «سؤال: اگر کاربر دو بار دکمه را بزند چه میشود؟ من فکر میکنم تخفیف دو بار کم میشود.»
- «جزئی: پسوند Async برای هماهنگی با بقیه کد. مانع ادغام نیست.»
یک برچسب کوچک اول هر نظر، کار نویسنده را خیلی راحت میکند. بعضی تیمها برای این کار قالبی به اسم Conventional Comments دارند:
- مانع ادغام. باید قبل از ادغام درست شود.
- پیشنهاد. فکر میکنم بهتر است، ولی تصمیم با نویسنده است.
- سؤال. نفهمیدم. شاید مشکل نباشد.
- جزئی (nit). خیلی کوچک. اگر نخواستی، درستش نکن.
- تعریف. کار خوبی دیدی؟ بگو. بازبینی فقط پیدا کردن اشکال نیست.
کار نویسنده: بازبینی را آسان کن
بازبینی خوب از نویسنده شروع میشود.
- تغییر کوچک. هر Pull Request یک ایده داشته باشد. تغییر اسمها را جدا بفرست.
- توضیح روشن. چه چیزی تغییر کرد؟ چرا؟ چطور تست شد؟ لینک کار مربوط را بگذار.
- اول خودت مرور کن. قبل از فرستادن، تغییرها را یک بار مثل یک بازبین بخوان. خیلی از اشکالها همینجا پیدا میشوند.
- جاهای حساس را نشان بده. «لطفاً به منطق همزمانی دقت کن. مطمئن نیستم.»
چکلیست بازبین
- رفتار. کد کاری را میکند که توضیح میگوید؟ حالتهای مرزی چه؟ ورودی خالی، تکراری، خیلی بزرگ؟
- خطا. اگر سرویس دیگر جواب ندهد چه میشود؟ خطا بیصدا خورده میشود؟
- امنیت. ورودی کاربر چک میشود؟ کاربر فقط به داده خودش میرسد؟ Secret در کد هست؟
- داده و همزمانی. دو درخواست همزمان چه میکنند؟ تراکنش درست است؟ Migration روی جدول بزرگ چه اثری دارد؟
- کارایی. کوئری داخل حلقه هست؟ داده بیشتر از لازم خوانده میشود؟
- طراحی. کد در جای درستی است؟ یک چیز تکراری ساخته شده که قبلاً وجود داشت؟
- تست. رفتار مهم تست دارد؟ تست واقعاً شکست میخورد اگر کد غلط باشد؟
- خوانایی. کسی که فردا این را میخواند، میفهمد؟ اسمها به زبان کسبوکار هستند؟
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| تمرکز روی فاصله و سبک | وقت هدر میرود و باگ واقعی دیده نمیشود. | ابزار خودکار برای سبک. آدم برای منطق. |
| تأیید سرسری یک تغییر بزرگ | باگها به Production میرسند. | تغییر کوچک بخواه. |
| نظر بدون دلیل یا با لحن تند | نویسنده دفاعی میشود و چیزی یاد نمیگیرد. | مشکل، دلیل و اهمیت را بگو. درباره کد حرف بزن. |
| رد کردن برای سلیقه شخصی | کار متوقف میشود و تیم دلسرد میشود. | اگر کد بهتر شده، تأیید کن. سلیقه را با برچسب جزئی بگو. |
| چند روز منتظر گذاشتن | نویسنده سراغ کار دیگری میرود و تغییرها با هم تداخل پیدا میکنند. | سریع جواب بده، حتی اگر فقط یک نظر اولیه باشد. |
| بازنویسی کد به جای نویسنده | نویسنده یاد نمیگیرد و حس مالکیت ندارد. | مشکل را بگو، راه حل را به او بسپار. |
| فقط خواندن تغییرها، نه بافت آنها | مشکل در کدی است که تغییر نکرده، ولی از این تغییر اثر میگیرد. | فراخوانندهها و جاهای مرتبط را هم باز کن. |
خلاصه در شش خط
- هدف اول بازبینی پیدا کردن مشکلهای مهم است، نه سبک کد.
- سبک کد را به ابزار بسپار: فایل editorconfig، دستور dotnet format و Analyzerها.
- از پایه هرم شروع کن: درستی، امنیت، طراحی و تست.
- نظر را با برچسب، دلیل و لحن مهربان بنویس. درباره کد حرف بزن، نه آدم.
- نویسنده تغییر را کوچک، با توضیح روشن و بعد از مرور خودش بفرستد.
- اگر کد کیفیت را بهتر میکند و مشکل مهمی ندارد، تأیید کن.