حالت تاریک
فصل ۵ — بازخورد
بازخورد بهعنوان «انتقال اطلاعات ارزیابیکننده یا اصلاحی درباره یک عمل، رویداد یا فرایند به منبع اصلی یا کنترلکننده» تعریف میشود.[^1]
بدون بازخورد، فرصتی برای یادگیری وجود ندارد. ما فقط میتوانیم حدس بزنیم، نه اینکه بر اساس واقعیت تصمیم بگیریم. با وجود این، شگفتانگیز است که چقدر افراد و سازمانها کمتر به آن توجه میکنند.
مثلاً، بسیاری از سازمانها برای نرمافزارهای جدید یک «مورد تجاری» (business case) میسازند. چند تا از این سازمانها بعداً هزینه توسعه را ردیابی میکنند و آن را همراه با منافع واقعی ارائهشده به مشتریان ارزیابی میکنند تا تأیید کنند «مورد تجاری»شان محقق شده است؟
مگر اینکه بتوانیم نتایج انتخابها و اقداماتمان را بدانیم و درک کنیم، نمیتوانیم بگوییم آیا پیشرفت میکنیم یا نه.
این آنقدر واضح به نظر میرسد که ارزش بیان نداشته باشد، اما در عمل حدسزنی، سلسلهمراتب و سنت، داوران بسیار رایجتری برای تصمیمگیری در اکثر سازمانها هستند.
بازخورد به ما امکان میدهد منبعی از شواهد برای تصمیماتمان ایجاد کنیم. وقتی چنین منبعی داشته باشیم، کیفیت تصمیماتمان ناگزیر بهبود مییابد. این به ما اجازه میدهد افسانه را از واقعیت جدا کنیم.
[^1]: منبع: Merriam Webster Dictionary. https://www.merriam-webster.com/dictionary/feedback
نمونه عملی اهمیت بازخورد
درک ایدههای انتزاعی میتواند دشوار باشد. بیایید یک نمونه ساده و عملی از این ببینیم که سرعت و کیفیت بازخورد واقعاً چقدر مهم هستند.
تصور کنید با مشکل متعادل کردن یک جارو روبرو شدهاید.
میتوانیم تصمیم بگیریم ساختار جارو را با دقت تحلیل کنیم، مرکز ثقل آن را محاسبه کنیم، ساختار دسته را بهدقت بررسی کنیم و دقیقاً نقطهای را محاسبه کنیم که جارو کاملاً متعادل خواهد بود. سپس میتوانیم جارو را با دقت زیاد به موقعیت دقیقی که برنامهریزی کردهایم هدایت کنیم و از طریق اجرای بینقص، اطمینان حاصل کنیم که هیچ تکانه باقیماندهای باقی نمانده که جارو را از تعادل خارج کند.
این رویکرد اول مشابه مدل توسعه آبشاری (waterfall) است. میتوان تصور کرد که کار کند، اما احتمالش فوقالعاده کم است. نتیجه بسیار ناپایدار است. به پیشبینیهای کامل ما متکی است و با کوچکترین اغتشاش یا نادقیقی در پیشبینیها، جارو میافتد.
در عوض، میتوانیم جارو را روی دستمان بگذاریم و دستمان را بر اساس نحوه کج شدن آن حرکت دهیم.
رویکرد دوم بر پایه بازخورد است. راهاندازی آن سریعتر است و سرعت و کیفیت بازخورد موفقیت آن را تعیین میکند. اگر در حرکت دادن دستمان خیلی کند باشیم، باید اصلاحات بزرگی انجام دهیم. اگر در حس کردن جهت کج شدن جارو خیلی کند باشیم، باید اصلاحات بزرگی انجام دهیم یا جارو میافتد. اگر بازخوردمان سریع و مؤثر باشد، میتوانیم اصلاحات کوچکی انجام دهیم و جارو پایدار خواهد ماند. در واقع، حتی اگر چیزی بیاید و جارو یا ما را بههم بزند، میتوانیم سریع واکنش نشان دهیم و مشکل را اصلاح کنیم.
این رویکرد دوم آنقدر موفق است که همینطور فضاپیماها روی نیروی موتورهایشان «متعادل» میمانند. آنقدر پایدار است که اگر در این کار خوب باشم، حتی اگر ناگهان مرا هل دهید و مجبور شوم لنگ لنگان بروم، احتمالاً میتوانم جارو را متعادل نگه دارم.
این رویکرد دوم بیشتر غیررسمی به نظر میرسد؛ در نوعی کمتر سختگیرانه به نظر میآید، اما عمیقاً مؤثرتر است.
میتوانم تصور کنم در این نقطه فکر میکنید: «نویسنده چه نوشیده؟ جارو چه ربطی به نرمافزار دارد؟» منظورم این است که چیزی عمیق و مهم درباره نحوه کار فرایندها وجود دارد.
مثال اول از رویکرد برنامهریزیشده و پیشبینیکننده است. این رویکرد تا زمانی که همه متغیرها را کاملاً درک کنید و تا زمانی که چیزی نیاید تا درک یا برنامهتان را تغییر دهد، خوب کار میکند. این واقعاً پایه هر رویکرد برنامهریزیشده و جزئی است. اگر برنامه جزئی دارید، فقط یک راهحل درست وجود دارد؛ پس یا مشکل باید آنقدر ساده باشد که این ممکن شود، یا باید در توان پیشبینی آینده همهچیزدان باشید.
رویکرد دوم، جایگزین، همچنان شامل برنامهای است: «میخواهم جارو را متعادل کنم»، اما برنامه همهچیز درباره نتایج است و چیزی درباره مکانیزمی که از طریق آن به نتیجه میرسید نمیگوید. در عوض، فقط کار را شروع میکنید و هر کاری لازم است برای رسیدن به نتیجه مطلوب انجام میدهید. اگر این یعنی پاسخ به بازخورد و حرکت دادن دستتان چند میلیمتر در چند ثانیه، خوب است. اگر یعنی چند قدم لنگ لنگان به جلو و پهلو در حالی که دستتان را یک متر یا بیشتر حرکت میدهید چون اتفاق غیرمنتظرهای افتاده، آن هم خوب است، تا زمانی که نتیجه حاصل شود.
رویکرد دوم، هرچند ممکن است غیررسمیتر و بیشتر شبیه «بدون برنامه جلو رفتن» به نظر برسد، در واقع عمیقاً مؤثرتر و از نظر نتیجه پایدارتر است. در رویکرد اول فقط یک راهحل درست وجود دارد. در رویکرد دوم بسیارند، پس احتمال رسیدن به یکی از آنها بیشتر است.
بازخورد جزء ضروری هر سیستمی است که در محیطی در حال تغییر عمل میکند. توسعه نرمافزار همیشه تمرینی در یادگیری است و محیطی که در آن انجام میشود همیشه در حال تغییر است؛ بنابراین بازخورد جنبه ضروری هر فرایند توسعه نرمافزار مؤثر است.
کنفرانس ناتو[^2]
تا اواخر دهه ۱۹۶۰، مشخص شده بود که برنامهنویسی کامپیوتر کار دشواری است. سیستمهای ساختهشده در اندازه، پیچیدگی و اهمیت در حال افزایش بودند. تعداد افرادی که آنها را برنامهنویسی میکردند بهسرعت در حال رشد بود. با آگاهی از این افزایش دشواری، مردم شروع به فکر کردن کردند که چه کاری میتوانند برای کارآمدتر و کمخطاتر کردن فرایند ساخت نرمافزار انجام دهند.
یکی از نتایج این تفکر، برگزاری کنفرانس مشهوری برای تعریف مهندسی نرمافزار بود. کنفرانس در سال ۱۹۶۸ برگزار شد و قرار بود معنای مهندسی نرمافزار و شیوه عمل آن را در مفاهیم گسترده بررسی کند.
کنفرانس رویدادی «فقط با دعوت» بود و متخصصان جهانی آن زمان در این حوزه را برای بحث درباره طیف گستردهای از ایدهها در بستر مهندسی نرمافزار گرد هم آورد. با توجه به رشد چشمگیر ظرفیت سختافزار کامپیوتر در ۵۰ سال گذشته، برخی ایدهها ناگزیر بسیار کهنهاند:
دکتر H J Helms: فقط در اروپا حدود ۱۰٬۰۰۰ کامپیوتر نصبشده وجود دارد — این عدد با نرخ ۲۵ تا ۵۰ درصد در سال در حال افزایش است. کیفیت نرمافزار ارائهشده برای این کامپیوترها بهزودی بیش از یکچهارم میلیون تحلیلگر و برنامهنویس را تحت تأثیر قرار خواهد داد.
ایدههای دیگر پایدارتر به نظر میرسند:
A J Perlis: تصویر Selig به یک حلقه بازخورد برای پایش سیستم نیاز دارد. باید دادههایی درباره عملکرد سیستم جمعآوری شود تا در بهبودهای آینده استفاده شود.
اگرچه زبان Perlis کهنه به نظر میرسد، ایده میتواند رویکرد DevOps مدرن به توسعه را توصیف کند، نه ساخت چیزی با Algol!
[^2]: منبع: «NATO Conference on Software Engineering 1968»، https://bit.ly/2rOtYvM
بسیاری از مشارکتهای دیگر نیز به همین اندازه پیشبینانه بودند:
F Selig: مشخصات خارجی، در هر سطحی، محصول نرمافزاری را بر اساس مواردی که توسط کاربر کنترل میشوند و در دسترس او هستند توصیف میکند. طراحی داخلی محصول نرمافزاری را بر اساس ساختارهای برنامهای که مشخصات خارجی را محقق میکنند توصیف میکند. باید درک شود که بازخورد بین طراحی مشخصات خارجی و داخلی بخش ضروری فرایند پیادهسازی واقعبینانه و مؤثر است.
این توصیف برای گوشهای مدرن شگفتانگیز شبیه داستانهای[^3] توسعه چابک است و اهمیت جداسازی «چه» از «چگونه» در فرایند نیازمندیها را توصیف میکند.
[^3]: user story توصیف غیررسمی یک ویژگی سیستم است که از دیدگاه کاربر سیستم نوشته میشود. یکی از ایدههایی بود که در Extreme Programming معرفی شد.
هستههایی از حقیقت جهانی وجود دارند که با مزیت دیدگاه قرن بیستویکم، مشکلات و شیوه حرفهمان را میشناسیم:
d'Agapeyeff: برنامهنویسی هنوز بیش از حد یک تلاش هنری است. به پایه محکمتری نیاز داریم که در عمل آموزش داده و پایش شود در: (i) ساختار برنامهها و جریان اجرای آنها؛ (ii) شکلدهی ماژولها و محیطی برای آزمایش آنها؛ (iii) شبیهسازی شرایط زمان اجرا.
با مزیت این دیدگاه، ایدههایی مثل «شکلدهی ماژولها و محیطها [برای تسهیل] آزمایش» و «شبیهسازی شرایط زمان اجرا» کاملاً مدرن و درست به نظر میرسند و بخش زیادی از پایه رویکرد تحویل مستمر (continuous delivery) به توسعه نرمافزار را تشکیل میدهند.
خواندن این مطالب امروز نشان میدهد ایدههای زیادی بهوضوح پایدارند. آزمون زمان را پس دادهاند و امروز به همان اندازه ۱۹۶۸ درست هستند.
تفاوتی وجود دارد، چیزی عمیقتر، در گفتن «حلقههای بازخورد ایجاد کنید» یا «فرض کنید اشتباه خواهید کرد» در مقایسه با «از زبان X استفاده کنید» یا «طراحیهایتان را با تکنیک نمودار Y اثبات کنید».
بازخورد در کدنویسی
در عمل، این نیاز به بازخورد سریع و باکیفیت چگونه بر نحوه کار ما تأثیر میگذارد؟
اگر بازخورد را جدی بگیریم، میخواهیم زیاد از آن داشته باشیم. نوشتن کد و تکیه بر تیم تست برای گزارش آن شش هفته بعد کافی نیست.
رویکرد خودم به نوشتن کد در طول دوران حرفهام بهطور قابلتوجهی تکامل یافته است. اکنون همیشه در چند سطح از بازخورد استفاده میکنم. تغییرات را در گامهای کوچک انجام میدهم.
بهطور کلی رویکرد test-driven به نحوه نوشتن کد دارم. اگر بخواهم رفتار جدیدی به سیستم اضافه کنم، ابتدا یک تست مینویسم.
وقتی شروع به نوشتن تست میکنم، میخواهم بدانم آیا تستم درست است. میخواهم بازخوردی داشته باشم که درستی تست را نشان دهد. پس تست را مینویسم و اجرا میکنم تا شکست آن را ببینم. ماهیت شکست به من بازخوردی میدهد که درک کنم آیا تستم درست است.
اگر تست قبل از نوشتن هر کدی برای پاس شدن، پاس شد، مشکلی در تست وجود دارد و باید قبل از ادامه آن را اصلاح کنم. همه اینها کاربرد تکنیکهای بازخورد ریزدانه متمرکز بر یادگیری سریع را توصیف میکند.
همانطور که در فصل قبل توضیح دادم، تغییرات کد را بهصورت مجموعهای از گامهای کوچک انجام میدهم. حداقل دو سطح بازخورد در اینجا در جریان است. مثلاً از ابزارهای refactoring در IDE زیاد برای کمک به اولی استفاده میکنم، اما در هر گام بازخوردی هم درباره اینکه آیا کدم کار میکند و — ذهنیتر — آیا از آنچه میبینم با تکامل طراحیام راضی هستم، دریافت میکنم. در نتیجه، توانایی تشخیص اشتباهات یا لغزشها بهشدت تقویت میشود.
این سطح دوم بازخورد از این واقعیت ناشی میشود که هر بار تغییری میدهم، میتوانم تستی که روی آن کار میکنم را دوباره اجرا کنم. این تأیید بسیار سریعی میدهد که کد پس از تغییر همچنان کار میکند.
این چرخههای بازخورد فوقالعاده کوتاه هستند، یا باید باشند. بیشتر چرخههای بازخوردی که اینجا ذکر کردم حداکثر چند ثانیه طول میکشند. برخی، مثل اجرای unit test برای تأیید اینکه همهچیز هنوز کار میکند، احتمالاً بیشتر بر حسب میلیثانیه اندازهگیری میشوند.
این چرخه بازخورد کوتاه و سریع بهخاطر سرعت و ارتباط فوریاش با کاری که روی آن هستید، فوقالعاده ارزشمند است.
سازماندهی کار به مجموعهای از گامهای کوچک، فرصتهای بیشتری برای تأمل در پیشرفت و هدایت طراحیها به سمت نتایج بهتر به ما میدهد.
بازخورد در یکپارچهسازی
وقتی کدم را commit میکنم، سیستم continuous integration را فعال میکند و تغییرم را در بستر تغییرات همه دیگران ارزیابی میکند. در این نقطه سطح جدیدی از بازخورد دریافت میکنم. درک عمیقتری به دست میآورم. در این حالت میتوانم بفهمم آیا چیزی در کدم «نشت» کرده و بخش دیگری از سیستم را خراب کرده است.
اگر در این مرحله همه تستها پاس شوند، بازخوردی میگیرم که ایمن است برای کار روی مورد بعدی ادامه دهم. این سطح حیاتی بازخورد است که ایده continuous integration را پشتیبانی میکند.
متأسفانه، continuous integration هنوز بهطور گسترده نادرست فهمیده و ضعیف اجرا میشود. اگر میخواهیم رویکردی فکری و سختگیرانه به توسعه نرمافزار، رویکردی مهندسی، داشته باشیم، ارزیابی بیطرفانه مزایا و معایب ایدهها مهم است. این اغلب برای صنعت ما دشوار به نظر میرسد. بسیاری از ایدهها بهخاطر اینکه بهتر احساس میشوند پذیرفته میشوند، نه بهخاطر اینکه بهتر هستند.
نمونه خوبی از این، بحث بین پیروان continuous integration (CI) و feature branching (FB) است.
بیایید مزایا و معایب این رویکردها را منطقی تحلیل کنیم.
continuous integration درباره ارزیابی هر تغییر در سیستم همراه با هر تغییر دیگر در سیستم، تا حد امکان مکرر و نزدیک به «بهطور پیوسته» است. تعریف CI میگوید:
(CI) شیوه ادغام همه نسخههای کاری توسعهدهندگان در یک خط اصلی مشترک چندین بار در روز است.[^4]
بیشتر متخصصان CI «چندین بار در روز» را به «حداقل یک بار در روز» بهعنوان سازش قابلقبول، هرچند نامطلوب، تسهیل میکنند.
پس، بهطور تعریفی، CI درباره در معرض قرار دادن تغییرات در افزایشهای کوچک برای ارزیابی، حداقل یک بار در روز، است.
branching، از هر نوعی، بهطور تعریفی درباره جداسازی تغییر است:
شاخهها به مشارکتکنندگان اجازه میدهند تغییرات را جدا کنند.[^5]
[^4]: تعریف continuous integration را میتوان اینجا یافت: https://bit.ly/2JVRGiv. [^5]: تعریف branching در کنترل نسخه را میتوان اینجا یافت: https://bit.ly/2NlAll8.
در اصطلاحات پایه و تعریفی، CI و FB واقعاً با هم سازگار نیستند. یکی هدفش در معرض قرار دادن تغییر در اسرع وقت است؛ دیگری کارش به تعویق انداختن آن در معرض قرار گرفتن است.
FB ساده به نظر میرسد و پیروانش از آن لذت میبرند چون زندگی را سادهتر به نظر میرساند. «میتوانم مستقل از همتیمیهایم کد بنویسم.» مشکل در نقطه ادغام تغییرات پیش میآید.
CI برای حل مشکل «جهنم ادغام» (merge-hell) اختراع شد.
در روزهای بد قدیم، و در برخی سازمانهای سرسخت تا امروز، تیمها و افراد روی بخشهایی از کد کار میکردند تا «کامل» تلقی شوند و بعد آنها را با کل ادغام میکردند. آنچه اتفاق میافتاد این بود که در این نقطه انواع مشکلات غیرمنتظره شناسایی میشد، پس ادغام پیچیده میشد و زمان طولانی و غیرقابلپیشبینی برای انجام میبرد.
دو رویکرد برای حل مشکل اتخاذ شد؛ CI یکی بود. رویکرد دیگر بهبود کیفیت ابزارهای ادغام بود.
استدلال رایج از پیروان FB این است که ابزارهای ادغام اکنون آنقدر خوبند که ادغام بهندرت مشکل است. با این حال، همیشه میتوان کدی نوشت که ابزارهای ادغام آن را از دست بدهند؛ ادغام کد لزوماً همان ادغام رفتار نیست.
فرض کنید شما و من در همان codebase کار میکنیم و تابعی داریم که چند کار برای تبدیل یک مقدار انجام میدهد. هر دو مستقل تصمیم میگیریم این تابع باید مقدار را یکی افزایش دهد، اما هر کدام این را در بخش متفاوتی از تابع پیادهسازی میکنیم. کاملاً ممکن است ادغام این دو تغییر مرتبط را از دست بدهد چون در بخشهای مختلف کد هستند و هر دو را میگیریم. حالا مقدارمان دو بهجای یک افزایش مییابد.
continuous integration، وقتی همانطور که تعریف شده اجرا شود، یعنی بازخوردهای منظم و مکرر میگیریم. بینش قدرتمندی به وضعیت کد و رفتار سیستم در طول روز کاری میدهد، اما هزینهای دارد.
برای کار کردن CI، باید تغییرات را بهاندازه کافی مکرر commit کنیم تا آن بازخورد و بینش را به دست آوریم. این یعنی بسیار متفاوت کار کردن.
بهجای کار روی یک ویژگی تا «تمام» یا «آماده تولید» شود، continuous integration و برادر بزرگترش continuous delivery از ما میخواهند تغییرات را در گامهای کوچک انجام دهیم و پس از هر گام کوچک چیزی آماده استفاده داشته باشیم. این نحوه فکر کردن به طراحی سیستم را به روشهای مهمی تغییر میدهد.
این رویکرد یعنی فرایند طراحی کد بیشتر شبیه تکامل هدایتشده است، با هر گام کوچک بازخورد میدهد، اما لزوماً هنوز به یک ویژگی کامل نمیرسد. این تغییر دیدگاه بسیار چالشبرانگیز برای بسیاری است، اما وقتی پذیرفته شود آزادکننده است و تأثیر مثبت بر کیفیت طراحیهایمان دارد.
این رویکرد نهتنها یعنی نرمافزارمان همیشه قابل انتشار است و بازخورد مکرر و ریزدانه درباره کیفیت و کاربرد کارمان میگیریم، بلکه ما را تشویق میکند کارمان را بهگونهای طراحی کنیم که این رویکرد را پایدار کند.
بازخورد در طراحی
یکی از دلایلی که TDD را بهعنوان یک شیوه اینقدر ارزشمند میدانم، بازخوردی است که درباره کیفیت طراحیام به من میدهد. اگر نوشتن تستها سخت باشد، چیز مهمی درباره کیفیت کدم به من میگوید.
توانایی ایجاد تست ساده و مؤثر و مؤثربودن طراحی، از طریق ویژگیهای کیفیتی که در کد «خوب» مهم میدانیم مرتبط است. میتوانیم مدتها درباره تعریف جامع «کیفیت خوب» در کد بحث کنیم، اما فکر نمیکنم برای اثبات نقطهام لازم باشد. پیشنهاد میکنم ویژگیهای زیر تقریباً بهعنوان نشانههای کیفیت در کد پذیرفته شدهاند؛ شاید تنها ویژگیهای کیفیت نباشند، اما مطمئنم موافق خواهید بود که مهم هستند:
- ماژولار بودن (Modularity)
- جداسازی نگرانیها (Separation of concerns)
- انسجام بالا (High cohesion)
- پنهانسازی اطلاعات (abstraction)
- جفتشدگی مناسب (Appropriate coupling)
انتظار دارم این فهرست تا الان آشنا به نظر برسد. علاوه بر «نشانههای کیفیت» در کد، ابزارهایی هستند که به ما اجازه میدهند پیچیدگی را مدیریت کنیم. این تصادفی نیست!
پس چگونه «کیفیت» را بر اساس این ویژگیها در کد قرار میدهید؟ در غیاب TDD، کاملاً به تجربه، تعهد و مهارت توسعهدهنده بستگی دارد.
با TDD، بهطور تعریفی ابتدا تست مینویسیم. اگر ابتدا تست ننویسیم، test-driven development نیست.
اگر میخواهیم ابتدا تست بنویسیم، باید نوع عجیب و احمقانهای از آدم باشیم که زندگی خود را سختتر کنیم. پس سعی میکنیم این کار را بهگونهای انجام دهیم که زندگی را آسانتر کند.
مثلاً، بسیار بعید است تستی بنویسیم که نتوانیم نتایج را از کدی که تست میکنیم برگردانیم. چون ابتدا تست را مینویسیم، قبل از نوشتن هر کد غیرتستی، در لحظه ایجاد تست، همزمان رابط کدمان را هم طراحی میکنیم. تعریف میکنیم کاربران خارجی کدمان چگونه با آن تعامل کنند.
چون برای تست به نتایج نیاز داریم، کد را بهگونهای طراحی میکنیم که دسترسی به نتایجی که به آنها علاقهمندیم آسان باشد. این یعنی در TDD فشاری برای نوشتن کد قابلآزمایشتر اعمال میشود. کد قابلآزمایش چه شکلی است؟
همه اینهاست:
- ماژولار است
- جداسازی نگرانیهای خوبی دارد
- انسجام بالا نشان میدهد
- از پنهانسازی اطلاعات (abstraction) استفاده میکند
- جفتشدگی مناسب دارد
نقش بنیادی آزمایش
در رویکردهای کلاسیک توسعه، آزمایش گاهی بهعنوان تمرینی برای پایان پروژه رها میشد، گاهی به مشتری واگذار میشد و گاهی بهخاطر فشار زمانی تقریباً کاملاً ناپدید میشد.
این نوع رویکرد حلقه بازخورد را آنقدر طولانی کرد که اساساً بیفایده بود. خطاهای معرفیشده در کدنویسی یا طراحی اغلب تا پس از خروج تیم توسعه از پروژه و واگذاری نگهداری به تیم پشتیبانی تولید کشف نمیشدند.
Extreme Programming (XP) و کاربرد TDD و CI آن این را برعکس کرد و آزمایش را در مرکز فرایند توسعه قرار داد. این حلقه بازخورد را به ثانیهها کاهش داد و بازخورد تقریباً فوری درباره اشتباهات داد که، وقتی خوب انجام شود، میتواند کل دستههای باگ را که در غیاب TDD اغلب به تولید میرسند حذف کند.
در این مکتب فکری، آزمایش فرایند توسعه و — مهمتر — طراحی خود نرمافزار را هدایت میکرد. نرمافزار نوشتهشده با TDD از نرمافزار نوشتهشده بدون آن متفاوت به نظر میرسید. برای قابلآزمایش کردن نرمافزار، مهم بود اطمینان حاصل شود رفتارهای مورد انتظار قابل ارزیابی باشند.
این طراحیها را به جهتهای خاصی هل میداد. نرمافزار «قابلآزمایش» ماژولار بود، جفتشدگی سست داشت، انسجام بالا نشان میداد، جداسازی نگرانیهای خوبی داشت و پنهانسازی اطلاعات را پیادهسازی میکرد. اینها هم اتفاقاً ویژگیهایی هستند که بهعنوان نشانگر کیفیت در نرمافزار شناخته میشوند. پس TDD نهتنها رفتار نرمافزار را ارزیابی میکرد، بلکه کیفیت طراحی آن را افزایش میداد.
آزمایش در نرمافزار فوقالعاده مهم است. نرمافزار بهشکلی شکننده است که چیزهای کمی در تجربه انسانی هستند. کوچکترین نقص — یک ویرگول جابهجا — میتواند منجر به شکست فاجعهبار شود.
نرمافزار همچنین بسیار پیچیدهتر از اکثر آفرینشهای انسانی است. یک هواپیمای مسافربری مدرن حدود ۴ میلیون قطعه دارد. نرمافزار یک کامیون Volvo مدرن حدود ۸۰ میلیون خط کد است، هر کدام از چندین دستورالعمل و متغیر تشکیل شده.
TDD ایده جدیدی نبود وقتی Kent Beck آن را در کتابش در اواخر دهه ۱۹۹۰ توصیف کرد. Alan Perlis چیزی مشابه در کنفرانس مهندسی نرمافزار ناتو در ۱۹۶۸ توصیف کرده بود، اما Beck مفهوم را معرفی و با عمق بیشتری توصیف کرد، پس پذیرش گستردهتری یافت. TDD در بسیاری از حوزهها همچنان ایده بحثبرانگیزی است، اما دادهها خوب است. این رویکرد میتواند تعداد باگ سیستم را بهشکل چشمگیری کاهش دهد و تأثیر مثبت بر کیفیت طراحی سیستم دارد.
TDD فشاری برای ایجاد کدی که عینیتر «کیفیت بالاتر» دارد اعمال میکند. این مستقل از استعداد یا تجربه توسعهدهنده نرمافزار است. توسعهدهندگان بد نرمافزار را عالی نمیکند، اما «توسعهدهندگان بد نرمافزار» را بهتر و «توسعهدهندگان عالی نرمافزار» را عالیتر میکند.
TDD و جنبههای دیگر رویکرد test-driven به توسعه، تأثیر مهمی بر کیفیت کدی که میسازیم دارد. این اثر بهینهسازی برای بازخورد بهتر است، اما این اثر آنجا متوقف نمیشود.
بازخورد در معماری
اثر ظریفتر کاربرد رویکرد مبتنی بر بازخورد در معماری گسترده نرمافزار سیستمهایی که میسازیم و همچنین تصمیمات طراحی سطح کد دیده میشود.
continuous delivery رویکردی با عملکرد بالا و مبتنی بر بازخورد به توسعه است. یکی از سنگبنایانش ایده تولید نرمافزاری است که همیشه برای انتشار در تولید آماده است. این استاندارد بالایی است و به فراوانی و کیفیت بسیار بالای بازخورد نیاز دارد.
دستیابی به این نیازمند تغییر جنبههای مختلف رویکرد توسعه سازمانهاست. دو جنبهای که به پیش میآیند میتوانند بهعنوان ویژگیهای معماری سیستمهایی که میسازیم در نظر گرفته شوند. باید قابلیت آزمایش (testability) و قابلیت استقرار (deployability) سیستمهایمان را جدی بگیریم.
به شرکتهایی که با آنها کار میکنم توصیه میکنم هدفشان ایجاد «نرمافزار قابل انتشار» حداقل یک بار در ساعت باشد. این یعنی باید احتمالاً دهها، شاید صدها هزار تست در هر ساعت اجرا کنیم.
با فرض پول و ظرفیت محاسباتی بینهایت، میتوانیم تستها را بهصورت موازی برای بهینهسازی بازخورد سریع اجرا کنیم، اما محدودیتی وجود دارد. میتوانیم تصور کنیم هر تست مستقل و موازی با همه دیگران اجرا شود.
برخی تستها باید استقرار و پیکربندی سیستم را آزمایش کنند، پس مورد محدودکننده برای زمان بازخورد بر اساس زمان استقرار سیستم و راهاندازی آن و زمان اجرای کندترین مورد تست است.
اگر هر تستی بیش از یک ساعت طول بکشد یا نرمافزارتان بیش از یک ساعت برای استقرار طول بکشد، هرچقدر هم روی سختافزار هزینه کنید، اجرای تستها به این سرعت ممکن نخواهد بود.
پس قابلیت آزمایش و قابلیت استقرار سیستم، محدودیتهایی به توانایی جمعآوری بازخورد اضافه میکنند. میتوانیم سیستمهایمان را برای قابلآزمایشتر و قابلاستقرارتر طراحی کنیم و بازخورد را کارآمدتر و در بازههای زمانی کوتاهتر جمعآوری کنیم.
ترجیح میدهیم تستهایی که ثانیه یا میلیثانیه طول میکشند و استقراری که در چند دقیقه یا بهتر چند ثانیه کامل شود.
دستیابی به این سطوح عملکرد در قابلیت استقرار و آزمایش، کار و تمرکز تیم و تعهد سازمان توسعه به ایدههای continuous delivery میخواهد، اما همچنین اغلب به تفکر معماری دقیق نیاز دارد.
دو مسیر مؤثر وجود دارد: یا میتوانید سیستمهای monolithic بسازید و آنها را برای قابلیت استقرار و آزمایش بهینه کنید، یا آنها را به واحدهای جداگانه و بهطور فردی «قابل استقرار» ماژولار کنید. این رویکرد دوم یکی از ایدههای محرک محبوبیت microservices است.
رویکرد معماری microservice به تیمها اجازه میدهد سرویسهایشان را مستقل از یکدیگر توسعه، آزمایش و مستقر کنند؛ همچنین آنها را از نظر سازمانی جدا میکند و به شرکتها امکان رشد مؤثرتر و کارآمدتر میدهد.
استقلال microservices مزیت قابلتوجهی است، اما همچنین پیچیدگی قابلتوجهی. microservices بهطور تعریفی واحدهای کد مستقلقابلاستقرار هستند. این یعنی نمیتوانیم آنها را با هم آزمایش کنیم.
اعمال continuous delivery روی سیستمهای monolithic مؤثر است، اما همچنان از ما میخواهد بتوانیم تغییرات کوچک انجام دهیم و آنها را چندین بار در روز ارزیابی کنیم. برای سیستمهای بزرگتر، همچنان باید در کنار افراد زیادی در یک codebase کار کنیم، پس به حفاظتهایی که طراحی خوب و continuous integration میآورند نیاز داریم.
چه سیستمهایمان را به ماژولهای کوچکتر و مستقلتر (microservices) تجزیه کنیم یا codebaseهای کارآمدتر اما جفتشدگی تنگتری (monoliths) توسعه دهیم، هر دو رویکرد تأثیرات قابلتوجهی بر معماری سیستمهای نرمافزاری که میسازیم دارند.
پذیرش continuous delivery در هر دو رویکرد monolith و microservice، طراحیهای ماژولارتر، abstract شدهتر و جفتشدگی سستتر را ترویج میکند، چون فقط آنگاه میتوانید آنها را بهاندازه کافی کارآمد مستقر و آزمایش کنید تا continuous delivery را عملی کنید.
این یعنی ارزشگذاری و اولویتدهی به بازخورد در رویکرد توسعهمان، تصمیمگیری معماری منطقیتر و مؤثرتر را ترویج میکند.
این ایدهای عمیق و مهم است. یعنی از طریق پذیرش برخی اصول عمومی میتوانیم تأثیر قابلاندازهگیری و قابلتوجهی بر کیفیت سیستمهایی که میسازیم به دست آوریم.
با تمرکز فرایند، فناوری، شیوه و فرهنگ بر تحویل کارآمد بازخورد باکیفیت، میتوانیم نرمافزار باکیفیتتر بسازیم و این کار را با کارایی بیشتر انجام دهیم.
ترجیح بازخورد زودهنگام
بهطور کلی، تلاش برای دریافت بازخورد قطعی در اسرع وقت شیوه مؤثری است. وقتی کد مینویسم، میتوانم از ابزارهای توسعه برای برجسته کردن خطاهای کد در حین تایپ استفاده کنم. این سریعترین و ارزانترین حلقه بازخورد و یکی از ارزشمندترینهاست. میتوانم از تکنیکهایی مثل type system برای بازخورد سریع و قطعی درباره کیفیت کارم بهره ببرم.
میتوانم تست (یا تستها) را در ناحیه کدی که روی آن کار میکنم در محیط توسعه اجرا کنم و بازخورد بسیار سریع — معمولاً کمتر از چند ثانیه — بگیرم.
unit testهای خودکارم، خروجی رویکرد TDD، سطح دوم بازخورد را هنگام کار و اجرای منظم آنها در محیط توسعه محلی میدهند.
مجموعه کامل unit testها و سایر commit testها پس از commit کد اجرا میشوند. این اعتبارسنجی کاملتر اما از نظر زمان پرهزینهتری میدهد که کدم همراه با کد دیگران کار میکند.
acceptance testها، performance testها، security testها و هر چیز دیگری که برای درک اعتبار تغییراتمان مهم میدانیم، اطمینان بیشتری در کیفیت و کاربرد کارمان میدهد، اما با هزینه زمان بیشتر برای بازگرداندن نتایج.
پس کار برای ترجیح شناسایی نقصها، ابتدا در compile-ability (شناساییشده در محیط توسعه) و سپس در unit testها و فقط پس از موفقیت آن اعتبارسنجیها در سایر اشکال تستهای سطح بالاتر، یعنی میتوانیم زودتر شکست بخوریم و بالاترین کیفیت و مؤثرترین بازخورد را بگیریم.
پیروان continuous delivery و DevOps گاهی به این فرایند ترجیح شکستهای زودهنگام shift-left میگویند، هرچند من «Fail fast!» کمابهامتر را ترجیح میدهم.
بازخورد در طراحی محصول
تأثیر جدی گرفتن بازخورد بر کیفیت سیستمهایی که میسازیم مهم و عمیق است، اما در نهایت، توسعهدهندگان نرمافزار برای ساخت نرمافزار طراحیشده خوب و قابلآزمایش پول نمیگیرند. برای ایجاد ارزشی از نوعی برای سازمانهایی که ما را استخدام میکنند پول میگیریم.
این یکی از تنشهایی است که اغلب در قلب رابطه بین افراد متمرکز بر کسبوکار و افراد متمرکز بر فنی در اکثر سازمانهای سنتی قرار دارد.
این مشکلی است که تمرکز بر امکانپذیر کردن تحویل مستمر ایدههای مفید به تولید به آن میپردازد.
چگونه بدانیم ایدههایی که داریم، محصولاتی که میسازیم، خوب هستند؟
پاسخ واقعی این است که تا بازخورد از مصرفکنندگان ایدههایمان (کاربران یا مشتریان) نگیریم نمیدانیم.
بستن حلقه بازخورد حول ایجاد ایدههای محصول و تحویل ارزش به تولید، ارزش واقعی continuous delivery است. دلیلی است که در سازمانهای سراسر جهان اینقدر محبوب شده، نه مزایای فنیتر (هرچند مهم) آن.
اعمال اصول بهکارگیری و بهینهسازی برای بازخورد سریع و باکیفیت به سازمانها امکان میدهد سریعتر یاد بگیرند؛ کشف کنند کدام ایدهها برای مشتریانشان کار میکند یا نمیکند؛ و محصولاتشان را برای برآورده کردن بهتر نیازهای مشتری تطبیق دهند.
مؤثرترین سازمانهای توسعه نرمافزار جهان این جنبه را بسیار جدی میگیرند.
افزودن telemetry به سیستمهایمان که به ما اجازه جمعآوری داده درباره کدام ویژگیهای سیستم استفاده میشوند و چگونه استفاده میشوند، اکنون هنجار است. جمعآوری اطلاعات (بازخورد) از سیستمهای تولید نهتنها برای تشخیص مشکلات، بلکه برای کمک به طراحی مؤثرتر نسل بعدی محصولات و خدمات، سازمانها را از «کسبوکار و IT» به «کسبوکارهای دیجیتال» تبدیل میکند. این در بسیاری از حوزهها آنقدر پیچیده شده که اطلاعات جمعآوریشده اغلب ارزشمندتر از خدمات ارائهشده است و میتواند بینشهایی درباره خواستهها، نیازها و رفتار مشتریان ارائه دهد که حتی خود مشتریان از آنها آگاه نیستند.
بازخورد در سازمان و فرهنگ
قابلاندازهگیری بودن توسعه نرمافزار مدتها مشکل بوده است. چگونه موفقیت را اندازه بگیریم و چگونه بهبود را اندازه بگیریم؟ چگونه بفهمیم تغییراتی که میدهیم مؤثر هستند یا نه؟
بیشتر تاریخ توسعه نرمافزار، این بر پایه اندازهگیری چیزهایی بود که اندازهگیری آسان بودند (مثلاً «تعداد خطوط کد» یا «روزهای توسعهدهنده» یا «پوشش تست») یا حدس زدن و تصمیمگیری ذهنی بر اساس شهود. مشکل این است که هیچکدام از اینها واقعاً بهشکل واقعبینانهای با موفقیت — هرچه معنا داشته باشد — همبستگی ندارند.
خطوط کد بیشتر به معنای کد بهتر نیست؛ احتمالاً کد بدتر است. پوشش تست بیمعناست مگر تستها چیز مفیدی را آزمایش کنند. میزان تلاشی که برای نرمافزار میگذاریم به ارزش آن مرتبط نیست. پس حدس و ذهنیت شاید بهاندازه این معیارها خوب باشد.
پس چگونه بهتر عمل کنیم؟ چگونه بازخورد مفید بدون نوعی معیار موفقیت ایجاد کنیم؟
دو رویکرد برای این مشکل وجود دارد. اولی مدتی است در حلقههای توسعه چابک شناخته شده است. میپذیریم قضاوتها تا حدی ذهنیاند، اما سعی میکنیم انضباط معقولی برای کاهش ذهنیت اتخاذ کنیم. موفقیت این رویکرد ناگزیر به افراد درگیر گره خورده است. «افراد و تعاملات بر فرایندها و ابزارها.»[^6]
[^6]: «افراد و تعاملات بر فرایندها و ابزارها» عبارتی از Agile Manifesto است؛ ببینید https://agilemanifesto.org/.
این استراتژی از نظر تاریخی در دور شدن از رویکردهای فرمولی و تشریفاتدار به توسعه نرمافزار مهم بود و بهعنوان اصل بنیادی مهم میماند.
رویکردهای چابک به توسعه، تیم — افراد در کار — را به حلقه بازخورد آوردند تا نتایج اقداماتشان را مشاهده، درباره آنها تأمل و انتخابهایشان را در طول زمان برای بهبود وضعیتشان اصلاح کنند. این رویکرد ذهنی و مبتنی بر بازخورد، بنیادیترین ایده چابک «بازبینی و تطبیق» (inspect and adapt) بود.
اصلاح کوچکی که به این رویکرد ذهنی بازخورد برای بهبود کیفیت بازخورد اضافه میکنم این است که درباره ماهیت آن مشخص باشیم.
مثلاً، اگر تیمتان ایدهای برای بهبود رویکردش به چیزی دارد، از کتاب دانشمندان الگو بگیرید و درباره جایی که فکر میکنید الان هستید (وضعیت فعلی) و جایی که ترجیح میدهید باشید (وضعیت هدف) روشن باشید. گامی را که فکر میکنید در جهت درست میبرد توصیف کنید. تصمیم بگیرید چگونه تشخیص دهید به هدف نزدیکتر یا دورتر شدهاید. گام را بردارید و بررسی کنید به هدف نزدیکتر یا دورتر شدهاید و تکرار کنید تا به هدف برسید.[^7]
[^7]: Mike Rother این رویکرد را با جزئیات بیشتر در کتاب Toyota Kata توصیف کرد؛ ببینید https://amzn.to/2FvsI74. با این حال، واقعاً فقط اصلاحی از روش علمی است.
این کاربرد ساده و سبک روش علمی است. این باید واضح باشد. این باید «مادر و سیب» باشد، اما آنچه بیشتر مردم در بیشتر سازمانها انجام میدهند نیست.
وقتی مردم این نوع رویکرد را بهکار میبرند، نتایج بسیار بهتری میگیرند. مثلاً، این ایدهای است که Lean thinking[^8] و بهطور خاص «Toyota Way»، رویکرد Lean به تولید که صنعت خودرو و بسیاری دیگر را متحول کرد، بر آن استوار است.
[^8]: Lean thinking اصطلاح کلی برای ایدههایی است که با تولید Lean و فرایند Lean همراستا و مرتبطاند.
سالها باور داشتم این همه کاری است که واقعاً میتوانیم برای اعمال رویکردهای ذهنی اما سازمانیافتهتر به حل مسئله انجام دهیم. در سالهای اخیر، ذهنم با کار عالی گروه Google DORA[^9] تغییر کرد. اکنون باور دارم کارشان برخی معیارهای مشخصتر و کمتر ذهنی را شناسایی کرده که میتوانیم برای ارزیابی تغییرات در سازمان و فرهنگ و همچنین تغییرات فنیتر بهکار ببریم.
[^9]: گروه DORA رویکرد علمی قابلدفاع به جمعآوری و تحلیل داده در قلب «گزارش State of DevOps» را طراحی کرد که از ۲۰۱۴ سالانه تولید میشد. رویکرد و یافتههایشان با جزئیات بیشتر در کتاب Accelerate: The Science of Lean Software and DevOps توصیف شده.
این به معنای کهن شدن رویکرد قبلی نیست. خلاقیت انسانی باید بهکار رود، تصمیمگیری مبتنی بر داده هم میتواند احمقانه باشد، اما میتوانیم ارزیابی ذهنی را با داده آگاه کنیم و تقویت کنیم و در ارزیابی موفقیت کمی کمیتر باشیم.
معیارهای پایداری (stability) و توان عملیاتی (throughput) که در فصل ۳ توصیف شدند مهم هستند. ایدهآل نیستند و مدلی که در آن عمل میکنند مدل همبستگی است، نه علّی. شواهدی نداریم بگوییم «X باعث Y میشود»؛ پیچیدهتر از آن است. همچنین سؤالات زیادی هست که دوست داریم کمیتر پاسخ دهیم اما نمیدانیم چگونه. پایداری و توان عملیاتی مهماند چون بهترین چیزی است که فعلاً درک میکنیم، نه چون کاملاند.
با این حال، این گام عظیمی به جلو است. اکنون میتوانیم از این معیارهای کارایی و کیفیت، که معیارهای نتایج منطقی و مفید هستند، برای ارزیابی تقریباً هر نوع تغییری استفاده کنیم. اگر تیمم تصمیم بگیرد محل نشستنشان را برای بهبود ارتباطات تغییر دهد، میتوانیم پایداری و توان عملیاتی را پایش کنیم تا ببینیم تغییر میکنند. اگر بخواهیم فناوری جدیدی امتحان کنیم، آیا نرمافزار را سریعتر تولید میکند، اعداد توان عملیاتی را بهبود میدهد یا کیفیت را برای بهبود اعداد پایداری بهبود میدهد؟
این بازخورد برای هدایت تلاشهایمان به سمت نتایج بهتر پیشبینیشده توسط مدل DORA بهعنوان «تابع تناسب» (fitness function) بیقیمت است. با ردیابی امتیازاتمان در پایداری و توان عملیاتی هنگام تکامل فرایند، فناوری، سازمان و فرهنگ، میتوانیم مطمئن باشیم تغییراتی که میدهیم واقعاً مفیدند. از قربانیان مد یا حدس به چیزی شبیه مهندسان تبدیل میشویم.
این تغییرات هنوز جانشین ارزش واقعی نرمافزاری است که تولید میکنیم هستند. آن ارزش در تأثیر تغییراتمان بر کاربران نشان داده میشود. با این حال، این تغییرات ویژگیهای مهمی از کارمان را اندازه میگیرند و قابل دستکاری نیستند. اگر اعداد پایداری و توان عملیاتی خوب باشند، تحویل فنیتان خوب است. پس اگر با پایداری و توان عملیاتی خوب موفق نیستید، ایدههای محصول یا استراتژی کسبوکارتان مقصر است.
خلاصه
بازخورد برای توانایی یادگیری ما ضروری است. بدون بازخورد سریع و مؤثر، حدس میزنیم. هم سرعت و هم کیفیت بازخورد مهم است. اگر بازخورد دیر باشد، بیفایده است. اگر گمراهکننده یا اشتباه باشد، تصمیماتی که بر اساس آن میگیریم هم اشتباه خواهند بود. اغلب به بازخوردی که برای آگاه کردن انتخابهایمان نیاز داریم و اهمیت واقعی جدول زمانی بازخوردی که جمع میکنیم فکر نمیکنیم.
هر دو continuous delivery و continuous integration ایدههایی هستند که اساساً بر ایده بهینهسازی فرایند توسعه برای حداکثر کردن کیفیت و سرعت بازخوردی که جمع میکنیم استوارند.