Skip to content

فصل ۱ — بازآرایی

یک‌بار از من پرسیدند چرا این‌قدر بازآرایی را دوست دارم. چه چیزی مرا بارها به این نوع پروژه‌ها در کار برمی‌گرداند؟ گفتم چیزی در آن اعتیادآور است. شاید همان کار سادهٔ مرتب‌کردن باشد، مثل دسته‌بندی و چیدن منظم ادویه‌ها؛ شاید لذت خلوت‌کردن و بالاخره کنار گذاشتن چیزی، مثل بردن کیسه‌ای لباس فراموش‌شده به مرکز خیریه؛ و شاید همان صدای کوچک در سرم که یادآور می‌کند این تغییرات کوچک و تدریجی در مجموع زندگی روزانهٔ همکارانم را به‌طور محسوسی بهتر می‌کند. فکر می‌کنم ترکیب همهٔ این‌هاست.

در خودِ عمل بازآرایی چیزی هست که می‌تواند همهٔ ما را جذب کند، چه در حال ساخت قابلیت‌های جدید محصول باشیم و چه در حال مقیاس‌پذیر کردن زیرساخت. همهٔ ما باید در کارمان بین نوشتن کد بیشتر یا کمتر تعادل برقرار کنیم. باید تلاش کنیم اثرات پایین‌دستی تغییراتمان را بفهمیم، چه عمدی باشند چه ناخواسته. کد موجود زنده است. وقتی به کدی که نوشته‌ام فکر می‌کنم که پنج یا ده سال دیگر هم زنده بماند، نمی‌توانم کمی دل‌درسوز نشوم. قطعاً امیدوارم تا آن زمان کسی بیاید و یا آن را کاملاً حذف کند یا با چیزی تمیزتر و — مهم‌تر از همه — متناسب‌تر با نیازهای آن زمان جایگزینش کند. بازآرایی دقیقاً دربارهٔ همین است.

در این فصل، ابتدا چند مفهوم را تعریف می‌کنیم. تعریف پایه‌ای برای بازآرایی در حالت کلی پیشنهاد می‌دهیم و روی آن می‌سازیم تا تعریف جداگانه‌ای برای بازآرایی در مقیاس بزرگ توسعه دهیم. برای چارچوب‌بندی انگیزه‌های این کتاب، بحث می‌کنیم چرا باید به بازآرایی اهمیت دهیم و اگر این مهارت را تقویت کنیم چه مزایایی برای تیم‌ها دارد. سپس به برخی مزایا و برخی خطرهایی که هنگام تصمیم‌گیری برای انجام آن باید در نظر بگیریم می‌پردازیم. با دانش از این مبادلات، سناریوهایی را بررسی می‌کنیم که زمان مناسب و نامناسب است. در پایان، یک مثال کوتاه می‌زنیم تا این مفاهیم زنده شوند.

بازآرایی چیست؟

به‌سادگی، بازآرایی فرایندی است که در آن کد موجود را (factoring) بدون تغییر رفتار بیرونی‌اش بازساختار می‌دهیم. اگر فکر می‌کنید این تعریف بسیار کلی است، نگران نباشید؛ عمداً همین‌طور است! بازآرایی بسته به کدی که روی آن اعمال می‌شود می‌تواند اشکال متعدد و به‌طور یکسان مؤثر داشته باشد. برای توضیح، «سیستم» را هر مجموعهٔ تعریف‌شده‌ای از کد می‌دانیم که از مجموعه‌ای ورودی، مجموعه‌ای خروجی تولید می‌کند.

فرض کنید پیاده‌سازی مشخصی از چنین سیستمی به نام S داریم، مانند شکل ۱-۱. سیستم تحت مهلت فشرده ساخته شده و نویسندگان را به کوتاه‌بردن راه انداخته است. با گذشت زمان به توده‌ای بزرگ از کد درهم‌تنیده تبدیل شده. خوشبختانه مصرف‌کنندگان سیستم مستقیماً با آشفتگی درونی آن روبه‌رو نیستند؛ با S از طریق رابط تعریف‌شده تعامل می‌کنند و به نتایج ثابت آن تکیه می‌کنند.

شکل ۱-۱. یک سیستم ساده با ورودی‌ها و خروجی‌ها.

چند توسعه‌دهندهٔ شجاع درون سیستم را مرتب کردند و اکنون آن را S' می‌نامیم، مانند شکل ۱-۲. شاید سیستم مرتب‌تری شده باشد، اما برای مصرف‌کنندگان S' مطلقاً هیچ‌چیز تغییر نکرده است.

شکل ۱-۲. یک سیستم سادهٔ بازآرایی‌شده با ورودی‌ها و خروجی‌ها.

سیستم S می‌تواند هر چیزی باشد؛ یک دستور if، تابع ده‌خطی، کتابخانهٔ متن‌باز محبوب، برنامهٔ چندمیلیونی خطی، یا هر چیز میان این‌ها. (ورودی‌ها و خروجی‌ها هم به‌طور یکسان متنوع می‌توانند باشند.) سیستم می‌تواند روی ورودی‌های پایگاه داده، مجموعهٔ فایل‌ها یا جریان داده کار کند. خروجی‌ها محدود به مقادیر برگشتی نیستند و می‌توانند اثرات جانبی مثل چاپ در کنسول یا ارسال درخواست شبکه هم باشند. می‌بینید چگونه یک سرویس RESTful مسئول عملیات روی موجودیت‌های کاربر می‌تواند به تعریف سیستم ما در شکل ۱-۳ نگاشت شود.

شکل ۱-۳. یک برنامهٔ ساده به‌عنوان سیستم.

با ادامهٔ ساختن تعریف بازآرایی و کاوش جنبه‌های مختلف فرایند، بهترین راه برای هم‌صفحه بودن همهٔ ما این است که هر ایده را به یک مثال مشخص و واحد وصل کنیم.

استفاده از مثال‌های برنامه‌نویسی دنیای واقعی به چند دلیل دشوار است. با توجه به گستردگی تجربه‌ها در صنعت، انتخاب یک مثال بر دیگری فوراً به گروهی از خوانندگان برتری می‌دهد. از طرف دیگر، کسانی که با مثال آشنا هستند ممکن است وقتی برخی مفاهیم برای اختصار ساده می‌شوند یا برخی ظرافت‌ها نادیده گرفته می‌شوند تا مفهومی تمیزتر اعمال شود، ناامید شوند. به امید ایجاد میدان بازی برابر، هر وقت بخواهیم مسئلهٔ کلی را در سطح بالا نشان دهیم، از کسب‌وکاری آشنا برای (امیدواریم) بیشتر ما استفاده می‌کنیم: یک خشک‌شویی.

خشک‌شویی سیمون یک کسب‌وکار خشک‌شویی محلی با یک شعبه در خیابانی پررفت‌وآمد در اسپرینگ‌فیلد است. دوشنبه تا شنبه در ساعات کاری معمول باز است. مشتریان هم لباس معمولی و هم اقلام فقط-خشک‌شویی می‌آورند. بسته به مقدار، فوریت و دشواری هر قلم، اقلام شسته شده و بین دو تا شش روز کاری بعد به مشتریان برگردانده می‌شوند.

این چگونه به تعریف سیستم ما نگاشت می‌شود؟ عملیات خشک‌شویی درون کسب‌وکار خودِ سیستم است. لباس‌های کثیف مشتریان را به‌عنوان ورودی پردازش می‌کند و شسته‌شده به صاحبانشان برمی‌گرداند. همهٔ پیچیدگی‌های عملیات خشک‌شویی از مصرف‌کننده پنهان است؛ فقط باید لباس‌ها را بیاوریم و امیدوار باشیم خشک‌شویی کارش را درست انجام دهد. خود سیستم پیچیده است؛ بسته به نوع ورودی (کت چرمی، تودهٔ جوراب، دامن ابریشمی) ممکن است یک یا چند عملیات انجام دهد تا خروجی درست (لباس تمیز) حاصل شود. فرصت زیادی هست که بین تحویل و تحویل‌گیری چیزی اشتباه شود: کمربند گم شود، لکه نادیده گرفته شود، پیراهن به مشتری اشتباه برگردد. اما اگر کارکنان پیش‌فعالانه با هم ارتباط بگیرند، ماشین‌ها سالم باشند و رسیدها مرتب نگه داشته شوند، سیستم روان کار می‌کند و سفارش‌ها به‌راحتی انجام می‌شوند.

