ویرگول
ورودثبت نام
مسعود بهرامی
مسعود بهرامیSoftware architect, developer, and writer with 10+ years of experience designing meaningful, maintainable systems. Exploring architecture, modeling, and software language. MasoudBahrami.com
مسعود بهرامی
مسعود بهرامی
خواندن ۱۴ دقیقه·۴ روز پیش

Big Bang Refactoring چرا این رویکرد اغلب به فاجعه ختم می‌شود؟

هیولای دو شاخ بزرگ درون اتاق تیم توسعه که خودش را خیلی دیر نمایان می‌کند. نقد رویکرد بازسازی همه‌جانبه و ارائه جایگزین‌های ایمن.

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

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

https://masoudbahrami.com/academy/why-big-bang-refactoring-leads-to-trouble/


پارادوکس The All-or-Nothing Fallacy

تیم‌هایی که اعتقاد به refactoring از نوع دوباره‌نویسی کامل سیستم دارند، معمولا دچار سندرم ریفکتور کردن همه سیستم یا هیچ بخشی از سیستم می‌شوند. این تفکر هم از آنجا ناشی می‌شود که سیستم لگسی به حدی بد است که شبیه باتلاقی است که هر دست و پا زدن درون آن جهت بهبود کیفیت کدهای موجود، کل تیم توسعه رو درون خود فرو می‌برد. کد کلاس order را تصور کنید که توسط بخش‌های مختلف سیستم از جمله سبد خرید، سفارشات، پرداخت، پیگیری سفارشات و تحویل، و همینطور لایه‌های presentation، application service، domain و data access در حال استفاده است. در این حالت هر تغییر در کد order شبیه انداختن یک تکه از یک دومینوی طولانی است. از نظر تیم توسعه ریفکتور مهم درون سیستم موجود مملو از خطرات بزرگی است که ارزش ریسک ندارد. از  طرف دیگر بهبودهای کم ریسک‌تر دارای اثرات جانبی مثبتی آن چنان کوتاهی هستند که افراد مجدد ارزشی در اعمال کردنشان نمی‌بینند. در همچین شرایطی تیم ممکن است سراغ استراتژی: باید تمام فعالیت‌های توسعه و نگهداری سیستم موجود را متوقف کنیم و ۳ ماه فقط روی بازنویسی/بازسازی کار کنیم تا سیستم جدید و تمیز شود، برود.


تحلیل شکست

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

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

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


عدم افزودن فیچر جدید به محصول در حین دوباره نویسی

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

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

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


رقبا و بازار، دشمن بزرگ بعدی شما بیرون از سازمان است

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


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

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

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


از دست دادن خلق ارزش کسب‌و‌کاری قابل لمس

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

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

هر میزان که این زمان طولانی‌تر شود، این امر می‌تواند منجر به حساسیت بیشتر نسبت به تیم و مهمتر از دست رفتن اعتماد(trust) نسبت به تیم شود. یکی از شالوده‌های کار تیمی اعتماد است که از بین بردنش، خسارت جبران ناپذیری به تیم و سازمان وارد می‌کند.

به بیان ساده‌تر، تا زمانی که محصول پیاده‌سازی نشود و افراد با آن کار نکنند، تمام ارزش اشاره شده در بالا در لابلای صحبت‌های افراد نهفته است.


دمو و چرخه فیدبک کوتاه‌تر

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

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


اما چه کنیم؟

ابتدا تصویر بزرگِ فضای مسئله را ترسیم کنید

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

رویکردهای Collaborative Modelling and Designing(CoMO) در این مرحله می‌تواند کمک شایانی به شما کمک کند. رویکردهایی از جمله ایونت استورمینگ (Event Storming) و کشف اکتشافی دامنه (Exploratory Domain Discovery) می‌توانند بسیار موثر باشند. در این مواقع پیشنهادم استفاده از رویکرد EDD است. EDD با حرکت از هدف نهایی هر محصول و با شروع از انتهای هر فرآیند فضای محصول را به تصویر می‌کشد.

تصویر بزرگ وضعیت سیستم لگسی موجود را مصور کنید

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

