Skip to content

فصل ۴ — ترسیم طرح

روزی قصد دارم سفر ۴٬۵۰۰ کیلومتری بین مونترال و ونکوور را به پایان برسانم. این سفر از ابتدا تا انتها حدود ۴۸ ساعت طول می‌کشد و سریع‌ترین مسیر بیشتر طول مرز کانادا و ایالات متحده را پوشش می‌دهد. با این حال، سریع‌ترین مسیر لزوماً پربارترین مسیر نیست؛ و اگر توقفی برای دیدن Parliament Hill در اتاوا، برج نمادین CN در تورنتو و Sleeping Giant Provincial Park اضافه کنم، سفر چند ساعت و حدود ۶۰۰ کیلومتر طولانی‌تر می‌شود.

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

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

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

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

هر کسی رویکرد متفاوتی برای ساخت طرح اجرا دارد. چه تیم شما آن‌ها را technical spec، product brief یا RFC بنامد، همه یک هدف دارند: مستندسازی آنچه قصد دارید انجام دهید و چگونه انجام دهید. داشتن طرح روشن و مختصر کلید موفقیت هر پروژهٔ نرم‌افزاری است، چه بازآرایی باشد چه ساخت قابلیت جدید؛ همه را روی وظایف مهم متمرکز نگه می‌دارد و مسئولیت پیشرفت را در کل تلاش enforce می‌کند.

تعریف وضعیت پایانی

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

در جاده

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

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

در محل کار

برای نشان دادن مفاهیم مهم این فصل، از مثال بازآرایی در مقیاس بزرگ در شرکت بیوتکنولوژی ۱۵ ساله‌ای به نام Smart DNA, Inc. استفاده می‌کنیم. بیشتر کارمندان آن دانشمندان پژوهشی هستند که به خط لولهٔ دادهٔ پیچیده‌ای متشکل از صدها اسکریپت Python در چند مخزن کمک می‌کنند. اسکریپت‌ها در پنج محیط مجزا deploy و اجرا می‌شوند. همهٔ این محیط‌ها به نسخه‌ای از Python 2.6 وابسته‌اند. متأسفانه Python 2.6 مدت‌هاست deprecated شده و شرکت را در برابر آسیب‌پذیری‌های امنیتی آسیب‌پذیر گذاشته و از به‌روزرسانی وابستگی‌های مهم جلوگیری می‌کند. اتکا به نرم‌افزار منسوخ ناخوشایند است، اما شرکت ارتقا به نسخهٔ جدیدتر Python را اولویت نداده؛ با توجه به تست بسیار محدود، تلاشی عظیم و پرریسک است. به‌طور خلاصه، این بزرگ‌ترین بدهی فنی شرکت بوده است.

تیم پژوهش اخیراً نگران ناتوانی در استفاده از نسخه‌های جدیدتر کتابخانه‌های هسته شده. چون ارتقا اکنون برای کسب‌وکار مهم است، مأمور شده‌ایم بفهمیم چطور هر مخزن و محیط را به Python 2.7 مهاجرت دهیم.

تیم پژوهش وابستگی‌ها را با pip مدیریت می‌کند. هر مخزن فهرست وابستگی‌های خود را در requirements.txt دارد. داشتن این فایل‌های مجزا به تیم کمک نکرده به‌خاطر بسپارد کدام وابستگی‌ها در هر پروژه نصب‌اند هنگام جابه‌جایی بین پروژه‌ها. همچنین تیم نرم‌افزار باید هر فایل را ممیزی و مستقل برای سازگاری با Python 2.7 ارتقا دهد. در نتیجه تیم نرم‌افزار تصمیم گرفت اگرچه ضروری نبود، برای ساده‌تر شدن ارتقای Python 2.7 (و ساده‌سازی فرایند توسعهٔ پژوهشگران) مخزن‌ها و در نتیجه وابستگی‌ها را یکپارچه کند.

طرح اجرا باید همهٔ معیارهای شروع و معیارهای هدف پایان را به‌روشنی مشخص کند، با ستون اختیاری اما مفید برای ثبت وضعیت پایانی واقعی مشاهده‌شده. برای مهاجرت Python، مجموعهٔ معیارهای شروع روشن بود: هر مخزن فهرست وابستگی مجزا داشت و هر محیط Python 2.6 اجرا می‌کرد. مجموعهٔ معیارهای مطلوب هم به همان سادگی بود: همهٔ محیط‌های کسب‌وکار روی Python 2.7 باشند و مجموعهٔ واضح و مختصر کتابخانه‌های لازم در یک جا مدیریت شود. جدول ۴-۱ نمونه‌ای است که معیارهای Smart DNA را فهرست کرده‌ایم.

جدول ۴-۱. نمودار مقایسهٔ معیارها در ابتدای پروژه، معیارهای هدف و مقدار مشاهده‌شده در پایان پروژه

شرح معیارشروعهدفمشاهده‌شده
محیط ۱Python 2.6.5Python 2.7.1-
محیط ۲Python 2.6.1Python 2.7.1-
محیط ۳Python 2.6.5Python 2.7.1-
محیط ۴Python 2.6.6Python 2.7.1-
محیط ۵Python 2.6.6Python 2.7.1-
تعداد فهرست‌های مجزای وابستگی۳۱-

وضعیت پایانی ایده‌آل و قابل‌قبول

هر دو وضعیت پایانی ایده‌آل و قابل‌قبول را ارائه دهید. گاهی رسیدن به ۸۰ درصد مسیر ۹۹ درصد منفعت بازآرایی را می‌دهد و کار اضافی برای رسیدن به ۱۰۰ درصد ارزشش را ندارد.

ترسیم کوتاه‌ترین مسیر

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

در جاده