فرض کنید خشک‌شویی سیمون هنوز با رسیدهای کاغذی کربن‌دار کار می‌کند. همهٔ مشتریانی که لباس می‌آورند نام و شماره تلفنشان را روی برگه می‌نویسند و کارمند سفارش را ثبت می‌کند. اگر مشتری رسید را گم کند، خشک‌شویی سیمون با ورق زدن سفارش‌های اخیر بر اساس نام خانوادگی به‌راحتی نسخه را پیدا می‌کند. متأسفانه وقتی مشتریان دیر می‌آیند و رسیدشان را گم کرده‌اند، کارمند باید رسیدهای بایگانی‌شده را از جعبه‌های اتاق پشتی بیاورد. تقریباً همهٔ سفارش‌ها پیدا می‌شوند، اما مشتری زمان بیشتری برای تحویل‌گیری صرف می‌کند. رسیدهای کاغذی هنگام محاسبهٔ درآمد پایان ماه هم ناراحت‌کننده‌اند؛ باید همهٔ تراکنش‌ها (کارت اعتباری و نقد) را دستی با سفارش‌های انجام‌شده تطبیق دهند. مشتاق مدرن‌سازی و بازآرایی فرایند، تیم تصمیم گرفت به سیستم فروش (point-of-sale) ارتقا دهد و دردهای کاغذ را از بین ببرد. تمام! بازآرایی تمام شد! مشتریان همچنان لباس می‌آورند و چند روز بعد با حداقل تغییر محسوس برمی‌دارند، اما پشت پیشخوان همه‌چیز روان‌تر کار می‌کند.

بازآرایی در مقیاس چیست؟

در اواخر ۲۰۱۳، در میانهٔ راه‌اندازی پرآشوب، همهٔ رسانه‌های خبری بزرگ آمریکا Healthcare.gov را فاجعهٔ کامل اعلام کردند؛ وب‌سایت با نگرانی‌های امنیتی، قطعی‌های چندساعته و انبوهی از باگ‌های جدی مواجه بود. قبل از راه‌اندازی، نه‌تنها هزینه به نزدیک دو میلیارد دلار رسیده بود، کدبیس به بیش از پنج میلیون خط کد منفجر شده بود. بخش زیادی از شکست Healthcare.gov به شیوه‌های توسعهٔ ناموفق درگیر سیاست‌های دولت فدرال برمی‌گشت، اما وقتی دولت اوباما بعداً اعلام کرد قصد سرمایه‌گذاری سنگین برای بهبود سرویس را دارد، دشواری بی‌چون‌وچرای بازمعماری و بازآرایی سیستم‌های نرم‌افزاری بیش‌ازحد رشد‌کرده به خبر اصلی تبدیل شد. در ماه‌های بعد، تیم‌هایی که مأمور بازنویسی Healthcare.gov بودند سر به سینه به بازسازی تقریباً کامل کدبیس — بازآرایی در مقیاس — پرداختند.

بازآرایی در مقیاس بازآرایی‌ای است که سطح قابل‌توجهی از سیستم‌های شما را تحت تأثیر قرار می‌دهد. معمولاً (اما نه منحصراً) شامل کدبیس بزرگ (یک میلیون خط یا بیشتر) است که برنامه‌هایی با کاربران زیاد را پشتیبانی می‌کند. تا وقتی سیستم‌های legacy وجود دارند، به این نوع بازآرایی‌ها نیاز خواهد بود؛ جایی که توسعه‌دهندگان باید دربارهٔ ساختار کد در گستره فکر کنند و چگونه می‌توان آن را به‌طور قابل اندازه‌گیری بهبود داد.

چه چیزی بازآرایی کدبیس چندمیلیونی را از بازآرایی برنامه‌های کوچک‌تر و بهتر تعریف‌شده متمایز می‌کند؟ شاید برای ما آسان باشد راه‌های مشخص و تکراری برای بهبود سیستم‌های کوچک و تعریف‌شده (توابع یا کلاس‌های منفرد) ببینیم، اما تقریباً غیرممکن است اثر تغییری را که یکنواخت در سیستم گسترده و پیچیده اعمال می‌شود تعیین کنیم. ابزارهای زیادی برای شناسایی code smell یا بهبود خودکار در زیربخش‌های کد وجود دارد، اما عمدتاً نمی‌توانیم استدلال انسانی دربارهٔ بازساخت برنامه‌های بزرگ را در کدبیس‌هایی که با سرعت فزاینده — به‌ویژه در شرکت‌های پررشد — رشد می‌کنند، خودکار کنیم.

برخی استدلال می‌کنند می‌توان با اعمال مداوم تبدیل‌های کوچک و افزایشی به‌طور اندازه‌گیری‌شده به این نوع سیستم بهبود داد. این روش ممکن است ترازو را مثبت کند، اما وقتی بیشتر میوه‌های در دسترس رفته و معرفی دقیق و تدریجی این تغییرات سخت‌تر می‌شود، پیشرفت به‌طور محسوسی افت می‌کند.

بازآرایی در مقیاس دربارهٔ شناسایی مسئلهٔ سیستمی در کدبیس، طراحی راه‌حل بهتر و اجرای آن به‌صورت راهبردی و منضبط است. برای شناسایی مسائل سیستمی و راه‌حل‌های متناظر، به درک محکمی از یک یا چند بخش گستردهٔ برنامه نیاز دارید. همچنین به استقامت بالا برای انتشار درست راه‌حل در کل ناحیهٔ تحت تأثیر نیاز دارید.

بازآرایی در مقیاس با بازآرایی سیستم‌های زنده هم‌راستاست. بسیاری از ما روی برنامه‌هایی با چرخهٔ استقرار مکرر کار می‌کنیم. در Slack، حدود دوازده بار در روز کد جدید به کاربران می‌فرستیم. باید مراقب باشیم تلاش‌های بازآرایی چگونه در این چرخه‌ها جا می‌گیرند تا ریسک و اختلال برای کاربران کمینه شود. درک استقرار راهبردی در نقاط مختلف تلاش بازآرایی اغلب تفاوت بین استقرار آرام و قطعی کامل سرویس است.

خشک‌شویی سیمون در مقیاس چگونه به نظر می‌رسد؟ فرض کنید استقرار سیستم فروش کسب‌وکار را به‌شدت بهینه کرد — آن‌قدر که در دو سال پنج شعبهٔ جدید در شهرهای همسایه باز کرد! حالا با چند شعبه، مقیاس کسب‌وکار متفاوت است و مجموعهٔ مسائل دیگری دارد. برای هزینهٔ کم، فقط دو از شش شعبه تجهیزات خشک‌شویی در محل دارند. وقتی مشتری در یکی از چهار شعبهٔ بدون تجهیزات لباس می‌آورد، باید با ون شرکت به نزدیک‌ترین تأسیسات فرستاده شود. ون در هر چهار ویترین توقف می‌کند، لباس‌ها را در سطل‌های بزرگ روی اسکلهٔ بارگیری دو محل خشک‌شویی می‌اندازد. کارکنان خشک‌شویی سیمون سخت کار می‌کنند تا از تودهٔ لباس‌ها مرتب‌سازی، شست‌وشو و بازگرداندن به ویترین درست انجام دهند. اما بیشتر روزها فرایند وحشت‌آور است. هر دو محل خشک‌شویی لباس هم شعبهٔ خود و هم چهار شعبهٔ کوچک‌تر را پردازش می‌کنند. جدا شدن یا درهم‌پیچیدن لباس‌ها هنگام انداختن در سطل‌های پردازش توسط راننده‌های ون غیرمعمول نیست. سفارش‌های فوری‌تر اغلب در توده گم می‌شوند و خشک‌شویی‌ها باید کل محموله را بگردند تا اول آن‌ها را پیدا کنند.

