حالت تاریک
فصل ۳ — اندازهگیری وضعیت اولیه
هر بهار، زمانی را صرف مرتبکردن کمد لباسم و بازنگری همهٔ لباسهایی میکنم که دارم. در حالی که برخی رویکردی شبیه Marie Kondo برای خالیکردن کمدشان دارند و میبینند آیا هر قلم «شادی میآفریند»، من رویکردی روشمندتر دارم. هر سال وقتی این فرایند را شروع میکنم، میدانم در پایان تعدادی از اقلام در پشتهٔ اهدا قرار میگیرند. نمیدانم کدام قطعات خواهند بود، چون کاملاً به این بستگی دارد که همهٔ لباسهایم در مجموع چگونه با هم کار میکنند.
قبل از اینکه چند کیسه برای Goodwill بستهبندی کنم، نگاهی جامع به کل میاندازم. همهچیز را بر اساس نوع لباس مرتب میکنم: ژاکتها در یک پشته، پیراهنها در پشتهٔ دیگر و همینطور ادامه میدهم، و در حین کار عملی بودن هر قلم را در نظر میگیرم. این پیراهن برای کدام فصلها مناسب است؟ چقدر راحت است؟ در سال گذشته چند بار پوشیدهام؟ سپس تقریباً تخمین میزنم هر قلم را میتوان در چند ست لباس ترکیب کرد. تنها وقتی درک محکمی از همهٔ آنچه دارم پیدا کردهام و نقش هر قلم در کمدم را فهمیدهام، میتوانم لباسهایی را که میتوانم با خیال راحت اهدا کنم شناسایی کنم.
همین منطق برای تلاشهای بزرگ بازآرایی هم صدق میکند؛ تنها وقتی میتوانیم سطح مسئلهای را که میخواهیم بهبود دهیم بهخوبی توصیف کنیم، میتوانیم بهترین راه بهبود آن را شناسایی کنیم. متأسفانه یافتن راههای معنادار برای اندازهگیری نقاط درد در کد امروز بسیار دشوارتر از دستهبندی لباسها در کمد است. این فصل تعدادی از تکنیکها برای کمیسازی و کیفیسازی وضعیت کدمان را پیش از شروع بازآرایی بررسی میکند. چند تکنیک شناختهشده و چند رویکرد جدیدتر و خلاقانهتر را پوشش میدهیم. در پایان فصل، امیدوارم یکی (یا بیشتر) از راههای اندازهگیری کدی که میخواهید بهبود دهید را پیدا کرده باشید؛ بهگونهای که مشکلاتی را که میخواهید حل کنید برجسته کند.
چرا اندازهگیری اثر بازآرایی دشوار است؟
راههای متعددی برای اندازهگیری سلامت یک کدبیس وجود دارد. با این حال، بسیاری از این معیارها ممکن است در نتیجهٔ یک بازآرایی در مقیاس بزرگ در جهت مثبت حرکت نکنند، صرفاً به این دلیل که با نقاط دردی که پروژه قصد دارد به آنها بپردازد، ناسازگار (orthogonal) هستند. بنابراین، در اندازهگیری وضعیت اولیهٔ کدبیس، میخواهیم معیاری را انتخاب کنیم که باور داریم مسئله را بهخوبی خلاصه میکند و اثر بازآرایی ما را بهدرستی برجسته میکند.
اندازهگیری اثر هر تلاش بازآرایی پیچیده است، عمدتاً به این دلیل که وقتی بازآرایی با موفقیت انجام شود، باید برای کاربران نامرئی باشد و هیچ تغییر رفتاری ایجاد نکند. این ویژگی جدیدی نیست که امید داریم پذیرش کاربر را افزایش دهد یا یک تنظیم جزئی باشد. اغلب تلاش زیادی برای پایش بخشهای حیاتی برنامههایمان میکنیم تا مطمئن شویم کاربران تجربهٔ قابلاعتمادی دارند، اما چون این معیارها رفتاری را ثبت میکنند که کاربران احتمالاً متوجه میشوند، بیشتر آنها وقتی درست بازآرایی کردهایم، بدون تغییر میمانند. برای بهترین توصیف اثر یک بازآرایی، باید معیارهایی را شناسایی کنیم که جنبههای دقیق کدی را که میخواهیم بهبود دهیم اندازهگیری کنند و پیش از ادامه، یک خط پایهٔ قوی برقرار کنیم.
یادداشتی دربارهٔ بازآرایی برای بهبود کارایی
فرض کنید یک برنامهٔ کوچک برای ردیابی سفارشهای مشتری داریم. برای اطمینان از روان بودن سیستم، زمان لازم برای بازیابی وضعیت یک سفارش با شناسهٔ آن را پایش میکنیم. پس از چند ماه متوجه میشویم زمان پاسخدهی کند شده و تصمیم میگیریم زمان را صرف بازآرایی کد زیرین کنیم. در این سناریو، معیار شروع را از قبل داریم: میانگین اولیهٔ زمان پاسخ. میتوانیم بهراحتی موفقیت تلاش را با مقایسهٔ میانگین اولیه با میانگین جدید پس از استقرار کد بازنویسیشده بسنجیم. تمام!
کمیسازی اثر بازآرایی با انگیزهٔ کارایی اغلب سادهترین است. معمولاً مجموعهٔ قابلاعتمادی از معیارهای شروع از قبل در دسترس داریم. همچنین شایان ذکر است که تلاشهای مبتنی بر کارایی، برخلاف بازآراییهایی که با هدف افزایش بهرهوری توسعهدهنده انجام میشوند، یکی از معدود انواع بازآراییاند که به بهبودهای روشن و قابلمشاهده برای کاربر منجر میشوند.
تلاشهای بزرگ بازآرایی بهویژه دشوار برای اندازهگیریاند چون بهندرت در چند هفته انجام میشوند. بیشتر اوقات، کار از شروع تا پایان فراتر از چرخهٔ معمول توسعهٔ ویژگی طول میکشد، و مگر اینکه توسعهٔ محصول کاملاً متوقف شده باشد، ممکن است جداسازی اثر آن از کار سایر توسعهدهندگان در همان بخش برنامه دشوار باشد. تکیه بر چند معیار متمایز میتواند به شما کمک کند تصویر جامعتری از پیشرفت بسازید و تغییرات خود را بهتر از تغییراتی که سایر توسعهدهندگان همزمان روی محصول اعمال میکنند، تفکیک کنید.
اندازهگیری پیچیدگی کد
بسیاری از ما با انگیزهٔ افزایش بهرهوری توسعهدهنده بازآرایی میکنیم؛ یعنی آسانتر کردن نگهداری برنامهها و ساخت ویژگیهای جدید. در عمل، این اغلب به معنای سادهسازی بخشهای پیچیده و درهمتنیدهٔ کد است. با توجه به اینکه هدف ما کاهش پیچیدگی کد است، باید راه معناداری برای اندازهگیری آن پیدا کنیم. کمیسازی پیچیدگی کد نقطهٔ شروعی به ما میدهد که از آن میتوانیم پیشرفت را ارزیابی کنیم.
اندازهگیری پیچیدگی نرمافزار از دو جهت آسان است. اول، اگر کد در تاریخچهٔ نسخه قرار دارد، میتوانیم بهراحتی در زمان سفر کنیم و محاسبات پیچیدگی را در هر بازه اعمال کنیم. دوم، تعداد زیادی کتابخانه و ابزار متنباز در بسیاری از زبانهای برنامهنویسی در دسترس است. تولید گزارش برای کل برنامه میتواند بهسادگی نصب یک بسته و اجرای یک دستور باشد.
در اینجا سه روش رایج محاسبهٔ پیچیدگی کد را بررسی میکنیم.
معیارهای Halstead
Maurice Halstead در سال ۱۹۷۵ برای اولین بار پیشنهاد کرد پیچیدگی نرمافزار را با شمارش تعداد عملگرها (operators) و عملوندها (operands) در یک برنامهٔ کامپیوتری اندازهگیری کنیم. باور داشت چون برنامهها عمدتاً از این دو واحد تشکیل شدهاند، شمارش نمونههای یکتای آنها معیار معناداری از اندازهٔ برنامه و در نتیجه چیزی دربارهٔ پیچیدگی آن به ما میدهد.
عملگرها ساختارهاییاند که مانند تابع رفتار میکنند، اما از نظر نحوی یا معنایی با توابع معمولی تفاوت دارند. اینها شامل نمادهای حسابی مانند - و +، عملگرهای منطقی مانند &&، عملگرهای مقایسهای مانند > و عملگرهای انتساب مانند = میشوند. مثلاً تابع سادهای که دو عدد را جمع میکند، مانند مثال ۳-۱.
javascript
function add(x, y) {
return x+y;
}این تابع یک عملگر دارد: عملگر جمع +. عملوندها از سوی دیگر هر موجودیتیاند که با مجموعهٔ عملگرهایمان روی آن عمل میکنیم. در مثال جمع، عملوندهای ما x و y هستند.
با این دادههای ساده، Halstead مجموعهای از معیارها برای محاسبهٔ مجموعهای از ویژگیها پیشنهاد کرد:
- حجم (volume) برنامه، یا میزان اطلاعاتی که خوانندهٔ کد باید جذب کند تا معنای آن را بفهمد.
- دشواری (difficulty) برنامه، یا میزان تلاش ذهنی لازم برای بازسازی نرمافزار؛ که معمولاً به آن معیار Halstead effort هم گفته میشود.
- تعداد باگهایی که احتمالاً در سیستم پیدا خواهید کرد.
برای بهتر نشان دادن ایدههای Halstead، میتوانیم تکنیک شمارش عملگر و عملوند را روی تابع کمی پیچیدهتر — که عوامل اول یک عدد صحیح را محاسبه میکند — مانند مثال ۳-۲ اعمال کنیم. هر یک از عملگرها و عملوندهای یکتا را همراه با تعداد دفعات وقوع آنها در برنامه، در جدول ۳-۱ فهرست کردهایم.
javascript
function primeFactors(number) {
function isPrime(number) {
for (let i = 2; i <= Math.sqrt(number); i++) {
if (number % i === 0) return false;
}
return true;
}
const result = [];
for (let i = 2; i <= number; i++) {
while (isPrime(i) && number % i === 0) {
if (!result.includes(i)) result.push(i);
number /= i;
}
}
return result;
}جدول ۳-۱. عملگرها و عملوندهای یکتا با فراوانی آنها
| عملگر | تعداد وقوع | عملوند | تعداد وقوع |
|---|---|---|---|
| function | 2 | 0 | 2 |
| for | 2 | 2 | 2 |
| let | 2 | primeFactors | 1 |
| = | 3 | number | 7 |
| <= | 2 | isPrime | 2 |
| () | 4 | i | 12 |
| . | 3 | Math | 1 |
| ++ (postfix) | 2 | sqrt | 1 |
| if | 2 | FALSE | 1 |
| === | 2 | TRUE | 1 |
| % | 2 | result | 4 |
| return | 3 | <anonymous> | 1 |
| const | 1 | includes | 1 |
| [] | 1 | push | 1 |
| while | 1 | ||
| && | 1 | ||
| ! (prefix) | 1 | ||
| /= | 1 | ||
| عملگرهای یکتا: ۱۸ — مجموع عملگرها: ۳۵ | عملوندهای یکتا: ۱۴ — مجموع عملوندها: ۳۷ |
با توجه به اینکه برنامهٔ فاکتورگیری اول ما ۱۸ عملگر یکتا (n1)، ۱۴ عملوند یکتا (n2) و مجموع ۳۷ عملوند (N2) دارد، میتوانیم از معیار دشواری Halstead برای محاسبهٔ دشواری نسبی خواندن برنامه با معادلهٔ پایه استفاده کنیم:
n1 N 2
D= ·
2 n2با جایگذاری مقادیر، امتیاز دشواری کلی ۲۳٫۷۸ به دست میآید:
18 37
D= ·
2 14
D = 23 . 78اگرچه ۲۳٫۷۸ بهتنهایی شاید معنای زیادی نداشته باشد، میتوانیم بهتدریج درک کنیم این امتیاز چگونه با تجربهٔ کار با بخشهای مختلف کدمان نگاشت میشود. با گذشت زمان و مواجههٔ مکرر با این مقادیر در کنار پیادهسازیهایشان، بهتر میتوانیم تفسیر کنیم امتیاز ۲۳٫۷۸ در بافت گستردهتر برنامهٔ ما چه معنایی دارد.
هر یک از سه معیار متمایز توصیفشده در این بخش را میتوان در مقیاسهای مختلف تولید کرد؛ میتوانند پیچیدگی یک تابع منفرد یا یک ماژول کامل را کمیسازی کنند. مثلاً میتوانید معیار دشواری Halstead را برای یک فایل کامل با جمع دشواری توابع منفرد درون آن محاسبه کنید.
پیچیدگی Cyclomatic
Thomas McCabe در سال ۱۹۷۶ پیچیدگی cyclomatic را بهعنوان معیار کمی تعداد مسیرهای خطیمستقل در کد منبع یک برنامه توسعه داد. در اصل شمارش تعداد دستورات جریان کنترل (control flow) در برنامه است. این شامل دستورات if، حلقههای while و for و دستورات case درون بلوکهای switch میشود.
مثلاً برنامهٔ سادهای بدون مؤلفهٔ جریان کنترل، مانند مثال ۳-۳. برای محاسبهٔ پیچیدگی cyclomatic آن، ابتدا ۱ برای اعلان تابع در نظر میگیریم و با هر نقطهٔ تصمیمگیری که میبینیم افزایش میدهیم. مثال ۳-۳ پیچیدگی cyclomatic برابر ۱ دارد چون تنها یک مسیر در تابع وجود دارد.
javascript
function convertToFahrenheit(celsius) {
return celsius * (9/5) + 32;
}بیایید مثال پیچیدهتری ببینیم، مانند تابع primeFactors از مثال ۳-۲. در مثال ۳-۴ آن را کاهش داده و هر نقطهٔ جریان کنترل را فهرست کردهایم تا پیچیدگی cyclomatic برابر ۶ به دست آید.
javascript
function primeFactors(number) {
function isPrime(number) {
for (let i = 2; i <= Math.sqrt(number); i++) {
if (number % i === 0) return false;
}
return true;
}
const result = [];
for (let i = 2; i <= number; i++) {
while (isPrime(i) && number % i === 0) {
if (!result.includes(i)) result.push(i);
number /= i;
}
}
return result;
}- اعلان تابع اولین نقطهٔ جریان کنترل است.
- حلقهٔ
forاول نقطهٔ دوم است. - دستور
ifاول نقطهٔ سوم است. - حلقهٔ
forدوم نقطهٔ چهارم است. whileنقطهٔ پنجم است.ifدوم نقطهٔ ششم است.
وقتی بخشی از کد را میخوانیم، هر بار که شاخهای وجود دارد (دستور if، حلقهٔ for و غیره)، باید دربارهٔ حالتهای متعدد با مسیرهای اجرای مختلف استدلال کنیم. باید اطلاعات بیشتری در ذهن نگه داریم تا بفهمیم کد چه میکند. بنابراین با پیچیدگی cyclomatic برابر ۶، میتوانیم حدس بزنیم primeFactors احتمالاً خواندن و فهم آن خیلی دشوار نیست.
شمارش نقاط تصمیمگیری در برنامه سادهسازی روش پیشنهادی McCabe برای محاسبهٔ پیچیدگی است. از نظر ریاضی، میتوانیم پیچیدگی cyclomatic یک برنامهٔ ساختیافته را با تولید گراف جهتدار نمایانگر جریان کنترل آن محاسبه کنیم؛ هر گره یک بلوک پایه (یعنی دنبالهٔ خطی کد بدون شاخه) را نشان میدهد و اگر راهی برای عبور از یک بلوک به بلوک دیگر باشد، یالی بین آنها وجود دارد. با این گراف، پیچیدگی M طبق معادلهٔ زیر تعریف میشود؛ که در آن E تعداد یالها، N تعداد گرهها و P تعداد مؤلفههای متصل است؛ مؤلفهٔ متصل زیرگرافی است که همهٔ گرههایش از یکدیگر قابل دسترسیاند.
M = E − N + 2Pشکل ۳-۱ نمونهای از جریان کنترل برای primeFactors را نشان میدهد.
شکل ۳-۱. گراف جریان کنترل برای
primeFactors، با گرههای آبی نشاندهندهٔ حالتهای غیرپایانی و گرههای قرمز نشاندهندهٔ حالتهای پایانی. در این مثال ۱۳ یال، ۱۱ گره و ۲ مؤلفهٔ متصل داریم.
کاربردهای بیشتر گرافهای جریان کنترل
گرافهای جریان کنترل (CFG) فراتر از کمک به محاسبهٔ پیچیدگی میتوانند مفید باشند. در عمل، وقتی میخواهم جریانهای کنترل بهویژه پیچیده را بفهمم، اغلب وقت میگذارم و CFG را بهصورت دستی تولید میکنم تا نقاط تصمیمگیری را برجسته کنم. ابزارهای زیادی برای تولید خودکار CFG وجود دارد، اما انجام دستی آن مرا مجبور میکند کد را بخوانم و جریان را بهتر در ذهن تثبیت کنم.
این ساختارهای داده همچنین میتوانند برای شناسایی مؤثر کد غیرقابل دسترس استفاده شوند. فرض کنید CFG را از مجموعهای از توابع تولید کردهایم. اگر در آن CFG زیرگرافی وجود داشته باشد که از هیچ نقطهٔ ورودی متصل نیست، میتوانیم با اطمینان فرض کنیم غیرقابل دسترس است و میتوان حذفش کرد. از سوی دیگر، اگر بلوک خروجی از نقطهٔ ورودی غیرقابل دسترس باشد، ممکن است نشاندهندهٔ وجود حلقهٔ بینهایت باشد.
پیچیدگی NPath
Brian Nejmeh در سال ۱۹۸۸ پیچیدگی NPath را بهعنوان جایگزینی برای معیارهای پیچیدگی موجود پیشنهاد کرد. استدلال میکند تمرکز بر مسیرهای اجرای بدون چرخه بهاندازهٔ کافی رابطهٔ بین زیرمجموعههای متناهی مسیرها و مجموعهٔ همهٔ مسیرهای اجرای ممکن را مدل نمیکند. این محدودیت را میتوان در این واقعیت دید که پیچیدگی cyclomatic تو در تو بودن ساختارهای جریان کنترل را در نظر نمیگیرد. تابعی با سه حلقهٔ for پشتسرهم همان معیار را میدهد که تابعی با سه حلقهٔ for تو در تو. تو در تو بودن میتواند بر پیچیدگی روانشناختی تابع تأثیر بگذارد و پیچیدگی روانشناختی میتواند اثر زیادی بر توانایی ما در حفظ کیفیت نرمافزار داشته باشد.
معیار McCabe شاید محاسبهاش آسان باشد، اما نمیتواند بین انواع مختلف ساختارهای جریان کنترل تمایز قائل شود و دستورات if را مانند while یا for یکسان مینگرد. Nejmeh میگوید همهٔ ساختارهای جریان کنترل برابر نیستند؛ برخی درک و استفادهٔ درست از آنها دشوارتر است. مثلاً حلقهٔ while ممکن است برای توسعهدهنده سختتر از دستور switch باشد. پیچیدگی NPath تلاش میکند این نگرانی را برطرف کند. متأسفانه این محاسبه را حتی برای برنامههای کوچک کمی دشوارتر میکند، چون محاسبه بازگشتی است و میتواند بهسرعت بزرگ شود. محاسبات چند مثال با دستورات if را مرور میکنیم تا با نحوهٔ کار آن آشنا شویم. اگر میخواهید درک بهتری از محاسبهٔ پیچیدگی NPath با طیف گستردهتری از دستورات جریان کنترل (از جمله جریانهای کنترل تو در تو) پیدا کنید، مطالعهٔ مقالهٔ Nejmeh را شدیداً توصیه میکنم.
معیارهای جریان کنترل میتوانند به شما کمک کنند تعداد موارد آزمونی که کدتان نیاز دارد را تعیین کنید. پیچیدگی cyclomatic حد پایینی ارائه میدهد و پیچیدگی NPath حد بالایی. مثلاً برای
primeFactors، پیچیدگی cyclomatic نشان میدهد حداقل شش مورد آزمون میخواهیم تا هر نقطهٔ تصمیمگیری را پوشش دهیم.
مورد پایهٔ پیچیدگی NPath همان تابع مبدل دما در مثال ۳-۳ است؛ برای برنامهٔ ساده بدون نقطهٔ تصمیم، پیچیدگی NPath برابر ۱ است. برای نشان دادن مؤلفهٔ ضربی معیار، تابع سادهای با چند شرط if تو در تو را بررسی میکنیم.
مثال ۳-۵ تابع کوتاهی را نشان میدهد که با توجه به سرعت دادهشده، احتمال دریافت جریمهٔ سرعت را برمیگرداند. با خواندن تابع به اولین دستور if میرسیم؛ در این نقطه سرعت دادهشده میتواند کمتر یا بیشتر از ۴۵ کیلومتر بر ساعت باشد. سپس دو مسیر ممکن داریم: اگر سرعت بیشتر از ۴۵ کیلومتر بر ساعت باشد، وارد کد داخل بلوک if میشویم؛ وگرنه ادامه میدهیم. بعد باید بررسی کنیم آیا سرعت بیش از ۱۰ کیلومتر بر ساعت بالاتر از حد مجاز است؛ دوباره دو مسیر ممکن داریم. در نهایت ضریب ریسک محاسبهشده را برمیگردانیم.
javascript
function likelihoodOfSpeedingTicket(currentSpeed, limit){
risk = 0; // A
if (currentSpeed < 45) {
risk = 1; // B
} // C
if (currentSpeed > (limit + 10)) {
risk = 2; // D
} // E
return risk; // F
}پیچیدگی NPath تعداد مسیرهای متمایز در یک تابع را محاسبه میکند. میتوانیم هر مسیر را با فراخوانی likelihoodOfSpeedingTicket با بازهای از مقادیر و اعمال هر مجموعه شرایط فهرست کنیم. یک ورودی را با هم مرور میکنیم و مسیر پیمودهشده را برجسته میکنیم. سایر مسیرهای یکتا در جدول ۳-۲ برچسبگذاری شدهاند.
جدول ۳-۲. همهٔ مسیرهای یکتا در likelihoodOfSpeedingTicket
| ورودیها | مسیر |
|---|---|
| 30, 10 | A, B, D, F |
| 30, 50 | A, B, E, F |
| 90, 50 | A, C, D, F |
| 90, 110 | A, C, E, F |
| مسیرهای یکتا: ۴ |
فرض کنید likelihoodOfSpeedingTicket را با currentSpeed برابر ۳۰ و limit برابر ۰ فراخوانی میکنیم. دستور if اول درست (true) میشود و به B میرسیم. دستور if دوم هم درست میشود و به D میرسیم. سپس به دستور return در F میرسیم. با تکرار این الگو برای ورودیهای مختلف، چهار مسیر یکتا در تابع وجود دارد. بنابراین امتیاز NPath ما ۴ است.
معایب پیچیدگی NPath
پیچیدگی NPath همیشه بیشبرآورد تعداد مسیرهای اجرا در بخشی از کد را ارائه میدهد. مثلاً اگر یک بررسی نهایی اضافه کنیم که ببینیم آیا
currentSpeedبیش از ۱۳۵ کیلومتر بر ساعت است، پیچیدگی NPath چقدر میشود؟ سه دستورifداریم، هر کدام با دو نتیجهٔ ممکن، در مجموع ۲ × ۲ × ۲ یا ۸ مسیر. اما غیرممکن است سرعت همزمان زیر ۴۵ و بالای ۱۳۵ کیلومتر بر ساعت باشد؛ پس یکی از این مسیرها در زمان اجرا غیرممکن است. مهم است به خاطر داشته باشید که اگرچه پیچیدگی NPath میتواند در توصیف دشواری استدلال دربارهٔ بخشی از کد ارزشمند باشد، فقط برآوردی از حد بالا است.پیچیدگی NPath شاید بهتر رفتار انواع متمایز دستورات جریان کنترل را نشان دهد و بار روانشناختی نقاط تصمیم تو در تو را بگیرد، اما مقادیری که روی کدبیسهای بزرگ قدیمی تولید میکند میتواند عظیم باشد (صدها هزار). این عمدتاً به ماهیت نمایی معیار برمیگردد. متأسفانه یعنی خود مقدار بهسرعت معنایش را از دست میدهد و بهبودهای کوچک سخت تشخیص داده میشوند. توصیه میکنم پیچیدگی NPath را برای بخشهای محدود کدی که میخواهید بهبود دهید اندازهگیری کنید؛ شاید میانگین بخشهای منفرد را بهعنوان نقطهٔ شروع بگیرید.
برخی اشکال سادهٔ بازآرایی هیچ اثری روی معیارهای CFG ندارند. برخی پیچیدگیها صرفاً بهخاطر منطق کسبوکار پیچیده اجتنابناپذیرند. باید هر یک از این بررسیها و تکرارها را انجام دهید تا مطمئن شوید برنامه کار لازم را میکند. وقتی کدی که میخواهید بازآرایی کنید شامل سادهسازی منطق غیرضروری پیچیده است، NPath یا پیچیدگی cyclomatic گزینههای عالیاند. وگرنه معیارهای دیگری توصیه میکنم. با این حال حتی اگر در حال باز کردن کد اسپاگتی هستید، NPath یا cyclomatic نباید تنها معیارهای شما باشند؛ با یک نقطهٔ دادهٔ واحد نمیتوانید اثر تلاش بازآرایی را بهصورت جامع و درست توصیف کنید.
تعداد خطوط کد
متأسفانه معیارهای گراف جریان کنترل میتوانند محاسبهشان دشوار (و گاهی پرهزینه) باشد، بهویژه برای کدبیسهای بسیار بزرگ — که دقیقاً همانهاییاند که میخواهیم بهبود دهیم. اینجاست که اندازهٔ برنامه وارد میشود. اگرچه شاید بهاندازهٔ الگوریتمهای Halstead، McCabe یا Nejmeh علمی نباشد، در ترکیب با سایر اندازهگیریها میتواند به یافتن نقاط درد محتمل در برنامه کمک کند. اگر به دنبال رویکرد عملگرایانه و کمتلاش برای کمیسازی پیچیدگی کد هستید، معیارهای مبتنی بر اندازه راه درست است.
وقتی طول کد را اندازه میگیریم، چند گزینه داریم. بیشتر توسعهدهندگان فقط خطوط منطقی کد (LOC) را میسنجند و خطوط خالی و کامنت را کاملاً حذف میکنند. مانند معیارهای جریان کنترل، میتوانیم این اطلاعات را در وضوحهای مختلف جمعآوری کنیم. این چند نقطهٔ داده را بهعنوان مرجع مفید یافتهام:
LOC (تعداد خطوط کد) به ازای هر فایل
هر کدبیسی فایلهایی دارد که انگار اگر از ابتدا اسکرول کنید به انتها نمیرسید. اندازهگیری LOC این فایلها احتمالاً بار روانشناختی لازم برای فهم محتوا و مسئولیتهایشان وقتی توسعهدهنده آن را در ویرایشگر باز میکند را بهدرستی ثبت میکند.
طول تابع
برای هر فایل بیپایان، یک تابع بیپایان وجود دارد. (اغلب توابع بیپایان در فایلهای بیپایان یافت میشوند.) اندازهگیری طول توابع یا متدها در برنامه میتواند راه مفیدی برای تقریب پیچیدگی هر کدام باشد.
میانگین طول تابع به ازای هر فایل، ماژول یا کلاس
بسته به سازماندهی برنامه، شاید بخواهید میانگین طول تابع یا متد در هر واحد منطقی را دنبال کنید. در کدبیسهای شیءگرا، احتمالاً میانگین طول هر متد در یک کلاس یا بسته را دنبال میکنید. در کدبیسهای دستوری (imperative) شاید میانگین طول هر تابع در یک فایل یا ماژول بزرگتر را بسنجید. هر واحد سازماندهی بزرگتر، دانستن میانگین طول اجزای منطقی کوچکتر درون آن میتواند نشانهای از پیچیدگی نسبی آن واحد بهعنوان کل باشد.
یادداشتی دربارهٔ شمارش خطوط کامنت
بهطور کلی، نادیده گرفتن کامنتها هنگام شمارش LOC روش خوبی است. بلوکهای مستندسازی (doc block) و TODOهای درونخطی رفتار برنامه را تغییر نمیدهند؛ پس گنجاندن آنها در محاسبات اندازه به توصیف بهتر پیچیدگی برنامه کمکی نمیکند. با این حال در عمل دیدهام با شمارش کامنتهای درونخطی در سطح تابع میتوانید بهراحتی بخشهای گیجکنندهٔ کد را پیدا کنید. بهطور کلی توسعهدهندگان وقتی منطق اطراف دشوار فهم است کامنت درونخطی میگذارند. چه بهخاطر منطق کسبوکار پیچیده باشد چه بهتدریج درهم شده، وقتی کد بهویژه دشوار است معمولاً راهنمایی برای کسانی که بعد از ما میآیند میگذاریم. بنابراین میتوانیم تعداد کامنتهای درونخطی در یک تابع — کوتاه یا بلند — را بهعنوان هشدار احتمالی استفاده کنیم.
LOC بسته به زبان برنامه یا سبک برنامهنویسی ممکن است بهشدت متفاوت باشد، اما اگر سیب با سیب مقایسه کنیم، نباید نگران باشیم. در بازآرایی در مقیاس، معمولاً به بهبود کد در یک کدبیس بزرگ واحد میپردازیم. در تجربهٔ من، اکثر توسعهدهندگان روی این کدبیسها در ایجاد راهنمای سبک، تعریف بهترین روشها و اغلب اعمال این قواعد با autoformatter سرمایهگذاری کردهاند. تفاوت در تیمها و اجزا اجتنابناپذیر است، اما بهطور کلی برنامه بهاندازهٔ کافی شبیه به هم است که دو مجموعه معیار LOC از بخشهای متمایز کدبیس هنوز قابل مقایسه باشند.
معیارهای پوشش تست
وقتی ویژگی جدید توسعه میدهیم، چند فلسفهٔ تست میتوانیم بپذیریم. میتوانیم رویکرد test-driven development (TDD) را انتخاب کنیم و ابتدا مجموعهٔ کامل تست بنویسیم و سپس پیادهسازی را تا پاس شدن تستها تکرار کنیم؛ میتوانیم ابتدا راهحل و سپس تستهای متناظر را بنویسیم؛ یا میتوانیم بین این دو جایگزین شویم و پیادهسازی را گامبهگام بسازیم و در هر تکرار چند تست بنویسیم. هر رویکردی که داشته باشیم، نتیجهٔ مطلوب یکسان است: ویژگی جدیدی که کاملاً با مجموعهٔ باکیفیت تست پشتیبانی شده است.
بازآرایی موجودیت متفاوتی است. وقتی پیادهسازی موجود را بهبود میدهیم — هرقدر گسترده — میخواهیم مطمئن شویم رفتار را درست حفظ میکنیم. میتوانیم با اطمینان بگوییم راهحل جدید مانند قبلی کار میکند اگر به مجموعهٔ تست پیادهسازی اصلی تکیه کنیم. چون به پوشش تست برای هشدار دربارهٔ رگرسیونهای احتمالی تکیه میکنیم، پیش از شروع بازآرایی باید دو چیز را تأیید کنیم: اول، پیادهسازی اصلی پوشش تست دارد؛ دوم، آن پوشش تست کافی است.
فرض کنید میخواهیم تابع primeFactors در مثال ۳-۲ را بازآرایی کنیم. پیش از هر تغییری باید بسنجیم آیا پوشش تست دارد و اگر دارد، آیا کافی است. تأیید وجود پوشش تست آسان است؛ فایل تست متناظر را باز میکنیم و نگاهی میاندازیم. برای مثال ما فقط یک تست پیدا میکنیم، مانند مثال ۳-۶.
javascript
describe('base cases', () => {
test('0', () => {
expect(primeFactors(0)).toStrictEqual([]);
});
});تعیین کفایت پوشش تست اما کار دشوارتر است. میتوانیم آن را به دو روش ارزیابی کنیم: کمی و کیفی. از نظر کمی، درصدی از نسبت کدی که هنگام اجرای مجموعهٔ تست در برابر آن اجرا میشود محاسبه میکنیم. میتوانیم معیارهایی هم برای تعداد خطوط منطقی کد و هم برای تعداد مسیرهای اجرا که unit test سادهٔ ما پوشش میدهد جمعآوری کنیم که بهترتیب ۴۰ درصد و ۳۵٫۷۱ درصد میشوند. مثال ۳-۷ خروجی تست تولیدشده با فریمورک Jest را نشان میدهد.
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 35.71 | 0 | 50 | 40 |
primeFactors.js | 35.71 | 0 | 50 | 40 | 3-6,11-13
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 totalحالا باید تصمیم بگیریم آیا این پوشش تست کافی است. هیچکدام از این معیارها اطمینان زیادی به من نمیدهد که primeFactors بهخوبی تست شده؛ بعد از همه این نشان میدهد بیش از سهچهارم تابع توسط مجموعهٔ فعلی اجرا نمیشود. پوشش تست عمدتاً در دو زمینه مفید است:
- کمک به شناسایی مسیرهای تستنشده در برنامه
- بهعنوان معیار تقریبی اینکه آیا بهاندازهٔ کافی تست کردهایم
اگر به دنبال راهبردهایی برای تست نرمافزار قدیمی (legacy) هستید، توصیه میکنم کتاب Working Effectively with Legacy Code اثر Michael Feathers را بخوانید. گزینههای زیادی برای معرفی unit test با تأخیر با بهرهگیری از درزها (seams) در کد — مکانهایی که میتوانید رفتار برنامه را بدون تغییر خود کد تغییر دهید — بحث میکند.
برای بهبود پوشش تست مثال، میتوانیم یک مورد آزمون دیگر اضافه کنیم، مانند مثال ۳-۸. اگر پوشش را دوباره محاسبه کنیم (مثال ۳-۹)، میبینیم با فقط یک مورد آزمون اضافی میتوانیم به پوشش نزدیک به کامل برسیم. آیا این یعنی پوشش تست کافی است؟ از نظر کمی شاید کافی به نظر برسد؛ از نظر کیفی شاید نه. با نگاه به پیادهسازی primeFactors بهراحتی چند مورد آزمون گمشده را شناسایی میکنیم، مثل عدد منفی یا عدد ۲.
javascript
describe('base cases', () => {
test('0', () => {
expect(primeFactors(0)).toStrictEqual([]);
});
});
describe('small non-prime numbers', () => {
test('20', () => {
expect(primeFactors(0)).toStrictEqual([2, 5]);
});
});-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 100 | 83.33 | 100 | 100 |
primeFactors.js | 100 | 83.33 | 100 | 100 | 12
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 totalدر تجربهٔ من، کد نوشتهشده با دقت معمولاً بین ۸۰ تا ۹۰ درصد پوشش تست دارد. این نشان میدهد بیشتر کد تست شده است. با این حال هشدار کنید که پوشش تست بهتنهایی نشانهٔ کیفیت تست نیست. نوشتن unit testهای کمکیفیت برای رسیدن به پوشش کامل یا نزدیک به کامل آسان است. اگر مدیریت پوشش تست بالا را تشویق کند، معمولاً بخش قابلتوجهی از unit testها تلاش کمی برای تأیید (assert) رفتار مهم کد متناظر میکنند.
از منظر کیفی، تعیین کفایت پوشش تست آنقدرها ساده نیست. نوشتههای متفکرانهٔ زیادی دربارهٔ این موضوع وجود دارد که بیشتر فراتر از حوزهٔ این کتاب است، اما در سطح بالا فکر میکنم اگر نکات زیر برقرار باشد کیفیت تست مناسب به دست آمده:
- تستها قابلاعتمادند. از یک اجرا به اجرای بعد، هنگام اجرا روی کد بدون تغییر بهطور پایدار نتیجهٔ pass میدهند و در طول توسعه باگ میگیرند.
- تستها تابآورند. آنقدر به پیادهسازی گره نخوردهاند که تغییر را خفه کنند.
- طیف متنوعی از انواع تست کد را اجرا میکنند. داشتن unit test، integration test و end-to-end test میتواند به ما کمک کند با سطوح مختلف وفاداری تأیید کنیم کد طبق انتظار کار میکند.
اگر تأیید کردیم پوشش و کیفیت تست بهاندازهٔ کافی است، باید با اطمینان به بازآرایی ادامه دهیم. اگر تستها از نظر پوشش یا کیفیت کمبود دارند، باید زمان لازم را از پیش صرف نوشتن تستهای بیشتر و بهتر کنیم. اندازهگیری کمیت و کیفیت تست هر بخشی از کدی که قصد بازآرایی داریم گام مهمی است تا تعیین کنیم پیش از شروع بازآرایی چقدر کار اضافی لازم است.
پوشش نوع (Type Coverage)
در فصل ۲ مزایا و معایب برخی زبانهای برنامهنویسی با نوعدهی پویا (dynamically typed) را بحث کردیم. توسعهدهندگان روی کدبیسهای بزرگ با نوعدهی پویا شاید بخواهند زبان با نوعدهی تدریجی (gradually typed) را بپذیرند تا معرفی نوعهای ایستا (static) ممکن شود. نوعهای ایستا میتوانند خطاها را زودتر در فرایند توسعه با هشدار دربارهٔ نوعهای ناسازگار بگیرند و بار ذهنی برنامهنویسی را با ردیابی خودکار اطلاعاتی که وگرنه باید به خاطر بسپاریم کاهش دهند. TypeScript برای JavaScript، Cython یا mypy برای Python، Hacklang و PHP (از نسخهٔ ۷٫۰) نمونههایی از زبانهای با نوعدهی تدریجی هستند.
اگر در حال افزودن نوع به کدبیس هستید، احتمالاً میخواهید پیشرفت را با دنبال کردن پوشش نوع (type coverage) بسنجید. پوشش نوع را بهعنوان درصد کدی که اطلاعات نوع دارد محاسبه میکنیم. (عمداً مبهم است چون به نحوهٔ پیادهسازی نوعدهی ایستا در هر زبان بستگی دارد. معیار حتی بسته به نسخهٔ زبان میتواند متفاوت باشد.) مشابه پوشش تست، امتیازهای پایین پوشش نوع میتوانند برای یافتن مؤثر کدی که سود میبرد از توجه بیشتر استفاده شوند. رسیدن به ۱۰۰ درصد پوشش نوع احتمالاً غیرممکن است، اما در تجربهٔ کار روی کدبیس با نوعدهی تدریجی، با کدی که تا حد امکان نوعدار است بیشترین اطمینان را دارم. اگر بتوانید امتیاز بالای ۹۵ درصد در کل برنامه به دست آورید، وضعیت خوبی دارید.
مستندسازی
پیش از شروع بازآرایی چیزی، باید هر مستند موجود دربارهٔ آن را بررسی کنیم. خواندن مستندات میتواند به ما بافت ارزشمند اضافی دربارهٔ کد بدهد. اگرچه مستندات منبع خوبی از معیارهای عددی برای اندازهگیری وضعیت اولیه نیست، منبع حیاتی شواهدی است که میتوانیم برای توصیف مشکلات فعلی که میخواهیم بهبود دهیم استفاده کنیم. دو شکل مستندسازی را که برای فهم و کمیسازی نقطهٔ شروع در پیشبینی یک تلاش بزرگ بازآرایی باید در نظر بگیریم بررسی میکنیم: مستندات رسمی و غیررسمی.
مستندات رسمی
مستندات رسمی همان چیزی است که بیشتر ما بهعنوان مستندات به آن فکر میکنیم. لازم نیست استاندارد رسمی صنعتی (مثل Unified Modeling Language [UML]) را رعایت کند. آنچه آن را رسمی میکند این است که عمداً (و در بسیاری موارد بهطور فعال نگهداری میشود) برای اطلاعرسانی به خواننده دربارهٔ سیستم شما نوشته شده است. مشخصات فنی، نمودارهای معماری، راهنمای سبک، مواد ورود به تیم (onboarding) و گزارشهای پس از حادثه (postmortem) چند نمونه از مستندات رسمی هستند.
میتوانیم از چیزهایی مثل مشخصات فنی بهعنوان شواهدی استفاده کنیم که بازآرایی ما لازم یا مفید است؛ با ارجاع به تصمیمهای طراحی، فرضیات یا طراحیهای دیگری که در نظر گرفته یا رد شدهاند. مثلاً فرض کنید روی زیربخشی از برنامهٔ مسئول پردازش همهٔ اقدامات مرتبط با کاربر در محصول کار میکنید. پیادهسازی فعلی از توسعهدهندگان میخواهد هنگام نوشتن ویژگیهای جدید به خاطر بسپارند و هر نوع رویدادی را که باید هنگام تغییر پروفایل کاربر اجرا و به زیرسیستمهای همسطح منتشر شود فهرست کنند. اگر تیم شما سابقهٔ نوشتن مشخصات طراحی فنی برای هر ویژگی دارد، میتوانید سند مشخصات اصلی انتشار رویداد را پیدا کنید. این سند پیادهسازی فعلی، محدودیتهایش و هر رویکرد جایگزین را توصیف میکند.
بخش محدودیتها میگوید اگرچه راهاندازی هر رویداد لازم بهصورت جداگانه در هر مکان ممکن است راحت باشد، اگر تیم تعداد زیادی رویداد جدید معرفی کند ممکن است دستوپاگیر و سنگین شود. امروز سیستم شما دقیقاً همان مشکل را تجربه میکند. بیش از یک دوجین نوع رویداد را مدیریت میکند و تیم در پیگیری گسترش آن مشکل دارد. با هر ویژگی جدید، تیم میترسد فراموش کند نوع رویداد حیاتی را راهاندازی کند و شاید باگ مزاحمی معرفی کند. بهترین تلاش را برای تأیید رفتار مطلوب با تست کردهاید اما تصمیم میگیرید بازآرایی نحوهٔ مدیریت این رویدادها بهترین راه برای مهار آشفتگی منطق تکراری است.
مشخصات فنی میتوانند در پشتیبانی از فرضیهٔ دقیقاً آنچه باید بهبود یابد و چگونه بسیار مفید باشند. گاهی این اسناد رویکردهای جایگزینی را که در نظر گرفته شده اما انتخاب نشدهاند شرح میدهند. شاید بتوانید یکی از این پیشنهادها را با تلاش بازآرایی خود کاوش کنید.
نگهدارندگان راهنمای سبک و مواد ورود به تیم گاهی اثر تجربیاتشان را در مستنداتی که تولید میکنند به جا میگذارند. اگر اخیراً کشفی غیرمنتظره دربارهٔ نحوهٔ کار چیزی داشتهاند و بهخاطر آن مستندات را بهبود دادهاند، شاید بتوانید در نوشتهٔ آنها لمحهای از آن ببینید. شاید هشدارهایی با متن بزرگ و پررنگ از دقیقاً آنچه نباید انجام داد پیدا کنید. همچنین غیرمعمول نیست مقدار نامتناسبی محتوا به بخشهای بهویژه پیچیدهٔ کدبیس در این نوع اسناد اختصاص یابد؛ افراد بیشتری در شرکت وقت بیشتری صرف هدایت خوانندگان به مسیر درست و دور از دامهایی که خودشان در آن افتادهاند کردهاند. اگر کدی که میخواهید بازآرایی کنید در این منابع مستند شده و این الگوها را دنبال میکند، شاید شواهد خوبی باشد که میتوان آن را بهطور قابل اندازهگیری بهبود داد. دربارهٔ لحن و محتوای ایدهآل مستندات برای کد هدف فکر کنید و از آن الهام بگیرید.
گزارشهای پس از حادثه میتوانند شواهد پشتیبان عالی باشند. اگر تیم شما فرایند پاسخ به رخداد PagerDuty را دنبال میکند و مدتی است این کار را میکند، احتمالاً به دهها سند postmortem دسترسی دارید که جزئیات چه، کجا، کی، چرا و چگونه هر موردی که برنامه طبق انتظار رفتار نکرده را شرح میدهند. هنگام ساختن پرونده برای کدی که ارزش بازآرایی دارد، به دنبال گزارشهای پس از حادثهای میگردم که رخدادهایی را که باور دارم مستقیماً آن کد را درگیر کردهاند خلاصه میکنند. سپس دو بخش را میخوانم: «عوامل مؤثر» (Contributing Factors) و «چه چیزهایی خوب پیش نرفت؟» (What Didn't Go So Well?). وقتی گمان میکنم پیچیدگی کد مستقیماً بر زمان رفع اثر گذاشته یا شاید حتی خود رخداد را ایجاد کرده، این دو بخش احتمالاً آن را تأیید میکنند. شمارش تعداد رخدادهایی که ناحیهای را که میخواهید بازآرایی کنید بهعنوان مشکل فهرست میکنند، معیار ارزشمندی است.
همچنین مهم است مستندات شخص ثالث یا رو به عموم را یادداشت کنید. اگرچه بازآرایی نباید رفتار را برای مصرفکنندگان برنامه تغییر دهد، این مستندات میتواند برای تقویت درک شما از کدی که قصد بازنویسی آن را دارید بهویژه مفید باشد.
مستندات غیررسمی
در کنار مستندات رسمی، طیف گستردهای از مستندات غیررسمی تولید میکنیم. اینها مصنوعاتی نوشتهشدهاند که بهعنوان مستندات مناسب در نظر نمیگیریم، صرفاً چون معمولاً به شکل سند نیستند. در تجربهٔ من، بیشتر از هر مستند رسمی در منابع غیررسمی پراکندهاند.
یافتن این منابع به تفکر خارج از چارچوب بستگی دارد. چند مورد را اینجا فهرست میکنم، اما چشمتان را برای منابع دیگر اطراف باز نگه دارید. شاید خودتان را غافلگیر کنید!
رونوشتهای چت و ایمیل میتوانند اطلاعات بینشبخشی دربارهٔ کدی که میخواهید بازآرایی کنید بدهند. بهتر از همه، اغلب بافت خوبی — هم تاریخی و هم سازمانی — میدهند. مثلاً فرض کنید میخواهید ساختار jobهای ناهمگام در برنامه را بازآرایی کنید. سیستم صف job فعلاً مجموعهٔ پویایی از آرگومانها با اندازهٔ دلخواه میپذیرد تا انعطاف برای مصرفکنندگانش حداکثر شود. متأسفانه این به سردرگمی زیادی دربارهٔ محدودیتهای واقعی منجر شده و سیستم را در معرض تمام شدن حافظه هنگام پردازش jobهای با بار آرگومان بسیار بزرگ، یا فروپاشی ناگهانی وقتی نمیتواند ورودیهای نامعتبر را تجزیه کند، قرار داده است.
میخواهید مطمئن شوید تجربهٔ شما با ابهام سیستم فقط برای شما و تیمتان گزینشی نیست. برای سنجش چقدر نوشتن jobهای جدید دردسرساز است، Slack شرکت (یا راهحل پیامرسانی دیگر) را برای مجموعهای از کلیدواژههای مرتبط با آرگومانهای صف job جستجو میکنید. بیتعجب تعدادی پیام پیدا میکنید که کسی از اینکه job طبق انتظار کار نکرده تعجب یا نگرانی کرده. توسعهدهندگان در سراسر شرکت میپرسند آیا باید شناسهٔ خام یا مبهم (opaque) بدهند. چرا یکی بر دیگری؟ آیا این آرگومانهای job را در لاگ ثبت میکنیم؟ اگر بله، آیا باید مراقب اطلاعات شناساییکنندهٔ شخص (PII) باشیم؟ چقدر داده میتوانیم از این آرگومانها بفرستیم؟ آیا میتوانیم کل اشیاء را سریالسازی کنیم و به سیستم صف job بدهیم؟
سندی میسازید که به هر یک از این پیامها اشاره میکند، با توضیح کوتاه بافت هر کدام. (با کمی مرور گفتوگو باید آسان باشد.) حالا میتوانید به این موارد برای نشان دادن دشواریای که توسعهدهندگان با آن روبهرو هستند ارجاع دهید.
تاریخچهٔ چت توانایی منحصربهفردی به شما میدهد که به گفتوگوهایی که پیش از ورود شما اتفاق افتاده نگاهی بیندازید. شاید غافلگیر شوید افرادی در تیمهای مهندسی مختلف ماهها یا سالها پیش از اولین روز کاری شما دربارهٔ مشکلاتی که مشتاق رفع آنها هستید صحبت کردهاند. شاید دیگران را ببینید که با توالی منظم همان سؤال را میپرسند. وقتی این اتفاق میافتد، نهتنها برای تلاش شما بسیار تأییدکننده است، بلکه شاید با تماس با افراد آن تیمها و پرسیدن تجربهٔشان با کدی که میخواهید بهبود دهید، متحدان ارزشمندی پیدا کنید.
از نظر کمی، میتوانید از این گفتوگوها برای تقریب ساعتهای مهندسی از دسترفته بهخاطر سردرگمی دربارهٔ کدی که میخواهید بهبود دهید و پاسخ به سؤالات دربارهٔ آن استفاده کنید.
بسته به ابزارهای مدیریت پروژهٔ تیم مهندسی، شاید بتوانید با جستجوی باگهای مرتبط در سیستم ردیابی باگ، معیارهای مهمی دربارهٔ کدی که میخواهید بازآرایی کنید جمعآوری کنید. شاید هم بتوانید تخمین بزنید تیمهای دیگر یا توسعهدهندگان منفرد چقدر وقت صرف بررسی و رفع باگ یا پیادهسازی تغییرات مرتبط با کد هدف کردهاند.
فرض کنید کد اطراف یک ویژگی یا مجموعه ویژگیها بهمرور پیچیدهتر شده. میخواهید تلاشی برای مرتبکردن آن سرمایهگذاری کنید تا تیم با سرعت بیشتری توسعه دهد. اگر گمان میکنید سرعت توسعهٔ تیم کاهش یافته، میتوانید از نرمافزار مدیریت پروژه تأیید کنید. توجه کنید این معیار بسیار درشت است (و مانند سایر معیارها فقط یک جنبه از مسئلهٔ کلی را کمیسازی میکند). احتمالاً به دانش صمیمی از نحوهٔ سازماندهی چرخههای توسعهٔ تیم و حذف با اطمینان دادههای پرت از دادهها نیاز دارید تا معیار قانعکنندهای استخراج کنید، اما برای برخی تیمها میتواند غیرقابل انکار باشد!
مدیران برنامهٔ فنی (technical program manager) در برخی شرکتها میتوانند منبع عالی برای کمک به جمعآوری، فیلتر و انتشار این نوع معیارها باشند. اغلب در پیمایش ابزارهای مدیریت پروژه و یافتن اسناد سختیافت عالیاند. شاید حتی دوست جدیدی پیدا کنید!
در این نقطه شاید همهٔ اینها مقدار زیادی کار تحقیقی برای کمیسازی یک مسئله به نظر برسد. اشکالی ندارد! به شما بستگی دارد کدام معیارها بیشترین اثر را در ارتباط شدت مسئله و منفعت احتمالی رفع آن دارند. شاید نخواهید یا نیاز نباشد وقت صرف جستجو در صدها کار (task) یا گزارش پس از حادثه کنید، اما اگر این اطلاعات بهراحتی قابل مصرف و جستجو باشد، شاید ارزش داشته باشد. این معیارها بهویژه هنگام متقاعد کردن تیمهای مدیریت و رهبری که از کد فاصلهٔ زیادی دارند که بازآرایی ارزش دارد، مفیدند.
کنترل نسخه
ما عمدتاً کنترل نسخه را ابزاری برای مدیریت تغییرات برنامههایمان میدانیم. از آن برای پیشروی گامبهگام استفاده میکنیم و توسعهٔ همزمان چند ویژگی و ارسال تدریجی آنها را ممکن میسازد. گاهی برای ارجاع به نسخههای قبلی کد و ردیابی باگ یا یافتن کسی که دربارهٔ بخشی از کدی که میخوانیم میداند از آن استفاده میکنیم. بهندرت کنترل نسخه را منبع اطلاعات دربارهٔ الگوهای توسعهٔ تیممان وقتی در مجموع تحلیل میشود میبینیم. معلوم میشود وقتی commitها را از زاویهٔ دیگری نگاه کنیم، میتوانیم دربارهٔ مشکلاتی که تیم مهندسی با آنها روبهرو است بسیار بیاموزیم.
پیامهای commit
اگرچه همه نوشتن پیامهای commit توصیفی را بخشی از روش توسعهٔ خود نمیدانند، اگر روی تیمی کار میکنید که اکثریت توسعهدهندگان این کار را میکنند، این توصیفهای کوتاه میتوانند نگاهی به مشکلاتی که ممکن است با آنها روبهرو شوند بدهند. میتوانیم الگوها را با جستجوی مجموعهای از کلیدواژهها یا جداسازی پیامهای commit مرتبط با تغییرات در مجموعهای از فایلهای مورد علاقه شناسایی کنیم.
فرض کنید به مشکل سیستم صف job از قبل نگاه میکنیم. میدانیم مهندسان اغلب فراموش میکنند آرگومانهایشان را قبل از صفبندی job پاکسازی کنند و این به ثبت PII در لاگ منجر میشود. میتوانیم پیامهای commit را جستجو کنیم و commitهایی را که پیامهایشان شامل کلماتی مثل «job»، «job handler» یا «PII» است شناسایی کنیم. از این مجموعه نتیجه شاید مجموعهٔ قابلتوجهی از commitها پیدا کنیم که یا job جدیدی معرفی کرده که PII را نشت میدهد یا jobی را که قبلاً نشت میکرده رفع کرده. از سوی دیگر، اگر handlerهای job بهراحتی در فایلهای متمایز سازماندهی شدهاند، میتوانیم جستجو را به commitهایی با تغییر در این فایلها محدود کنیم و مجموعهٔ حاصل را برای الگوهای مشابه مرور کنیم.
برخی تیمهای توسعه commitها یا مجموعهٔ تغییرات (changeset) خود را به ابزارهای مدیریت پروژه با برجسته کردن شمارهٔ باگ یا تیکت (ticket) در پیام commit یا نام branch مرتبط میکنند. اگر این اطلاعات در دسترس باشد، میتوانیم مجموعهٔ تغییرات را به مجموعهٔ قبلی معیارهای سرعت توسعه و تعداد باگ پیوند دهیم. همهچیز به نقطهٔ شروع برمیگردد!
commitها در مجموع
در کتاب Software Design X-Rays، Adam Tornhill مجموعهای از تکنیکها برای استخراج الگوهای مهم توسعه از تاریخچهٔ نسخه پیشنهاد میکند. فرض میکند این رفتارهای توسعه میتوانند به شما کمک کنند کدام بخشهای برنامه را هنگام بازآرایی اولویتبندی کنید، نشان دهند پیچیدگی برخی توابع چگونه در طول زمان تغییر کرده و فایلها یا ماژولهای گرهخورده را برجسته کنند. مطالعهٔ تحقیقاتش را برای درک کامل روانشناسی پشت اینکه چرا این اندازهگیریها اینقدر روشنگرند شدیداً توصیه میکنم، اما در اینجا تکنیکهای پایه را خلاصه میکنم تا شاید پیش از بازآرایی بزرگ بعدی در نظر بگیرید.
فراوانی تغییر (change frequencies) تعداد commitهایی است که به هر فایل در کل تاریخچهٔ نسخهٔ برنامه انجام شده. میتوانید بهراحتی این نقاط داده را با استخراج نام فایلها از تاریخچهٔ commit، تجمیع و مرتبسازی از پرتکرارترین به کمتکرارترین تولید کنید. در عمل، Tornhill متوجه شد این فراوانیها تمایل به توزیع توان دارد، جایی که تعداد نامتناسبی از تغییرات در زیرمجموعهٔ کوچکی از فایلهای هسته رخ میدهد. دانستن فایلهایی که بیشترین commit را دارند دقیقاً به ما میگوید کدام فایلها باید برای توسعهدهندگان آسانترین فهم و پیمایش را داشته باشند و بنابراین کدام فایلها را از منظر بهرهوری توسعهدهنده بیشترین تلاش را صرف نگهداری کنیم.
میتوانیم همان مفهوم فراوانی تغییر را روی توابع هم اعمال کنیم. با نگاه به commitهای منفرد، میتوانیم تغییرات را با دقت به توابع مربوط در فایلهای منفرد نسبت دهیم و اعداد فراوانی کل برای هر کدام تولید کنیم. با ترکیب این داده با یکی از معیارهای پیچیدگی قبلی — تعداد خطوط کد — میتوانیم تغییرات پیچیدگی را در طول زمان در کل کدبیس نقشهبرداری کنیم. این اطلاعات نقاط داغ بالقوهٔ آماده بهبود را نشان میدهد. بعداً پس از تکمیل بازآرایی میتوانیم این معیارها را دوباره تولید کنیم تا تأیید کنیم نهتنها پیچیدگی این نقاط داغ کاهش یافته، بلکه امیدواریم فراوانی تغییرشان هم کمتر شده باشد.
Tornhill همچنین روشی برای شناسایی دقیق ماژولهای گرهخورده با نگاه به مجموعههای فایلهایی که در همان commit تغییر کردهاند توصیف میکند. برای تصویرسازی این ایده، فرض کنید سه فایل داریم: superheroes.js، supervillains.js و sidekicks.js. در زیرمجموعهای از commitها تغییرات زیر را داریم: commit یک هر دو superheroes.js و sidekicks.js را تغییر میدهد؛ commit دو هر سه فایل را؛ commit سه دوباره superheroes.js و sidekicks.js را؛ و commit چهار فقط superheroes.js را لمس میکند.
از این زیرمجموعهٔ تاریخچهٔ نسخه، که در جدول ۳-۳ نشان داده شده، میبینیم از چهار commit، سهتای آنها هر دو superheroes.js و sidekicks.js را تغییر دادهاند. این القا میکند نوعی وابستگی (coupling) بین این دو فایل وجود دارد. قطعاً همهٔ وابستگی بد نیست (مثل تغییرات در کد منبع و فایلهای unit test متناظر)، اما در برخی موارد این الگوها میتوانند نشاندهندهٔ انتزاع (abstraction) اشتباه، کد کپیشده یا گاهی هر دو باشند. وقتی این مشکلات را شناسایی کردیم، میتوانیم روی رفع آنها کار کنیم و بعداً تحلیل را دوباره اجرا کنیم تا تأیید کنیم دیگر وجود ندارند.
جدول ۳-۳. فایلهای تغییریافته در هر commit
| شماره commit | superheroes.js | supervillains.js | sidekicks.js |
|---|---|---|---|
| ۱ | x | x | |
| ۲ | x | x | x |
| ۳ | x | x | |
| ۴ | x |
مانند هر معیار کمی در این فصل، برای این نوع اندازهگیری هم چند نکتهٔ احتیاط وجود دارد. توسعهدهندگان مختلف عادتهای متفاوتی در commit کردن تغییرات دارند. برخی تعداد زیادی commit کوچک میزنند؛ برخی commitهای بزرگ با دهها تغییر در چند فایل در یک مجموعهٔ تغییر میسازند. علاوه بر این، احتمالاً این تحلیل دادههای پرتی را نشان دهد (فایلهای پیکربندی که مکرر تغییر میکنند یا نقاط داغ در کد خودکار تولیدشده). هنگام بررسی دادهها باید مراقب این ناهنجاریها باشیم تا ریسک یافتن مشکلاتی که شاید وجود نداشته باشند کاهش یابد.
نویسندگی کد (Code Authorship)
Tornhill همچنین دادهای دربارهٔ نویسندگی کد از کنترل نسخه استخراج میکند. فایلها یا ماژولهایی با تعداد زیاد نویسندهٔ جزئی در معرض خطر بالاتر نقص هستند. اگرچه تحقیقی که استناد میکند توضیح نمیدهد چرا، حدس میزند احتمالاً بهخاطر نیاز بیشتر به هماهنگی بین نویسندگان و ناآشنایی با پیادهسازی است. شاید کنجکاو باشید پیامدهای نویسندگی را در کد خود کاوش کنید و معیارهای اضافی از آن استخراج کنید.
شهرت (Reputation)
چه آگاه باشیم چه نه، هر یک از بخشهای متعدد سیستمهای نرمافزاری ما شهرت متمایزی دارند. برخی شهرتها قویترند؛ برخی مثبت، برخی عمیقاً منفی. هر شهرتی که باشد، بهآرامی در طول زمان ساخته میشود و با تعامل مهندسهای بیشتر در سازمان مهندسی گسترش مییابد. خبر بدترین کدبیسها گاهی حتی از شرکت شما بیرون میرود و در صنعت گستردهتر مطرح میشود — در شام بین دوستان و در انجمنهای اینترنتی. چه این شهرتها همچنان درست باشند یا نه، میتوانند دربارهٔ دردسرسازترین بخشهای برنامهٔ ما و اینکه چقدر بهطور فوری به توجه ما نیاز دارند بسیار بگویند.
راه ساده و کمتلاش جمعآوری دادهٔ شهرت، مصاحبه با همتیمیهای توسعهدهنده است. فرض کنید روی برنامهای کار میکنید که به مشتریان برای سرویس ماهانه هزینه میگیرد و میخواهید کد صورتحساب (billing) برنامه را بهبود دهید. چند مصاحبه با توسعهدهندگانی در چند دسته برگزار میکنید: کسانی که مستقیماً و منظم با کد صورتحساب کار میکنند، و کسانی که گاهبهگاه با آن کار کردهاند. برای هر دو مجموعه میخواهید با توسعهدهندگانی با طیف سنوات در تیم فعلی و در شرکت صحبت کنید؛ تجربهٔ کسانی که سالها با کد صورتحساب بهطور جدی کار کردهاند احتمالاً با مهندسی که شش ماه پیش استخدام شده بسیار متفاوت است.
سپس مجموعهای از سؤالات میسازیم که به توصیف تجربهٔ آنها کمک کند. با چند سؤال برای قاببندی پیشینه شروع میکنیم و سپس به افکارشان دربارهٔ کد میپردازیم. چند سؤال پیشنهادی در جدول ۳-۴ برای شروع آورده شده است.
با توجه به تجربهتان با کد صورتحساب، وقتی ارزیابی میکردید کدام فایلها بیشترین سود را از بازآرایی کامل دارند، فوراً به chargeCustomerCard.js فکر کردید. تصمیم میگیرید از مصاحبهشوندگان دربارهٔ این فایل بپرسید تا ببینید چه واکنشی برمیانگیزد. اگر به محض ذکر chargeCustomerCard.js مصاحبهشونده چهرهاش را در هم میکشد — چه دانش عمیق از درون فایل داشته باشد چه نداشته باشد — نشانهٔ قوی است که فایل احتمالاً به کمی توجه نیاز دارد.
اگر میخواهیم بازخورد گروه بزرگتری از مهندسان را بگیریم یا برای برقراری معیارهای شروع وقت کم داریم، میتوانیم سؤالات مصاحبه را طوری بازنویسی کنیم که با مجموعهٔ استاندارد پاسخها سازگار باشند. این تجمیع پاسخها را آسانتر و استنتاج سریعتر میکند. اما هشدار: با کاهش افکار همتیمیها به مجموعهای از امتیازها، بخشی از ظرافتی را که شاید از مصاحبهٔ حضوری (یا مجازی) میتوانستید بگیرید از دست میدهید.
از تجربه، مصاحبهها انعطاف بیشتری برای کاوش ایدهها و موضوعاتی که صادقانه مطرح میشوند میدهند. اغلب همان گفتوگوی رفتوبرگشتی است که بهترین لحظات «آها!» را بیرون میآورد. اگر نظرسنجی با سؤالات شبیه مصاحبهٔ بلند بفرستیم، نهتنها نمیتوانیم در زمان واقعی از پاسخدهندگان جزئیات بیشتر بخواهیم، بلکه احتمالاً پاسخ کمتری میگیریم. خودم گناهکار باز کردن نظرسنجی، دیدن شش سؤال باز و تقریباً فوراً یادآوری برای انجام بعدی هستم. اگر میخواهید بازخورد مهندسان را به شکل نظرسنجی بگیرید، کوتاه نگه دارید؛ اینطور شانس پاسخدهی بالا بیشتر است.
جدول ۳-۴. سؤالات پیشنهادی مصاحبه و نظرسنجی با توسعهدهندگان
| سؤال مصاحبه | سؤال نظرسنجی | یادداشت |
|---|---|---|
| چقدر با کد X کار کردهاید؟ | گزینهای را انتخاب کنید که بهترین توصیف میزان زمانی که با کد X کار کردهاید است: بیش از ۶ ماه؛ ۶ ماه تا ۱ سال؛ بیش از یک سال. | در نسخهٔ نظرسنجی، بازههای زمانی را متناسب با سازمان مهندسی خود انتخاب کنید. در شرکتهای جوان با رشد بالا، بازهها احتمالاً به ماه است؛ در شرکتهای بزرگتر و تثبیتشده، به سال. |
| اگر میتوانستید یک چیز دربارهٔ کار با کد X تغییر دهید، چه میبود؟ چرا؟ | اگر فقط یکی از گزینههای فهرستشده را برای بهبود تجربهٔ کار با کد X انتخاب کنید، کدام را میزنید؟ | برای سؤال نظرسنجی، گزینههایی انتخاب کنید که فکر میکنید بیشترین اثر را دارند و اختیاراً فیلد نوشتن آزاد بدهید. اگر کد تست ندارد، گزینهای اضافه کنید که بگوید کد کاملاً تست شده است. اگر بخش زیادی از کد در چند تابع صدها خطی است، گزینهای اضافه کنید که بگوید کد به توابع کوچک و ماژولار تقسیم شده است. |
| دربارهٔ باگی که اخیراً با کد X درگیر بودید بگویید. چه چیزی حل آن را آسانتر میکرد؟ | از گزینههای Y فهرستشده، چه چیزی دربارهٔ کد X رفع باگ را برایتان دشوارترین میکند؟ | |
| آیا قبلاً عمداً از کار روی کد X اجتناب کردهاید (مثلاً رفع باگ در سطح بالاتر یا پایینتر از ناحیهٔ مشکل)؟ از آن تجربه بگویید. | از مقیاس ۱ تا ۵، ۱ یعنی اصلاً محتمل نیست و ۵ یعنی بسیار محتمل، چقدر احتمال دارید راهی برای اجتناب از تغییر کد X پیدا کنید؟ | |
| پیچیدگی کد X چگونه توانایی شما برای توسعهٔ ویژگیهای جدید را محدود میکند؟ | با ۱ یعنی کاملاً مخالف و ۵ یعنی کاملاً موافق، این جمله را امتیاز دهید: پیچیدگی کد X عامل مهمی در زمان لازم برای توسعهٔ ویژگیهای جدید برای من است. | |
| پیچیدگی کد X چگونه توانایی شما برای تست و/یا دیباگ کد را محدود میکند؟ | با ۱ یعنی کاملاً مخالف و ۵ یعنی کاملاً موافق، این جمله را امتیاز دهید: پیچیدگی کد X عامل مهمی در دشواری تست و/یا دیباگ کد برای من است. | |
| پیچیدگی کد X چگونه توانایی شما برای بازبینی تغییرات سایر توسعهدهندگان روی کد را محدود میکند؟ | با ۱ یعنی کاملاً مخالف و ۵ یعنی کاملاً موافق، این جمله را امتیاز دهید: پیچیدگی کد X عامل مهمی در زمان و دشواری بازبینی تغییرات سایر توسعهدهندگان روی کد برای من است. |
شهرت همچنین میتواند توانایی تیم در استخدام و نگهداشتن مهندسان را محدود کند. فرض کنید کد صورتحساب در شرکت شما بهویژه خطرناک شناخته میشود. اگرچه تیم احتمالاً تعدادی توسعهدهندهٔ متعهد به نقشهایشان دارد، کار در کدبیس بهطرز ناامیدکنندهای پیچیده میتواند بر روحیه اثر بگذارد. سازمانها دوست ندارند اعتراف کنند مهندسان را بهخاطر کیفیت کد و شیوههای توسعه از دست دادهاند، اما همیشه اتفاق میافتد. اگر بتوانید اطلاعاتی دربارهٔ دلایل ترک تیم جمعآوری کنید و آنها را به پیچیدگی کد پیوند دهید، میتواند معیار بسیار قانعکنندهای برای اختصاص منابع بهمراتب لازم به بازآرایی باشد.
ساختن تصویر کامل
اکنون که با طیف گستردهای از معیارهای بالقوه آشنا شدیم، باید انتخاب کنیم کدامها را استفاده کنیم. برای ساخت جامعترین نمای وضعیت فعلی، باید معیارهایی را شناسایی کنید که بهترین تصویر مشکلات خاصی را که میخواهید به آنها بپردازید نشان دهند. هیچیک از این معیارها بهتنهایی نمیتواند جنبههای منحصربهفرد زیادی از یک تلاش بزرگ بازآرایی را کمیسازی کند، اما در ترکیب میتوانید توصیف چندوجهی از مسئله بسازید.
توصیه میکنم از هر دسته یک معیار انتخاب کنید. راهی برای تقریب پیچیدگی کد پیدا کنید که با ماهیت مسئله و ابزارهایی که از قبل در دسترس دارید بیشترین معنا را دارد. معیارهای پوشش تست تولید کنید تا مطمئن شوید درست شروع میکنید. منبعی از مستندات رسمی شناسایی کنید که با آن مشکلاتی را که بازآرایی قصد حل آنها را دارد نشان دهید؛ با مستندات غیررسمی هم پشتیبانی کنید. با برش و تکهتکه کردن دادهٔ کنترل نسخه دربارهٔ نقاط داغ و الگوهای برنامهنویسی اطلاعات جمعآوری کنید. در پایان شهرت کد را با گفتوگو با همکاران در نظر بگیرید.
اگر بیشتر این معیارها میتوانند به کمیسازی وضعیت فعلی کدی که قصد بازآرایی آن را دارید و اثر آن بر سازمان کمک کنند، زیرمجموعهای را انتخاب کنید که بیشترین شانس نشان دادن بهبودهای قابلتوجه را دارند. اینها معیارهاییاند که قانعکنندهترین پرونده را برای همتیمیها و در نهایت مدیریت میسازند. در پایان باید استدلال قانعکنندهای به کسانی که به آنها گزارش میدهید ارائه دهید که زمان و انرژی که شما و همتیمیهایتان آمادهٔ اختصاص به بازآرایی هستند، ارزش دارد.
با موفقیت شواهدی جمعآوری کردیم که به توصیف درست مسئلهای که تجربه میکنیم کمک میکند، اما آمادهسازی صحنه فقط یک قطعه از پازل است. بعد باید از دادهای که جمعآوری کردهایم برای سرهم کردن یک برنامهٔ اجرای مشخص استفاده کنیم.