فصل ۷ — XP، سادگی و طراحی افزایشی: Code Smell، Refactoring و تصمیم در آخرین لحظهٔ مسئولانه
«من برنامهنویس بزرگی نیستم؛ فقط برنامهنویس خوبی هستم با عادتهای عالی.» — Kent Beck، سازندهٔ XP
هدف XP فقط بهترکردن Teamwork نیست. هدف بزرگتر Practiceها، Values و Principles این است که Team نرمافزاری بسازد که بهسادگی Extend و Change شود و افراد بتوانند در Environmentی که Change را میپذیرد، با هم Work، Plan و Grow کنند.
Adoptکردن XP چیزی بیش از Pair Programming برای Code Review دائم یا TDD برای Test Coverage بیشتر است. Quality بالاتر و رابطهٔ بهتر با همتیمیها By-productهای مهماند، اما Goal اصلی را کامل نمیکنند، چون بهتنهایی Design نرمافزار را Fundamentally عوض نمیکنند.
تکرار این نکته ارزش دارد: یکی از مهمترین هدفهای XP، Software آسان برای Change است. Team وقتی میتواند Change را Embrace کند که Software خودش Changeable باشد. این Goal اثر عمیقی روی Approach تیم به Code و Design دارد. Mindset درست XP یعنی واقعاً باور کنید Practiceهایی مثل TDD، Pair Programming و Slack کمک میکنند Design متفاوتی بسازید؛ حذفشان میتواند Codebase Inferior و سختتغییر تولید کند.
این فصل نشان میدهد حتی Programmerهای Smart چگونه Code با Problem جدی Design میسازند، سه Primary Practice پایانی XP چگونه از این Problemها جلوگیری میکنند، و عادتهای خوب چگونه Ecosystemی برای Maintainable، Flexible و Changeable Code میسازند.
صفحهٔ PDF 239
روایت
- Justin — Developer.
- Danielle — Developer دیگر.
- Bridget — Project Manager.
پردهٔ چهارم: رفتن به وقت اضافه، بخش دوم
Justin معمولاً میخواست به قطار 5:42 عصر برسد، اما بهندرت میشد. تقریباً همیشه از Bug کوچک یا Tweak جزئی یک ساعت مانده به رفتن شروع میشد و somehow همان Change کوچک به Monster تبدیل میشد.
Pattern آشنا بود: Change ساده در یک بخش، Change بخش دوم را لازم میکرد. Change دوم به بخش سوم و چهارم میرسید؛ یکی از آنها به قسمت دیگری از Codebase و همینطور زنجیره ادامه پیدا میکرد. گاهی تا پایان کار Justin تقریباً فراموش میکرد Change نخست چرا شروع شده بود.
بعضی وقتها Change آنقدر شدید بود که Team اصلاً انجامش نمیداد. Justin، Danielle و Bridget نتیجه میگرفتند Risk آن Codebase را Destabilize میکند. هیچ چیز Demoralizingتر از ساعتها Rework نیست که در آخر Backout شود.
امشب هم همین بود. Justin فقط میخواست یک Option تازه به Drop-down تنظیمات اضافه کند. به Danielle گفته بود: «به Girlfriend قول دادهام زود خانه باشم، فقط همین یک کار آخر.» پنج ساعت گذشته بود.
Option باید امکان Hideکردن Ranking Player را بدهد، اما Validator هر بار پیام «You must select an item from the list» میداد. Debug نشان داد Validator آیتمها را از Cache یک Database Table میخواند. Justin هنگام Click روی OK یک Refresh Cache اضافه کرد؛ این Fix خودش سه Change دیگر در Code خواست. سه Page دیگر هم Settings Page را Reuse میکردند و باید Modify میشدند.
در مجموع بیش از دوازده Change برای یک Option ساده لازم شد. در پایان Code کار میکرد، اما با یک Kludge بد.¹ Danielle پیشنهاد داد چون Cache Refresh فقط User ID میخواهد، Fake User Class بسازند که فقط ID دارد و بقیه را null برمیگرداند. Ugly بود، ولی کار کرد.
Justin گفت بالاخره Done شده و میرود؛ «آنقدر بین قسمتهای Code بالا و پایین پریدم که فهمیدم Pinball چه حسی دارد.» Danielle گفت Change کوچک Login Page او هم به سه محل دیگر و چند Class و Service Response ناشناخته کشیده شده است.
Justin پرسید: «چرا همیشه اینطوری است؟» Danielle جواب داد: «Programming همین است. سؤال واقعی این است: مطمئنی Codeی که الآن نوشتی کار میکند؟» چند ثانیه به هم نگاه کردند. Justin دوباره Headphone گذاشت، چند دقیقه Test بیشتر انجام داد و پیام عذرخواهی دیگری برای Girlfriend نوشت.
¹ Kludge اصطلاح Engineering برای Solution سریع و کثیف است که کار را راه میاندازد اما Ugly، Clumsy، Inelegant و سخت Maintain میشود.
Code و Design
Alistair Cockburn دربارهٔ پروژهٔ Chrysler Comprehensive Compensation (C3) نقل میکند که Kent Beck باید Culture تیم را از Clever Code به Simple Solution تغییر میداد. یکی از Ritualها نوعی Peer Pressure بود: برای Solution بیش از حد Clever، Propeller Beanie روی سر Developer میگذاشتند و «Cleverness» آن را برجسته میکردند؛ Attention منفی Peerها افراد را از Cleverness افراطی دور و Appreciation برای Simple Design را بیشتر میکرد. همه با این Style راحت نبودند و یکی از افراد در نهایت پروژه را ترک کرد.
XP Team Code آسان برای Change میسازد. کسی عمداً Code سختتغییر نمیسازد؛ حتی Developer بسیار Smart معمولاً هدفش Reusability و Design کامل است. Problem اینجاست که Planning برای Reuse دشوار است و بهسادگی به Abstract/General Code بیش از حد میرسد؛ Framework Code ممکن است به اندازهٔ Business Functionality حجم داشته باشد. Design «خیلی هوشمند» امروز میتواند مانع فردا شود که Team از Touchکردنش میترسد.
Brittle Code فقط اشتباه Rookie نیست. گاهی بهترین Developerها Code سختتغییر میسازند، نه چون Poorly Written است، بلکه چون Clever بهجای Simple است. Developer طبیعی است Problem فردا را هم امروز حل کند. Planning Meetingها با Edge Caseهای بیپایان طولانی میشوند و هرچه Problem بزرگتر باشد، Clever Architecture وسوسهکنندهتر است.
Simplicity در XP یعنی Code فقط چیزی را انجام دهد که امروز لازم است، با کمترین Complexity معقول، در حالی که Structure آن Change آینده را ممکن نگه دارد.
Complexity فقط در Code نیست
همان Problem در Project Planning هم رخ میدهد. مثال رایج Command-and-control Manager است که میخواهد تمام «Resource»ها 100% Allocated باشند و هر دقیقهٔ Developer روی Task Approved ثبت شود. ظاهراً Goal این است که کسی Idle نباشد، اما System برای Tracking 100% Allocation، Overhead زیادی تولید میکند: Time Entry، Plan Update، Review و Meeting برای «چرا فقط 95% کار کردهای؟».
Team ممکن است 20% Time را صرف Tracking و Discussion دربارهٔ 80% باقی کند. Status Meetingها پر میشوند از سؤالهایی مثل «چند درصد Complete هستی؟» و «چند دقیقه دیگر لازم داری؟» Complexity Management Value Project را بالا نمیبرد، حتی Project را طولانیتر میکند.
XP مثل Scrum Iterative Plan میکند و Decision را تا Last Responsible Moment عقب میاندازد. Monitoring دائمی Time معمولاً کمکی به Delivery نمیکند و Data جمعشده اغلب نه برای Forecast واقعی استفاده میشود و نه بعد از Project Review میشود. 100% Allocation همچنین نیاز دارد Decisionهای زیادی از اول گرفته شوند و با هر Change کل Plan و Allocation بازسازی شود.
این Example یک Antipattern را نشان میدهد: Pattern رفتاری تکرارشوندهای که Problem ایجاد میکند. Recognizeکردن Antipattern نخستین قدم برای Simplifyکردن Process است.
XP Teamها Code Smell را پیدا و Fix میکنند
Code نیز Antipattern دارد. وقتی Antipattern به Structure یا Design Code مربوط باشد Code Smell نامیده میشود. XP Team بهطور دائم Smellها را میجوید و بهجای رهاکردنشان، Fix میکند.
اصطلاح Code Smell و Antipattern در دههٔ 1990 با بحث Developerها، از جمله در WikiWikiWeb ساختهٔ Ward Cunningham ــ از نویسندگان Agile Manifesto ــ مشهور شد. Developerها فهمیدند Problemهای Design در Projectهای مختلف Symptom مشترک دارند. مهم است به یاد داشته باشیم این Problemها توسط انسان ساخته، توسط انسان شناخته و توسط انسان حل میشوند؛ بنابراین Root Cause فقط Technical نیست، Team-oriented هم هست.
Shotgun Surgery
یکی از شناختهشدهترین Smellها: برای Change کوچک در یک محل، مجبور میشوید دو یا سه بخش ظاهراً نامرتبط را تغییر دهید؛ آن Changeها خودشان Changeهای بیشتری میخواهند. در Codebase خیلی Smelly ممکن است Developer برای Fix ساده بیش از دوازده Hop بزند و در نهایت از بار Mental Tracking منصرف شود. تجربهٔ Justin دقیقاً همین بود.
Half-baked Code / Half-baked Object
برای استفاده از یک Object یا Module مجبورید Objectهای دیگری را با ترتیب و Value خاص Initialize کنید. مثلاً بعد از Init یک Library باید چند Variable Default ست و Library وابسته جداگانه Init شود. فقط Documentation یا Sample Code میگوید این کار لازم است؛ در غیر این صورت Crash یا Behavior غیرقابل پیشبینی رخ میدهد. Unit بهتنهایی «پخته» و Self-contained نیست.
Very Large Class / Method
Class یا Method/Function/Module بسیار بزرگ خواندن و Maintain را سخت میکند و غالباً علامت این است که بیش از یک Responsibility دارد. شکستن آن به Unitهای کوچکتر Understanding و Changeability را بهتر میکند.
Duplicated Code
Block یکسان یا تقریباً یکسان در چند محل. Source رایج Bug است، چون Developer سه Copy را Update میکند و چهارمی فراموش میشود.
شکل ۷-۱ — XP Developer مؤثر عادت دارد Smellهایی مثل Very Large Class و Duplicated Code را پیدا و اصلاح کند.
Spaghetti Code
Structure پیچیده و Tangled که مسیر Dependency فهمش سخت است؛ معمولاً Commentهای ترسناک یا عذرخواهانهٔ Developerهای قبلی دورش دیده میشود.
Lasagna Code
Layering افراطی. Modern Design معمولاً Layer دارد، اما وقتی تعداد Layer زیاد و Pattern آنها نامنسجم باشد فهم Responsibility هر Layer سخت میشود. Leak بین Layerها نیز Type، Idea یا Codeی را که باید Encapsulated باشد به Neighbor Layer میکشاند.
شکل ۷-۲ — بعضی Code Smellها Problem Design کلی System و Interaction Unitها را نشان میدهند.
Hook، Edge Case و Codeی که بیش از حد کار میکند
Problem فقط Structure نیست؛ Behavior هم Smell دارد.
Hookهای زودهنگام
Team Future Use یک Class را حدس میزند و Hook/Placeholder میگذارد. Hook «تقریباً رایگان» به نظر میرسد، ولی Cost دارد: Team را به Decision امروز Bind میکند که میتوانست Later گرفته شود. وقتی زمان استفاده میرسد Understanding بهتر شده و Hook باید Change کند، اما دیگر بخشها حول همان Hook Design شدهاند و حتی Empty Hook هم سختتغییر شده است.
Programmerی که Hook زیاد میگذارد بعداً به TODOهایی میرسد که فکر میکرد Code واقعی هستند. این Antipattern یعنی Future Speculation بیش از حد باعث میشود نتوانید بفهمید Code امروز واقعاً چه میکند.
وسواس Edge Case
Edge Case موقعیتی Rare با Conditionهای خاص است. File Loader باید File-not-found را Handle کند؛ شاید Folder-not-found، Permission، Delete در وسط Read، Wrong Character Set و دهها حالت دیگر را هم در نظر بگیرد. سؤال این است: کجا Line بکشیم؟
Handleکردن Edge Case لازم است، اما Return آن Diminishing میشود. بعضی Developerها به اندازهٔ کل Main Code برای Bullet-proof کردن Rare Case Code مینویسند. Result پیچیده و سختفهم است و خود Complexity Change را سخت میکند. Developer خوب Detail-oriented است و Cleverness میتواند این Detail Orientation را به افراط ببرد، تا Meeting با بحث بیپایان روی Obscure Edge Case اشغال شود.
Framework Trap و YAGNI
Framework Trap وقتی است که برای حل یک Problem منفرد، بهجای Code مستقیم، Framework عمومی میسازید تا Future Problemهای مشابه را هم حل کند. Ron Jeffries میگوید: چیزی را وقتی Implement کنید که واقعاً نیاز دارید، نه وقتی فقط پیشبینی میکنید نیاز خواهید داشت. این با acronym معروف YAGNI — You Ain’t Gonna Need It بیان میشود.
Developer ممکن است از Web Page به Framework تولید Page، از Performance Fix به General-purpose Cache، یا از Scheduled Downloader به Scripting Engine برسد. Intention شریف است: «اگر قبلاً Framework وجود داشت کارم آسانتر بود، پس من برای دیگران Framework میسازم.» اما همین نیت میتواند Complexity نامرتبط وارد Project کند.
شکل ۷-۳ — Comic xkcd با طنز نشان میدهد Developer چگونه در Framework Trap میافتد.
مگر Reusable Framework بد است؟
خیر. XP مخالف Framework یا Reuse نیست. .NET Framework یا Frameworkهای Web/Graphics/Network ارزش عظیم دارند. Programmer معمولاً ابتدا دنبال Library آماده میگردد و Shareکردن Solution قابل Reuse رفتاری طبیعی و Productive است.
تفاوت مهم Library و Framework: Library معمولاً Component مستقل و کوچک است که میتوانید جدا وارد Project کنید. Separation of Concerns میگوید هر Component یک کار مشخص انجام دهد و Unitی که کارهای زیادی میکند شکسته شود. Framework در مقابل مجموعهای از Componentهای Reusable است که System بزرگتر و Ruleهای خودش را شکل میدهد. Framework خوب ارزشمند است؛ اما Project نباید صرفاً بهخاطر Cleverness هدفش ساخت Framework شود.
نکتههای کلیدی Code و Design
- XP Team Simple Code میسازد و Complexity غیرضروری را حذف میکند.
- Smellهایی مثل Half-baked Code و Very Large Class Problem Unit را نشان میدهند.
- Lasagna Code Problem Design بزرگتر را آشکار میکند.
- Shotgun Surgery یعنی Change کوچک Cascading Change در نقاط مختلف ایجاد کند.
- Overplanning برای Edge Case، Hook زیاد و Framework برای Problem منفرد نمونهٔ Cleverness افراطیاند.
- حتی Team بسیار Skilled با Habit بد وارد این Problemها میشود.
تصمیم Code و Design را در Last Responsible Moment بگیرید
Kent Beck توضیح میدهد XP-style Design بیشتر از اینکه Design را حذف کند، زمان Decision Design را عوض میکند. Decision تا وقتی Experience کافی وجود دارد عقب میرود و سپس فوراً استفاده میشود. Benefitها:
- Deploy زودتر.
- Decision با Certainty بیشتر.
- Avoidکردن زندگی طولانی با Decision بد.
- حفظ Pace Development وقتی Assumptionهای Design قدیمی میشوند.
Cost این Strategy Discipline برای سرمایهگذاری Design در تمام عمر Project و انجام Change بزرگ با Stepهای کوچک است تا Flow Value قطع نشود.
Scrum Last Responsible Moment را برای Simplify Project Planning استفاده میکند. XP همین Value Simplicity را به Architecture، Design و Code میآورد. بسیاری از Decisionهای Technical بعد از نوشتن Code گرفته میشوند، چون XP Team دائماً Refactor میکند: Structure Code را بدون Change در Behavior تغییر میدهد.
Refactoring فقط XP نیست و IDEهای مدرن Toolهای Refactor دارند.
نمونهٔ Refactoring
نویسندگان در Head First C# برای Bee Hive Simulator یک Block Code داشتند. Reviewer گفت Method بیش از حد بزرگ است. آنها دو Block چهارسطره را به Methodهای MoveBeeFromFieldToHive() و MoveBeeFromHiveToField() Extract کردند.
شکل ۷-۶ — Code قبل از Refactoring.
شکل ۷-۷ — Extract Method باعث شد Code واضحتر و سادهتر شود.
Refactoring Understanding محلی را بهتر کرد و Complexity کل Project را هم پایین آورد. اگر جای دیگری نیاز به Move Bee باشد Path of Least Resistance اکنون Reuse همین Methodهاست؛ و اگر Duplicated Code بعداً دیده شود Replace کردن آن با Method Call سادهتر است.
Technical Debt را با Refactoring بیرحمانه Fix کنید
Ward Cunningham میگوید Shipping First-time Code شبیه رفتن زیر Debt است. کمی Debt Development را Fast میکند اگر Promptly با Rewrite پرداخت شود؛ Danger وقتی است که Debt بازپرداخت نمیشود.
Design/Code Problem در طول زمان Compound میشود. حتی Developer عالی Code قابل Improvement مینویسد. Problemهای باقیمانده کمکم Shotgun Surgery و Headache تولید میکنند و به آن Technical Debt میگوییم.
XP Team در هر Cycle زمانی برای Pay down Debt میگذارد. Slack Buffer خوبی است. بهترین راه مثل Debt مالی این است که از ابتدا Debt غیرضروری نسازید. به همین دلیل Team refactor mercilessly میکند: دائماً Code Smell را میجوید و Structure را Simplify میکند. با هر Refactor، Understanding از Actual Use بیشتر میشود و Unitها به Usage واقعی نزدیکتر Design میشوند.
گرچه Refactoring Time میگیرد، Codebase ساده در مجموع Time بسیار بیشتری Save میکند و Change آینده را آسانتر میسازد؛ در نتیجه Team میتواند Change را واقعاً Embrace کند.
مگر Refactoring همان Rework و Source Bug نیست؟
بله Rework است، اما نوعی Rework که میتواند Bug را Prevent کند. Constant Refactoring Unitهای کوچکتر و Reusableتر میسازد. Risk اصلی این است که Structure را عوض کنید و ناخواسته Behavior را هم Change دهید؛ پاسخ XP Test-first Development است. اگر Unit Test قبل و بعد Pass شود، Refactor ایمنتر است. Test به Developer Courage میدهد Refactor عمیقتری انجام دهد. تعریف Refactoring دقیقاً Change Structure بدون Change Behavior است، پس همان Testها باید قبل و بعد Pass باشند.
مگر بهتر نیست Design را از اول درست بسازیم؟
بله، اگر واقعاً بتوانید؛ اما Understanding Team از Problem با Project و Code تکامل مییابد. Working Software Feedback میدهد و فهم امروز با شش ماه قبل فرق میکند. بنابراین «درست بار اول» Goal خوبی است ولی همیشه واقعبینانه نیست.
Weekly/Quarterly Cycle XP فضای Unit Test و Refactor را در Iteration لحاظ میکند. هر Delivery، Story و Understanding Problem را Refine میکند و Constant Smell Detection Codebase را Changeable نگه میدارد.
نباید Teamهای قدیمی BRUF را سادهلوح تصور کنیم. Tool و Practice مدرن برای Unit Test و Refactoring نداشتند. Compile ممکن بود Day یا Week طول بکشد، Computerها Network نبودند و Release با CD، Floppy یا Tape انجام میشد. Minimum Cost Rework آن زمان واقعاً بالا بود، بنابراین Documentation و Review Up-front منطقیتر بود.
Continuous Integration برای یافتن Design Problem
CI یکی از Toolهای Mature امروز است و Design را بهتر میکند چون Integration Failure را زود نشان میدهد.
شکل ۷-۸ — Continuous Integration مسئله را در ابتدای مسیر آشکار میکند.
Systemی که Failure را بهمحض رخدادن Report میکند Fail-fast System است. Fail-fast برای Fault-tolerance و Root-cause Discovery مهم است. Early Failure Feedback Loop میسازد و Knowledge را سریع به Project برمیگرداند.
Failure یکی از XP Principles است. CI اجازه میدهد Project وقتی دو عضو Code ناسازگار Add کردهاند Fast Fail شود. Team با XP Mindset Failure را مثبت میبیند چون Problem را زمانی پیدا میکند که Fix ارزانتر است.
شکل ۷-۹ — Design Problem زودهنگام آسانتر Fix میشود و از Kludgeهای بعدی جلوگیری میکند.
TDD Unitهای کوچک و مستقل میسازد که Integration سادهتری دارند. CI نیز جلوی این را میگیرد که Developer مدت زیادی تنها روی Componentی کار کند که در پایان با بقیهٔ System Fit نیست.
از Monolithic Design دوری کنید
اگر وسط Project Understanding از Problem تغییر کند ــ وضعیتی کاملاً معمول ــ Team باید Decision را تا Last Responsible Moment نگه دارد و Codebase بتواند Adapt شود.
شکل ۷-۱۰ — بدون Habitهای Simple/Decoupled Code، Design به Monolith تبدیل میشود.
Waterfall Team با Requirement Up-front و Build یکجای System Incentive کمتری برای Design for Change دارد؛ Change سخت کنترل میشود و Habitهای Refactor/Smell Detection رشد نمیکنند. نتیجه میتواند Monolithic Design باشد: Unitهای بزرگ و Interconnected با Dependency زیاد که جداکردنشان دشوار است.
در Construction، خرابکردن Wall با Sledgehammer آسیب جدی است؛ در Software حذف Code معمولاً با Version Control قابل Recover است. آسیب جدیتر این است که Bad Code بنویسید و بعد Code بیشتری را به آن وابسته کنید. این Dependency یا Coupling دو بخش را به هم میبندد و Change را سخت میکند.
صفحهٔ PDF 264
تصویر منبع - صفحهٔ PDF 264
تصاویر زیر از صفحات مربوط به همین مقاله در PDF اصلی استخراج و بهصورت دادهٔ داخلی Base64 جاسازی شدهاند.
تصویر منبع - صفحهٔ PDF 247
تصویر منبع - صفحهٔ PDF 248
تصویر منبع - صفحهٔ PDF 251
تصویر منبع - صفحهٔ PDF 254
تصویر منبع - صفحهٔ PDF 255
تصویر منبع - صفحهٔ PDF 258
تصویر منبع - صفحهٔ PDF 258
تصویر منبع - صفحهٔ PDF 262
تصویر منبع - صفحهٔ PDF 263