خشک‌شویی سیمون چگونه می‌تواند کارایی را بیشترین بهبود دهد؟ آیا باید مرکز خشک‌شویی مشخصی برای هر شعبه اختصاص دهد تا هر تأسیسات حداکثر سه ویترین را مدیریت کند؟ اگر بله، آیا باید مسیر ون‌ها را به‌طور خاص تغییر دهد؟ اگر هر دو؟ آیا باز کردن تأسیسات دیگر مقرون‌به‌صرفه است اگر زمان تحویل کاهش یابد؟ اسکله‌های بارگیری چگونه چیده شوند تا کمتر لباس درهم بپیچد؟ آیا می‌توان به رانندگان آموخت قبل از دور بعدی سفارش‌ها را بر اساس فوریت آویزان و دسته‌بندی کنند؟ آیا باید تحویل‌گیری را محدود به بعد از ناهار و کمی بعد از تعطیلی کند تا محل‌های خشک‌شویی زمان بیشتری برای سازمان‌دهی تحویل‌ها داشته باشند؟ گزینه‌های زیادی هست که می‌توان ترکیب و روی سفارش‌های متعدد یا هم‌زمان اجرا کرد. تصور کنید با همهٔ این احتمالات روبه‌رو باشید و باید تصمیم بگیرید کدام اهرم را اول بکشید. واقعاً فلج‌کننده است! معلوم می‌شود بازآرایی برنامه‌های بزرگ همین‌طور احساس می‌شود.

چرا باید به بازآرایی اهمیت دهیم؟

بازآرایی در تئوری جذاب به نظر می‌رسد، اما چگونه می‌دانید خواندن بقیهٔ این کتاب اتلاف وقت نیست؟ امیدوارم همهٔ خوانندگان با چند ابزار جدید از کتاب خارج شوند، اما اگر یک دلیل برای ادامهٔ خواندن بدهم این است:

اطمینان به توانایی بازآرایی به شما اجازه می‌دهد به سمت عمل متمایل شوید و زودتر شروع به ساخت سیستم کنید، خیلی قبل از اینکه درک قوی از همهٔ قطعات، دام‌ها و موارد حاشیه‌ای داشته باشید. اگر بدانید در طول توسعه می‌توانید فرصت‌های بهبود مؤثر شناسایی کنید و با پیچیده‌تر شدن سیستم همچنان بتوانید، لازم نیست زمان زیادی از پیش روی معماری صرف کنید. وقتی مهارت‌های لازم برای دستکاری راحت کد را تقویت کردید، کمتر نگران این می‌شوید که با یک تصمیم طراحی خود را محدود کرده‌اید. هنگام برنامه‌نویسی، به‌جای عقب‌نشینی و برنامه‌ریزی شش حرکت بعدی، چیزی ساده می‌نویسید که در شرایط فعلی کار کند. می‌فهمید همیشه (گاهی دشوار) مسیری به راه‌حل بهتر هست.

برنامه‌نویسی بازی شطرنج نیست. وقتی به بازیکنان رقابتی پیکربندی صفحه داده می‌شود و حریفان بهینه فرض می‌شوند، ده‌ها مسابقهٔ کامل را در چند دقیقه بازی می‌کنند. متأسفانه در کار ما مجموعهٔ کاملاً شمارش‌شده‌ای از حرکت‌های ممکن و وضعیت پایانی از پیش تعیین‌شده نداریم. نمی‌خواهم بگویم نشستن و طوفان فکری برای راه‌حل محکم با مجموعهٔ معقولی از نیازمندی‌ها بی‌ارزش است؛ اما هشدار می‌دهم زمان زیادی برای صاف‌کردن ۱۰ تا ۲۰ درصد نهایی صرف نکنید. اگر توانایی بازآرایی را تقویت کرده‌اید، می‌توانید راه‌حل را برای مشخصات نهایی هم تکامل دهید.

مزایای بازآرایی

بازآرایی مزایای ملموسی فراتر از شروع با اطمینان حل مسئله دارد. شاید برای هر مسئله ابزار درست نباشد، اما می‌تواند اثر مثبت پایدار روی برنامه، تیم مهندسی و سازمان گسترده‌تر داشته باشد. دو مزیت اصلی را بحث می‌کنیم: افزایش بهره‌وری توسعه‌دهنده و شناسایی آسان‌تر باگ‌ها. برخی ممکن است بگویند مزایای بیشتری هست، اما من استدلال می‌کنم همه به دو موضوع زیر برمی‌گردند.

بهره‌وری توسعه‌دهنده

یکی از اهداف اصلی بازآرایی تولید کدی است که فهمیدنش آسان‌تر باشد. ساده‌سازی راه‌حل متراکم هنگام استدلال نه‌تنها به شما کمک می‌کند بهتر بفهمید کد چه می‌کند، به همهٔ کسانی که بعد از شما می‌آیند هم کمک می‌کند. کدی که به‌راحتی درک می‌شود همهٔ اعضای تیم را — فارغ از سابقه و سطح تجربه — ارتقا می‌دهد.

اگر مهندس باسابقهٔ تیم باشید، با بخش‌هایی از کدبیس آشنا هستید اما با رشد کدبیس، بخش‌های بیشتری ناآشنا می‌شوند و کد شما بیشتر به آن بخش‌ها وابسته می‌شود. تصور کنید قابلیت جدیدی پیاده می‌کنید و در بافت راه‌حل از کدی که خوب می‌شناسید به قلمرو ناآشنا می‌روید. اگر ناحیهٔ ناشناخته به‌خوبی نگهداری شده و مرتب بازآرایی شده تا نیازمندی‌های در حال تکامل محصول و رفع باگ را در نظر بگیرد، سریع‌تر محل ایده‌آل تغییر را محدود و راه‌حل بی‌دردسری پیدا می‌کنید. اگر کد با وصله‌های پراکندهٔ باگ و طولانی‌شدن تدریجی فرسوده شده، زمان نمایی بیشتری برای فهم هر خط قبل از استدلال راه‌حل صرف می‌کنید. (غیرمعمول نیست کسی دیگر — هم‌تیمی یا کسی که با کد به‌طور صمیمی آشناست — را به تونل وحشت‌آور کد شکنجه‌شده بکشید.)

تکامل آشنایی با کدبیس

در کدبیس‌های کوچک با چند مهندس، غیرمعمول نیست بیشتر مهندسان با همهٔ بخش‌ها راحت باشند. آشنایی با افزودن و تغییر ماژول‌ها و تخصصی‌شدن مهندسان تدریجاً کم می‌شود؛ سرانجام کدبیس به جرم بحرانی می‌رسد که هیچ مهندسی (حتی اولین استخدام!) نمی‌تواند با همه‌چیز آشنا باشد.

سناریو را برعکس کنیم. اگر همکاری از تیم دیگر که با کد تیم شما آشنا نیست بخواهد آن را بخواند، آیا فهمیدن آسان است؟ بیشتر انتظار سؤال و نگاه گیج دارید یا درخواست بازبینی کد؟

اگر مهندس تازهٔ تیم باشید — شاید اخیراً خودتان بودید یا کسی را به تیم راه انداختید — هیچ الگوی ذهنی از کدبیس ندارند. توانایی اطمینان در هر ناحیه مستقیماً متناسب با خوانایی کد است. نه‌تنها می‌توانند به‌طور طبیعی نمایش ذهنی دقیقی از روابط بین واحدهای مختلف بسازند، می‌توانند بدون نیاز به سر زدن به هم‌تیمی‌ها بفهمند کد چه می‌کند. (شایان ذکر است دانستن زمان و نحوهٔ پرسیدن سؤال از همکاران مهارت بسیار مهمی است. ارزیابی چقدر زمان مناسب است قبل از کمک گرفتن سخت اما برای رشد به‌عنوان توسعه‌دهنده حیاتی است. پرسیدن سؤال بد نیست، اما اگر مهندس باسابقهٔ تیم هستید و سیل سؤالات می‌آید، شاید وقت نوشتن مستندات و بازآرایی کد است.)

همهٔ ما هنگام توسعهٔ چیز جدید تمایل داریم الگوهای جاافتاده را کپی کنیم. اگر راه‌حل‌های مرجع واضح و مختصر باشند، احتمال بیشتری دارد کد واضح و مختصر منتشر کنیم. برعکس هم درست است: اگر تنها مراجع شلوغ باشند، کد شلوغ منتشر می‌کنیم. اطمینان از اینکه بهترین الگوها رایج‌ترین باشند برای حلقهٔ بازخورد مثبت با توسعه‌دهندگان تازه‌کار حیاتی است. اگر کدی که مرتب با آن کار می‌کنند آسان فهم باشد، تمرکز مشابهی در راه‌حل‌های خود الگوبرداری می‌کنند.

شناسایی باگ‌ها

ردیابی و حل باگ بخش لازم (و سرگرم‌کننده!) کار ماست. بازآرایی ابزار مؤثری برای هر دو است! با شکستن عبارت‌های پیچیده به قطعات کوچک‌تر و استخراج منطق به توابع جدید، هم درک بهتری از کد می‌سازید و — امیدواریم — باگ را جدا می‌کنید. بازآرایی هنگام نوشتن فعال کد هم شناسایی زودهنگام باگ را آسان‌تر می‌کند و از وقوع کامل جلوگیری می‌کند.