برای سفر جاده‌ای، جستجوی سریعی می‌کنیم تا ببینیم مستقیم‌ترین مسیر بین مونترال و ونکوور چگونه است (شکل ۴-۱). با فرض ترافیک کم، اگر از مونترال بدون توقف به سمت غرب برانیم حدود ۴۷ ساعت طول می‌کشد.

شکل ۴-۱. مستقیم‌ترین مسیر بین آدرس ما در مونترال و محلهٔ Yaletown در ونکوور

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

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

شکل ۴-۲. مسیری کمی کندتر محدود به جاده‌های کانادا

در محل کار

متأسفانه Google Maps برای پروژه‌های نرم‌افزاری هنوز وجود ندارد. پس چطور کوتاه‌ترین مسیر از اکنون تا پایان پروژه را تعیین کنیم؟ چند روش داریم:

  • یک سند خالی باز کنید و ۱۵ تا ۲۰ دقیقه (یا تا تمام ایده‌ها) هر گامی که به ذهن می‌رسد بنویسید. سند را حداقل چند ساعت (ایده‌آل یک یا دو روز) کنار بگذارید، سپس دوباره باز کنید و گام‌ها را به ترتیب زمانی مرتب کنید. هنگام مرتب‌سازی، مدام بپرسید آیا هر گام برای رسیدن به هدف نهایی کاملاً لازم است؛ اگر نه، حذفش کنید. وقتی مجموعهٔ مرتبی دارید، روال را دوباره بخوانید و شکاف‌های آشکار را پر کنید. اگر بعضی گام‌ها خیلی مبهم‌اند نگران نباشید؛ هدف فقط تولید حداقل گام‌های لازم برای تکمیل پروژه است. این محصول نهایی نیست.
  • چند همکار علاقه‌مند به پروژه یا کسانی که می‌دانید مشارکت می‌کنند جمع کنید. حدود یک ساعت وقت بگذارید. برای هر نفر یک بسته post-it و خودکار بردارید. ۱۵ تا ۲۰ دقیقه (یا تا همه خودکارها را زمین بگذارند) هر گامی که فکر می‌کنید لازم است روی post-it جداگانه بنویسید. سپس نفر اول گام‌هایش را به ترتیب زمانی بچیند. بقیه post-itهای خود را یا با تکراری‌ها جفت می‌کنند یا در جای مناسب در خط زمانی قرار می‌دهند. وقتی همه یادداشت‌ها را مرتب کردند، هر گام را مرور کنید و از اتاق بپرسید آیا برای رسیدن به هدف کاملاً لازم است؛ اگر نه، دور بیندازید. محصول نهایی باید مجموعهٔ معقولی از گام‌های حداقلی باشد. (برای تیم‌های توزیع‌شده می‌توانید همهٔ گام‌های فردی را در سند مشترک ترکیب کنید. در هر صورت خروجی نهایی باید سند نوشته‌ای باشد که توزیع و بهبود مشارکتی آسان باشد.)

اگر هیچ‌کدام برایتان مناسب نیست، اشکالی ندارد! هر روشی که مؤثرتر می‌دانید استفاده کنید. تا وقتی فهرستی از گام‌ها تولید کنید که مسیر مستقیمی به هدف مدل می‌کند، هرچند مبهم، این گام حیاتی را با موفقیت انجام داده‌اید.

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

  • ساخت دستی یک فهرست واحد از همهٔ بسته‌ها در همهٔ مخزن‌ها
  • محدود کردن فهرست به بسته‌های ضروری
  • شناسایی نسخه‌ای که هر بسته باید در Python 2.7 به آن ارتقا یابد
  • ساخت کانتینر Docker با همهٔ بسته‌های لازم
  • تست کانتینر Docker در هر محیط
  • یافتن تست‌های هر مخزن؛ تعیین کدام تست‌ها قابل‌اعتمادند
  • ادغام همهٔ مخزن‌ها در یک مخزن واحد
  • انتخاب linter و پیکربندی متناظر
  • یکپارچه‌سازی linter در continuous integration
  • استفاده از linter برای شناسایی مشکلات کد (متغیرهای تعریف‌نشده، خطاهای نحوی و غیره)
  • رفع مشکلاتی که linter شناسایی کرد
  • نصب Python 2.7.1 در همهٔ محیط‌ها و تست
  • استفاده از Python 2.7 روی زیرمجموعه‌ای از اسکریپت‌های کم‌ریسک
  • rollout Python 2.7 به همهٔ اسکریپت‌ها

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

شناسایی نقاط عطف میانی راهبردی

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

در جاده

برای بخش سفر بین Winnipeg و Vancouver از دوستان و خانواده توصیهٔ مکان‌ها و فعالیت‌ها می‌گیریم. پس از وزن‌دهی به پیشنهادها با علایق خود، برنامهٔ تقریبی می‌سازیم که از کمپینگ و بازدید موزه تا توقف‌های خوشمزه و دیدار خانوادهٔ گسترده را شامل می‌شود (شکل ۴-۳). اما هیچ‌کدام از این نقاط علاقه ما را به‌شدت از مسیر منحرف نمی‌کند.

شکل ۴-۳. برنامهٔ تقریبی ما

در محل کار

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

۱. آیا این گام در بازهٔ زمانی معقول قابل‌دستیابی به نظر می‌رسد؟

به مثال قبلی در بخش «در محل کار» برگردیم. نقطه عطف منطقی و شدنی می‌تواند ادغام مخزن‌های مجزا در یک مخزن برای راحتی باشد. تیم نرم‌افزار Smart DNA پیش‌بینی می‌کند شش هفته طول بکشد مخزن‌ها را درست ادغام کند بدون اختلال در فرایند توسعهٔ تیم پژوهش. چون تیم نرم‌افزار به ship سریع‌تر عادت دارد و نگران روحیه اگر ادغام را خیلی زود در مهاجرت شروع کنند، نقطه عطف اولیهٔ ساده‌تری انتخاب می‌کنند: تولید یک فایل requirements.txt واحد برای همهٔ وابستگی‌های بسته در همهٔ مخزن‌ها. با کاهش زودهنگام مجموعهٔ وابستگی‌ها، فرایند توسعهٔ تیم پژوهش ساده می‌شود، گام قابل‌توجهی به سمت امکان ادغام مخزن‌ها برداشته می‌شود، و همهٔ این‌ها خیلی قبل از تکمیل مهاجرت به Python 2.7.