دو تصویر ایجاد شده را کنار هم قرار دهید. این فضا باید در دسترس همه اعضای تیم باشد. این دو تصویر وضعیت فعلی یا نقطه شروع و نقطه هدف رو نمایش می‌دهد.

ریفکتور کردن(refactoring) شبیه پیمایش یک مسیر و یک سفر است. حیاتی است که مبدا و مقصد مشخص باشد. در ریفکتور کردن کد مقصد گاهی یک نقطه مشخص در نیست و بصورتی یک سریquality attribute برای کد در نظر گرفته می‌شود. راه‌های بسیار جهت رسیدن از نقطه مبدا تا مقصد وجود دارد. انتخاب بهترین و امن‌ترین مسیر یکی از تسک‌های روزانه توسعه دهندگان است.

مرزهای Bounded Context را بکشید

برای هر دو مدل ایجاد شده در مراحل قبل-تصویر بزرگ سیستم فعلی و تصویر بزرگ مورد انتظار از محصول، Bounded Contextها و context map را بکشید. توجه داشته باشید که bounded contextها و context map برای سیستم فعلی باید فقط وضعیت فعلی را صرفنظر از صحیح یا غلط بودن ارتباطات نمایش دهد. وابستگی‌های ورودی و خروجی از BC را بصورت کامل مشخص کنید. می‌توانید از Bounded Context Canvas نیز استفاده کنید.


ریفکتورینگ افزایشی(Incremental Refactoring)

تا جای ممکن از دوباره‌نویسی یک سیستم کامل اجتناب کنید. شرایط خیلی محدودی وجود دارد که دوباره‌نوشتن کامل یک سیستم را توجیه می‌کند. حتی اگر فکر می‌کنید این تنها راه موجود است(من شک دارم )  مجدد از خود این سوال رو بپرسید که آیا راه مفری هنوز وجود دارد یا خیر. بجای دوباره نویسی کامل سیستم موجود، شروع به بهبود کیفیت کد موجود کنید. متوجه خواهید شد که پس از مدتی اعمال کردن ریفکتورهایی بر روی کد فعلی و استفاده از تکنیک‌های از جمله strangler pattern اثری از کد کثیف وجود ندارد و عملا با کد بهتر جایگزین شده است. در این فرآیند شما می‌توانید هم ارزش بیزنسی خلق کنید و هم همزمان کیفیت کدبیس موجود را نیز روز به روز بهبود ببخشید. رویکردی که به boy scout معروف است.

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

بخاطر همین refactoring یکی از ساده‌ترین و در عین حال موثرترین تکنیک‌های ریفکتور است که بصورت پیش فرض در بسیاری از IDE وجود دارد.

شما باید مسیر طولانی ریفکتور کردن خود را به یکسری ریفکتورهای کوتاه و کوچک‌تر بشکنید و بصورت incremental جلو بروید. متوجه خواهید شد که از یکجایی به بعد تاثیر مثبت این ریفکتورهای کوچک نه بصورت خطی که بصورت نمایی خواهد بود. پدیده‌ای که به breakthrough refactoring معروف است. باید توجه داشت که این نوع ریفکتور کردن به میزان بسیار زیادی به درک صحیح از فضای مسئله وابسته است. همینطور این نوع ریفکتور کردن نیازمند یکسری پرکتیس‌های چابکی است که در ادامه مختصرا اشاره خواهد شد.


پرکتیس‌های چابکی برای Breakthrough Refactoring

یک اصل حیاتی به شما کمک می‌کند که مسیر ریفکتورینگ خود را بصورت امن پیش ببرید:

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

شفاف کردن انتظارات از ریفکتور کردن هر بخش کد