سناریو: تیم چند ساعت پیش کد جدید را در محیط تولید مستقر کرد. برخی تغییرات در چند فایلی بود که همه از دست‌زدن به آن‌ها می‌ترسند: کد غیرقابل خواندن و مین‌مید باگ‌های در انتظار. متأسفانه تست‌ها یکی از موارد حاشیه‌ای زیاد را پوشش نداد و کسی از پشتیبانی مشتری دربارهٔ باگ مزاحمی که کاربران با آن روبه‌رو می‌شوند تماس می‌گیرد. فوراً شروع به کاوش می‌کنید و می‌فهمید باگ — همان‌طور که انتظار می‌رفت — در ترسناک‌ترین بخش است. خوشبختانه هم‌تیمی می‌تواند مشکل را به‌طور پایدار بازتولید کند و با هم تستی برای تأیید رفتار درست می‌نویسید. حالا باید باگ را محدود کنید. گام‌های منظم برای شکستن کد پشمالو: خط‌های یک‌تکهٔ طولانی را به عبارت‌های چندخطی مختصر تبدیل می‌کنید و محتوای چند بلوک شرطی را به توابع جدا منتقل می‌کنید. سرانجام باگ را پیدا می‌کنید. حالا که کد ساده شده، سریع اصلاح می‌کنید، تست را اجرا می‌کنید و اصلاح را به مشتریان می‌فرستید. پیروزی!

گاهی برای مشتری باگ فقط مزاحمت جزئی است، گاهی استفادهٔ کامل از برنامه را متوقف می‌کند. باگ‌های پراختلال‌تر معمولاً اصلاح فوری می‌خواهند، اما تیم باید بتواند باگ‌های همهٔ سطوح شدت را سریع حل کند تا کاربران راضی بمانند. کار در کدبیس خوش‌نگهداری‌شده زمان لازم برای یافتن و رفع باگ را به‌شدت کاهش می‌دهد و وقتی در کمترین زمان به محیط تولید می‌رود خوشحال می‌شوید.

خطرات بازآرایی

با وجود مزایای جذاب، خطرات و دام‌های جدی قبل از سفر برای بهبود هر اینچ (یا سانتی‌متر) کدبیس وجود دارد. شاید تکراری به نظر برسم، اما تکرار می‌کنم: بازآرایی نیاز دارد در هر تکرار رفتار یکسان بماند. با نوشتن مجموعهٔ تست (unit، integration، end to end) اطمینان را افزایش می‌دهیم و نباید جدی به بازآرایی ادامه دهیم تا پوشش تست کافی برقرار نشده. اما حتی با تست کامل، همیشه احتمال کوچکی هست چیزی از شکاف‌ها رد شود. باید هدف نهایی را هم در نظر بگیریم: بهتر کردن کد به‌گونه‌ای که برای شما و توسعه‌دهندگان آینده واضح باشد.

رگرسیون‌های جدی

بازآرایی کد بدون تست بسیار خطرناک و شدیداً توصیه نمی‌شود. تیم‌هایی با کامل‌ترین مجموعه‌های تست هم باگ به محیط تولید می‌فرستند. چرا؟ با هر تغییر، کوچک یا بزرگ، تعادل سیستم را به‌طور قابل اندازه‌گیری مختل می‌کنیم. تلاش می‌کنیم کمترین اختلال را ایجاد کنیم، اما هر تغییر ریسک رگرسیون ناخواسته دارد. هنگام بازآرایی گوشه‌های به‌ویژه ترسناک و گیج‌کنندهٔ کدبیس، معرفی رگرسیون جدی نگرانی ویژه‌ای است. این بخش‌ها اغلب به‌خاطر زمان کافی برای فرسودگی در وضعیت فعلی‌اند. در شرکت‌های پررشد، هم جزء جدایی‌ناپذیر کار برنامه و هم کم‌ترین تست را دارند. باز کردن این فایل‌ها یا توابع مثل راه رفتن روی مین‌مید بدون آسیب است — ممکن اما بسیار خطرناک.

آشکار شدن باگ‌های خاموش

همان‌طور که بازآرایی کمک می‌کند باگ شناسایی شود، می‌تواند ناخواسته باگ‌های خاموش را هم آشکار کند. اینجا باگ خاموش را رگرسیون‌هایی می‌دانم که بیشتر با بازساخت کد آشکار می‌شوند. دوباره خشک‌شویی سیمون: کسب‌وکار شروع کرده محصولات شوینده را در دسته‌های بزرگ‌تر با همان ریتم تحویل سفارش دهد تا تخفیف بهتری بگیرد. متأسفانه جا برای انبار در پشت ویترین اصلی کم است، پس خشک‌شویی سیمون تصمیم می‌گیرد جعبه‌ها را نزدیک‌تر به در اسکلهٔ بارگیری انباشته کند. بعد از چند هفته باران، تیم می‌بیند جعبه‌های نزدیک در خیس و در حال پوسیدن‌اند. صاحب می‌بیند در پشتی به‌بدی آب‌بندی شده و در روزهای بارانی آب نفوذ می‌کند. خشک‌شویی سیمون هرگز با انبار نزدیک در مشکل نبود چون قبلاً این کار را نکرده بود؛ الگوی انبارداری جدید نقص بحرانی در زیرساخت را آشکار کرد که شاید هرگز کشف نمی‌شد.

گسترش دامنه

بازآرایی کمی مثل خوردن براونی است: چند گاز اول خوشمزه است و آسان است غرق شوید و ناخواسته یک دوجین بخورید. وقتی آخرین گاز را زدید، کمی پشیمانی و شاید تهوع می‌آید. بهبود فوری و بسیار قابل‌توجه هنگام تغییرات متمرکز و محلی بسیار رضایت‌بخش است! آسان است غرق شوید و سطح تغییرات از مرزهای معقول فراتر رود. مرزهای معقول بسته به کدبیس می‌تواند یک ناحیهٔ عملکردی یا مجموعهٔ کوچکی از کتابخانه‌های وابسته به هم باشد. ایده‌آل این است که کد بازآرایی‌شده محدود به تغییراتی باشد که توسعه‌دهندهٔ دیگر در یک مجموعهٔ تغییرات راحت بازبینی کند.

هنگام نقشه‌کشی تلاش بازآرایی بزرگ‌تر، به‌ویژه چند ماه یا بیشتر، حفظ دامنهٔ تنگ ضروری است. همهٔ ما هنگام بازآرایی سطح کوچک (چند خط، توابع منفرد) عیب‌های غیرمنتظره پیدا می‌کنیم؛ می‌توانیم چند بهبود را به‌صورت پایدار زنجیر کنیم، اما این رویکرد برای سطح قابل‌توجه خطرناک می‌شود. هرچه سطح بازآرایی برنامه‌ریزی‌شده بزرگ‌تر، مسائل بیشتری پیدا می‌کنید که پیش‌بینی نکرده‌اید. این شما را برنامه‌نویس بد نمی‌کند، فقط انسان می‌کند. با برنامهٔ مشخص، شانس رگرسیون جدی یا باگ خاموش کمتر و بهره‌وری بیشتر می‌شود. تلاش‌های منظم و پایدار از قبل سخت‌اند؛ هدف متحرک آن‌ها را غیرممکن می‌کند.

پیچیدگی غیرضروری

مراقب طراحی بیش‌ازحد در شروع باشید و به تغییر برنامهٔ اولیه باز باشید. هدف اصلی باید تولید کد دوستدار انسان باشد، حتی به قیمت طراحی اصلی. اگر تمرکز لیزری روی راه‌حل باشد نه فرایند، احتمال بیشتری دارد برنامه مصنوعی‌تر و پیچیده‌تر از قبل شود. بازآرایی در همهٔ سطوح باید تکراری باشد. با گام‌های کوچک و عمدی در یک جهت و حفظ رفتار موجود در هر تکرار، روی هدف نهایی تمرکز بهتری دارید. این وقتی فقط به اندازهٔ صفحهٔ نمایش کد را برمی‌دارید آسان‌تر است تا سه دوجین کتابخانه هم‌زمان. هنگام برنامه‌ریزی پروژهٔ جدید، بیشتر ما سند مشخصات و برنامهٔ اجرای دقیق می‌سازیم. حتی برای بازآرایی بزرگ، داشتن تصور خوب از شکل کد نهایی پس از اتمام مهم است.