۲. آیا این گام به خودی خود ارزشمند است؟

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

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

۳. اگر اتفاقی بیفتد، آیا می‌توانیم در این گام توقف کنیم و بعداً راحت ادامه دهیم؟

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

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

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

  • ایجاد یک فایل requirements.txt واحد
    • فهرست‌کردن همهٔ بسته‌های استفاده‌شده در همهٔ مخزن‌ها
    • ممیزی همهٔ بسته‌ها و محدود کردن فهرست به بسته‌های لازم با نسخه‌های متناظر
    • شناسایی نسخه‌ای که هر بسته باید در Python 2.7 به آن ارتقا یابد
  • ادغام همهٔ مخزن‌ها در یک مخزن واحد
    • ایجاد مخزن جدید
    • برای هر مخزن، افزودن به مخزن جدید با git submodule
  • ساخت image Docker با همهٔ بسته‌های لازم
    • تست image Docker در هر محیط
  • فعال‌سازی lint از طریق continuous integration برای monorepo
    • انتخاب linter و پیکربندی متناظر
    • یکپارچه‌سازی linter در continuous integration
    • استفاده از linter برای شناسایی مشکلات منطقی کد (متغیرهای تعریف‌نشده، خطاهای نحوی و غیره)
  • نصب و rollout Python 2.7.1 در همهٔ محیط‌ها
    • یافتن تست‌های هر مخزن؛ تعیین کدام تست‌ها قابل‌اعتمادند
    • استفاده از Python 2.7 روی زیرمجموعه‌ای از اسکریپت‌های کم‌ریسک
    • rollout Python 2.7 به همهٔ اسکریپت‌ها

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

گام‌های تکرارپذیر

یک راه تقسیم پروژهٔ بازآرایی به نقاط عطف معنادار، انتخاب بخش‌های منطقی واحد کد برای اعمال بازآرایی است. این نوعی mini-refactor است که هدف کلی بازآرایی را در مقیاس کوچک‌تر نشان می‌دهد. با این رویکرد می‌توانید بخشی از کد را که فوری‌ترین نیاز به بازآرایی دارد، یا تغییراتش نسبتاً کم‌تلاش اما بهبودها را خوب مدل می‌کند انتخاب کنید.

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

مثلاً تیم Smart DNA می‌تواند فرایند ادغام مخزن را برای هر مخزن به چند گام تکرارپذیر تقسیم کند: اول ادغام requirements.txt مخزن در requirements.txt سراسری؛ بعد افزودن مخزن به مخزن بزرگ‌تر با git submodule؛ در پایان تست اجراپذیری اسکریپت‌ها. برای همهٔ مخزن‌های باقی‌مانده تکرار کنید.

انتزاع قبل از تغییر

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

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

اینجا می‌توانیم بفهمیم آیا بخش‌هایی از بازآرایی ترتیب‌ناوابسته‌اند؛ یعنی در هر زمان با پیش‌نیازهای کم یا بدون پیش‌نیاز قابل تکمیل‌اند. فرض کنید نقاط عطف A، B، C و D دارید. A قبل از B یا C لازم است و B قبل از D. برای C سه گزینه دارید: توسعهٔ موازی C همزمان با D، اول C بعد D، یا اول D بعد C.

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

همهٔ این‌ها به تعادل زمان و تلاش هر گام لازم برمی‌گردد، در حالی که اثرشان روی کدبیس و رفاه تیم را در نظر می‌گیرید.

انتخاب راهبرد rollout

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

معمولاً تیم‌های مهندسی محصول که continuous deployment دارند توسعهٔ قابلیت جدید را آغاز می‌کنند و در کل فرایند آن را دستی و خودکار تست می‌کنند. وقتی همهٔ چک‌لیست‌ها تیک خورد، قابلیت با دقت و تدریج به کاربران زنده rollout می‌شود. قبل از فاز rollout نهایی، بسیاری از تیم‌ها قابلیت را روی build داخلی محصول deploy می‌کنند تا فرصت دیگری برای ریشه‌کن کردن مشکلات قبل از deploy به کاربران داشته باشند. اندازه‌گیری موفقیت در این حالت ساده است؛ اگر قابلیت طبق انتظار کار کرد، عالی! اگر باگی پیدا کردیم، fix می‌سازیم و بسته به اثر fix، یا فرایند rollout تدریجی را تکرار می‌کنیم یا فوراً به همهٔ کاربران push می‌کنیم.

feature flag در continuous deployment

در محیط‌های continuous deployment رایج است از feature flag برای پنهان، فعال یا غیرفعال کردن قابلیت‌ها یا مسیرهای کد در runtime استفاده شود. راه‌حل‌های خوب feature flag به تیم‌های توسعه انعطاف می‌دهند گروه‌هایی از کاربران را به قابلیت‌های مشخص (گاه بر اساس ویژگی‌های مختلف) اختصاص دهند. مثلاً در اپلیکیشن شبکهٔ اجتماعی شاید بخواهید قابلیتی را به همهٔ کاربران یک منطقهٔ جغرافیایی، ۱ درصد تصادفی کاربران جهانی، یا همهٔ کاربران بالای ۴۰ سال release کنید.

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

حالت تاریک/روشن (Dark Mode/Light Mode)

می‌توانیم رفتار قبل و بعد از بازآرایی را با تکنیکی که در Slack light/dark نامیده‌ایم مقایسه کنیم. این‌طور کار می‌کند.