پیش از آغاز هر چرخه ریفکتور انتظار خود را بصورت شفاف مشخص کنید. انتظارات نباید شامل جزئیات باشد. همینطور نباید شامل اعمال یک design pattern مشخص مثل factory method یا strategy pattern و … باشد. در غیر اینصورت شما بصورت ناخواسته یک پیاده‌سازی فنی که یکی از راه‌های رسیدن به هدف ریفکتورینگ است را تبدیل به هدف اصلی کرده‌اید. همینطور این کار باعث می‌شود که بحث و ارتباط گرفتن و pair  زدن با افراد محصولی را نیز با مشکل جدی مواجه کند. در عوض هدفی شبیه اینکه، تبدیل فرآیند خطی و synchronous صدور بلیت یا notify کردن رستوران پس از فرآیند تسویه حساب سفارش توسط مشتری،  به یک فرآیند eventual، می‌تواند یک هدف و انتظار شفاف‌تر و دقیق‌تر از یک فرآیند ریفکتور کردن بخشی از کد باشد.

ترجمه‌ی انتظارات به تست‌های قابل اندازه‌گیری

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

در سیستم‌های لگسی نوشتن تست اتوماتیک براحتی نیست. سیستم‌های لگسی عمل برای قابل تست بودن طراحی نشده‌اند و یکی از نشانه‌های لگسی بودن این سیتسم‌ها همین test friendly نبودن آنها است. دقت کنید که designing for testability نبودن این سیتسم‌ها لزوما ناظر بر جنبه تست‌های اتوماتیک نیست، در این سیستم‌های تست‌های مانوال توسط تیم‌ تست هم شامل یک فرآیند پیچیده و زمانبر است که عملا چرخه فیدبک را بسیار ناکارآمد خواهد کرد. کتاب Working Effectively with Legacy Code برای کشتی گرفتن با این سیستم‌ها می‌تواند منبع غنی برای شما باشد.

Test Driven Development (TDD)

همانطور که در بخش قبل اشاره شد، تبدیل انتظارت به تست کیس‌ها و اتومات کردن آنها قبل از شروع فرآیند ریفکتورینگ یک ضرورت حیاتی در داشتن یک چرخه ریفکتور موثر، ساده و قابل اعتماد است. هر چند چرخه test-first development به TDD معروف است، اما TDD چیزی فراتر از صرفا نوشتن تست قبل از پیاده‌سازی کد است. شما عملا وقتی تستی را می‌نویسید، طراحی و contract سرویس‌های خود را از دید یک استفاده کننده طراحی می‌کنید. TDD بیشتر یک رویکرد طراحی است تا یک رویکرد نوشتن تست.

Smaller (even more) Commit

پس از تغییر کوچیک در کد تغییرات را commit کنید. هر commit باید شامل یک تغییر کوچیک شامل تغییر اسم، یا شکست یک متد ساده به دو متد کوچکتر باشد. Commitهای کوچک شبیه فریم‌های یک استریم است. شما براحتی می‌توانید در صورت که نیاز داشتید تغییرات یک ریفکتور را به عقب برگردانید، بجای زدن بی نهایت ctrl + z کامیت آخری را ریورت کنید!

Continues Integration (CI)

تغییرات خود را زود به زود بر روی برنج اصلی پوش کنید. نباید برنج اصلی برای مدت طولانی از برنج لوکال هر کدام از دولوپرها فاصله بگیرد. بحث‌های زیادی حول محور برنچینگ وجود دارد و نظرات متفاوت بسیار زیادی درباره trunk-based branching وجود دارد. به شخصه من یکی از طرفدران این رویکرد هستم. صرفنظر از استراتژی برنجینگ، پرکتیس‌هایی از جمله feature toggling و test automation suite برای داشتن یک چرخه سالم CI حیاتی است.


سیستم نرم‌افزاری شما شبیه به یک باغ است

در نظر داشته باشید که ریفکتور کردن یک سیستم نرم‌افزاری باید مانند یک نگهداری مداوم از باغ باشد، نه مانند یک تخریب و بازسازی کل ساختمان.

ریفکتور کردن یک سیستم نرم‌افزاری مانند یک نگهداری مداوم از باغ است، نه مانند یک تخریب و بازسازی کامل یک ساختمان.


ریفکتورینگrefactoringclean codeddd
۱
۰
مسعود بهرامی
مسعود بهرامی
Software architect, developer, and writer with 10+ years of experience designing meaningful, maintainable systems. Exploring architecture, modeling, and software language. MasoudBahrami.com
شاید از این پست‌ها خوشتان بیاید