چه زمانی بازآرایی کنیم؟

آسان بود بگوییم «وقتی مزایا از خطرات بیشتر است»، اما پاسخ مفیدی نبود. بله، در عمل وقتی مزایا از خطرات بیشتر است بازآرایی ارزشمند است، اما چگونه وزن درست به هر قطعه بدهیم؟ چگونه بدانیم به نقطهٔ عطف رسیده‌ایم؟

در تجربهٔ من نقطهٔ عطف بیشتر یک بازهٔ عطف است و برای هر کس و هر برنامه متفاوت است. تعیین مرزهای بالا و پایین این بازه چیزی است که بازآرایی را کمی علم ذهنی می‌کند: فرمول قطعی «بله» یا «خیر» نداریم. خوشبختانه می‌توانیم به شواهد تجربی از تجربهٔ دیگران تکیه کنیم.

دامنهٔ کوچک

برای بازآرایی بخش کوچک و سرراست کد با تست خوب، چیز زیادی شما را نگه نمی‌دارد. مگر مطمئن نباشید راه‌حل بازآرایی‌شده بهبود عینی نسبت به پیشینی است یا می‌ترسید سطح تغییر خیلی بزرگ است، احتمالاً ارزشمند است. چند ثبت تغییر (commit) با دقت بسازید و تغییرات را به راه بیندازید! مثالی که به‌وضوح در این دسته است را بعداً در این فصل می‌بینیم.

پیچیدگی کد فعالاً توسعه را مختل می‌کند

گاهی باید به بخش‌هایی از کدبیس که می‌ترسیم برویم. هر بار که می‌خوانیم، ابروها درهم، قلب تند می‌زند، نورون‌ها شلیک می‌کنند. بعد لحظه‌ای می‌رسد که باید دندان روی جیگر بگذاریم و تغییر را انجام دهیم. اما توسعه تحت فشار راه مطمئن ایجاد مشکل بیشتر است. وقتی بیش‌ازحد روی انجام دقیق درست متمرکز هستید و ابعاد مسئله را در سر نگه می‌دارید، خطر دارید هدف واقعی را از دست بدهید. چگونه به‌اندازهٔ کافی روی آن هدف اجرا کنید وقتی ذهنتان جای دیگر است؟

اگر این بخش هنوز گزیده‌مان نکرده، اغلب شانس می‌گیریم. اگر گزیده یا هم‌تیمی (گاهی بیش از یک بار)، خطر جراحی روی کد برای جلوگیری از اشتباهات آینده ممکن است از خطر گذاشتن در وضعیت فعلی بیشتر باشد. اگر مطمئن نیستید ترازو به کدام سمت می‌رود، با هم‌تیمی‌ها صحبت کنید و داده‌ای از تعداد باگ‌های شش ماه گذشته که به این بخش ردیابی می‌شوند جمع کنید.

تغییر در نیازمندی‌های محصول

تغییرات شدید در نیازمندی‌های محصول اغلب به تغییرات شدید در کد منجر می‌شوند. هرچقدر راه‌حل‌های انتزاعی و قابل گسترش بنویسیم، آینده را پیش‌بینی نمی‌کنیم؛ کد برای انحرافات کوچک آسان سازگار می‌شود اما به‌ندرت برای بزرگ‌ترها کاملاً سازگار است. این تغییرات فرصت نادر مرتبط با کسب‌وکار برای بازگشت به تختهٔ طراحی و بازنگری طراحی می‌دهد.

شاید فکر کنید این تغییرات نمی‌توانند رفتار را حفظ کنند. با همان ورودی‌ها، حالا باید خروجی‌های متفاوت بدهیم! چگونه فرصت بازآرایی است؟ اگر کد در وضعیت فعلی به نیازمندی‌های جدید خوب نمی‌خورد، باید راه‌حلی بسازید که عملکرد امروز را پشتیبانی کند و فردا بی‌وقفه پشتیبانی کند. می‌توانید استدلال کنید: اول بازآرایی، سپس (و فقط سپس!) قابلیت جدید روی آن. این‌طور استاندارد کد با کیفیت بالا را حفظ می‌کنید، همهٔ مزایای بازآرایی را به دست می‌آورید و اهداف کسب‌وکار را پشتیبانی می‌کنید. برد، برد، برد!

کارایی

بهبود کارایی کار دشواری است؛ ابتدا باید درک عمیق از رفتار موجود و سپس شناسایی اهرم‌هایی که می‌توانند ترازو را مثبت کجا کنند. شروع با صفحهٔ پاک (یا ساخت آن به‌عنوان گام اول) بهترین امکان را فراهم می‌کند. جدا کردن اهرم‌های شناسایی‌شده تا دستکاری بدون خطر اثرات پایین‌دستی آسان‌تر باشد هم کلیدی است.

یادداشت دربارهٔ کارایی

همهٔ توسعه‌دهندگان باور ندارند بهبود کارایی دلیل معتبری برای بازآرایی است؛ برخی می‌گویند کارایی سیستم ذاتاً بخشی از رفتار آن است و تغییر آن به هر شکلی رفتار را تغییر می‌دهد. مخالفم. اگر بازآرایی را با سیستم کلی تعریف کنیم که مجموعه‌ای ورودی می‌دهیم و مجموعهٔ مورد انتظار خروجی تولید می‌کنیم، بهبود سرعت (یا بار حافظه) لازم برای تولید این خروجی‌ها شکل معتبری از بازآرایی است.

بازآرایی برای این هدف در یک راه مهم منحصربه‌فرد است: تضمین نمی‌کند کد در دسترس‌تر نتیجه شود. گاهی بلوک توضیح طولانی بالای کد می‌بینیم که خواننده را از یک (یا چند) پیچیدگی هشدار می‌دهد: رفتار عجیب برنامه، راه‌حل موقت، و وصلهٔ کارایی عجیب. بیشتر بهبودهای کارایی با این داستان‌های کوتاه هوشمندانه نوشته شده و درک عمیق کدبیس را برای کمینه‌کردن سطح تحت تأثیر به کار می‌گیرند. این «بهبودها» آسیب‌پذیرتر به فرسودگی در مدت کوتاه‌ترند و نمونهٔ خوبی از پایداری که بازآرایی باید پرورش دهد نیستند. بهبودهای کارایی ارزشمند، شایستهٔ زیر چتر بازآرایی، عمیق و فراگیرند؛ نمونهٔ بازآرایی مؤثر در مقیاس. در بخش II عمیق‌تر پوشش می‌دهیم.

استفاده از فناوری جدید

در توسعهٔ نرم‌افزار مرتب فناوری‌های جدید می‌پذیریم. چه برای همگام ماندن با روندهای صنعت، مقیاس به کاربران بیشتر، یا بلوغ محصول به شکل جدید — دائماً کتابخانه‌های open-source، پروتکل‌ها، زبان‌ها، ارائه‌دهندگان سرویس و بیشتر را ارزیابی می‌کنیم. تصمیم برای چیز جدید سبک گرفته نمی‌شود؛ بخشی به هزینهٔ یکپارچه‌سازی در کدبیس موجود است. اگر راه‌حل موجود را با جدید جایگزین کنیم، برنامهٔ کنار گذاشتن با شناسایی همهٔ محل‌های فراخوانی تحت تأثیر و مهاجرت (گاهی یکی‌یکی) لازم است. اگر از این پس فناوری جدیدی را بپذیریم، باید نامزدهای با اهرم بالا برای پذیرش زودهنگام با برنامهٔ گسترش به همهٔ موارد مرتبط شناسایی کنیم.

هر راهی که فناوری جدید سیستم را تحت تأثیر قرار دهد فهرست نمی‌کنم (زیادند)، اما از این دو سناریو روشن است هر کدام ممیزی دقیق سیستم فعلی می‌خواهد. خوشبختانه ممیزی فرصت‌های عالی برای بازآرایی آشکار می‌کند! می‌خواهم تأکید کنم این نظر تا حدی بحث‌برانگیز است. به‌خاطر خطرهای پذیرش فناوری جدید به‌تنهایی، توسعه‌دهندگان دیگر ممکن است از هر تغییر دیگری منع کنند. اما قویاً باور دارم بدترین راه معرفی چیز جدید چسباندن آن کنار تودهٔ بزرگ و درهم‌تنیده است. برای بهترین شانس تحقق هدف، بهتر است اول نواحی تماس را تمیز کنید.