اول منطق بازآرایی‌شده را جدا از منطق فعلی پیاده کنید. مثال ۴-۱ این گام را در مقیاس کوچک نشان می‌دهد.

مثال ۴-۱. پیاده‌سازی‌های جدید و قدیم، شاید در فایل‌های مختلف

javascript
// Linear search; this is the old implementation
function search(name, alphabeticalNames) {
  for(let i = 0; i < alphabeticalNames.length; i++) {
    if (alphabeticalNames[i] == name) return i;
  }
  return -1;
}

// Binary search; this is the new implementation
function searchFaster(name, alphabeticalNames) {
  let startIndex = 0;
  let endIndex = alphabeticalNames.length - 1;

  while (startIndex <= endIndex) {
    let middleIndex = Math.floor((startIndex+endIndex)/2);
    if (alphabeticalNames[middleIndex] == name) return middleIndex;

    if (alphabeticalNames[middleIndex] > name) {
      endIndex = middleIndex - 1;
    } else if (alphabeticalNames[middleIndex] &lt; name) {
      startIndex = middleIndex + 1;
    }
  }

  return -1;
}

سپس، همان‌طور که در مثال ۴-۲ نشان داده شده، منطق پیاده‌سازی فعلی را به تابع جداگانه منتقل کنید.

مثال ۴-۲. پیاده‌سازی قدیم به تابع جداگانه منتقل شد

javascript
// Existing function now calls into relocated implementation
function search(name, alphabeticalNames) {
  return searchOld(name, alphabeticalNames);
}

// Linear search logic moved to a new function.
function searchOld(name, alphabeticalNames) {
  for(let i = 0; i < alphabeticalNames.length; i++) {
    if (alphabeticalNames[i] == name) return i;
  }
  return -1;
}

// Binary search; this is the new implementation
function searchFaster(name, alphabeticalNames) {
  let startIndex = 0;
  let endIndex = alphabeticalNames.length - 1;

  while (startIndex <= endIndex) {
    let middleIndex = Math.floor((startIndex+endIndex)/2);
    if (alphabeticalNames[middleIndex] == name) return middleIndex;

    if (alphabeticalNames[middleIndex] > name) {
      endIndex = middleIndex - 1;
    } else if (alphabeticalNames[middleIndex] &lt; name) {
      startIndex = middleIndex + 1;
    }
  }

  return -1;
}

سپس تابع قبلی را به abstraction تبدیل کنید که شرطی یکی از پیاده‌سازی‌ها را فراخوانی کند. در dark mode هر دو پیاده‌سازی فراخوانی می‌شوند، نتایج مقایسه می‌شوند و نتیجهٔ پیاده‌سازی قدیم برگردانده می‌شود. در light mode هر دو فراخوانی می‌شوند، مقایسه می‌شوند و نتیجهٔ پیاده‌سازی جدید برگردانده می‌شود. همان‌طور که در مثال ۴-۳ دیده می‌شود، استفاده مجدد از تعریف تابع موجود کمترین تغییر کد را ممکن می‌سازد. (اگرچه در مثال ما نشان داده نشده، برای جلوگیری از افت کارایی در فرایند light/dark، هر دو پیاده‌سازی قدیم و جدید باید همزمان اجرا شوند.)

مثال ۴-۳. رابط موجود به‌عنوان abstraction برای فراخوانی هر دو پیاده‌سازی جدید و قدیم

javascript
// Existing function now an abstraction for calling into either implementation
function search(name, alphabeticalNames) {
  // If we're in dark mode, return the old result.
  if (darkMode) {
    const oldResult = searchOld(name, alphabeticalNames);
    const newResult = searchFaster(name, alphabeticalNames);

    compareAndLog(oldResult, newResult);

    return oldResult;
  }

  // If we're in light mode, return the new result.
  if (lightMode) {
    const oldResult = searchOld(name, alphabeticalNames);
    const newResult = searchFaster(name, alphabeticalNames);

    compareAndLog(oldResult, newResult);

    return newResult;
  }

  return search(name, alphabeticalNames);
}

// Linear search logic moved to a new function.
function searchOld(name, alphabeticalNames) {
  for(let i = 0; i < alphabeticalNames.length; i++) {
    if (alphabeticalNames[i] == name) return i;
  }
  return -1;
}

// Binary search; this is the new implementation
function searchFaster(name, alphabeticalNames) {
  let startIndex = 0;
  let endIndex = alphabeticalNames.length - 1;

  while (startIndex <= endIndex) {
    let middleIndex = Math.floor((startIndex+endIndex)/2);
    if (alphabeticalNames[middleIndex] == name) return middleIndex;

    if (alphabeticalNames[middleIndex] > name) {
      endIndex = middleIndex - 1;
    } else if (alphabeticalNames[middleIndex] &lt; name) {
      startIndex = middleIndex + 1;
    }
  }

  return -1;
}

function compareAndLog(oldResult, newResult) {
  if (oldResult != newResult) {
    console.log(`Diff found; old result: ${oldResult}, new result: ${newResult}`);
  }
}

وقتی abstraction درست جا افتاد، dark mode را فعال کنید (یعنی اجرای دو مسیر کد، برگرداندن نتایج کد قدیم). تفاوت‌های ثبت‌شده بین دو مجموعه نتیجه را پایش کنید. باگ‌های احتمالی پیاده‌سازی جدید که باعث اختلاف می‌شوند را ردیابی و رفع کنید. تا وقتی همهٔ اختلاف‌ها مدیریت شوند تکرار کنید و dark mode را به گروه‌های گسترده‌تر کاربران فعال کنید.