به خشک‌شویی سیمون اعمال می‌کنیم. فرض کنید تازه ماشین‌آلات خشک‌شویی پیشرفته و سازگار با محیط زیست سفارش داده. در برنامهٔ نصب، صاحبان می‌بینند نقشهٔ کف موجود ناکارآمدی جدی دارد. کارکنان باید تمام خط ماشین‌ها را طی کنند تا لباس از پیش مرتب‌شده را از رگ‌های نزدیک سی فوت بردارند. اگر ماشین‌ها را طوری بچینند که چند فوت برسند، شاید چند دقیقه از هر چرخه کم شود. تصمیم می‌گیرند ماشین‌های جدید در چیدمان بازنگری‌شده نصب شوند. خشک‌شویی سیمون اثر محیطی را کم و بهره‌وری کارکنان را زیاد کرد. برد، برد!

چه زمانی بازآرایی نکنیم؟

بازآرایی ابزار فوق‌العاده مفیدی برای توسعه‌دهنده است. بسیاری باور دارند زمان بازآرایی همیشه خوب صرف شده است، اما ساده نیست. زمان و مکان برای بازآرایی هست و توسعه‌دهندگان باتجربه اهمیت دانستن چه زمانی بازآرایی کنیم و چه زمانی نکنیم را می‌فهمند.

برای تفریح یا از سر حوصله‌سری

چشم‌ها را ببندید و خود را جلوی کامپیوتر تصور کنید. تابعی به‌ویژه پیچیده را می‌بینید. خیلی بلند است؛ کارهای زیاد می‌کند. نامش دیگر مسئولیتش را معنادار توصیف نمی‌کند. می‌خواهید اصلاحش کنید. دوست دارید به واحدهای مشخص و مختصر با نام متغیر بهتر تقسیمش کنید. لذت‌بخش می‌شود. اما مهم‌ترین کاری که الان می‌توانید بکنید این است؟ شاید هم‌تیمی چند روز بازبینی کد شما را منتظر مانده یا نوشتن تست را به تعویق انداخته‌اید؟ اگر کد کهنهٔ فرسوده را جابه‌جا می‌کنید تا سرگرم بمانید، شاید به خود (و هم‌تیمی‌ها) آسیب می‌زنید.

احتمالاً اگر برای تفریح بازآرایی می‌کنید، روی اثر تغییر روی کد اطراف، کل سیستم و همکاران تمرکز ندارید. انگیزه‌های متفاوت است: احتمال بیشتر ویژگی‌های زبان دور از ذهن یا الگوی کاملاً جدیدی که می‌خواستید امتحان کنید. زمان و مکان برای امتحان چیزهای جدید هست، اما بازآرایی آن زمان نیست. بازآرایی باید عمدی باشد با تمرکز صرفاً روی (ایده‌آل) کوچک‌ترین تغییر برای بزرگ‌ترین اثر مثبت.

چون اتفاقاً از آنجا رد شدید

تصور کنید: کد می‌نویسید، به محیط تولید می‌فرستید، قابلیت جدید شروع می‌کنید. چند ماه بعد برای گسترش برمی‌گردید. شبیه چیزی که نوشتید نیست. میلیون سؤال: اینجا چه شد؟

شاید قربانی بازآرایی‌کنندهٔ عبوری شده‌اید. همکاری باتجربه با نظرهای آگاهانه دربارهٔ نوشتن کد. مهندسان دیگر برای تصمیم‌های طراحی با او مشورت می‌کنند. تمایل ناگوار دارد کد دیگران را هنگام برخورد بازنویسی کند. فکر می‌کند به همه لطف می‌کند.

ممکن است وسوسه موافقت شوید، اما در نظر بگیرید: اگر مهندس در ناحیه‌ای که مشارکت‌کنندهٔ فعال نیست کد را تغییر داده، بهره‌وری کسانی که مسئول آن هستند را کاهش داده. مولدترین وقتی‌ایم که با کدی که مسئولش هستیم آشنا باشیم. وقتی سریع مسئله‌ای را حل کنیم — حادثهٔ جدی یا باگ کوچک — از الگوی ذهنی برای محدود کردن فایل‌ها، کلاس‌ها یا توابع استفاده می‌کنیم. اگر ویرایشگر را باز کنیم و هیچ‌چیز جایی که گذاشته بودیم نباشد، گیج می‌شویم و سریع اصلاح نمی‌کنیم. برای کارفرما در ساعت‌های مهندسی، ساعت‌های پشتیبانی مشتری و احتمالاً از دست رفتن کسب‌وکار فوق‌العاده پرهزینه است.

نگفتن به نویسندهٔ اصلی در دو راه آسیب است. اول، اعتماد نویسنده را فرسوده. هرچقدر از کد جدا شویم، همیشه تکهٔ کوچکی غرور و مالکیت در کدی که نوشته‌ایم می‌گذاریم. ترجیح می‌دهم صادقانه دربارهٔ کاستی‌های راه‌حلم بگویند و نشان دهند چگونه اصلاح کنم تا بعد از رفع مشکلات بفهمم. برای مهندسان تازه‌تر به‌ویژه مضر است. تصور کنید یک سال از مدرسه؛ می‌آیید کدی که هفته‌ها سرهم کرده بودید در چند ساعت توسط مهندس ارشدی که هرگز با او صحبت نکرده‌اید بازنویسی شده. حس خوبی نمی‌دهد.

دوم، شاید از شرایط اولیهٔ اطراف کد هنگام نوشتن آگاه نباشد. به‌ویژه وقتی بازآرایی‌کنندهٔ عبوری فعالاً نگهداری نمی‌کند مشکل‌ساز است. چرا مهم است؟ برنامه‌نویسی همه مبادله است؛ راه‌حل سریع‌تر با ساختار دادهٔ پرحافظه یا اثر حافظهٔ کمتر با تقریب به‌جای محاسبات دقیق. به همین ترتیب، هر خط «بد» کد تلاشی برای حل مسئله بود. با بازآرایی کورکورانه، ممکن است قربانی باگ یا ضعفی شوید که نویسندگان اصلی با دقت از آن اجتناب می‌کردند.

دربارهٔ نویسندگان کد اصلی

متأسفانه همه در یک شرکت برای کل دوران کاری نمی‌مانند. نویسندهٔ اصلی کد ممکن است دیگر در ردیف‌های شما نباشد. خوشبختانه افراد معمولاً قبل از رفتن به سرزمین سبزتر اطلاعات را به تیم منتقل می‌کنند، پس احتمالاً تیمی هست که می‌توانید با آن هماهنگ شوید و دیدگاه بیشتری بگیرید.

بازآرایی‌کنندهٔ عبوری نباشید، بازآرایی‌کنندهٔ با نیت خوب باشید. به‌ندرت کدی را بازآرایی کنید که فعالاً نگهداری نمی‌کنید، و وقتی می‌کنید با نظر مسئولان آن باشد.

برای گسترش‌پذیرتر کردن کد

استادان بازآرایی آن را برای قابل گسترش‌تر کردن کد تبلیغ می‌کنند. اگرچه این می‌تواند نتیجهٔ واضح بازآرایی خوب باشد، بازنویسی برای انعطاف‌پذیری آینده احتمالاً نادرست است. زمان بازآرایی بدون درک بردهای فوری و ملموس ممکن است تلاش بیهوده باشد؛ تغییرات شاید در مدت معقول — یا در بدترین حالت در طول عمر کد — سود ندهند.

اگر می‌توانید تغییرات کافی برای پیشبرد پروژه بدهید، احتمالاً نباید بازآرایی کنید. بیشتر شرکت‌ها قابلیت جدید و رفع باگ دارند. به‌طور کلی این‌ها تقریباً همیشه اولویت بالاتر دارند. مگر مجموعهٔ اهداف مشخص و استدلال قانع‌کننده که مستقیماً سود شرکت را تحت تأثیر قرار دهد، زنجیرهٔ مدیریت قانع نمی‌شود. اما ناامید نشوید! در فصل‌های بعد برای بازآرایی پروندهٔ کسب‌وکار می‌سازیم.

وقتی وقت ندارید

تنها چیز بدتر از کدی که به‌شدت به بازآرایی نیاز دارد، کد نیمه‌بازآرایی‌شده است. کد در بلاتکلیفی برای توسعه‌دهندگانی که با آن کار می‌کنند گیج‌کننده است. وقتی نقطهٔ زمانی مشخصی برای اتمام کامل بازآرایی نیست، آشفتگی نیمه‌دائمی پیدا می‌کند. خواننده اغلب جهت یا پیاده‌سازی دنبال‌شده را تشخیص نمی‌دهد، به‌ویژه اگر بازآرایی‌کننده توضیحی نگذاشته. شاید فرض نادرستی کنید کدام کد بلندمدت پذیرفته می‌شود و تغییر لازم را در بلوکی که قرار است کنار گذاشته شود پیاده کنید. این اشتباهات سریع انباشته می‌شوند و فرسودگی سریع‌تر و جدی‌تر از کدی که امید بهبودش را داشتید.

وقتی بازآرایی می‌کنید، وقت کافی برای دیدن برنامه‌ها تا پایان داشته باشید. اگر نه، دامنه را کوچک کنید تا بهبودهایی بدهید اما راحت به خط پایان برسید. هیچ سود موقت از بازآرایی ناقص، سردرگمی و ناامیدی توسعه‌دهندگان آینده را جبران نمی‌کند.

اولین مثال بازآرایی ما

حالا که پایهٔ محکمی برای درک اهداف بازآرایی و چگونگی تبدیل شدن به برنامه‌نویسان بهتر در شرایط مناسب ساختیم، با مثال کوچکی آن را زنده می‌کنیم. دامنهٔ این مثال کوچک‌تر از تلاش‌های بازآرایی‌ای است که در این کتاب بحث می‌کنیم، اما مفاهیم را در مقیاس کوچک‌تر نشان می‌دهد تا زود با آن‌ها آشنا شویم.

فرض کنید در دانشگاهی کار می‌کنیم و برنامهٔ ابتدایی‌ای را که دستیاران آموزشی (TA) برای ارسال نمرهٔ تکالیف استفاده می‌کنند توسعه و پشتیبانی می‌کنیم. دستیاران با برنامه تأیید می‌کنند نمره‌ها در بازه‌ای مشخص‌شده توسط استاد باشند. این بازه قابل پیکربندی است چون اساتید تکالیف را متفاوت ساختار می‌دهند؛ پس همهٔ مجموعهٔ مسائل روی مقیاس ۰ تا ۱۰۰ نمره‌دهی نمی‌شوند. مثلاً مجموعهٔ مسئله با ۱۰ سؤال؛ هر سؤال حداکثر ۶ نمره. اگر همهٔ سؤالات را درست جواب دهید، نمرهٔ نهایی ۶۰ از ۶۰ است. اگر اصلاً تکلیف را ارسال نکنید، ۰ نمره می‌گیرید.

اساتید همان ابزار را برای اطمینان از اینکه میانگین نمرهٔ یک تکلیف در بازهٔ مورد انتظار است استفاده می‌کنند. در مثال قبلی، فرض کنید استاد می‌خواهد میانگین مجموعهٔ مسئله بین ۴۲ و ۴۸ نمره باشد (برای درصد بین ۷۰٪ و ۸۰٪). بازهٔ مورد انتظار را به برنامه می‌دهد که نمره‌های نهایی را پردازش و تعیین می‌کند آیا میانگین در این مرزهاست یا نه.

تابع مسئول این منطق checkValid نام دارد و در مثال ۱-۱ نشان داده شده است.

مثال ۱-۱. نمونهٔ کوچک و گیج‌کنندهٔ کد

javascript
function checkValid(
  minimum,
  maximum,
  values,
  useAverage = false)
{
  let result = false;
  let min = Math.min(...values);
  let max = Math.max(...values);
  if (useAverage) {
    min = max = values.reduce((acc, curr) => acc + curr, 0)/values.length;
  }

    if (minimum < 0 || maximum > 100) {
      result = false;
    } else if (!(minimum <= min) || !(maximum >= max)) {
      result = false;
    } else if (maximum >= max && minimum <= min) {
      result = true;
    }
    return result;
}

فوراً چند مشکل می‌بینیم. اول، نام تابع مسئولیت‌هایش را کاملاً پوشش نمی‌دهد. از تابعی با نام کلی مثل checkValid دقیقاً چه انتظاری داریم (به‌ویژه اگر توضیحی بالای اعلان تابع نباشد). دوم، مقادیر درون‌خطی (۰، ۱۰۰) نامشخص‌اند. با دانستن رفتار مورد انتظار تابع، می‌توانیم استنباط کنیم این اعداد حداقل و حداکثر مطلق مجاز نمره برای هر تکلیف هستند. در این زمینه، مقدار حداقل ۰ منطقی است، اما چرا حد بالای ۱۰۰ را تأیید می‌کنیم؟ سوم، منطق دشوار دنبال کردن است؛ نه‌تنها شرط‌های زیادی برای استدلال هست، منطق درون‌خطی می‌تواند پیچیده باشد و استدلال سریع هر مورد را سخت کند. با نگاه سریع، تقریباً غیرممکن است بدانیم آیا تابع باگ دارد یا نه. می‌توان زمان قابل‌توجهی صرف فهرست کردن مشکلات زیاد در این چند خط کوتاه کرد، اما برای سادگی اینجا متوقف می‌شویم.

چگونه این همه خط کم می‌تواند این‌قدر سخت فهمیده شود؟ کد در توسعهٔ فعال مرتب برای تغییرات کوچک و کم‌اثر (رفع باگ، قابلیت جدید، بهینه‌سازی کارایی و غیره) تغییر می‌کند. متأسفانه این تغییرات انباشته می‌شوند و اغلب کد طولانی‌تر و پیچیده‌تر می‌شود. از ساختار کد، دو تغییری را می‌توان شناسایی کرد که احتمالاً بعد از نوشتن اولیهٔ تابع رخ داده‌اند:

  • امکان اعتبارسنجی بازه روی میانگین مجموعهٔ مقادیر داده‌شده به‌جای مجموع آن‌ها. می‌توانم استنباط کنم این قابلیت بعداً اضافه شده؛ useAverage آرگومان بولی اختیاری با مقدار پیش‌فرض false است، یعنی محل‌های فراخوانی موجود آرگومان چهارم را انتظار ندارند. آرگومان‌های بولی code smell هستند؛ به‌زودی به آن می‌پردازیم. علاوه بر این، کد هر دو min و max را برای نمایش میانگین واحد جدید بازنویسی می‌کند؛ یعنی نویسنده آسان‌ترین راه با کمترین تغییر کد را می‌خواسته.
  • اطمینان از اینکه هیچ بازه‌ای زیر ۰ یا بالای ۱۰۰ نباشد. عجیب است اساتید را از ساخت تکالیف بیش از ۱۰۰ نمره منع کنیم، اما فعلاً این رفتار عمدی فرض می‌کنیم. اگرچه سرنخ قطعی نیست، می‌توان حدس زد این رفتار بعداً اضافه شده چون شرط تأیید حدود مطلق بازه در انتها قرار دارد. چرا بلافاصله تأیید نکنیم که حداقل و حداکثر داده‌شده در بازهٔ قابل قبول است؟ نویسندهٔ تغییر احتمالاً سری شرط‌ها را دیده و آسان‌ترین جا برای شرط جدید را انتها دانسته. می‌توان فرضیه را با تاریخچهٔ نسخه و ثبت تغییر (commit) اصلی با پیام مفید تأیید کرد.

ساده‌سازی شرط‌ها

اول منطق دستور if را ساده می‌کنیم. می‌توانیم به‌جای ارزیابی هر شاخه و برگرداندن مقدار نهایی، زود از تابع نتیجه برگردانیم. همچنین اگر مقادیر حداقل و حداکثر داده‌شده خارج از بازهٔ ۰، ۱۰۰ باشند زود برمی‌گردیم، مانند مثال ۱-۲.

مثال ۱-۲. نمونهٔ کوچک با بازگشت زودهنگام

javascript
function checkValid(
  minimum,
  maximum,
  values,
  useAverage = false
) {

    if (minimum < 0 || maximum > 100) return false;

    let min = Math.min(...values);
    let max = Math.max(...values);

    if (useAverage) {
      min = max = values.reduce((acc, curr) => acc + curr, 0)/values.length;
    }

    if (!(minimum <= min) || !(maximum >= max)) return false;
    if (maximum >= max && minimum <= min) return true;

    return false;
}