وقتی همهٔ کاربران (از کم‌ریسک‌ترین محیط‌ها شروع) در dark mode هستند، light mode را برای زیرمجموعه‌های کوچک کاربران فعال کنید (یعنی برگرداندن داده از مسیر کد جدید). به ثبت تفاوت‌ها در مجموعه‌های نتیجه ادامه دهید؛ اگر توسعه‌دهندگان دیگر روی کد مرتبط کار می‌کنند و ممکن است تغییری در پیاده‌سازی قدیم ایجاد کنند که در جدید منعکس نشده، مفید است. به opt-in گروه‌های گسترده‌تر به light mode ادامه دهید تا همه با موفقیت از پیاده‌سازی جدید نتیجه پردازش کنند.

در پایان اجرای هر دو مسیر کد را غیرفعال کنید، به پایش باگ‌های گزارش‌شده ادامه دهید، abstraction، feature flag و منطق اجرای شرطی را حذف کنید و پس از مدت کافی live بودن بازآرایی برای کاربران (هرچه برای use case شما مناسب است)، منطق قدیم را کاملاً حذف کنید. فقط پیاده‌سازی جدید باید جای قدیم بماند. مثال ۴-۴ نمونه‌ای است.

مثال ۴-۴. پیاده‌سازی جدید داخل تعریف تابع قدیم

javascript
// Binary search; this is the new implementation
function search(name, alphabeticalNames) {
  let startIndex = 0;
  let endIndex = alphabeticalNames.length - 1;

  while (startIndex <= endIndex) {
    let middleIndex = Math.floor((startIndex+endIndex)/2);
    if (alphabeticalNames[middleIndex] == name) return middleIndex;

    if (alphabeticalNames[middleIndex] > name) {
      endIndex = middleIndex - 1;
    } else if (alphabeticalNames[middleIndex] &lt; name) {
      startIndex = middleIndex + 1;
    }
  }

  return -1;
}

مانند هر رویکردی، معایبی هست که باید در نظر بگیرید. اگر کدی که بازآرایی می‌کنید حساس به کارایی است و در محیطی کار می‌کنید که multi-threading واقعی ندارد (PHP، Python یا Node)، اجرای دو نسخهٔ یک منطق کنار هم شاید گزینهٔ خوبی نباشد. فرض کنید کدی را بازآرایی می‌کنید که یک یا چند درخواست شبکه دارد؛ با فرض اینکه وابستگی‌ها با بازآرایی تغییر نمی‌کنند، دو برابر درخواست شبکه را به‌صورت سریال اجرا می‌کنید. باید توانایی ممیزی تغییرات با وفاداری بالا را در برابر افزایش متناظر latency بسنجید. یک trade-off اجرای دو مسیر کد و مقایسهٔ بعدی با نرخ نمونه‌گیری است؛ اگر این مسیر خیلی مکرر hit می‌شود، مقایسه فقط ۵ درصد زمان ممکن است دادهٔ کافی دربارهٔ درست کار کردن راه‌حل جمع کند بدون اینکه زیاد به کارایی لطمه بزند.

همچنین باید به بار اضافی روی منابع downstream توجه کنیم؛ از پایگاه داده تا صف پیام تا سیستم‌هایی که برای log تفاوت‌ها بین codepathهای مقایسه‌شده استفاده می‌کنیم. اگر مسیر پرترافیک را بازآرایی می‌کنیم و می‌خواهیم مقایسه را مکرر اجرا کنیم، باید مطمئن باشیم زیرساخت را تصادفی overload نمی‌کنیم. در تجربهٔ من مقایسه‌ها می‌توانند انبوهی از تفاوت‌های غیرمنتظره را آشکار کنند (به‌ویژه هنگام بازآرایی کد قدیمی پیچیده). امن‌تر است رویکرد کند و تدریجی به ramp-up اجرای دوگانه و مقایسه داشته باشیم تا ریسک overload سیستم log. نرخ نمونه‌گیری اولیهٔ کوچک تنظیم کنید، تفاوت‌های پرتکرار را با ظهورشان رفع کنید و تکرار کنید، نرخ نمونه‌گیری را گام‌به‌گام تا ۱۰۰ درصد یا وضعیت پایدار که مطمئنید اختلاف بیشتری نباید بیاید افزایش دهید.

rollout در Smart DNA

در بازآرایی Smart DNA، ریسک بزرگ‌تر مهاجرت وابستگی‌های زیاد هر مخزن به نسخه‌های سازگار با Python 2.7 بود، نه اجرای کد موجود با نسخهٔ جدیدتر Python. تیم نرم‌افزار تصمیم گرفت ابتدا چند تست مقدماتی انجام دهد: زیرمجموعه‌ای از خط لولهٔ داده در محیط ایزوله، نصب هر دو نسخه Python، اجرای چند job با فایل وابستگی جدید در محیط 2.7. وقتی از نتایج تست‌های مقدماتی مطمئن شدند، به‌آرامی و با دقت استفاده از مجموعهٔ جدید وابستگی‌ها را در production معرفی کردند.

برای محدود کردن ریسک، تیم jobهای خط لولهٔ دادهٔ پژوهشگران را ممیزی و بر اساس اهمیت گروه‌بندی کرد. سپس مهندس‌ها job کم‌ریسک با کمترین وابستگی downstream را برای مهاجرت اول انتخاب کردند. با تیم پژوهش زمان مناسب برای swap پیکربندی به requirements.txt و نسخهٔ جدید Python هماهنگ کردند. پس از تغییر، تیم قصد داشت logهای job را پایش کند تا رفتار عجیب زود گرفته شود. اگر مشکلی پیش آمد، پیکربندی به نسخهٔ اصلی برگردانده می‌شد تا تیم نرم‌افزار fix بسازد. وقتی fix آماده بود، آزمایش تکرار می‌شد. در طرح rollout، تیم خواست تغییر پیکربندی چند روز در production بماند تا job ده‌ها بار با موفقیت اجرا شود قبل از رفتن به job دوم.

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

پاک‌سازی artifactهای گذار