اگر حداقل یا حداکثر خارج از بازه باشد، زود برگردان.

منطق را با بازگشت زودهنگام هر جا ممکن است ساده کن.

حالا جلو می‌رویم! ببینیم آیا می‌توانیم منطق را با استدلال همهٔ مواردی که تابع مقدار نادرست برمی‌گرداند بیشتر ساده کنیم: موردی که حداقل محاسبه‌شده کوچک‌تر از حداقل داده‌شده است و موردی که حداکثر محاسبه‌شده بزرگ‌تر از حداکثر داده‌شده است. می‌توانیم شرط‌های فعلی را با شکست زودهنگام جایگزین کنیم و فقط بعد از تأیید هر مورد شکست ساده مقدار درست برگردانیم. مثال ۱-۳ این تغییرات را نشان می‌دهد.

مثال ۱-۳. نمونهٔ کوچک با منطق ساده‌شده

javascript
function checkValid(
  minimum,
  maximum,
  values,
  useAverage = false
) {

     if (minimum < 0 || maximum > 100) return false;

     let min = Math.min(...values);
     let max = Math.max(...values);

     if (useAverage) {
       min = max = values.reduce((acc, curr) => acc + curr, 0)/values.length;
     }

     if (min < minimum) return false;
     if (max > maximum) return false;
     return true;
}

منطق را با استدلال موارد ساده کن؛ یک شرط در هر if.

زود شکست بخور و فقط وقتی مطمئنی مقادیر معتبرند مقدار درست برگردان.

استخراج اعداد جادویی

گام بعد استخراج اعداد درون‌خطی (اعداد جادویی) به متغیرهای با نام گویاست. values را برای وضوح به grades تغییر نام می‌دهیم. (یا می‌توانستیم آن‌ها را ثابت در همان محدودهٔ اعلان تابع تعریف کنیم، اما فعلاً ساده نگه می‌داریم.) مثال ۱-۴ این شفاف‌سازی‌ها را نشان می‌دهد.

مثال ۱-۴. نمونهٔ کوچک با متغیرهای واضح‌تر

javascript
function checkValid(
  minimumBound,
  maximumBound,
  grades,
  useAverage = false
) {

     // Valid assignments should never allow fewer than 0 points
     var absoluteMinimum = 0;

     // Valid assignments should never exceed more than 100 possible points
     var absoluteMaximum = 100;

     if (minimumBound < absoluteMinimum) return false;
     if (maximumBound > absoluteMaximum) return false;

    let min = Math.min(...grades);
    let max = Math.max(...grades);

    if (useAverage) {
      min = max = grades.reduce((acc, curr) => acc + curr, 0)/grades.length;
    }

    if (min < minimumBound) return false;
    if (max > maximumBound) return false;
    return true;
}

تغییر نام پارامتر برای توصیف نقش آن.

اعداد جادویی با نام مناسب برای زمینهٔ بیشتر.

ساده‌سازی بیشتر منطق با تقسیم شرط پیچیده به دو if ساده‌تر.

استخراج منطق مستقل

بعد می‌توانیم محاسبهٔ میانگین را به تابع جدا استخراج کنیم، مانند مثال ۱-۵.

مثال ۱-۵. نمونهٔ کوچک با توابع بیشتر با مسئولیت‌های مشخص

javascript
function checkValid(
   minimum,
   maximum,
   grades,
   useAverage = false
){
   // Valid assignments should never allow fewer than 0 points
   var absoluteMinimum = 0;

    // Valid assignments should never exceed more than 100 possible points
    var absoluteMaximum = 100;

    if (minimumBound < absoluteMinimum) return false;
    if (maximumBound > absoluteMaximum) return false;

    let min = Math.min(...grades);
    let max = Math.max(...grades);

    if (useAverage) {
      min = max = calculateAverage(grades);
    }

    if (min < minimumBound) return false;
    if (max > maximumBound) return false;
    return true;
}

function calculateAverage(grades) {
  return grades.reduce((acc, curr) => acc + curr, 0)/grades.length;
}

محاسبهٔ میانگین به تابع جدید استخراج شد.

با تکرار روی راه‌حل، منطق میانگین مجموعهٔ نمره‌ها به‌طور فزاینده‌ای نامتناسب به نظر می‌رسد. بعد دو تابع می‌سازیم: یکی تأیید می‌کند میانگین مجموعهٔ نمره‌ها در مرزهاست و دیگری تأیید می‌کند همهٔ نمره‌ها در حداقل و حداکثر هستند. می‌توان در این نقطه به روش‌های متعدد کد را به توابع متمرکزتر سازمان داد. پاسخ درست یا غلطی نیست تا وقتی منطق دو حالت متمایز را مؤثر جدا کرده‌ایم. مثال ۱-۶ یکی از راه‌های بیشتر ساده‌سازی checkValid را نشان می‌دهد.

مثال ۱-۶. نمونهٔ کوچک با توابع بهتر تعریف‌شده

javascript
function checkValid(
   minimum,
   maximum,
   grades,
   useAverage = false
){

     // Valid assignments should never allow fewer than 0 points
     var absoluteMinimum = 0;

     // Valid assignments should never exceed more than 100 possible points
     var absoluteMaximum = 100;

     if (minimumBound < absoluteMinimum) return false;
     if (maximumBound > absoluteMaximum) return false;

     let min = Math.min(...grades);
     let max = Math.max(...grades);

     if (useAverage) {
       return checkAverageInBounds(minimumBound, maximumBound, grades);
     }

     return checkAllGradesInBounds(minimumBound, maximumBound, grades);
}

function calculateAverage(grades) {
    return grades.reduce((acc, curr) => acc + curr, 0)/grades.length;
}

function checkAverageInBounds(
   minimumBound,
   maximumBound,
   grades
){
   var avg = calculateAverage(grades);
   if (avg < minimumBound) return false;
   if (avg > maximumBound) return false;
   return true;
}

function checkAllGradesInBounds(
   minimumBound,
   maximumBound,
   grades
){
  var min = Math.min(...grades);
   var max = Math.max(...grades);

    if (min < minimumBound) return false;
    if (max > maximumBound) return false;
    return true;
}

منطق تعیین اینکه آیا میانگین نمره‌ها در حداقل و حداکثر است در تابع خودش استخراج شد.

منطق تعیین اینکه آیا همهٔ نمره‌ها در حداقل و حداکثر هستند در تابع جدا استخراج شد.

تمام! checkValid را در شش گام ساده بازآرایی کردیم.

راه‌حل پالایش‌یافته‌تر

در این مورد، بازنگری کاملاً کامل این تابع چند تغییر اضافی می‌خواست. تابع جدیدی برای کپسوله‌کردن منطق خطوط ۷ تا ۱۴ می‌نوشتیم. بعد آن را به‌عنوان گام اول در هر دو checkAverageInBounds و checkAllGradesInBounds فراخوانی می‌کردیم. در پایان همهٔ محل‌های فراخوانی در checkValid را شناسایی و با فراخوانی مستقیم به checkAverageInBounds اگر useAverage برابر true بود یا checkAllGradesInBounds اگر useAverage حذف شده یا برابر false بود جایگزین می‌کردیم. دیگر لازم نبود تعریف checkValid را بخوانیم تا بفهمیم پارامتر بولی اختیاری چه می‌کند، و نه لازم بود کد را بخوانیم تا بفهمیم منظورمان از مجموعهٔ «معتبر» نمره‌ها چیست. سرانجام checkValid را از کدبیس حذف می‌کردیم.

نسخهٔ جدید مزایای واضعی دارد. با یک نگاه می‌توانیم درک محکمی از هدف کد پیدا کنیم. همچنین کمی کارآمدتر شده و منطق مستعد باگ را با ساده‌سازی شرط‌ها آسان‌تر کرده‌ایم. در مجموع، توسعه‌دهندهٔ بعدی احتمال بیشتری دارد بدون دردسر زیاد روی این راه‌حل بسازد. این فقط نگاهی گذرا به اثر مثبت بالقوهٔ بازآرایی راهبردی در سطح خرد روی برنامه است؛ حالا تصور کنید وقتی در مقیاس اعمال شود چه اثری دارد.

اما قبل از اینکه پشت میز بنشینیم و با جدیت بازآرایی کنیم، باید درست جهت‌گیری کنیم. باید تاریخچهٔ کدی که می‌خواهیم بهبود دهیم را بفهمیم، و برای آن باید بفهمیم کد چگونه فرسوده می‌شود.