در فصل ۱ گفتم نباید بازآرایی را آغاز کنید مگر زمان اجرا تا پایان را داشته باشید. هیچ بازآرایی تا زمانی که artifactهای گذار باقی‌مانده درست پاک نشوند کامل نیست. فهرست کوتاه و ناکامل از artifactهایی که در فرایند بازآرایی تولید می‌کنیم:

feature flag بیشتر ما گناهکار گذاشتن یک یا دو feature flag پشت سرمان هستیم. فراموش کردن حذف flag برای چند روز (یا حتی چند هفته) چندان بد نیست، اما ریسک ملموسی با پاک نکردن آن‌ها همراه است. اول، تأیید فعال بودن feature flag پیچیدگی اضافه می‌کند. مهندسانی که کد gated با feature flag می‌خوانند باید رفتار در حالت فعال یا غیرفعال را در نظر بگیرند. این overhead لازم برای توسعهٔ قابلیت در محیط continuous deployment است، اما باید به‌محض امکان حذف را اولویت دهیم. دوم، feature flagهای کهنه انباشته می‌شوند. یک flag برنامه را سنگین نمی‌کند، اما صدها flag کهنه ممکن است سنگین کند. آداب خوب feature flag را رعایت کنید؛ نویسنده و تاریخ انقضا اضافه کنید و پس از گذشت تاریخ با آن مهندسین پیگیری کنید.

abstractionها می‌توانیم بازآرایی را از توسعه‌دهندگان دیگر با ساخت abstraction برای پنهان کردن گذار محافظت کنیم. شاید یکی برای روش deploy توضیح‌داده‌شده در «حالت تاریک/روشن» نوشته باشیم. اما پس از اتمام بازآرایی، این abstractionها معمولاً دیگر معنادار نیستند و می‌توانند گیج‌کننده باشند. وقتی abstraction هنوز منطق معناداری دارد، باید ساده‌سازی کنیم تا مهندسانی که بعداً می‌خوانند دلیلی برای مشکوک بودن به نوشته‌شدن برای بازآرایی نداشته باشند.

کد مرده هنگام بازآرایی، به‌ویژه در مقیاس بزرگ، معمولاً مقدار قابل‌توجهی کد مرده پس از rollout باقی می‌ماند. کد مرده به خودی خود خطرناک نیست، اما برای مهندسین بعدی که می‌خواهند بفهمند آیا هنوز استفاده می‌شود آزاردهنده است. به «کد استفاده‌نشده» در صفحه ۳۲ و معایب نگه‌داشتن کد استفاده‌نشده در کدبیس مراجعه کنید.

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

تست‌های واحد بسته به نحوهٔ اجرای بازآرایی، ممکن است تست‌های واحد تکراری کنار تست‌های موجود برای تأیید درستی تغییرات نوشته باشیم. باید تست‌های اضافی جدید را پاک کنیم تا توسعه‌دهندگانی که بعداً به آن‌ها ارجاع می‌دهند گیج نشوند. (تست‌های واحد تکراری هم برای تیم‌هایی که می‌خواهند suite واحد سریع نگه دارند خوب نیست.)

هزینهٔ محاسبهٔ feature flag

چند سال پیش هم‌تیمی من آزمایشی کرد تا بفهمد چقدر زمان صرف محاسبهٔ feature flag می‌کنیم. برای درخواست متوسط به سیستم‌های backend ما، نزدیک ۵ درصد زمان اجرا بود. متأسفانه بخش زیادی از flagهایی که زمان صرفشان می‌شد قبلاً برای همهٔ workspaceهای production فعال بودند و می‌توانستند کاملاً حذف شوند. ابزاری ساختیم تا توسعه‌دهندگان را به پاک‌سازی flagهای منقضی وادار کند و در چند هفته زمان پردازش آن‌ها را به‌شدت کم کردیم. feature flag واقعاً جمع می‌شوند!

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

برچسب برای artifactهای گذار

هنگام اجرای بازآرایی، برچسبی انتخاب کنید که تیم بتواند artifactهایی که باید پاک شوند را با آن علامت بزند. می‌تواند به سادگی کامنت درون‌خطی مثل TODO: project-name, clean up post release باشد. هرچه هست، جستجوی آسان باشد تا در مراحل پایانی پروژه سریع همهٔ جاهایی که نیاز به پولیش نهایی دارند را پیدا کنید.

ارجاع به معیارها در طرح

در فصل ۳ راه‌های متنوعی برای توصیف وضعیت جهان قبل از شکل‌گیری طرح اقدام بحث کردیم. گفتیم این معیارها باید برای هم‌تیمی‌ها و مدیریت استدلال قانع‌کننده‌ای برای پروژه بسازند. در ابتدای این فصل هم اهمیت استفاده از این معیارها برای تعریف وضعیت پایانی را توضیح دادیم (بخش «تعریف وضعیت پایانی»). حالا باید گام‌های میانی شناسایی‌شده قبلی (بخش «شناسایی نقاط عطف میانی راهبردی») را با معیارهای خودشان تکمیل کنیم. این‌ها برای شما و تیم مفیدند تا بفهمید پیشرفت مورد انتظار را می‌بینید و اگر مسیر off به نظر رسید زود مسیر را اصلاح کنید.

طرح‌های اجرا یکی از اولین نگاه‌های مدیریت (چه product manager تیم، skip-level یا CTO) به پروژه است. برای حمایت از ابتکار، علاوه بر statement مسئلهٔ قانع‌کننده با معیارهای موفقیت روشن، پیشنهاد باید معیارهای پیشرفت قطعی هم داشته باشد. نشان دادن جهت قوی باید نگرانی‌هایشان دربارهٔ go-ahead برای بازآرایی طولانی را کم کند.

درون‌یابی معیارهای هدف به نقاط عطف میانی

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

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

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

معیارهای مجزای نقاط عطف

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

گزینه‌های ساده زیادی هست. فرض کنید در Smart DNA continuous integration راه‌اندازی کرده‌ایم و linter را برای هشدار متغیرهای تعریف‌نشده فعال کرده‌ایم. می‌توانیم تعداد هشدارهای باقی‌مانده را معیار پیشرفت در محدودهٔ آن گام بدانیم. جدول ۴-۲ هر نقطه عطف بزرگ طوفان‌فکری‌شده در «شناسایی نقاط عطف میانی راهبردی» را با معیار متناظر نشان می‌دهد. (توجه: مقدار شروع برای نقطه عطف linting تقریبی است. تیم با اجرای pylint با پیکربندی پیش‌فرض روی سه مخزن و جمع هشدارها تخمین زده است.)

جدول ۴-۲. نمودار معیارهای نقاط عطف برای مهاجرت Python در Smart DNA

شرح نقطه عطفشرح معیارشروعهدفمشاهده‌شده
ایجاد یک فایل requirements.txt واحدتعداد فهرست‌های مجزای وابستگی۳۱-
ادغام همهٔ مخزن‌ها در یک مخزنتعداد مخزن‌های مجزا۳۱-
ساخت image Docker با همهٔ بسته‌های لازمتعداد محیط‌هایی که image Docker جدید استفاده می‌کنند۰۵-
فعال‌سازی lint از طریق CI برای monorepoتعداد هشدارهای linterحدود ۱۵٬۰۰۰۰-
نصب و rollout Python 2.7.1 در همهٔ محیط‌هاتعداد jobهایی که روی Python 2.7.1 با requirements.txt جدید اجرا می‌شوند۰۱۵۸-

تخمین‌زنی

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

به سفر جاده‌ای کانادا برگردیم. راهنماهای کلی برای زمان و مکان توقف برای غذا و خواب در مسیر مونترال به ونکوور تعیین کرده‌ایم. طولانی‌ترین رانندگی برنامه‌ریزی‌شده بین Regina, SK و Calgary, AB است؛ کمی کمتر از ۸۰۰ کیلومتر بزرگراه، حدود ۷٫۵ ساعت. با اطمینان از اینکه هرگز بیش از هشت ساعت در روز رانندگی نمی‌کنیم، وقت کافی برای بسته‌بندی صبح و توزیع روز داریم. مهم این است که به خودمان وقت کافی برای لذت بردن از سفر داده‌ایم؛ هنوز قصد داریم هر روز پیشرفت جدی کنیم، اما نه آنقدر که به ونکوور رسیدن burnout کنیم.

بیشتر تیم‌ها راهنما و فرایند خود را برای تخمین دارند، اما اگر ندارید (یا نمی‌دانید چطور پروژهٔ نرم‌افزاری به‌ویژه بزرگ را تخمین بزنید)، تکنیک ساده‌ای این است: برای هر نقطه عطف عددی از ۱ تا ۱۰ بدهید که ۱ کار نسبتاً کوتاه و ۱۰ کار طولانی است. تخمین بزنید طولانی‌ترین نقطه عطف چقدر طول بکشد. تصور کنید در آن نقطه عطف چه چیزی محتمل‌تر است اشتباه شود و تخمین را برای آن به‌روز کنید. (زیاده‌روی نکنید! معقول بودن buffer مهم است؛ وگرنه رهبری ممکن است بازآرایی را ارزشمند ندانند.) سپس هر نقطه عطف کوتاه‌تر را با آن طولانی مقایسه کنید. اگر طولانی‌ترین ۱۰ هفته طول بکشد و دومین طولانی‌ترین تقریباً همانقدر، شاید نه هفته مناسب باشد. تا آخر فهرست ادامه دهید.

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

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

دوم، این تخمین‌ها به تنظیم انتظارات با ذی‌نفعان (product managerها، مدیران، CTOها) و تیم‌هایی که ممکن است تحت‌تأثیر بازآرایی باشند کمک می‌کند. در بخش بعد دیدگاه آن‌ها را می‌پرسیم و اگر buffer کافی در تخمین‌ها بگذاریم، فضای مذاکره داریم.

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

به‌اشتراک‌گذاری طرح با تیم‌های دیگر

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

دو دلیل اصلی برای به‌اشتراک‌گذاری طرح اجرا با تیم‌های دیگر وجود دارد. اول و شاید مهم‌تر، شفافیت است. دوم، جمع‌آوری دیدگاه برای تقویت طرح قبل از جلب تأیید مدیریت.

شفافیت

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

باید در نظر داشته باشید تغییرات پیشنهادی شما ممکن است کدی را که آن‌ها مالک آن‌اند به‌شدت تغییر دهد یا فرایندهای مهمی که نگه می‌دارند تحت‌تأثیر بگذارد. در مهاجرت Python در Smart DNA، سه مخزن را در یکی ادغام می‌کنیم. این تغییر قابل‌توجهی برای هر توسعه‌دهنده یا پژوهشگری در این مخزن‌هاست. تیم‌های تحت‌تأثیر باید به‌اندازهٔ کافی از قبل هشدار ببینند که فرایند توسعه‌شان تغییر می‌کند.

بازآرایی هم ریسک اثر روی بهره‌وری تیم‌های دیگر را دارد. مثلاً اگر همهٔ بسته‌های لازم را در یک requirements.txt سراسری ادغام کنیم، ممکن است به کمک تیم‌های دیگر برای review و تأیید تغییراتشان نیاز داشته باشیم. شاید حتی دربارهٔ قرض گرفتن مهندس از تیم‌های دیگر بپرسیم (فصل ۶ نگاه عمیق‌تری به جذب هم‌تیمی‌ها دارد).

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

دیدگاه

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

اگر محصول پیچیده‌ای را بازآرایی می‌کنید، احتمالاً edge caseهایی هست که در نظر نگرفته‌اید. گرفتن نگاه دوم (و سوم و چهارم) تفاوت بزرگی می‌سازد. فرض کنید هنگام ممیزی بسته‌های تیم پژوهش در Smart DNA متوجه نشویم بعضی پژوهشگران requirements.txt را مستقیماً روی یکی از ماشین‌ها به‌روز کرده‌اند، نه در تاریخچهٔ نسخه و deploy کد جدید. وقتی طرح را با پژوهشگران به‌اشتراک بگذاریم، می‌گویند معمولاً وابستگی‌ها را روی خود ماشین به‌روز می‌کنند و تیم نرم‌افزار باید نسخهٔ آنجا را تأیید کند نه نسخهٔ check-in‌شده در مخزن. این بینش درد و شرم زیادی را برای تیم نرم‌افزار جلوگیری می‌کرد اگر بدون مشورت با پژوهشگران اجرا را شروع کرده بودیم.

به خاطر داشته باشید مهم است نظر ذی‌نفعان را قبل از kick-off اجرا بگیرید، اما در این مرحله چیزی سنگ‌بنا نیست. طرح در طول بازآرایی احتمالاً تغییر می‌کند؛ edge case غیرمنتظره، وقت بیشتر برای باگ لجوج، یا بخشی از رویکرد اولیه که اصلاً کار نمی‌کند. در این مرحله عمدتاً دیدگاه‌های دیگران را برای شفافیت و ریشه‌کن کردن مشکلات آشکار زود می‌خواهیم. در فصل ۷ بحث می‌کنیم چطور این ذی‌نفعان را درگیر و مطلع نگه داریم وقتی طرح تکامل می‌یابد.

اجتناب از گسترش محدوده

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

مراقب گفت‌وگوهایی باشید که همکار می‌گوید «حالا که داریم می‌توانیم…» یا «همیشه می‌خواستم X هم…». مگر در هنر گفتن «نه» ماهر باشید، ممکن است بیش از آنچه برنامه‌ریزی کرده بودید موافقت کنید. همه می‌خواهیم بازآرایی تا حد امکان دردها را برطرف و مهندسین را راضی کند. متأسفانه ورود به بازآرایی بزرگ با این ذهنیت تقریباً تضمین می‌کند پایدار نباشد؛ همیشه مسئله یا مهندس دیگری برای راضی کردن هست. باید بازآرایی‌ای را برنامه‌ریزی و اجرا کنیم که مطمئنیم در زمان معقول تحویل می‌دهیم. شاید همه‌چیز را درست نکند، اما حداقل چیزهای درست را درست می‌کند.

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

در Smart DNA، تیم نرم‌افزار با diligence طرح اجرای جامعی برای مهاجرت از Python 2.6 به 2.7 ساخت. پس از گام‌زدن در هر مرحله‌ای که توضیح دادیم — تعریف وضعیت هدف، شناسایی نقاط عطف مهم، انتخاب راهبرد rollout و غیره — طرحی داشت که به آن اطمینان داشت:

  • ایجاد یک فایل requirements.txt واحد
    • معیار: تعداد فهرست‌های مجزای وابستگی؛ شروع: ۳؛ هدف: ۱
    • تخمین: ۲–۳ هفته
    • زیروظایف:
      • فهرست‌کردن همهٔ بسته‌های استفاده‌شده در همهٔ مخزن‌ها
      • ممیزی همهٔ بسته‌ها و محدود کردن فهرست به بسته‌های لازم با نسخه‌های متناظر
      • شناسایی نسخه‌ای که هر بسته باید در Python 2.7 به آن ارتقا یابد
  • ادغام همهٔ مخزن‌ها در یک مخزن واحد
    • معیار: تعداد مخزن‌های مجزا؛ شروع: ۳؛ هدف: ۱
    • تخمین: ۲–۳ هفته
    • زیروظایف:
      • ایجاد مخزن جدید
      • برای هر مخزن، افزودن به مخزن جدید با git submodule
  • ساخت image Docker با همهٔ بسته‌های لازم
    • معیار: تعداد محیط‌هایی که image Docker جدید استفاده می‌کنند؛ شروع: ۰؛ هدف: ۵
    • تخمین: ۱–۲ هفته
    • زیروظایف:
      • تست image Docker در هر محیط
  • فعال‌سازی lint از طریق continuous integration برای monorepo
    • معیار: تعداد هشدارهای linter؛ شروع: حدود ۱۵٬۰۰۰؛ هدف: ۰
    • تخمین: ۱–۱٫۵ ماه
    • زیروظایف:
      • انتخاب linter و پیکربندی متناظر
      • یکپارچه‌سازی linter در continuous integration
      • استفاده از linter برای شناسایی مشکلات منطقی کد (متغیرهای تعریف‌نشده، خطاهای نحوی و غیره)
  • نصب و rollout Python 2.7.1 در همهٔ محیط‌ها
    • معیار: تعداد jobهایی که روی Python 2.7.1 با requirements.txt جدید اجرا می‌شوند؛ شروع: ۰؛ هدف: ۱۵۸
    • تخمین: ۲–۲٫۵ ماه
    • زیروظایف:
      • یافتن تست‌های هر مخزن؛ تعیین کدام تست‌ها قابل‌اعتمادند
      • استفاده از Python 2.7 روی زیرمجموعه‌ای از اسکریپت‌های کم‌ریسک
      • rollout Python 2.7 به همهٔ اسکریپت‌ها

مدیریت پروژه

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

برای زیروظایف فردی، ورودی برای یک یا دو نقطه عطف اول که قصد اجرا دارید در نظر بگیرید. وظایف کوچک‌تر را می‌توانید با cadence منظم‌تر در طول توسعه مشخص کنید. نقاط عطف بعدی بیشتر تحت‌تأثیر کار قبلی‌اند و جزئیات زیروظایفشان ممکن است تغییر کند. ورودی زیروظایف نقاط عطف بعدی را فقط هنگام kick-off آن‌ها بسازید.

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